Semua Habr dalam satu pangkalan data

Selamat petang. Sudah 2 tahun ia ditulis. artikel terakhir tentang menghuraikan Habr, dan beberapa perkara telah berubah.

Apabila saya ingin memiliki salinan Habr, saya memutuskan untuk menulis parser yang akan menyimpan semua kandungan pengarang ke pangkalan data. Bagaimana ia berlaku dan apa ralat yang saya temui - anda boleh membaca di bawah potongan.

TLDR- pautan pangkalan data

Versi pertama penghurai. Satu benang, banyak masalah

Sebagai permulaan, saya memutuskan untuk membuat prototaip skrip di mana artikel itu akan dihuraikan dan diletakkan dalam pangkalan data sebaik sahaja dimuat turun. Tanpa berfikir dua kali, saya menggunakan sqlite3, kerana. ia adalah kurang intensif buruh: tidak perlu mempunyai pelayan tempatan, dicipta-kelihatan-dipadam dan sebagainya.

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)

Semuanya klasik - kami menggunakan Sup Cantik, permintaan dan prototaip pantas sudah sedia. Itu sahaja…

  • Muat turun halaman berada dalam satu utas

  • Jika anda mengganggu pelaksanaan skrip, maka keseluruhan pangkalan data tidak akan ke mana-mana. Lagipun, komit dilakukan hanya selepas semua penghuraian.
    Sudah tentu, anda boleh melakukan perubahan pada pangkalan data selepas setiap sisipan, tetapi kemudian masa pelaksanaan skrip akan meningkat dengan ketara.

  • Menghuraikan 100 artikel pertama mengambil masa 000 jam.

Seterusnya saya mencari artikel pengguna sepadu, yang saya baca dan temui beberapa peretasan hayat untuk mempercepatkan proses ini:

  • Menggunakan multithreading mempercepatkan muat turun pada masa-masa tertentu.
  • Anda tidak boleh mendapatkan versi penuh habr, tetapi versi mudah alihnya.
    Sebagai contoh, jika artikel bersepadu dalam versi desktop mempunyai berat 378 KB, maka dalam versi mudah alih ia sudah 126 KB.

Versi kedua. Banyak benang, larangan sementara dari Habr

Apabila saya menjelajah Internet mengenai topik multithreading dalam python, saya memilih pilihan paling mudah dengan multiprocessing.dummy, saya perhatikan bahawa masalah muncul bersama dengan multithreading.

SQLite3 tidak mahu berfungsi dengan lebih daripada satu utas.
tetap check_same_thread=False, tetapi ralat ini bukan satu-satunya, apabila cuba memasukkan ke dalam pangkalan data, ralat kadang-kadang berlaku yang tidak dapat saya selesaikan.

Oleh itu, saya memutuskan untuk meninggalkan penyisipan segera artikel terus ke dalam pangkalan data dan, mengingati penyelesaian kointegrasi, saya memutuskan untuk menggunakan fail, kerana tidak ada masalah dengan penulisan berbilang benang ke fail.

Habr mula melarang kerana menggunakan lebih daripada tiga utas.
Percubaan yang bersungguh-sungguh untuk mencapai Habr boleh berakhir dengan larangan ip selama beberapa jam. Jadi anda perlu menggunakan hanya 3 utas, tetapi ini sudah bagus, kerana masa untuk mengulang lebih 100 artikel dikurangkan daripada 26 kepada 12 saat.

Perlu diingat bahawa versi ini agak tidak stabil, dan muat turun secara berkala jatuh pada sejumlah besar artikel.

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)

Versi ketiga. Akhir

Semasa menyahpepijat versi kedua, saya mendapati bahawa Habr, secara tiba-tiba, mempunyai API yang diakses oleh versi mudah alih tapak tersebut. Ia dimuatkan lebih cepat daripada versi mudah alih, kerana ia hanya json, yang tidak perlu dihuraikan. Akhirnya, saya memutuskan untuk menulis semula skrip saya sekali lagi.

Jadi, setelah menjumpai pautan ini API, anda boleh mula menghuraikannya.

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)

Ia mengandungi medan yang berkaitan dengan artikel itu sendiri dan pengarang yang menulisnya.

API.png

Semua Habr dalam satu pangkalan data

Saya tidak membuang json penuh setiap artikel, tetapi hanya menyimpan medan yang saya perlukan:

  • id
  • is_tutorial
  • time_published
  • tajuk
  • kandungan
  • bilangan_komen
  • lang ialah bahasa di mana artikel itu ditulis. Setakat ini, ia hanya mempunyai en dan ru.
  • tags_string - semua tag daripada siaran
  • kiraan_bacaan
  • pengarang
  • skor — rating artikel.

Oleh itu, menggunakan API, saya mengurangkan masa pelaksanaan skrip kepada 8 saat setiap 100 url.

Selepas kami memuat turun data yang kami perlukan, kami perlu memprosesnya dan memasukkannya ke dalam pangkalan data. Saya tidak mempunyai sebarang masalah dengan ini sama ada:

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

Nah, secara tradisinya, akhirnya, anda boleh mengekstrak beberapa statistik daripada data:

  • Daripada jangkaan 490 muat turun, hanya 406 artikel dimuat turun. Ternyata lebih separuh (228) artikel mengenai Habré telah disembunyikan atau dipadamkan.
  • Seluruh pangkalan data, yang terdiri daripada hampir setengah juta artikel, mempunyai berat 2.95 GB. Dalam bentuk termampat - 495 MB.
  • Secara keseluruhan, 37804 orang adalah pengarang Habré. Saya mengingatkan anda bahawa statistik ini hanya dari siaran langsung.
  • Pengarang paling produktif di Habré - alizar - 8774 artikel.
  • Artikel dengan penilaian tertinggi - 1448 tambah
  • Artikel yang paling banyak dibaca — 1660841 pandangan
  • Artikel Paling Banyak Dibincangkan — 2444 ulasan

Nah, dalam bentuk gasing15 pengarang teratasSemua Habr dalam satu pangkalan data
15 teratas mengikut penilaianSemua Habr dalam satu pangkalan data
Bacaan 15 teratasSemua Habr dalam satu pangkalan data
Top 15 DibincangkanSemua Habr dalam satu pangkalan data

Sumber: www.habr.com

Tambah komen