God eftermiddag. Det er 2 år siden den blev skrevet. sidste artikel om at parse Habr, og nogle punkter er ændret.
Da jeg ville have en kopi af Habr, besluttede jeg at skrive en parser, der ville gemme alt indholdet af forfatterne i databasen. Hvordan det skete og hvilke fejl jeg stødte på – du kan læse under klippet.
Den første version af parseren. Én tråd, mange problemer
Til at begynde med besluttede jeg at lave en script-prototype, hvor artiklen ville blive parset og placeret i databasen umiddelbart efter download. Uden at tænke to gange brugte jeg sqlite3, fordi. det var mindre arbejdskrævende: ingen grund til at have en lokal server, oprettet-så-slettet og sådan noget.
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)
Alt er klassisk - vi bruger Smuk suppe, forespørgsler og en hurtig prototype er klar. Det er bare…
Sidedownload er i én tråd
Hvis du afbryder udførelsen af scriptet, vil hele databasen gå ingen vegne. Når alt kommer til alt, udføres commit først efter al parsing.
Selvfølgelig kan du foretage ændringer i databasen efter hver indsættelse, men så vil scriptudførelsestiden stige betydeligt.
At analysere de første 100 artikler tog mig 000 timer.
Dernæst finder jeg brugerens artikel kointegreret, som jeg læste og fandt et par life hacks for at fremskynde denne proces:
Brug af multithreading fremskynder downloading til tider.
Du kan ikke få den fulde version af habr, men dens mobile version.
For eksempel, hvis en cointegrated artikel i desktopversionen vejer 378 KB, så er den i mobilversionen allerede 126 KB.
Anden version. Mange tråde, midlertidigt forbud fra Habr
Da jeg gennemsøgte internettet om emnet multithreading i python, valgte jeg den enkleste mulighed med multiprocessing.dummy, jeg bemærkede, at der dukkede problemer op sammen med multithreading.
SQLite3 ønsker ikke at arbejde med mere end én tråd.
fast check_same_thread=False, men denne fejl er ikke den eneste, når jeg forsøger at indsætte i databasen, opstår der nogle gange fejl, som jeg ikke kunne løse.
Derfor beslutter jeg mig for at opgive den øjeblikkelige indsættelse af artikler direkte i databasen, og husker den kointegrerede løsning, beslutter jeg at bruge filer, fordi der ikke er problemer med multi-threaded skrivning til en fil.
Habr begynder at banne for at bruge mere end tre tråde.
Særligt nidkære forsøg på at komme igennem til Habr kan ende med et ip-forbud i et par timer. Så du skal kun bruge 3 tråde, men det er allerede godt, da tiden til at iterere over 100 artikler er reduceret fra 26 til 12 sekunder.
Det er værd at bemærke, at denne version er ret ustabil, og downloads falder med jævne mellemrum af på et stort antal artikler.
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)
Tredje version. Finale
Mens jeg fejlede den anden version, opdagede jeg, at Habr pludselig har en API, som mobilversionen af webstedet tilgår. Den indlæses hurtigere end mobilversionen, da det kun er json, som ikke engang skal parses. Til sidst besluttede jeg mig for at omskrive mit manuskript igen.
Altså efter at have fundet dette link API, kan du begynde at parse det.
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)
Den indeholder felter relateret både til selve artiklen og til forfatteren, der har skrevet den.
API.png
Jeg dumpede ikke den fulde json for hver artikel, men gemte kun de felter, jeg havde brug for:
id
is_tutorial
tid_udgivet
titel
indhold
comments_count
lang er det sprog, artiklen er skrevet på. Indtil videre har det kun en og ru.
tags_string - alle tags fra indlægget
læsning_antal
forfatter
score — artiklens vurdering.
Ved at bruge API'et reducerede jeg scriptudførelsestiden til 8 sekunder pr. 100 url.
Efter at vi har downloadet de data, vi skal bruge, skal vi behandle dem og indtaste dem i databasen. Jeg havde heller ingen problemer med dette:
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)
Statistikker
Nå, traditionelt, endelig, kan du udtrække nogle statistikker fra dataene:
Af de forventede 490 downloads blev kun 406 artikler downloadet. Det viser sig, at mere end halvdelen (228) af artiklerne om Habré blev skjult eller slettet.
Hele databasen, der består af næsten en halv million artikler, vejer 2.95 GB. I komprimeret form - 495 MB.
I alt er 37804 personer forfatterne til Habré. Jeg minder dig om, at disse statistikker kun er fra liveopslag.
Den mest produktive forfatter på Habré - alizar - 8774 artikler.