Buon pomeriggio. Sono passati 2 anni da quando è stato scritto. ultimo articolo sull'analisi di Habr e alcuni punti sono cambiati.
Quando volevo avere una copia di Habr, ho deciso di scrivere un parser che salvasse tutti i contenuti degli autori nel database. Come è successo e quali errori ho riscontrato: puoi leggere sotto il taglio.
La prima versione del parser. Un filo, molti problemi
Per cominciare, ho deciso di realizzare un prototipo di script, in cui l'articolo sarebbe stato analizzato immediatamente dopo il download e inserito nel database. Senza pensarci due volte, ho usato sqlite3, perché. era meno laborioso: non c'era bisogno di avere un server locale, creato-sembrava-cancellato e cose del genere.
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)
Tutto è classico: usiamo Beautiful Soup, richieste e un prototipo veloce è pronto. Questo è solo...
Il download della pagina è in un thread
Se interrompi l'esecuzione dello script, l'intero database non andrà da nessuna parte. Dopotutto, il commit viene eseguito solo dopo tutta l'analisi.
Naturalmente, puoi eseguire il commit delle modifiche al database dopo ogni inserimento, ma il tempo di esecuzione dello script aumenterà in modo significativo.
L'analisi dei primi 100 articoli mi ha richiesto 000 ore.
Successivamente trovo l'articolo dell'utente cointegrato, che ho letto e ho trovato alcuni trucchetti per accelerare questo processo:
L'uso del multithreading a volte velocizza il download.
Non puoi ottenere la versione completa dell'habr, ma la sua versione mobile.
Ad esempio, se un articolo cointegrato nella versione desktop pesa 378 KB, nella versione mobile è già 126 KB.
Seconda versione. Molti thread, divieto temporaneo da Habr
Quando ho setacciato Internet sull'argomento del multithreading in Python, ho scelto l'opzione più semplice con multiprocessing.dummy, ho notato che i problemi apparivano insieme al multithreading.
SQLite3 non vuole lavorare con più di un thread.
fisso check_same_thread=False, ma questo errore non è l'unico, quando si tenta di inserire nel database, a volte si verificano errori che non sono riuscito a risolvere.
Pertanto, decido di abbandonare l'inserimento istantaneo degli articoli direttamente nel database e, ricordando la soluzione cointegrata, decido di utilizzare i file, perché non ci sono problemi con la scrittura multithread su un file.
Habr inizia a vietare per l'utilizzo di più di tre thread.
Tentativi particolarmente zelanti di mettersi in contatto con Habr possono finire con un ip ban per un paio d'ore. Quindi devi usare solo 3 thread, ma questo va già bene, visto che il tempo per iterare oltre 100 articoli si riduce da 26 a 12 secondi.
Vale la pena notare che questa versione è piuttosto instabile e che i download cadono periodicamente su un gran numero di articoli.
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)
Terza versione. Finale
Durante il debug della seconda versione, ho scoperto che Habr, all'improvviso, ha un'API a cui accede la versione mobile del sito. Si carica più velocemente della versione mobile, poiché è solo json, che non ha nemmeno bisogno di essere analizzato. Alla fine, ho deciso di riscrivere di nuovo la mia sceneggiatura.
Quindi, avendo trovato questo link API, puoi iniziare ad analizzarlo.
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 campi relativi sia all'articolo stesso che all'autore che lo ha scritto.
API.png
Non ho scaricato il json completo di ogni articolo, ma ho salvato solo i campi di cui avevo bisogno:
id
è_tutorial
tempo_pubblicato
titolo
contenuto
conteggio_commenti
lang è la lingua in cui è scritto l'articolo. Finora, ha solo en e ru.
tags_string - tutti i tag del post
conteggio_lettura
autore
punteggio — valutazione dell'articolo.
Pertanto, utilizzando l'API, ho ridotto il tempo di esecuzione dello script a 8 secondi per 100 URL.
Dopo aver scaricato i dati di cui abbiamo bisogno, dobbiamo elaborarli e inserirli nel database. anche io non ho avuto problemi con questo:
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)
Statistiche
Bene, tradizionalmente, finalmente, puoi estrarre alcune statistiche dai dati:
Dei 490 download previsti, sono stati scaricati solo 406 articoli. Si scopre che più della metà (228) degli articoli su Habré sono stati nascosti o cancellati.
L'intero database, composto da quasi mezzo milione di articoli, pesa 2.95 GB. In forma compressa - 495 MB.
In totale, 37804 persone sono gli autori di Habré. Vi ricordo che queste statistiche provengono solo da post in diretta.
L'autore più produttivo su Habré - alizar - 8774 articoli.