Wszystkie Habr w jednej bazie danych

Dzień dobry. Minęły 2 lata od napisania. ostatni artykuł o parsowaniu Habr, a niektóre punkty uległy zmianie.

Kiedy chciałem mieć kopię Habr, postanowiłem napisać parser, który zapisywałby całą zawartość autorów do bazy danych. Jak to się stało i jakie błędy napotkałem - przeczytacie pod nacięciem.

TLDR- link do bazy danych

Pierwsza wersja parsera. Jeden wątek, wiele problemów

Na początek postanowiłem zrobić prototyp skryptu, w którym artykuł byłby analizowany natychmiast po pobraniu i umieszczony w bazie danych. Nie zastanawiając się dwa razy, użyłem sqlite3, ponieważ. było mniej pracochłonne: nie trzeba było mieć lokalnego serwera, tworzyć-wyglądać-usuwać i tym podobne.

jeden_wątek.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)

Wszystko klasycznie - używamy Pięknej Zupy, prośby i szybki prototyp gotowy. To jest tylko…

  • Pobieranie strony odbywa się w jednym wątku

  • Jeśli przerwiesz wykonywanie skryptu, cała baza danych nie trafi donikąd. W końcu zatwierdzenie jest wykonywane dopiero po całym parsowaniu.
    Oczywiście można zatwierdzić zmiany w bazie danych po każdym wstawieniu, ale wtedy czas wykonania skryptu znacznie się wydłuży.

  • Analiza pierwszych 100 000 artykułów zajęła mi 8 godzin.

Następnie znajduję artykuł użytkownika skointegrowany, który przeczytałem i znalazłem kilka życiowych hacków, aby przyspieszyć ten proces:

  • Korzystanie z wielowątkowości czasami przyspiesza pobieranie.
  • Możesz pobrać nie pełną wersję habr, ale jego wersję mobilną.
    Przykładowo, jeśli skointegrowany artykuł w wersji desktopowej waży 378 KB, to w wersji mobilnej już 126 KB.

Druga wersja. Wiele wątków, chwilowy ban od Habr

Kiedy przeszukiwałem Internet na temat wielowątkowości w pythonie, wybrałem najprostszą opcję z multiprocessing.dummy, zauważyłem, że problemy pojawiły się wraz z wielowątkowością.

SQLite3 nie chce pracować z więcej niż jednym wątkiem.
naprawił check_same_thread=False, ale ten błąd nie jest jedyny, przy próbie wstawienia do bazy czasami pojawiają się błędy, których nie mogłem rozwiązać.

Dlatego rezygnuję z błyskawicznego wstawiania artykułów bezpośrednio do bazy i pamiętając o rozwiązaniu skointegrowanym, decyduję się na użycie plików, bo nie ma problemów z wielowątkowym zapisem do pliku.

Habr zaczyna banować za używanie więcej niż trzech wątków.
Szczególnie gorliwe próby dotarcia do Habr mogą skończyć się blokadą ip na kilka godzin. Musisz więc użyć tylko 3 wątków, ale to już jest dobre, ponieważ czas iteracji ponad 100 artykułów skraca się z 26 do 12 sekund.

Warto zauważyć, że ta wersja jest raczej niestabilna, a pobieranie okresowo spada przy dużej liczbie artykułów.

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)

Trzecia wersja. Finał

Podczas debugowania drugiej wersji odkryłem, że nagle Habr ma interfejs API, do którego uzyskuje dostęp mobilna wersja witryny. Ładuje się szybciej niż wersja mobilna, ponieważ to tylko json, którego nawet nie trzeba analizować. W końcu zdecydowałem się ponownie napisać mój scenariusz.

A więc po odkryciu link API, możesz zacząć go analizować.

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)

Zawiera pola związane zarówno z samym artykułem, jak iz autorem, który go napisał.

API.png

Wszystkie Habr w jednej bazie danych

Nie zrzuciłem pełnego pliku json każdego artykułu, ale zapisałem tylko potrzebne pola:

  • id
  • jest_samouczek
  • czas_opublikowany
  • tytuł
  • zawartość
  • liczba_komentarzy
  • lang to język, w którym napisany jest artykuł. Jak dotąd ma tylko en i ru.
  • tags_string - wszystkie tagi z posta
  • liczba_czytań
  • autor
  • ocena — ocena artykułu.

Tym samym korzystając z API skróciłem czas wykonania skryptu do 8 sekund na 100 url.

Po pobraniu potrzebnych nam danych musimy je przetworzyć i wprowadzić do bazy danych. Z tym też nie miałem problemów:

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)

Statystyki

Cóż, tradycyjnie w końcu można wyodrębnić statystyki z danych:

  • Z oczekiwanych 490 406 pobrań pobrano tylko 228 512 artykułów. Okazuje się, że ponad połowa (261894) artykułów na temat Habré została ukryta lub usunięta.
  • Cała baza danych, składająca się z prawie pół miliona artykułów, waży 2.95 GB. W skompresowanej formie - 495 MB.
  • W sumie autorami Habré są 37804 osoby. Przypominam, że te statystyki dotyczą tylko postów na żywo.
  • Najbardziej produktywny autor na Habré - Alizar - 8774 artykułów.
  • Najwyżej oceniany artykuł — 1448 plusów
  • Najczęściej czytany artykuł — 1660841 wyświetleń
  • Najczęściej omawiany artykuł — 2444 komentarze

Cóż, w formie blatów15 najlepszych autorówWszystkie Habr w jednej bazie danych
Top 15 według rankinguWszystkie Habr w jednej bazie danych
Top 15 przeczytanychWszystkie Habr w jednej bazie danych
Omówiono 15 najlepszychWszystkie Habr w jednej bazie danych

Źródło: www.habr.com

Dodaj komentarz