Ĉiuj Habr en unu datumbazo

Bonan posttagmezon. Jam pasis 2 jaroj de kiam ĝi estis skribita. lasta artikolo pri analizado de Habr, kaj kelkaj punktoj ŝanĝiĝis.

Kiam mi volis havi kopion de Habr, mi decidis skribi analizilon, kiu konservus la tutan enhavon de la aŭtoroj al la datumbazo. Kiel ĝi okazis kaj kiajn erarojn mi renkontis - vi povas legi sub la tranĉo.

TLDR- datumbaza ligo

La unua versio de la analizilo. Unu fadeno, multaj problemoj

Komence, mi decidis fari skriptoprototipon, en kiu la artikolo estus analizita tuj post elŝuto kaj metita en la datumbazon. Sen pripensi dufoje, mi uzis sqlite3, ĉar. ĝi estis malpli laborintensa: ne necesas havi lokan servilon, kreitan-aspektis-forigitan kaj tiajn aĵojn.

unu_fadeno.py

from bs4 import BeautifulSoup
import sqlite3
import requests
from datetime import datetime

def main(min, max):
    conn = sqlite3.connect('habr.db')
    c = conn.cursor()
    c.execute('PRAGMA encoding = "UTF-8"')
    c.execute("CREATE TABLE IF NOT EXISTS habr(id INT, author VARCHAR(255), title VARCHAR(255), content  TEXT, tags TEXT)")

    start_time = datetime.now()
    c.execute("begin")
    for i in range(min, max):
        url = "https://m.habr.com/post/{}".format(i)
        try:
            r = requests.get(url)
        except:
            with open("req_errors.txt") as file:
                file.write(i)
            continue
        if(r.status_code != 200):
            print("{} - {}".format(i, r.status_code))
            continue

        html_doc = r.text
        soup = BeautifulSoup(html_doc, 'html.parser')

        try:
            author = soup.find(class_="tm-user-info__username").get_text()
            content = soup.find(id="post-content-body")
            content = str(content)
            title = soup.find(class_="tm-article-title__text").get_text()
            tags = soup.find(class_="tm-article__tags").get_text()
            tags = tags[5:]
        except:
            author,title,tags = "Error", "Error {}".format(r.status_code), "Error"
            content = "При парсинге этой странице произошла ошибка."

        c.execute('INSERT INTO habr VALUES (?, ?, ?, ?, ?)', (i, author, title, content, tags))
        print(i)
    c.execute("commit")
    print(datetime.now() - start_time)

main(1, 490406)

Ĉio estas klasika - ni uzas Belan Supon, petojn kaj rapida prototipo estas preta. Tio estas nur…

  • Paĝa elŝuto estas en unu fadeno

  • Se vi interrompas la ekzekuton de la skripto, tiam la tuta datumbazo iros nenien. Post ĉio, la transdono estas farita nur post la tuta analizado.
    Kompreneble, vi povas fari ŝanĝojn al la datumbazo post ĉiu enmeto, sed tiam la tempo de ekzekuto de skripto pliiĝos signife.

  • Analizi la unuajn 100 000 artikolojn prenis min 8 horojn.

Poste mi trovas la artikolon de la uzanto kunintegrita, kiun mi legis kaj trovis kelkajn vivhakojn por akceli ĉi tiun procezon:

  • Uzado de multfadenado akcelas elŝutadon kelkfoje.
  • Vi povas akiri ne la plenan version de la habr, sed ĝian moveblan version.
    Ekzemple, se kunintegra artikolo en la labortabla versio pezas 378 KB, tiam en la movebla versio ĝi jam estas 126 KB.

Dua versio. Multaj fadenoj, provizora malpermeso de Habr

Kiam mi traserĉis la Interreton pri la temo de multithreading en python, mi elektis la plej simplan opcion kun multiprocessing.dummy, mi rimarkis, ke problemoj aperis kune kun multithreading.

SQLite3 ne volas labori kun pli ol unu fadeno.
fiksita check_same_thread=False, sed ĉi tiu eraro ne estas la sola, kiam oni provas enmeti en la datumbazon, foje okazas eraroj, kiujn mi ne povis solvi.

Tial mi decidas forlasi la tujan enmeton de artikoloj rekte en la datumbazon kaj, memorante la kunintegran solvon, mi decidas uzi dosierojn, ĉar ne estas problemoj pri plurfadena skribo al dosiero.

