Tere päevast. Selle kirjutamisest on möödas 2 aastat. viimane artikkel Habri sõelumise kohta ja mõned punktid on muutunud.
Kui tahtsin saada Habri koopiat, otsustasin kirjutada parseri, mis salvestaks kogu autorite sisu andmebaasi. Kuidas see juhtus ja milliseid vigu kohtasin - saate lugeda lõike alt.
Parseri esimene versioon. Üks teema, palju probleeme
Alustuseks otsustasin teha skripti prototüübi, milles artikkel sõelutakse ja paigutatakse kohe pärast allalaadimist andmebaasi. Kaks korda mõtlemata kasutasin sqlite3, sest. see oli vähem töömahukas: pole vaja kohalikku serverit, loodud-vaadatud-kustutatud ja muud taolist.
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)
Kõik on klassikaline – kasutame Kaunist Suppi, palveid ja kiire prototüüp ongi valmis. See on lihtsalt…
Lehe allalaadimine toimub ühes lõimes
Kui katkestate skripti täitmise, ei kao kogu andmebaas kuhugi. Kinnitus sooritatakse ju alles pärast kogu parsimist.
Loomulikult saate pärast iga sisestamist andmebaasi muudatusi teha, kuid siis pikeneb skripti täitmise aeg oluliselt.
Esimese 100 000 artikli sõelumine võttis mul 8 tundi.
Järgmisena leian kasutaja artikli kointegreeritud, mida lugesin ja leidsin selle protsessi kiirendamiseks mõned elunäpud:
Mitme lõime kasutamine kiirendab mõnikord allalaadimist.
Saate hankida mitte habri täisversiooni, vaid selle mobiiliversiooni.
Näiteks kui lauaarvuti versioonis kaalub kointegreeritud artikkel 378 KB, siis mobiiliversioonis on see juba 126 KB.
Teine versioon. Palju niite, ajutine keeld Habr
Kui ma pythonis mitme lõimestamise teemal Internetti uurisin, valisin multiprocessing.dummyga lihtsaima variandi, märkasin, et koos mitmelõimega tekkisid probleemid.
SQLite3 ei taha töötada rohkem kui ühe lõimega.
fikseeritud check_same_thread=False, kuid see viga pole ainuke, andmebaasi sisestamisel tuleb vahel ette vigu, mida ei osanud lahendada.
Seetõttu otsustan loobuda artiklite kohesest sisestamisest otse andmebaasi ja kointegreeritud lahendust meenutades otsustan kasutada faile, sest mitme lõimega faili kirjutamisega probleeme ei teki.
Habr hakkab keelama rohkem kui kolme lõime kasutamise eest.
Eriti innukad katsed Habrile läbi saada võivad lõppeda paaritunnise ip-keeluga. Seega peate kasutama ainult 3 lõime, kuid see on juba hea, nii et üle 100 artikli kordamise aeg väheneb 26 sekundilt 12 sekundile.
Väärib märkimist, et see versioon on üsna ebastabiilne ja allalaadimine langeb perioodiliselt paljudele artiklitele.
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)
Kolmas versioon. Lõplik
Teise versiooni silumisel avastasin, et Habril on äkki API, millele saidi mobiiliversioon juurde pääseb. See laaditakse kiiremini kui mobiiliversioon, kuna see on lihtsalt json, mida pole vaja isegi sõeluda. Lõpuks otsustasin oma stsenaariumi uuesti ümber kirjutada.
Niisiis, olles leidnud see link API, võite alustada selle sõelumist.
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)
See sisaldab välju, mis on seotud nii artikli enda kui ka selle kirjutanud autoriga.
API.png
Ma ei tühjendanud iga artikli täielikku JSON-i, vaid salvestasin ainult vajalikud väljad:
id
is_tutorial
avaldamisaeg
pealkiri
sisu
kommentaaride_arv
lang on keel, milles artikkel on kirjutatud. Siiani on sellel ainult en ja ru.
tags_string – kõik postituse sildid
lugemiste_arv
autor
skoor — artikli hinnang.
Seega API-d kasutades vähendasin skripti täitmise aega 8 sekundini 100 URL-i kohta.
Kui oleme vajalikud andmed alla laadinud, peame need töötlema ja andmebaasi sisestama. Mul polnud ka sellega probleeme:
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)
Statistika
Noh, traditsiooniliselt saate lõpuks andmetest statistikat välja võtta:
Eeldatavast 490 406 allalaadimisest laaditi alla vaid 228 512 artiklit. Selgub, et üle poole (261894) Habrét puudutavatest artiklitest olid peidetud või kustutatud.
Kogu ligi poolest miljonist artiklist koosnev andmebaas kaalub 2.95 GB. Tihendatud kujul - 495 MB.
Kokku on Habré autoriteks 37804 inimest. Tuletan meelde, et see statistika on ainult reaalajas postitustest.
Habré kõige produktiivsem autor - alizar - 8774 artiklit.