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