Habr komencas malpermesi por uzi pli ol tri fadenojn.
Precipe fervoraj provoj trairi al Habr povas fini kun ip-malpermeso dum kelkaj horoj. Do vi devas uzi nur 3 fadenojn, sed ĉi tio jam estas bona, ĉar la tempo por ripeti pli ol 100 artikoloj estas reduktita de 26 al 12 sekundoj.

Indas noti, ke ĉi tiu versio estas sufiĉe malstabila, kaj elŝutoj periode falas sur granda nombro da artikoloj.

async_v1.py

from bs4 import BeautifulSoup
import requests
import os, sys
import json
from multiprocessing.dummy import Pool as ThreadPool
from datetime import datetime
import logging

def worker(i):
    currentFile = "files\{}.json".format(i)

    if os.path.isfile(currentFile):
        logging.info("{} - File exists".format(i))
        return 1

    url = "https://m.habr.com/post/{}".format(i)

    try: r = requests.get(url)
    except:
        with open("req_errors.txt") as file:
            file.write(i)
        return 2

    # Запись заблокированных запросов на сервер
    if (r.status_code == 503):
        with open("Error503.txt", "a") as write_file:
            write_file.write(str(i) + "n")
            logging.warning('{} / 503 Error'.format(i))

    # Если поста не существует или он был скрыт
    if (r.status_code != 200):
        logging.info("{} / {} Code".format(i, r.status_code))
        return r.status_code

    html_doc = r.text
    soup = BeautifulSoup(html_doc, 'html5lib')

    try:
        author = soup.find(class_="tm-user-info__username").get_text()

        timestamp = soup.find(class_='tm-user-meta__date')
        timestamp = timestamp['title']

        content = soup.find(id="post-content-body")
        content = str(content)
        title = soup.find(class_="tm-article-title__text").get_text()
        tags = soup.find(class_="tm-article__tags").get_text()
        tags = tags[5:]

        # Метка, что пост является переводом или туториалом.
        tm_tag = soup.find(class_="tm-tags tm-tags_post").get_text()

        rating = soup.find(class_="tm-votes-score").get_text()
    except:
        author = title = tags = timestamp = tm_tag = rating = "Error" 
        content = "При парсинге этой странице произошла ошибка."
        logging.warning("Error parsing - {}".format(i))
        with open("Errors.txt", "a") as write_file:
            write_file.write(str(i) + "n")

    # Записываем статью в json
    try:
        article = [i, timestamp, author, title, content, tm_tag, rating, tags]
        with open(currentFile, "w") as write_file:
            json.dump(article, write_file)
    except:
        print(i)
        raise

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print("Необходимы параметры min и max. Использование: async_v1.py 1 100")
        sys.exit(1)
    min = int(sys.argv[1])
    max = int(sys.argv[2])

    # Если потоков >3
    # то хабр банит ipшник на время
    pool = ThreadPool(3)

    # Отсчет времени, запуск потоков
    start_time = datetime.now()
    results = pool.map(worker, range(min, max))

    # После закрытия всех потоков печатаем время
    pool.close()
    pool.join()
    print(datetime.now() - start_time)

Tria versio. Finalo

Sencimigante la duan version, mi malkovris, ke Habr, subite, havas API, kiun la movebla versio de la retejo aliras. Ĝi ŝarĝas pli rapide ol la movebla versio, ĉar ĝi estas nur json, kiu eĉ ne bezonas esti analizita. Fine mi decidis reverki mian skripton denove.

Do, trovinte ĉi tiu ligo API, vi povas komenci analizi ĝin.

async_v2.py

import requests
import os, sys
import json
from multiprocessing.dummy import Pool as ThreadPool
from datetime import datetime
import logging

def worker(i):
    currentFile = "files\{}.json".format(i)

    if os.path.isfile(currentFile):
        logging.info("{} - File exists".format(i))
        return 1

    url = "https://m.habr.com/kek/v1/articles/{}/?fl=ru%2Cen&hl=ru".format(i)

    try:
        r = requests.get(url)
        if r.status_code == 503:
            logging.critical("503 Error")
            return 503
    except:
        with open("req_errors.txt") as file:
            file.write(i)
        return 2

    data = json.loads(r.text)

    if data['success']:
        article = data['data']['article']

        id = article['id']
        is_tutorial = article['is_tutorial']
        time_published = article['time_published']
        comments_count = article['comments_count']
        lang = article['lang']
        tags_string = article['tags_string']
        title = article['title']
        content = article['text_html']
        reading_count = article['reading_count']
        author = article['author']['login']
        score = article['voting']['score']

        data = (id, is_tutorial, time_published, title, content, comments_count, lang, tags_string, reading_count, author, score)
        with open(currentFile, "w") as write_file:
            json.dump(data, write_file)

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print("Необходимы параметры min и max. Использование: asyc.py 1 100")
        sys.exit(1)
    min = int(sys.argv[1])
    max = int(sys.argv[2])

    # Если потоков >3
    # то хабр банит ipшник на время
    pool = ThreadPool(3)

    # Отсчет времени, запуск потоков
    start_time = datetime.now()
    results = pool.map(worker, range(min, max))

    # После закрытия всех потоков печатаем время
    pool.close()
    pool.join()
    print(datetime.now() - start_time)

