すべての Habr を XNUMX つのデータベースに

こんにちは。 書かれてから2年が経ちました。 最後の記事 Habr の解析に関して、いくつかの点が変更されました。

Habr のコピーが必要になったとき、著者のすべてのコンテンツをデータベースに保存するパーサーを作成することにしました。 それがどのように起こったのか、どのようなエラーが発生したか - カットの下で読むことができます。

TLDR- データベースリンク

パーサーの最初のバージョン。 XNUMX つのスレッドに多くの問題がある

まず、記事をダウンロードするとすぐに解析されてデータベースに配置されるスクリプトのプロトタイプを作成することにしました。 何も考えずに sqlite3 を使用したからです。 労働集約的ではありませんでした。ローカル サーバー、作成、見た目、削除などの作業が必要ありませんでした。

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)

すべてが古典的です - 私たちは美しいスープを使用しており、リクエストがあり、簡単なプロトタイプが準備ができています。 それはただ…

  • ページのダウンロードは XNUMX つのスレッドで行われます

  • スクリプトの実行を中断すると、データベース全体が失われます。 結局のところ、コミットはすべての解析が完了した後にのみ実行されます。
    もちろん、挿入のたびにデータベースに変更をコミットすることもできますが、その場合、スクリプトの実行時間は大幅に増加します。

  • 最初の 100 件の記事を解析するのに 000 時間かかりました。

次にユーザーの記事を見つけます 共統合されたを読んで、このプロセスをスピードアップするためのいくつかのライフハックを見つけました。

  • マルチスレッドを使用すると、ダウンロードが高速化される場合があります。
  • habr の完全版ではなく、モバイル版を入手できます。
    たとえば、デスクトップ バージョンで統合された記事の重さが 378 KB である場合、モバイル バージョンではすでに 126 KB になっています。

XNUMX 番目のバージョン。 多数のスレッド、Habr からの一時的な禁止

Python でのマルチスレッドのトピックについてインターネットを調べたとき、multiprocessing.dummy を使用した最も単純なオプションを選択しましたが、マルチスレッドに伴って問題が発生することに気付きました。

SQLite3 は複数のスレッドで動作することを望んでいません.
修理済み check_same_thread=False, しかし、このエラーだけではなく、データベースに挿入しようとすると、解決できないエラーが発生することがあります。

したがって、データベースへの記事の即時挿入を放棄し、マルチスレッドによるファイルへの書き込みには問題がないため、統合されたソリューションを思い出して、ファイルを使用することにしました。

Habr が XNUMX つ以上のスレッドの使用を禁止し始める.
特にハブルにアクセスしようとする熱心な試みは、数時間の IP 禁止につながる可能性があります。 したがって、使用するスレッドは 3 つだけですが、100 件の記事を反復する時間が 26 秒から 12 秒に短縮されるため、これで十分です。

このバージョンはかなり不安定で、多数の記事で定期的にダウンロードが落ちてしまうことに注意してください。

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)

XNUMX番目のバージョン。 最後の

XNUMX 番目のバージョンをデバッグしているときに、突然、Habr にモバイル バージョンのサイトがアクセスする API があることがわかりました。 解析する必要がない単なる JSON であるため、モバイル版よりも高速に読み込まれます。 結局、もう一度脚本を書き直すことにしました。

それで、見つけたので、 このリンク API の解析を開始できます。

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)

これには、記事自体と記事を書いた著者の両方に関連するフィールドが含まれます。

API.png

すべての Habr を XNUMX つのデータベースに

各記事の完全な JSON をダンプするのではなく、必要なフィールドのみを保存しました。

  • id
  • is_チュートリアル
  • time_published
  • タイトル
  • コンテンツ
  • comments_count
  • lang は記事が書かれている言語です。 今のところenとruしかありません。
  • tags_string - 投稿のすべてのタグ
  • 読書数
  • 著者
  • スコア — 記事の評価。

そこで、API を使用することで、スクリプトの実行時間を 8 URL あたり 100 秒に短縮しました。

必要なデータをダウンロードしたら、それを処理してデータベースに入力する必要があります。 これに関しても問題はありませんでした。

パーサー.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)

統計

さて、伝統的に、最終的にはデータからいくつかの統計を抽出できます。

  • 予想された 490 件のダウンロードのうち、ダウンロードされた記事は 406 件のみでした。 ハブレに関する記事の半分以上 (228) が非表示または削除されていたことが判明しました。
  • 約2.95万件の記事で構成されるデータベース全体の重さは495 GBです。 圧縮形式 - XNUMX MB。
  • 合計 37804 人がハブレの著者です。 これらの統計はライブ投稿のみから得られたものであることを思い出してください。
  • ハブレで最も生産的な著者 - アリザール - 8774 件の記事。
  • 最高評価の記事 — 1448 プラス
  • よく読まれている記事 — 1660841 ビュー
  • 最も話題になった記事 — 2444 コメント

さて、トップスの形で上位 15 人の著者すべての Habr を XNUMX つのデータベースに
評価別トップ 15すべての Habr を XNUMX つのデータベースに
読まれたトップ15すべての Habr を XNUMX つのデータベースに
トップ15について議論すべての Habr を XNUMX つのデータベースに

出所: habr.com

コメントを追加します