Tất cả Habr trong một cơ sở dữ liệu

Chào buổi chiều. Đã 2 năm kể từ khi nó được viết. bài viết cuối cùng về phân tích cú pháp Habr, và một số điểm đã thay đổi.

Khi tôi muốn có một bản sao của Habr, tôi quyết định viết một trình phân tích cú pháp để lưu tất cả nội dung của các tác giả vào cơ sở dữ liệu. Nó đã xảy ra như thế nào và tôi đã gặp phải lỗi gì - bạn có thể đọc ở phần cắt.

TL; DR — liên kết cơ sở dữ liệu

Phiên bản đầu tiên của trình phân tích cú pháp. Một chủ đề, nhiều vấn đề

Để bắt đầu, tôi quyết định tạo một nguyên mẫu tập lệnh trong đó bài báo sẽ được phân tích cú pháp và đưa vào cơ sở dữ liệu ngay sau khi tải xuống. Không cần suy nghĩ kỹ, tôi đã sử dụng sqlite3, bởi vì. nó ít tốn công sức hơn: không cần phải có máy chủ cục bộ, tạo-tìm-xóa và những thứ tương tự.

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)

Mọi thứ đều cổ điển - chúng tôi sử dụng Beautiful Soup, các yêu cầu và nguyên mẫu nhanh đã sẵn sàng. Đó chỉ là…

  • Trang tải xuống là trong một chủ đề

  • Nếu bạn làm gián đoạn việc thực thi tập lệnh, thì toàn bộ cơ sở dữ liệu sẽ chẳng đi đến đâu. Rốt cuộc, cam kết chỉ được thực hiện sau khi phân tích cú pháp.
    Tất nhiên, bạn có thể cam kết thay đổi cơ sở dữ liệu sau mỗi lần chèn, nhưng sau đó thời gian thực thi tập lệnh sẽ tăng lên đáng kể.

  • Tôi mất 100 giờ để phân tích 000 bài báo đầu tiên.

Tiếp theo tôi tìm thấy bài viết của người dùng đồng liên kết, mà tôi đã đọc và tìm thấy một vài mẹo nhỏ để tăng tốc quá trình này:

  • Sử dụng đa luồng đôi khi tăng tốc độ tải xuống.
  • Bạn không thể nhận được phiên bản đầy đủ của habr, mà là phiên bản di động của nó.
    Ví dụ: nếu một bài viết đồng tích hợp trong phiên bản dành cho máy tính để bàn nặng 378 KB, thì ở phiên bản di động, nó đã là 126 KB.

Phiên bản thứ hai. Nhiều chủ đề, lệnh cấm tạm thời từ Habr

Khi tôi lùng sục trên Internet về chủ đề đa luồng trong python, tôi đã chọn tùy chọn đơn giản nhất với multiprocessing.dummy, tôi nhận thấy rằng các vấn đề xuất hiện cùng với đa luồng.

SQLite3 không muốn làm việc với nhiều luồng.
đã sửa check_same_thread=False, nhưng lỗi này không phải là duy nhất, khi cố gắng chèn vào cơ sở dữ liệu đôi khi xảy ra lỗi mà tôi không thể giải quyết được.

Do đó, tôi quyết định từ bỏ việc chèn ngay các bài báo trực tiếp vào cơ sở dữ liệu và ghi nhớ giải pháp đồng liên kết, tôi quyết định sử dụng các tệp, vì không có vấn đề gì với việc ghi đa luồng vào một tệp.

Habr bắt đầu cấm sử dụng hơn ba chủ đề.
Đặc biệt là những nỗ lực nhiệt tình để truy cập vào Habr có thể bị cấm ip trong vài giờ. Vì vậy, bạn chỉ phải sử dụng 3 luồng, nhưng điều này đã tốt rồi, vì thời gian lặp lại hơn 100 bài viết đã giảm từ 26 xuống 12 giây.

Điều đáng chú ý là phiên bản này khá không ổn định và việc tải xuống định kỳ bị giảm trên một số lượng lớn bài báo.

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)

Phiên bản thứ ba. Cuối cùng

Trong khi gỡ lỗi phiên bản thứ hai, tôi đột nhiên phát hiện ra rằng Habr có một API mà phiên bản di động của trang web truy cập. Nó tải nhanh hơn phiên bản dành cho thiết bị di động, vì nó chỉ là json, thậm chí không cần phải phân tích cú pháp. Cuối cùng, tôi quyết định viết lại kịch bản của mình một lần nữa.

Vì vậy, đã tìm thấy liên kết này API, bạn có thể bắt đầu phân tích cú pháp.

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)

Nó chứa các trường liên quan đến cả bản thân bài báo và tác giả đã viết nó.

API.png

Tất cả Habr trong một cơ sở dữ liệu

Tôi đã không đổ toàn bộ json của mỗi bài viết mà chỉ lưu những trường tôi cần:

  • id
  • là_hướng dẫn
  • time_published
  • tiêu đề
  • nội dung
  • comment_count
  • lang là ngôn ngữ mà bài báo được viết. Cho đến nay, nó chỉ có en và ru.
  • tags_string - tất cả các thẻ từ bài viết
  • đọc_đếm
  • tác giả
  • điểm - đánh giá bài viết.

Do đó, bằng cách sử dụng API, tôi đã giảm thời gian thực thi tập lệnh xuống còn 8 giây trên 100 url.

Sau khi chúng tôi đã tải xuống dữ liệu chúng tôi cần, chúng tôi cần xử lý dữ liệu đó và nhập dữ liệu đó vào cơ sở dữ liệu. Tôi cũng không gặp vấn đề gì với điều này:

trình phân tích cú pháp.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)

thống kê

Chà, theo truyền thống, cuối cùng, bạn có thể trích xuất một số thống kê từ dữ liệu:

  • Trong số 490 lượt tải xuống dự kiến, chỉ có 406 bài báo được tải xuống. Hóa ra hơn một nửa (228) bài báo trên Habré đã bị ẩn hoặc bị xóa.
  • Toàn bộ cơ sở dữ liệu, bao gồm gần nửa triệu bài viết, nặng 2.95 GB. Ở dạng nén - 495 MB.
  • Tổng cộng có 37804 người là tác giả của Habré. Tôi xin nhắc bạn rằng những số liệu thống kê này chỉ từ các bài đăng trực tiếp.
  • Tác giả làm việc hiệu quả nhất trên Habré - bí danh - 8774 bài viết.
  • Bài viết được xếp hạng cao nhất — 1448 điểm cộng
  • Bài viết được đọc nhiều nhất — 1660841 lượt xem
  • Bài viết được thảo luận nhiều nhất — 2444 bình luận

Vâng, ở dạng ngọn15 tác giả hàng đầuTất cả Habr trong một cơ sở dữ liệu
Top 15 theo xếp hạngTất cả Habr trong một cơ sở dữ liệu
Top 15 đã đọcTất cả Habr trong một cơ sở dữ liệu
Top 15 thảo luậnTất cả Habr trong một cơ sở dữ liệu

Nguồn: www.habr.com

Thêm một lời nhận xét