Ĝi enhavas kampojn rilatajn kaj al la artikolo mem kaj al la aŭtoro kiu verkis ĝin.

API.png

Ĉiuj Habr en unu datumbazo

Mi ne forĵetis la plenan json de ĉiu artikolo, sed konservis nur la kampojn kiujn mi bezonis:

  • id
  • estas_lernilo
  • tempo_publikigita
  • titolo
  • enhavo
  • komentoj_kalkulo
  • lang estas la lingvo en kiu la artikolo estas skribita. Ĝis nun ĝi havas nur en kaj ru.
  • tags_string - ĉiuj etikedoj de la afiŝo
  • legado_kalkulo
  • aŭtoro
  • poentaro — artikola takso.

Tiel, uzante la API, mi reduktis la skripton ekzekuttempon al 8 sekundoj po 100 url.

Post kiam ni elŝutis la datumojn, kiujn ni bezonas, ni devas prilabori ĝin kaj enigi ĝin en la datumbazon. Mi ankaŭ ne havis problemojn pri ĉi tio:

analizilo.py

import json
import sqlite3
import logging
from datetime import datetime

def parser(min, max):
    conn = sqlite3.connect('habr.db')
    c = conn.cursor()
    c.execute('PRAGMA encoding = "UTF-8"')
    c.execute('PRAGMA synchronous = 0') # Отключаем подтверждение записи, так скорость увеличивается в разы.
    c.execute("CREATE TABLE IF NOT EXISTS articles(id INTEGER, time_published TEXT, author TEXT, title TEXT, content TEXT, 
    lang TEXT, comments_count INTEGER, reading_count INTEGER, score INTEGER, is_tutorial INTEGER, tags_string TEXT)")
    try:
        for i in range(min, max):
            try:
                filename = "files\{}.json".format(i)
                f = open(filename)
                data = json.load(f)

                (id, is_tutorial, time_published, title, content, comments_count, lang,
                 tags_string, reading_count, author, score) = data

                # Ради лучшей читаемости базы можно пренебречь читаемостью кода. Или нет?
                # Если вам так кажется, можно просто заменить кортеж аргументом data. Решать вам.

                c.execute('INSERT INTO articles VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (id, time_published, author,
                                                                                        title, content, lang,
                                                                                        comments_count, reading_count,
                                                                                        score, is_tutorial,
                                                                                        tags_string))
                f.close()

            except IOError:
                logging.info('FileNotExists')
                continue

    finally:
        conn.commit()

start_time = datetime.now()
parser(490000, 490918)
print(datetime.now() - start_time)

Статистика

Nu, tradicie, finfine, vi povas ĉerpi kelkajn statistikojn el la datumoj:

  • El la atendataj 490 406 elŝutoj, nur 228 512 artikoloj estis elŝutitaj. Montriĝas, ke pli ol duono (261894) de la artikoloj pri Habré estis kaŝitaj aŭ forigitaj.
  • La tuta datumbazo, konsistanta el preskaŭ duonmiliono da artikoloj, pezas 2.95 GB. En kunpremita formo - 495 MB.
  • Entute, 37804 homoj estas la aŭtoroj de Habré. Mi memorigas vin, ke ĉi tiuj statistikoj estas nur de vivaj afiŝoj.
  • La plej produktiva aŭtoro pri Habré - alizar - 8774 artikoloj.
  • Plej taksata artikolo — 1448 plusoj
  • Plej legita artikolo — 1660841 vidoj
  • Plej Diskutita Artikolo — 2444 komentoj

Nu, en formo de suprojPlej bonaj 15 aŭtorojĈiuj Habr en unu datumbazo
Supraj 15 laŭ taksoĈiuj Habr en unu datumbazo
Supraj 15 legisĈiuj Habr en unu datumbazo
Supraj 15 DiskutitaĈiuj Habr en unu datumbazo

fonto: www.habr.com

Aldoni komenton