Tutto Habr in un unico database

Buon pomeriggio. Sono passati 2 anni da quando è stato scritto. ultimo articolo sull'analisi di Habr e alcuni punti sono cambiati.

Quando volevo avere una copia di Habr, ho deciso di scrivere un parser che salvasse tutti i contenuti degli autori nel database. Come è successo e quali errori ho riscontrato: puoi leggere sotto il taglio.

TL; DR — collegamento alla banca dati

La prima versione del parser. Un filo, molti problemi

Per cominciare, ho deciso di realizzare un prototipo di script, in cui l'articolo sarebbe stato analizzato immediatamente dopo il download e inserito nel database. Senza pensarci due volte, ho usato sqlite3, perché. era meno laborioso: non c'era bisogno di avere un server locale, creato-sembrava-cancellato e cose del genere.

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)

Tutto è classico: usiamo Beautiful Soup, richieste e un prototipo veloce è pronto. Questo è solo...

  • Il download della pagina è in un thread

  • Se interrompi l'esecuzione dello script, l'intero database non andrà da nessuna parte. Dopotutto, il commit viene eseguito solo dopo tutta l'analisi.
    Naturalmente, puoi eseguire il commit delle modifiche al database dopo ogni inserimento, ma il tempo di esecuzione dello script aumenterà in modo significativo.

  • L'analisi dei primi 100 articoli mi ha richiesto 000 ore.

Successivamente trovo l'articolo dell'utente cointegrato, che ho letto e ho trovato alcuni trucchetti per accelerare questo processo:

  • L'uso del multithreading a volte velocizza il download.
  • Non puoi ottenere la versione completa dell'habr, ma la sua versione mobile.
    Ad esempio, se un articolo cointegrato nella versione desktop pesa 378 KB, nella versione mobile è già 126 KB.

Seconda versione. Molti thread, divieto temporaneo da Habr

Quando ho setacciato Internet sull'argomento del multithreading in Python, ho scelto l'opzione più semplice con multiprocessing.dummy, ho notato che i problemi apparivano insieme al multithreading.

SQLite3 non vuole lavorare con più di un thread.
fisso check_same_thread=False, ma questo errore non è l'unico, quando si tenta di inserire nel database, a volte si verificano errori che non sono riuscito a risolvere.

Pertanto, decido di abbandonare l'inserimento istantaneo degli articoli direttamente nel database e, ricordando la soluzione cointegrata, decido di utilizzare i file, perché non ci sono problemi con la scrittura multithread su un file.

Habr inizia a vietare per l'utilizzo di più di tre thread.
Tentativi particolarmente zelanti di mettersi in contatto con Habr possono finire con un ip ban per un paio d'ore. Quindi devi usare solo 3 thread, ma questo va già bene, visto che il tempo per iterare oltre 100 articoli si riduce da 26 a 12 secondi.

Vale la pena notare che questa versione è piuttosto instabile e che i download cadono periodicamente su un gran numero di articoli.

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)

Terza versione. Finale

Durante il debug della seconda versione, ho scoperto che Habr, all'improvviso, ha un'API a cui accede la versione mobile del sito. Si carica più velocemente della versione mobile, poiché è solo json, che non ha nemmeno bisogno di essere analizzato. Alla fine, ho deciso di riscrivere di nuovo la mia sceneggiatura.

Quindi, avendo trovato questo link API, puoi iniziare ad analizzarlo.

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)

Contiene campi relativi sia all'articolo stesso che all'autore che lo ha scritto.

API.png

Tutto Habr in un unico database

Non ho scaricato il json completo di ogni articolo, ma ho salvato solo i campi di cui avevo bisogno:

  • id
  • è_tutorial
  • tempo_pubblicato
  • titolo
  • contenuto
  • conteggio_commenti
  • lang è la lingua in cui è scritto l'articolo. Finora, ha solo en e ru.
  • tags_string - tutti i tag del post
  • conteggio_lettura
  • autore
  • punteggio — valutazione dell'articolo.

Pertanto, utilizzando l'API, ho ridotto il tempo di esecuzione dello script a 8 secondi per 100 URL.

Dopo aver scaricato i dati di cui abbiamo bisogno, dobbiamo elaborarli e inserirli nel database. anche io non ho avuto problemi con questo:

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)

Statistiche

Bene, tradizionalmente, finalmente, puoi estrarre alcune statistiche dai dati:

  • Dei 490 download previsti, sono stati scaricati solo 406 articoli. Si scopre che più della metà (228) degli articoli su Habré sono stati nascosti o cancellati.
  • L'intero database, composto da quasi mezzo milione di articoli, pesa 2.95 GB. In forma compressa - 495 MB.
  • In totale, 37804 persone sono gli autori di Habré. Vi ricordo che queste statistiche provengono solo da post in diretta.
  • L'autore più produttivo su Habré - alizar - 8774 articoli.
  • Articolo più votato — 1448 plus
  • Articolo più letto — 1660841 visualizzazioni
  • Articolo più discusso — 2444 commenti

Bene, sotto forma di cimeI 15 migliori autoriTutto Habr in un unico database
Top 15 per valutazioneTutto Habr in un unico database
Le prime 15 lettureTutto Habr in un unico database
Top 15 discussiTutto Habr in un unico database

Fonte: habr.com

Aggiungi un commento