Guten Tag. Es ist 2 Jahre her, seit es geschrieben wurde. letzter Artikel über das Parsen von Habr, und einige Punkte haben sich geändert.
Als ich eine Kopie von Habr haben wollte, beschloss ich, einen Parser zu schreiben, der den gesamten Inhalt der Autoren in der Datenbank speichern würde. Wie es dazu kam und auf welche Fehler ich gestoßen bin, können Sie unter dem Schnitt nachlesen.
Die erste Version des Parsers. Ein Thread, viele Probleme
Zunächst beschloss ich, einen Skript-Prototyp zu erstellen, in dem der Artikel sofort nach dem Herunterladen analysiert und in der Datenbank abgelegt wird. Ohne lange nachzudenken, habe ich SQLite3 verwendet, weil. Es war weniger arbeitsintensiv: Es war kein lokaler Server erforderlich, erstellt, angezeigt, gelöscht und so weiter.
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)
Alles ist klassisch – wir verwenden Beautiful Soup, Anfragen und ein schneller Prototyp ist fertig. Das ist einfach…
Der Seitendownload erfolgt in einem Thread
Wenn Sie die Ausführung des Skripts unterbrechen, geht die gesamte Datenbank verloren. Schließlich wird der Commit erst nach dem gesamten Parsen durchgeführt.
Natürlich können Sie nach jedem Einfügen Änderungen an der Datenbank festschreiben, aber dann erhöht sich die Ausführungszeit des Skripts erheblich.
Das Parsen der ersten 100 Artikel hat mich 000 Stunden gekostet.
Als nächstes finde ich den Artikel des Benutzers kointegriert, das ich gelesen und ein paar Life-Hacks gefunden habe, um diesen Prozess zu beschleunigen:
Die Verwendung von Multithreading beschleunigt das Herunterladen zeitweise.
Sie können nicht die Vollversion des Habr erhalten, sondern die mobile Version.
Wenn beispielsweise ein mitintegrierter Artikel in der Desktop-Version 378 KB wiegt, sind es in der mobilen Version bereits 126 KB.
Zweite Version. Viele Threads, vorübergehendes Verbot von Habr
Als ich das Internet zum Thema Multithreading in Python durchforstete und mit multiprocessing.dummy die einfachste Variante wählte, fiel mir auf, dass beim Multithreading auch Probleme auftraten.
SQLite3 möchte nicht mit mehr als einem Thread arbeiten.
Fest check_same_thread=False, aber dieser Fehler ist nicht der einzige, beim Versuch, in die Datenbank einzufügen, treten manchmal Fehler auf, die ich nicht beheben konnte.
Daher entscheide ich mich, auf das sofortige Einfügen von Artikeln direkt in die Datenbank zu verzichten und mich an die kointegrierte Lösung zu erinnern. Ich entscheide mich für die Verwendung von Dateien, da es beim Multithread-Schreiben in eine Datei keine Probleme gibt.
Habr beginnt mit dem Verbot für die Nutzung von mehr als drei Threads.
Besonders eifrige Versuche, zu Habr durchzudringen, können mit einer IP-Sperre für ein paar Stunden enden. Sie müssen also nur 3 Threads verwenden, aber das ist schon gut, da die Zeit zum Durchlaufen von über 100 Artikeln von 26 auf 12 Sekunden reduziert wird.
Es ist erwähnenswert, dass diese Version ziemlich instabil ist und die Downloads bei einer großen Anzahl von Artikeln regelmäßig abbrechen.
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)
Dritte Version. Finale
Beim Debuggen der zweiten Version habe ich festgestellt, dass Habr plötzlich über eine API verfügt, auf die die mobile Version der Website zugreift. Es wird schneller geladen als die mobile Version, da es sich nur um JSON handelt, das nicht einmal geparst werden muss. Am Ende beschloss ich, mein Drehbuch noch einmal umzuschreiben.
Also, gefunden Link API, Sie können mit dem Parsen beginnen.
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)
Es enthält Felder, die sich sowohl auf den Artikel selbst als auch auf den Autor beziehen, der ihn geschrieben hat.
API.png
Ich habe nicht den vollständigen JSON-Code jedes Artikels gespeichert, sondern nur die Felder gespeichert, die ich brauchte:
id
is_tutorial
time_published
Titel
Inhalt
comments_count
lang ist die Sprache, in der der Artikel verfasst ist. Bisher gibt es nur en und ru.
tags_string – alle Tags aus dem Beitrag
reading_count
Autor
Punktzahl – Artikelbewertung.
Daher habe ich mithilfe der API die Skriptausführungszeit auf 8 Sekunden pro 100 URLs reduziert.
Nachdem wir die benötigten Daten heruntergeladen haben, müssen wir sie verarbeiten und in die Datenbank eingeben. Auch hiermit hatte ich keine Probleme:
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)
Statistik
Nun, traditionell können Sie schließlich einige Statistiken aus den Daten extrahieren:
Von den erwarteten 490 Downloads wurden nur 406 Artikel heruntergeladen. Es stellt sich heraus, dass mehr als die Hälfte (228) der Artikel über Habré ausgeblendet oder gelöscht wurden.
Die gesamte Datenbank, bestehend aus fast einer halben Million Artikeln, wiegt 2.95 GB. In komprimierter Form - 495 MB.
Insgesamt sind 37804 Personen die Autoren von Habré. Ich erinnere Sie daran, dass diese Statistiken nur aus Live-Beiträgen stammen.
Der produktivste Autor auf Habré - Alizar - 8774 Artikel.