Todos os Habr em um banco de dados

Boa tarde. Já se passaram 2 anos desde que foi escrito. último artigo sobre a análise de Habr, e alguns pontos mudaram.

Quando quis ter uma cópia do Habr, decidi escrever um parser que salvasse todo o conteúdo dos autores no banco de dados. Como aconteceu e quais erros encontrei - você pode ler abaixo do corte.

TLDR- link do banco de dados

A primeira versão do analisador. Um fio, muitos problemas

Para começar, decidi fazer um protótipo de script no qual o artigo seria analisado e colocado no banco de dados imediatamente após o download. Sem pensar duas vezes, usei sqlite3, porque. era menos trabalhoso: não era necessário ter um servidor local, criado-parecia-excluído e coisas assim.

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)

Tudo é clássico - usamos Beautiful Soup, pedidos e um protótipo rápido está pronto. Isso é apenas…

  • O download da página está em um tópico

  • Se você interromper a execução do script, todo o banco de dados não irá a lugar nenhum. Afinal, o commit é realizado somente após toda a análise.
    Obviamente, você pode confirmar as alterações no banco de dados após cada inserção, mas o tempo de execução do script aumentará significativamente.

  • Analisar os primeiros 100 artigos levou 000 horas.

Em seguida, encontro o artigo do usuário cointegrado, que li e encontrei alguns truques para acelerar esse processo:

  • O uso de multithreading acelera o download às vezes.
  • Você pode obter não a versão completa do habr, mas sua versão móvel.
    Por exemplo, se um artigo cointegrado na versão desktop pesa 378 KB, na versão móvel já tem 126 KB.

Segunda versão. Muitos tópicos, banimento temporário do Habr

Quando vasculhei a Internet sobre o tema multithreading em python, escolhi a opção mais simples com multiprocessing.dummy, percebi que surgiram problemas com multithreading.

SQLite3 não quer trabalhar com mais de um thread.
fixo check_same_thread=False, mas esse erro não é o único, ao tentar inserir no banco de dados as vezes ocorrem erros que não consegui resolver.

Portanto, decido abandonar a inserção instantânea de artigos diretamente no banco de dados e, lembrando da solução cointegrada, decido usar arquivos, pois não há problemas com a gravação multithread em um arquivo.

Habr começa a banir por usar mais de três tópicos.
Tentativas especialmente zelosas de entrar em contato com o Habr podem resultar em um banimento de IP por algumas horas. Então você tem que usar apenas 3 threads, mas isso já é bom, pois o tempo para iterar mais de 100 artigos é reduzido de 26 para 12 segundos.

É importante notar que esta versão é bastante instável e os downloads caem periodicamente em um grande número de artigos.

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)

Terceira versão. Final

Ao depurar a segunda versão, descobri que o Habr, de repente, tem uma API que a versão mobile do site acessa. Ele carrega mais rápido que a versão mobile, já que é apenas json, que nem precisa ser analisado. No final, decidi reescrever meu roteiro novamente.

Então, tendo encontrado este link API, você pode começar a analisá-la.

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ém campos relacionados tanto ao artigo em si quanto ao autor que o escreveu.

API.png

Todos os Habr em um banco de dados

Não despejei o json completo de cada artigo, mas salvei apenas os campos necessários:

  • id
  • is_tutorial
  • hora_publicada
  • título
  • conteúdo
  • comentários_contagem
  • lang é o idioma no qual o artigo foi escrito. Até agora, tem apenas en e ru.
  • tags_string - todas as tags do post
  • leitura_contagem
  • autor
  • pontuação — classificação do artigo.

Assim, usando a API, reduzi o tempo de execução do script para 8 segundos por 100 url.

Depois de baixar os dados de que precisamos, precisamos processá-los e inseri-los no banco de dados. Também não tive problemas com isso:

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

Estatísticas

Bem, tradicionalmente, finalmente, você pode extrair algumas estatísticas dos dados:

  • Dos 490 downloads esperados, apenas 406 artigos foram baixados. Acontece que mais da metade (228) dos artigos sobre Habré foram ocultados ou deletados.
  • Todo o banco de dados, composto por quase meio milhão de artigos, pesa 2.95 GB. Em formato compactado - 495 MB.
  • No total, 37804 pessoas são os autores de Habré. Relembro que estas estatísticas são apenas de postagens ao vivo.
  • O autor mais produtivo de Habré - alizar - 8774 artigos.
  • Artigo melhor avaliado - 1448 pontos positivos
  • artigo mais lido — 1660841 visualizações
  • Artigo mais discutido — 2444 comentários

Bem, na forma de tops15 principais autoresTodos os Habr em um banco de dados
Os 15 melhores por classificaçãoTodos os Habr em um banco de dados
15 melhores lidosTodos os Habr em um banco de dados
15 principais discutidosTodos os Habr em um banco de dados

Fonte: habr.com

Adicionar um comentário