Laba diena. Praėjo 2 metai, kai jį parašiau paskutinis straipsnis apie Habr analizavimą, o kai kurie dalykai pasikeitė.
Kai norėjau turėti Habr kopiją, nusprendžiau parašyti analizatorių, kuris visą autorių turinį išsaugotų duomenų bazėje. Kaip tai atsitiko ir su kokiomis klaidomis susidūriau - galite perskaityti po pjūviu.
Pirmoji analizatoriaus versija. Viena tema, daug problemų
Pirmiausia nusprendžiau sukurti scenarijaus prototipą, kuriame iškart po atsisiuntimo straipsnis būtų išanalizuotas ir patalpintas į duomenų bazę. Du kartus negalvodamas panaudojau sqlite3, nes... tai buvo mažiau darbo jėgos: jums nereikia turėti vietinio serverio, kurti, ieškoti, ištrinti ir panašiai.
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)
Viskas pagal klasiką – naudojame Gražią sriubą, prašymus ir greitasis prototipas paruoštas. Tai tik...
Puslapis atsisiunčiamas vienoje temoje
Jei nutrauksite scenarijaus vykdymą, visa duomenų bazė niekur nedings. Juk įsipareigojimas vykdomas tik po visų analizavimo.
Žinoma, galite atlikti duomenų bazės pakeitimus po kiekvieno įterpimo, tačiau tada scenarijaus vykdymo laikas žymiai padidės.
Pirmųjų 100 000 straipsnių analizė užtruko 8 valandas.
Tada randu vartotojo straipsnį kointegruotas, kurį perskaičiau, ir radau kelis gyvenimo įsilaužimus, kurie pagreitina šį procesą:
Kelių gijų naudojimas žymiai pagreitina atsisiuntimą.
Galite gauti ne pilną Habr versiją, o jos mobiliąją versiją.
Pavyzdžiui, jei darbalaukio versijoje kointegruotas straipsnis sveria 378 KB, tai mobiliojoje versijoje jis jau yra 126 KB.
Antroji versija. Daug gijų, laikinas draudimas iš Habr
Kai naršiau internetą daugiasriegio python tema ir pasirinkau paprasčiausią variantą su multiprocessing.dummy, pastebėjau, kad kartu su multithreading atsiranda ir problemų.
SQLite3 nenori dirbti su daugiau nei viena gija.
Fiksuotas check_same_thread=False, tačiau ši klaida nėra vienintelė, bandant įterpti į duomenų bazę kartais iškyla klaidų, kurių negaliu išspręsti.
Todėl nusprendžiu atsisakyti momentinio straipsnių įterpimo tiesiai į duomenų bazę ir, prisimindamas kointegruotą sprendimą, nusprendžiu naudoti failus, nes nekyla problemų su kelių gijų įrašymu į failą.
Habras pradeda drausti naudoti daugiau nei tris gijas.
Ypač uolūs bandymai pasiekti Habrą gali sukelti IP uždraudimą kelioms valandoms. Taigi jūs turite naudoti tik 3 gijas, bet tai jau gerai, nes 100 straipsnių rūšiavimo laikas sutrumpėja nuo 26 iki 12 sekundžių.
Verta paminėti, kad ši versija yra gana nestabili, o daugelio straipsnių atsisiuntimas periodiškai nepavyksta.
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)
Trečia versija. Galutinis
Derindamas antrąją versiją sužinojau, kad Habras staiga turi API, kurią pasiekia mobilioji svetainės versija. Jis įkeliamas greičiau nei mobilioji versija, nes tai tik json, kurio net nereikia analizuoti. Galiausiai nusprendžiau dar kartą perrašyti savo scenarijų.
Taigi, atradęs ši nuoroda API, galite pradėti ją analizuoti.
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)
Jame yra laukai, susiję tiek su pačiu straipsniu, tiek su jį parašiusiu autoriumi.
API.png
Neišverčiau viso kiekvieno straipsnio Json, bet išsaugojau tik tuos laukus, kurių man reikėjo:
id
is_tutorial
paskelbimo laikas
pavadinimas
turinys
komentarų_skaičius
lang yra kalba, kuria parašytas straipsnis. Kol kas jame yra tik en ir ru.
tags_string – visos įrašo žymos
skaitymo_skaičius
autorius
balas — straipsnio įvertinimas.
Taigi, naudodamas API, scenarijaus vykdymo laiką sumažinau iki 8 sekundžių 100 URL.
Atsisiuntę mums reikalingus duomenis, turime juos apdoroti ir įvesti į duomenų bazę. Su tuo taip pat nebuvo jokių problemų:
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
Na, pagaliau tradiciškai galite išgauti statistiką iš duomenų:
Iš numatytų 490 406 buvo atsisiųsta tik 228 512 straipsnių. Pasirodo, daugiau nei pusė (261894) straipsnių apie Habré buvo paslėpti arba ištrinti.
Visa duomenų bazė, kurią sudaro beveik pusė milijono straipsnių, sveria 2.95 GB. Suglaudinta forma - 495 MB.
Iš viso Habré yra 37804 XNUMX autoriai. Priminsiu, kad tai statistika tik iš tiesioginių įrašų.
Produktyviausias Habré autorius - alizar — 8774 straipsniai.