Všetci Habr v jednej databáze

Dobrý deň. Od jeho napísania prešli 2 roky. posledný článok o analýze Habra a niektoré body sa zmenili.

Keď som chcel mať kópiu Habra, rozhodol som sa napísať parser, ktorý by uložil všetok obsah autorov do databázy. Ako sa to stalo a s akými chybami som sa stretol - si môžete prečítať pod strihom.

TLDR- odkaz na databázu

Prvá verzia syntaktického analyzátora. Jedno vlákno, veľa problémov

Na začiatok som sa rozhodol urobiť prototyp skriptu, v ktorom by sa článok hneď po stiahnutí analyzoval a umiestnil do databázy. Bez rozmýšľania som použil sqlite3, pretože. bolo to menej náročné na prácu: nie je potrebné mať lokálny server, vytvorený-vyzeral-vymazaný a podobne.

one_thread.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)

Všetko je klasické - používame Krásna polievka, požiadavky a rýchly prototyp je hotový. To je len…

  • Sťahovanie stránky je v jednom vlákne

  • Ak prerušíte vykonávanie skriptu, celá databáza nikam nepôjde. Koniec koncov, odovzdanie sa vykoná až po všetkých analýzach.
    Samozrejme, po každom vložení môžete opraviť zmeny v databáze, ale potom sa výrazne zvýši čas vykonávania skriptu.

  • Analýza prvých 100 000 článkov mi trvala 8 hodín.

Ďalej nájdem článok používateľa kointegrované, ktorý som si prečítal a našiel som niekoľko životných trikov na urýchlenie tohto procesu:

  • Používanie multithreadingu občas zrýchľuje sťahovanie.
  • Môžete získať nie plnú verziu habr, ale jeho mobilnú verziu.
    Ak napríklad kointegrovaný článok v desktopovej verzii váži 378 KB, tak v mobilnej verzii je to už 126 KB.

Druhá verzia. Veľa vlákien, dočasný zákaz od Habr

Keď som prehľadával internet na tému multithreading v pythone, vybral som si najjednoduchšiu možnosť s multiprocessing.dummy, všimol som si, že problémy sa objavili spolu s multithreadingom.

SQLite3 nechce pracovať s viac ako jedným vláknom.
pevné check_same_thread=False, ale táto chyba nie je jediná, pri pokuse o vloženie do databázy sa občas vyskytnú chyby, ktoré som nevedel vyriešiť.

Rozhodol som sa preto upustiť od okamžitého vkladania článkov priamo do databázy a pamätajúc na kointegrované riešenie sa rozhodujem pre súbory, pretože s viacvláknovým zápisom do súboru nie sú žiadne problémy.

Habr začína banovať za používanie viac ako troch vlákien.
Obzvlášť horlivé pokusy dostať sa k Habrovi môžu skončiť zákazom IP na pár hodín. Takže musíte použiť iba 3 vlákna, ale to je už dobré, pretože čas na opakovanie viac ako 100 článkov sa skrátil z 26 na 12 sekúnd.

Stojí za zmienku, že táto verzia je dosť nestabilná a sťahovanie pravidelne klesá pri veľkom počte článkov.

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)

Tretia verzia. Finálny

Pri ladení druhej verzie som zistil, že Habr má zrazu API, ku ktorému pristupuje mobilná verzia stránky. Načítava sa rýchlejšie ako mobilná verzia, keďže je to len json, ktorý ani netreba analyzovať. Nakoniec som sa rozhodol opäť prepísať svoj scenár.

Takže po nájdení tento odkaz API, môžete ho začať analyzovať.

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)

Obsahuje polia súvisiace so samotným článkom aj s autorom, ktorý ho napísal.

API.png

Všetci Habr v jednej databáze

Nevypísal som celý json každého článku, ale uložil som iba polia, ktoré som potreboval:

  • id
  • is_tutorial
  • time_published
  • titul
  • obsah
  • počet_komentárov
  • lang je jazyk, v ktorom je článok napísaný. Zatiaľ má len en a ru.
  • tags_string – všetky značky z príspevku
  • počet_čítaní
  • autor
  • skóre — hodnotenie článku.

Pomocou API som teda skrátil čas vykonania skriptu na 8 sekúnd na 100 url.

Po stiahnutí potrebných údajov ich musíme spracovať a vložiť do databázy. S týmto som tiež nemal žiadne problémy:

parser.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)

štatistika

Tradične nakoniec môžete z údajov získať nejaké štatistiky:

  • Z očakávaných 490 406 stiahnutí bolo stiahnutých len 228 512 článkov. Ukazuje sa, že viac ako polovica (261894) článkov o Habrém bola skrytá alebo vymazaná.
  • Celá databáza pozostávajúca z takmer pol milióna článkov váži 2.95 GB. V komprimovanej forme - 495 MB.
  • Celkovo je autormi Habrého 37804 ľudí. Pripomínam, že tieto štatistiky sú len zo živých príspevkov.
  • Najproduktívnejší autor na Habré - alizar - 8774 článkov.
  • Najlepšie hodnotený článok — 1448 plusov
  • Najčítanejší článok — 1660841 zobrazení
  • Najdiskutovanejší článok — 2444 komentárov

No v podobe topov15 najlepších autorovVšetci Habr v jednej databáze
15 najlepších podľa hodnoteniaVšetci Habr v jednej databáze
Top 15 prečítanýchVšetci Habr v jednej databáze
Top 15 diskutovanýchVšetci Habr v jednej databáze

Zdroj: hab.com

Pridať komentár