Alle Habr in einer Datenbank

Guten Tag. Es ist 2 Jahre her, seit es geschrieben wurde. letzter Artikel über das Parsen von Habr, und einige Punkte haben sich geändert.

Als ich eine Kopie von Habr haben wollte, beschloss ich, einen Parser zu schreiben, der den gesamten Inhalt der Autoren in der Datenbank speichern würde. Wie es dazu kam und auf welche Fehler ich gestoßen bin, können Sie unter dem Schnitt nachlesen.

TLDR- Datenbankverknüpfung

Die erste Version des Parsers. Ein Thread, viele Probleme

Zunächst beschloss ich, einen Skript-Prototyp zu erstellen, in dem der Artikel sofort nach dem Herunterladen analysiert und in der Datenbank abgelegt wird. Ohne lange nachzudenken, habe ich SQLite3 verwendet, weil. Es war weniger arbeitsintensiv: Es war kein lokaler Server erforderlich, erstellt, angezeigt, gelöscht und so weiter.

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)

Alles ist klassisch – wir verwenden Beautiful Soup, Anfragen und ein schneller Prototyp ist fertig. Das ist einfach…

  • Der Seitendownload erfolgt in einem Thread

  • Wenn Sie die Ausführung des Skripts unterbrechen, geht die gesamte Datenbank verloren. Schließlich wird der Commit erst nach dem gesamten Parsen durchgeführt.
    Natürlich können Sie nach jedem Einfügen Änderungen an der Datenbank festschreiben, aber dann erhöht sich die Ausführungszeit des Skripts erheblich.

  • Das Parsen der ersten 100 Artikel hat mich 000 Stunden gekostet.

Als nächstes finde ich den Artikel des Benutzers kointegriert, das ich gelesen und ein paar Life-Hacks gefunden habe, um diesen Prozess zu beschleunigen:

  • Die Verwendung von Multithreading beschleunigt das Herunterladen zeitweise.
  • Sie können nicht die Vollversion des Habr erhalten, sondern die mobile Version.
    Wenn beispielsweise ein mitintegrierter Artikel in der Desktop-Version 378 KB wiegt, sind es in der mobilen Version bereits 126 KB.

Zweite Version. Viele Threads, vorübergehendes Verbot von Habr

Als ich das Internet zum Thema Multithreading in Python durchforstete und mit multiprocessing.dummy die einfachste Variante wählte, fiel mir auf, dass beim Multithreading auch Probleme auftraten.

SQLite3 möchte nicht mit mehr als einem Thread arbeiten.
Fest check_same_thread=False, aber dieser Fehler ist nicht der einzige, beim Versuch, in die Datenbank einzufügen, treten manchmal Fehler auf, die ich nicht beheben konnte.

Daher entscheide ich mich, auf das sofortige Einfügen von Artikeln direkt in die Datenbank zu verzichten und mich an die kointegrierte Lösung zu erinnern. Ich entscheide mich für die Verwendung von Dateien, da es beim Multithread-Schreiben in eine Datei keine Probleme gibt.

Habr beginnt mit dem Verbot für die Nutzung von mehr als drei Threads.
Besonders eifrige Versuche, zu Habr durchzudringen, können mit einer IP-Sperre für ein paar Stunden enden. Sie müssen also nur 3 Threads verwenden, aber das ist schon gut, da die Zeit zum Durchlaufen von über 100 Artikeln von 26 auf 12 Sekunden reduziert wird.

Es ist erwähnenswert, dass diese Version ziemlich instabil ist und die Downloads bei einer großen Anzahl von Artikeln regelmäßig abbrechen.

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)

Dritte Version. Finale

Beim Debuggen der zweiten Version habe ich festgestellt, dass Habr plötzlich über eine API verfügt, auf die die mobile Version der Website zugreift. Es wird schneller geladen als die mobile Version, da es sich nur um JSON handelt, das nicht einmal geparst werden muss. Am Ende beschloss ich, mein Drehbuch noch einmal umzuschreiben.

Also, gefunden Link API, Sie können mit dem Parsen beginnen.

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)

Es enthält Felder, die sich sowohl auf den Artikel selbst als auch auf den Autor beziehen, der ihn geschrieben hat.

API.png

Alle Habr in einer Datenbank

Ich habe nicht den vollständigen JSON-Code jedes Artikels gespeichert, sondern nur die Felder gespeichert, die ich brauchte:

  • id
  • is_tutorial
  • time_published
  • Titel
  • Inhalt
  • comments_count
  • lang ist die Sprache, in der der Artikel verfasst ist. Bisher gibt es nur en und ru.
  • tags_string – alle Tags aus dem Beitrag
  • reading_count
  • Autor
  • Punktzahl – Artikelbewertung.

Daher habe ich mithilfe der API die Skriptausführungszeit auf 8 Sekunden pro 100 URLs reduziert.

Nachdem wir die benötigten Daten heruntergeladen haben, müssen wir sie verarbeiten und in die Datenbank eingeben. Auch hiermit hatte ich keine Probleme:

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)

Statistik

Nun, traditionell können Sie schließlich einige Statistiken aus den Daten extrahieren:

  • Von den erwarteten 490 Downloads wurden nur 406 Artikel heruntergeladen. Es stellt sich heraus, dass mehr als die Hälfte (228) der Artikel über Habré ausgeblendet oder gelöscht wurden.
  • Die gesamte Datenbank, bestehend aus fast einer halben Million Artikeln, wiegt 2.95 GB. In komprimierter Form - 495 MB.
  • Insgesamt sind 37804 Personen die Autoren von Habré. Ich erinnere Sie daran, dass diese Statistiken nur aus Live-Beiträgen stammen.
  • Der produktivste Autor auf Habré - Alizar - 8774 Artikel.
  • Am besten bewerteter Artikel — 1448 Pluspunkte
  • Meistgelesener Artikel — 1660841 Aufrufe
  • Am meisten diskutierter Artikel — 2444 Kommentare

Nun, in Form von TopsTop 15 AutorenAlle Habr in einer Datenbank
Top 15 nach BewertungAlle Habr in einer Datenbank
Top 15 gelesenAlle Habr in einer Datenbank
Top 15 diskutiertAlle Habr in einer Datenbank

Source: habr.com

Kommentar hinzufügen