Všichni Habr v jedné databázi

Dobré odpoledne. Jsou to 2 roky, co byla napsána. poslední článek o analýze Habra a některé body se změnily.

Když jsem chtěl mít kopii Habra, rozhodl jsem se napsat parser, který by uložil veškerý obsah autorů do databáze. Jak se to stalo a na jaké chyby jsem narazil - si můžete přečíst pod střihem.

TLDR- odkaz na databázi

První verze analyzátoru. Jedno vlákno, mnoho problémů

Pro začátek jsem se rozhodl vytvořit prototyp skriptu, ve kterém by byl článek analyzován a umístěn do databáze ihned po stažení. Bez přemýšlení jsem použil sqlite3, protože. bylo to méně náročné na práci: není potřeba mít lokální server, vytvořený-vypadal-vymazaný a podobně.

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še je klasické - používáme Krásnou polévku, požadavky a rychlý prototyp je hotový. To je jen…

  • Stahování stránky je v jednom vlákně

  • Pokud přerušíte provádění skriptu, celá databáze nikam nepůjde. Potvrzení se totiž provádí až po všech analýzách.
    Samozřejmě můžete provést změny v databázi po každém vložení, ale pak se výrazně prodlouží doba provádění skriptu.

  • Analýza prvních 100 000 článků mi zabrala 8 hodin.

Dále najdu článek uživatele kointegrované, který jsem si přečetl a našel jsem pár životních hacků, jak tento proces urychlit:

  • Použití multithreadingu občas urychlí stahování.
  • Nemůžete získat plnou verzi habr, ale jeho mobilní verzi.
    Pokud například kointegrovaný článek v desktopové verzi váží 378 KB, tak v mobilní verzi je to již 126 KB.

Druhá verze. Mnoho vláken, dočasný zákaz od Habr

Když jsem prohledal internet na téma multithreading v pythonu, zvolil jsem nejjednodušší možnost s multiprocessing.dummy, všiml jsem si, že problémy se objevily spolu s multithreadingem.

SQLite3 nechce pracovat s více než jedním vláknem.
pevný check_same_thread=False, ale tato chyba není jediná, při pokusu o vložení do databáze se občas vyskytnou chyby, které jsem nedokázal vyřešit.

Rozhoduji se proto upustit od okamžitého vkládání článků přímo do databáze a s ohledem na kointegrované řešení se rozhoduji pro použití souborů, protože s vícevláknovým zápisem do souboru nejsou žádné problémy.

Habr začíná banovat za použití více než tří vláken.
Zvláště horlivé pokusy dostat se k Habrovi mohou skončit zákazem ip na několik hodin. Musíte tedy použít pouze 3 vlákna, ale to už je dobré, protože čas na iteraci více než 100 článků se zkrátí z 26 na 12 sekund.

Stojí za zmínku, že tato verze je poněkud nestabilní a stahování u velkého počtu článků pravidelně klesá.

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)

Třetí verze. Finále

Při ladění druhé verze jsem zjistil, že Habr má najednou API, ke kterému přistupuje mobilní verze webu. Načítá se rychleji než mobilní verze, protože je to jen json, který ani není třeba analyzovat. Nakonec jsem se rozhodl svůj scénář znovu přepsat.

Takže po nalezení odkaz API, můžete jej začít analyzovat.

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 pole týkající se jak článku samotného, ​​tak autora, který jej napsal.

API.png

Všichni Habr v jedné databázi

Nevypsal jsem celý json každého článku, ale uložil jsem pouze pole, která jsem potřeboval:

  • id
  • is_tutorial
  • time_published
  • titul
  • obsah
  • počet_komentářů
  • lang je jazyk, ve kterém je článek napsán. Zatím má jen en a ru.
  • tags_string – všechny tagy z příspěvku
  • počet_čtení
  • autor
  • skóre — hodnocení článku.

Pomocí API jsem tedy zkrátil dobu provádění skriptu na 8 sekund na 100 URL.

Poté, co jsme si stáhli potřebná data, musíme je zpracovat a vložit do databáze. Ani s tímto jsem neměl problém:

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)

Statistika

Tradičně nakonec můžete z dat získat nějaké statistiky:

  • Z očekávaných 490 406 stažení bylo staženo pouze 228 512 článků. Ukazuje se, že více než polovina (261894) článků o Habrém byla skryta nebo smazána.
  • Celá databáze skládající se z téměř půl milionu článků váží 2.95 GB. V komprimované podobě - ​​495 MB.
  • Celkem je autory Habrého 37804 lidí. Připomínám, že tyto statistiky jsou pouze z živých příspěvků.
  • Nejproduktivnější autor na Habré - alizar - 8774 článků.
  • Nejlépe hodnocený článek — 1448 plusů
  • Nejčtenější článek — 1660841 zobrazení
  • Nejdiskutovanější článek — 2444 komentářů

Tedy v podobě svrškůTop 15 autorůVšichni Habr v jedné databázi
15 nejlepších podle hodnoceníVšichni Habr v jedné databázi
Top 15 přečtenýchVšichni Habr v jedné databázi
Top 15 diskutovanýchVšichni Habr v jedné databázi

Zdroj: www.habr.com

Přidat komentář