Toate Habr într-o singură bază de date

Bună ziua. Au trecut 2 ani de când am scris-o ultimul articol despre analiza Habr și unele lucruri s-au schimbat.

Când am vrut să am o copie a lui Habr, am decis să scriu un parser care să salveze tot conținutul autorilor într-o bază de date. Cum s-a întâmplat și ce erori am întâlnit - puteți citi sub tăietură.

TLDR- link la baza de date

Prima versiune a parserului. Un fir, multe probleme

Pentru început, am decis să fac un prototip de script în care, imediat după descărcare, articolul să fie analizat și plasat în baza de date. Fără să mă gândesc de două ori, am folosit sqlite3, pentru că... a fost mai puțin laborioasă: nu trebuie să aveți un server local, să creați, să căutați, să ștergeți și alte chestii de genul ăsta.

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)

Totul este conform clasicilor - folosim Beautiful Supp, cereri și prototipul rapid este gata. asta e doar...

  • Pagina este descărcată într-un singur fir

  • Dacă întrerupeți execuția scriptului, atunci întreaga bază de date nu va merge nicăieri. La urma urmei, commit-ul este executat numai după toată analiza.
    Desigur, puteți efectua modificări în baza de date după fiecare inserare, dar apoi timpul de execuție a scriptului va crește semnificativ.

  • Analizarea primelor 100 de articole mi-a luat 000 ore.

Apoi găsesc articolul utilizatorului cointegrat, pe care l-am citit și am găsit câteva trucuri de viață pentru a accelera acest proces:

  • Utilizarea multithreading-ului accelerează semnificativ descărcarea.
  • Puteți primi nu versiunea completă a Habr, ci versiunea sa mobilă.
    De exemplu, dacă un articol cointegrat în versiunea desktop cântărește 378 KB, atunci în versiunea mobilă este deja de 126 KB.

A doua versiune. Multe fire, interdicție temporară de la Habr

Când am căutat pe internet pe tema multithreading în python și am ales cea mai simplă opțiune cu multiprocessing.dummy, am observat că au apărut probleme odată cu multithreading.

SQLite3 nu vrea să lucreze cu mai mult de un fir de execuție.
Fix check_same_thread=False, dar această eroare nu este singura; atunci când încerc să o introduc în baza de date, uneori apar erori pe care nu le-am putut rezolva.

Prin urmare, decid să renunț la inserarea instantanee a articolelor direct în baza de date și, amintindu-mi soluția cointegrată, decid să folosesc fișiere, deoarece nu există probleme cu scrierea multi-threaded într-un fișier.

Habr începe să interzică pentru utilizarea a mai mult de trei fire.
Încercările deosebit de zeloase de a ajunge la Habr pot duce la interzicerea IP pentru câteva ore. Deci trebuie să folosiți doar 3 fire, dar acest lucru este deja bun, deoarece timpul de sortare a 100 de articole se reduce de la 26 la 12 secunde.

Este de remarcat faptul că această versiune este destul de instabilă, iar descărcarea eșuează periodic pentru un număr mare de articole.

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 treia versiune. Final

În timp ce depanam cea de-a doua versiune, am descoperit că Habr are brusc un API care este accesat de versiunea mobilă a site-ului. Se încarcă mai repede decât versiunea mobilă, deoarece este doar json, care nici măcar nu trebuie analizat. În cele din urmă, am decis să-mi rescriu din nou scenariul.

Deci, după ce am descoperit acest link API, puteți începe să îl analizați.

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)

Conține câmpuri legate atât de articolul în sine, cât și de autorul care l-a scris.

API.png

Toate Habr într-o singură bază de date

Nu am aruncat json complet al fiecărui articol, ci am salvat doar câmpurile de care aveam nevoie:

  • id
  • este_tutorial
  • timp_publicat
  • titlu
  • conţinut
  • numărul de comentarii
  • lang este limba în care este scris articolul. Până acum conține doar en și ru.
  • tags_string — toate etichetele din postare
  • reading_count
  • autor
  • scor — evaluarea articolului.

Astfel, folosind API-ul, am redus timpul de execuție a scriptului la 8 secunde la 100 de url.

După ce am descărcat datele de care avem nevoie, trebuie să le procesăm și să le introducem în baza de date. Nici cu asta nu au fost probleme:

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)

statistică

Ei bine, în mod tradițional, în sfârșit, puteți extrage câteva statistici din date:

  • Din cele 490 așteptate, doar 406 articole au fost descărcate. Se dovedește că mai mult de jumătate (228) dintre articolele despre Habré au fost ascunse sau șterse.
  • Întreaga bază de date, formată din aproape jumătate de milion de articole, cântărește 2.95 GB. În formă comprimată - 495 MB.
  • În total, există 37804 de autori pe Habré. Permiteți-mi să vă reamintesc că acestea sunt statistici doar din postări live.
  • Cel mai productiv autor despre Habré - alizar — 8774 articole.
  • Cel mai bine cotat articol — 1448 plusuri
  • Cel mai citit articol — 1660841 vizualizări
  • Cel mai mult s-a vorbit despre articol — 2444 de comentarii

Ei bine, sub formă de vârfuriTop 15 autoriToate Habr într-o singură bază de date
Top 15 după ratingToate Habr într-o singură bază de date
Top 15 cititToate Habr într-o singură bază de date
Top 15 discutateToate Habr într-o singură bază de date

Sursa: www.habr.com

Adauga un comentariu