Tutti Habr in una basa di dati

Bonghjornu. Sò 2 anni chì l'aghju scrittu ultimu articulu circa l'analisi di Habr, è alcune cose sò cambiate.

Quandu vulia avè una copia di Habr, decisu di scrive un parser chì salvassi tuttu u cuntenutu di l'autori in una basa di dati. Cumu hè accadutu è chì errori aghju scontru - pudete leghje sottu u cut.

TL;DR - ligame cù a basa di dati

Prima versione di u parser. Un filu, parechji prublemi

Per principià, aghju decisu di fà un prototipu di un script in quale, immediatamente dopu a scaricamentu, l'articulu serà analizatu è postu in a basa di dati. Senza pensà duie volte, aghju utilizatu sqlite3, perchè ... era menu di travagliu intensivu: ùn avete micca bisognu di avè un servitore lucale, creà, vede, sguassate è cose cusì.

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)

Tuttu hè sicondu i classici - usemu Beautiful Soup, dumande è u prototipu rapidu hè prestu. Hè solu...

  • A pagina hè scaricata in un filu

  • Se interrompe l'esekzione di u script, a basa di dati sana ùn andarà in nulla. Dopu tuttu, u commit hè eseguitu solu dopu à tuttu u parsing.
    Di sicuru, pudete committemu cambiamenti à a basa di dati dopu ogni inserimentu, ma allora u tempu d'esekzione di script aumenterà significativamente.

  • L'analisi di i primi 100 000 articuli m'hà pigliatu 8 ore.

Allora aghju trovu l'articulu di l'utilizatori cointegrata, chì aghju lettu è aghju trovu parechji pirate di vita per accelerà stu prucessu:

  • L'usu di multithreading accelera significativamente u scaricamentu.
  • Pudete riceve micca a versione completa di Habr, ma a so versione mobile.
    Per esempiu, se un articulu cointegratu in a versione desktop pesa 378 KB, allora in a versione mobile hè digià 126 KB.

Seconda versione. Parechje fili, pruibitu tempurale di Habr

Quandu aghju scupertu l'Internet nantu à u tema di multithreading in python è hà sceltu l'opzione più simplice cù multiprocessing.dummy, aghju nutatu chì i prublemi apparsu cù multithreading.

SQLite3 ùn vole micca travaglià cù più di un filu.
Fixed check_same_thread=False, ma questu errore ùn hè micca u solu; quandu pruvate d'inserisce in a basa di dati, qualchì volta sorgenu errori chì ùn pudia micca risolve.

Dunque, decisu di abbandunà l'inserzione immediata di l'articuli direttamente in a basa di dati è, ricurdendu a suluzione cointegrata, decide di utilizà i schedari, postu chì ùn ci sò micca prublemi cù scrittura multi-threaded à un schedariu.

Habr principia à pruibisce l'usu di più di trè fili.
I tentativi particularmente zelosi di ghjunghje à Habr pò esse risultatu in una prohibizione IP per un paru d'ore. Cusì avete aduprà solu 3 fili, ma questu hè digià bonu, postu chì u tempu di sorte per l'articuli 100 hè ridutta da 26 à 12 seconde.

Hè da nutà chì sta versione hè abbastanza inestabile, è a scaricazione periodicamente falla in un gran numaru d'articuli.

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)

A terza versione. Finale

Mentre debugging a seconda versione, aghju scupertu chì Habr di colpu hà una API chì hè accessu da a versione mobile di u situ. Carica più veloce di a versione mobile, postu chì hè solu json, chì ùn hà mancu bisognu di analizà. In fine, aghju decisu di riscrive u mo script di novu.

Allora, avè scupertu stu ligame API, pudete cumincià à analizà.

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)

Il contient des champs liés à l'article lui-même et à l'auteur qui l'a écrit.

API.png

Tutti Habr in una basa di dati

Ùn aghju micca scaricatu u json sanu di ogni articulu, ma hà salvatu solu i campi chì avia bisognu:

  • id
  • hè_tutorial
  • tempu_publicatu
  • titre
  • cuntenutu
  • comments_count
  • lang hè a lingua in quale l'articulu hè scrittu. Finu à avà cuntene solu en è ru.
  • tags_string - tutte e tag da u post
  • lettura_count
  • auturi
  • score - classificazione di l'articulu.

Cusì, utilizendu l'API, aghju riduciutu u tempu di esecuzione di script à 8 seconde per 100 url.

Dopu avè scaricatu i dati chì avemu bisognu, avemu bisognu di processà è entre in a basa di dati. Ùn ci era micca prublemi cù questu:

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)

Статистика

Ebbè, tradiziunale, infine, pudete estrae alcune statistiche da e dati:

  • Di i 490 previsti, solu 406 articuli sò stati scaricati. Risulta chì più di a mità (228) di l'articuli nantu à Habré sò stati oculati o sguassati.
  • L'intera basa di dati, custituita da quasi mezzo milione d'articuli, pesa 2.95 GB. In forma cumpressa - 495 MB.
  • In totale, ci sò 37804 XNUMX autori nantu à Habré. Lasciami ricurdà chì queste sò statistiche solu da i posti in diretta.
  • L'autore più produttivu nantu à Habré - alizar — 8774 articuli.
  • Articulu più votatu - 1448 plus
  • L'articulu più lettu — 1660841 viste
  • U più parlatu di l'articulu — 2444 cumenti

Ebbè, in forma di cimeTop 15 autoriTutti Habr in una basa di dati
Top 15 per ratingTutti Habr in una basa di dati
Top 15 leghjeTutti Habr in una basa di dati
Top 15 DiscussioniTutti Habr in una basa di dati

Source: www.habr.com

Add a comment