Todo Habr en una base de datos

Buenas tardes. Han pasado 2 años desde que se escribió. último artículo sobre el análisis de Habr, y algunos puntos han cambiado.

Cuando quise tener una copia de Habr, decidí escribir un analizador que guardara todo el contenido de los autores en la base de datos. Cómo sucedió y qué errores encontré: puede leer debajo del corte.

TL; DR - enlace de base de datos

La primera versión del analizador. Un hilo, muchos problemas

Para empezar, decidí hacer un prototipo de script en el que el artículo sería analizado y colocado en la base de datos inmediatamente después de la descarga. Sin pensarlo dos veces, usé sqlite3, porque. requería menos mano de obra: no era necesario tener un servidor local, creado-parecido-eliminado y cosas por el estilo.

un_hilo.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)

Todo es clásico: usamos Beautiful Soup, solicitudes y un prototipo rápido está listo. Eso es solo...

  • La descarga de la página está en un hilo

  • Si interrumpe la ejecución del script, toda la base de datos no irá a ninguna parte. Después de todo, la confirmación se realiza solo después de todo el análisis.
    Por supuesto, puede realizar cambios en la base de datos después de cada inserción, pero luego el tiempo de ejecución del script aumentará significativamente.

  • Analizar los primeros 100 artículos me tomó 000 horas.

A continuación encuentro el artículo del usuario. cointegrado, que leí y encontré algunos trucos para acelerar este proceso:

  • El uso de subprocesos múltiples acelera la descarga a veces.
  • No puede obtener la versión completa del habr, sino su versión móvil.
    Por ejemplo, si un artículo cointegrado en la versión de escritorio pesa 378 KB, en la versión móvil ya pesa 126 KB.

Segunda versión. Muchos hilos, prohibición temporal de Habr

Cuando busqué en Internet el tema de los subprocesos múltiples en python, elegí la opción más simple con multiprocessing.dummy, noté que aparecían problemas junto con los subprocesos múltiples.

SQLite3 no quiere trabajar con más de un hilo.
fijado check_same_thread=False, pero este error no es el único, al intentar insertar en la base de datos a veces se dan errores que no pude solucionar.

Por lo tanto, decido abandonar la inserción instantánea de artículos directamente en la base de datos y, recordando la solución cointegrada, decido usar archivos, porque no hay problemas con la escritura de subprocesos múltiples en un archivo.

Habr comienza a banear por usar más de tres hilos.
Los intentos especialmente entusiastas de comunicarse con Habr pueden terminar con una prohibición de ip durante un par de horas. Por lo tanto, debe usar solo 3 hilos, pero esto ya es bueno, ya que el tiempo para iterar más de 100 artículos se reduce de 26 a 12 segundos.

Vale la pena señalar que esta versión es bastante inestable y las descargas caen periódicamente en una gran cantidad de artículos.

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ón. Final

Mientras depuraba la segunda versión, descubrí que Habr, de repente, tiene una API a la que accede la versión móvil del sitio. Se carga más rápido que la versión móvil, ya que es solo json, que ni siquiera necesita analizarse. Al final, decidí volver a escribir mi guión.

Entonces, habiendo encontrado este enlace API, puede comenzar a analizarlo.

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 campos relacionados tanto con el artículo en sí como con el autor que lo escribió.

API.png

Todo Habr en una base de datos

No volqué el json completo de cada artículo, sino que guardé solo los campos que necesitaba:

  • id
  • es_tutorial
  • tiempo_publicado
  • título
  • contenido
  • conteo_comentarios
  • lang es el idioma en el que está escrito el artículo. Hasta ahora, solo tiene en y ru.
  • tags_string - todas las etiquetas de la publicación
  • recuento_de_lecturas
  • autor
  • puntuación — calificación del artículo.

Por lo tanto, usando la API, reduje el tiempo de ejecución del script a 8 segundos por 100 url.

Después de haber descargado los datos que necesitamos, debemos procesarlos e ingresarlos en la base de datos. Tampoco tuve ningún problema con esto:

analizador.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ísticas

Bueno, tradicionalmente, finalmente, puedes extraer algunas estadísticas de los datos:

  • De las 490 descargas esperadas, solo se descargaron 406 artículos. Resulta que más de la mitad (228) de los artículos sobre Habré fueron ocultados o borrados.
  • La base de datos completa, compuesta por casi medio millón de artículos, pesa 2.95 GB. En forma comprimida - 495 MB.
  • En total, 37804 personas son los autores de Habré. Les recuerdo que estas estadísticas son solo de publicaciones en vivo.
  • El autor más productivo sobre Habré - alizar - 8774 artículos.
  • Artículo mejor valorado — 1448 ventajas
  • Artículo más leído — 1660841 visitas
  • Artículo más discutido — 2444 comentarios

Bueno, en forma de tops.15 mejores autoresTodo Habr en una base de datos
Los 15 mejores por clasificaciónTodo Habr en una base de datos
Las 15 mejores lecturasTodo Habr en una base de datos
Los 15 principales discutidosTodo Habr en una base de datos

Fuente: habr.com

Añadir un comentario