همه هابر در یک پایگاه داده

عصر بخیر. 2 سال از نگارشش می گذرد. آخرین مقاله در مورد تجزیه هابر، و برخی از نکات تغییر کرده است.

وقتی می‌خواستم نسخه‌ای از Habr داشته باشم، تصمیم گرفتم تجزیه‌کننده‌ای بنویسم که تمام محتوای نویسندگان را در پایگاه داده ذخیره کند. چگونه اتفاق افتاد و با چه خطاهایی روبرو شدم - می توانید در زیر برش بخوانید.

TLDR- لینک پایگاه داده

اولین نسخه تجزیه کننده. یک رشته، مشکلات زیاد

برای شروع، تصمیم گرفتم یک نمونه اولیه از اسکریپت بسازم که در آن مقاله تجزیه شود و بلافاصله پس از دانلود در پایگاه داده قرار گیرد. بدون اینکه دوبار فکر کنم، از 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)

همه چیز کلاسیک است - ما از سوپ زیبا استفاده می کنیم، درخواست ها و نمونه اولیه سریع آماده است. فقط همین…

  • دانلود صفحه در یک موضوع است

  • اگر اجرای اسکریپت را قطع کنید، کل پایگاه داده به جایی نمی رسد. پس از همه، commit تنها پس از تمام تجزیه انجام می شود.
    البته می توانید پس از هر بار درج تغییراتی را در پایگاه داده اعمال کنید، اما پس از آن زمان اجرای اسکریپت به میزان قابل توجهی افزایش می یابد.

  • تجزیه 100 مقاله اول من 000 ساعت طول کشید.

بعد مقاله کاربر را پیدا می کنم یکپارچه شده است، که من آن را خواندم و چند هک زندگی برای سرعت بخشیدن به این روند پیدا کردم:

  • استفاده از چند رشته ای سرعت دانلود را گاهی افزایش می دهد.
  • شما می توانید نه نسخه کامل habr، بلکه نسخه موبایل آن را دریافت کنید.
    به عنوان مثال، اگر یک مقاله ادغام شده در نسخه دسکتاپ 378 کیلوبایت وزن داشته باشد، در نسخه موبایل از قبل 126 کیلوبایت است.

نسخه دوم. بسیاری از موضوعات، ممنوعیت موقت از Habr

وقتی اینترنت را در مورد موضوع Multithreading در پایتون جست و جو کردم، ساده ترین گزینه را با multiprocessing.dummy انتخاب کردم، متوجه شدم که مشکلات همراه با multithreading ظاهر می شوند.

SQLite3 نمی خواهد با بیش از یک رشته کار کند.
درست شد check_same_thread=False، اما این خطا تنها نیست، هنگام تلاش برای درج در پایگاه داده، گاهی اوقات خطاهایی رخ می دهد که من نتوانستم آنها را حل کنم.

بنابراین، تصمیم می‌گیرم از درج فوری مقالات به طور مستقیم در پایگاه داده صرف نظر کنم و با یادآوری راه‌حل یکپارچه، تصمیم می‌گیرم از فایل‌ها استفاده کنم، زیرا هیچ مشکلی در نوشتن چند رشته‌ای روی یک فایل وجود ندارد.

هابر برای استفاده از بیش از سه رشته شروع به ممنوع کردن می کند.
به‌ویژه تلاش‌های غیرتمندانه برای عبور از Habr می‌تواند با ممنوعیت 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)

نسخه سوم. نهایی

هنگام اشکال زدایی نسخه دوم، متوجه شدم که 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

همه هابر در یک پایگاه داده

من json کامل هر مقاله را حذف نکردم، بلکه فقط فیلدهایی را که نیاز داشتم ذخیره کردم:

  • id
  • is_tutorial
  • زمان_منتشر شده
  • عنوان
  • محتوا
  • comments_count
  • lang زبانی است که مقاله به آن نوشته شده است. تا کنون فقط en و ru دارد.
  • tags_string - همه برچسب‌ها از پست
  • reading_count
  • نویسنده
  • امتیاز - رتبه بندی مقاله.

بنابراین، با استفاده از API، زمان اجرای اسکریپت را به 8 ثانیه در هر 100 url کاهش دادم.

پس از اینکه داده های مورد نیاز خود را دانلود کردیم، باید آن ها را پردازش کرده و در پایگاه داده وارد کنیم. من هم مشکلی با این موضوع نداشتم:

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)

آمار

خوب، به طور سنتی، در نهایت، می توانید برخی از آمارها را از داده ها استخراج کنید:

  • از 490 دانلود مورد انتظار، تنها 406 مقاله دانلود شد. به نظر می رسد که بیش از نیمی (228) از مقالات در Habré پنهان یا حذف شده است.
  • کل پایگاه داده، متشکل از تقریباً نیم میلیون مقاله، 2.95 گیگابایت وزن دارد. به صورت فشرده - 495 مگابایت.
  • در مجموع، 37804 نفر نویسنده هابره هستند. یادآوری می کنم که این آمار فقط از پست های زنده است.
  • سازنده ترین نویسنده در هابره - آلیزار - 8774 مقاله.
  • مقاله با رتبه برتر - 1448 پلاس
  • پرخواننده ترین مقاله — 1660841 بازدید
  • بحث شده ترین مقاله — 2444 نظر

خوب، به شکل تاپ15 نویسنده برترهمه هابر در یک پایگاه داده
15 برتر از نظر رتبه بندیهمه هابر در یک پایگاه داده
15 برتر خوانده شدههمه هابر در یک پایگاه داده
15 مورد بحثهمه هابر در یک پایگاه داده

منبع: www.habr.com

اضافه کردن نظر