Tots els Habr en una base de dades

Bona tarda. Fa 2 anys que el vaig escriure últim article sobre l'anàlisi de Habr, i algunes coses han canviat.

Quan volia tenir una còpia de Habr, vaig decidir escriure un analitzador que desa tot el contingut dels autors en una base de dades. Com va passar i quins errors vaig trobar: podeu llegir-lo sota el tall.

TL;DR - enllaç a la base de dades

Primera versió de l'analitzador. Un fil, molts problemes

Per començar, vaig decidir fer un prototip d'script en el qual, immediatament després de descarregar-lo, s'analitzaria l'article i es col·locaria a la base de dades. Sense pensar-m'ho dues vegades, vaig fer servir sqlite3, perquè... va ser menys intensiu de mà d'obra: no cal tenir un servidor local, crear, mirar, suprimir i coses així.

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)

Tot és segons els clàssics: fem servir Beautiful Soup, les peticions i el prototip ràpid està llest. Això és només...

  • La pàgina es descarrega en un sol fil

  • Si interrompeu l'execució de l'script, tota la base de dades no anirà enlloc. Després de tot, la confirmació només s'executa després de tota l'anàlisi.
    Per descomptat, podeu fer canvis a la base de dades després de cada inserció, però aleshores el temps d'execució de l'script augmentarà significativament.

  • L'anàlisi dels primers 100 articles em va portar 000 hores.

Aleshores trobo l'article de l'usuari cointegrats, que vaig llegir i vaig trobar diversos trucs de vida per accelerar aquest procés:

  • L'ús de multithreading accelera significativament la descàrrega.
  • Podeu rebre no la versió completa de Habr, sinó la seva versió mòbil.
    Per exemple, si un article cointegrat a la versió d'escriptori pesa 378 KB, a la versió mòbil ja és de 126 KB.

Segona versió. Molts fils, prohibició temporal d'Habr

Quan vaig buscar a Internet sobre el tema del multithreading a Python i vaig triar l'opció més senzilla amb multiprocessing.dummy, em vaig adonar que els problemes apareixien juntament amb el multithreading.

SQLite3 no vol treballar amb més d'un fil.
Arreglat check_same_thread=False, però aquest error no és l'únic; quan intento inserir a la base de dades, de vegades sorgeixen errors que no he pogut resoldre.

Per tant, decideixo abandonar la inserció instantània d'articles directament a la base de dades i, recordant la solució cointegrada, decideixo utilitzar fitxers, ja que no hi ha problemes amb l'escriptura multifils en un fitxer.

Habr comença a prohibir per utilitzar més de tres fils.
Els intents especialment zelosos d'arribar a Habr poden provocar una prohibició de la IP durant un parell d'hores. Per tant, només cal utilitzar 3 fils, però això ja és bo, ja que el temps per ordenar 100 articles es redueix de 26 a 12 segons.

Val la pena assenyalar que aquesta versió és força inestable i la descàrrega falla periòdicament en un gran nombre d'articles.

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)

Tercera versió. Final

Mentre depurava la segona versió, vaig descobrir que Habr de sobte té una API a la qual s'accedeix mitjançant la versió mòbil del lloc. Es carrega més ràpid que la versió mòbil, ja que només és json, que ni tan sols cal analitzar. Al final, vaig decidir reescriure el meu guió de nou.

Així, havent descobert aquest enllaç API, podeu començar a analitzar-lo.

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)

Conté camps relacionats tant amb el propi article com amb l'autor que l'ha escrit.

API.png

Tots els Habr en una base de dades

No vaig bolcar el json complet de cada article, però només vaig desar els camps que necessitava:

  • id
  • és_tutorial
  • temps_publicat
  • title
  • contingut
  • comentaris_count
  • lang és l'idioma en què està escrit l'article. Fins ara només conté en i ru.
  • tags_string — totes les etiquetes de la publicació
  • lectura_compte
  • autor
  • puntuació — puntuació de l'article.

Així, utilitzant l'API, vaig reduir el temps d'execució de l'script a 8 segons per 100 URL.

Després d'haver baixat les dades que necessitem, les hem de processar i introduir-les a la base de dades. Tampoc hi va haver problemes amb això:

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)

estadística

Bé, tradicionalment, finalment, podeu extreure algunes estadístiques de les dades:

  • Dels 490 previstos, només es van descarregar 406 articles. Resulta que més de la meitat (228) dels articles sobre Habré estaven amagats o esborrats.
  • Tota la base de dades, formada per gairebé mig milió d'articles, pesa 2.95 GB. En forma comprimida - 495 MB.
  • En total, hi ha 37804 autors a Habré. Permeteu-me recordar-vos que aquestes només són estadístiques de publicacions en directe.
  • L'autor més productiu sobre Habré - alitzar — 8774 articles.
  • Article millor valorat — 1448 més
  • Article més llegit — 1660841 visualitzacions
  • L'article més parlat — 2444 comentaris

Bé, en forma de cimsEls 15 millors autorsTots els Habr en una base de dades
Top 15 per valoracióTots els Habr en una base de dades
Top 15 de lecturaTots els Habr en una base de dades
Els 15 millors comentatsTots els Habr en una base de dades

Font: www.habr.com

Afegeix comentari