Dober večer. Minili sta 2 leti odkar je bila napisana. zadnji članek o razčlenjevanju Habra in nekatere točke so se spremenile.
Ko sem želel imeti kopijo Habra, sem se odločil, da napišem razčlenjevalec, ki bi shranil vso vsebino avtorjev v bazo. Kako se je to zgodilo in na katere napake sem naletel - si lahko preberete pod rezom.
Prva različica razčlenjevalnika. Ena nit, veliko težav
Za začetek sem se odločil narediti prototip skripte, v kateri bi članek takoj po prenosu razčlenil in dal v bazo. Brez dvakratnega razmišljanja sem uporabil sqlite3, ker. bilo je manj delovno intenzivno: ni potrebe po lokalnem strežniku, ustvarjenem-pogledanem-izbrisanem in podobnih stvareh.
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)
Vse je klasično - uporabljamo Beautiful Soup, zahtevamo in hitri prototip je pripravljen. To je samo …
Prenos strani je v eni niti
Če prekinete izvajanje skripta, potem celotna baza podatkov ne bo šla nikamor. Navsezadnje se potrditev izvede šele po celotnem razčlenjevanju.
Seveda lahko po vsakem vstavljanju potrdite spremembe v bazo podatkov, vendar se bo takrat čas izvajanja skripta znatno povečal.
Razčlenjevanje prvih 100 člankov mi je vzelo 000 ur.
Nato najdem članek uporabnika kointegriran, ki sem ga prebral in našel nekaj življenjskih trikov za pospešitev tega procesa:
Uporaba večnitnosti včasih pospeši prenos.
Ne morete dobiti polne različice habra, ampak njegovo mobilno različico.
Na primer, če kointegrirani članek v namizni različici tehta 378 KB, potem je v mobilni različici že 126 KB.
Druga različica. Veliko tem, začasna prepoved dostopa do Habra
Ko sem brskal po internetu na temo večnitnosti v pythonu in sem izbral najpreprostejšo možnost z multiprocessing.dummy, sem opazil, da se težave pojavljajo skupaj z večnitnostjo.
SQLite3 ne želi delati z več kot eno nitjo.
fiksno check_same_thread=False, vendar ta napaka ni edina, pri poskusu vstavljanja v bazo se včasih pojavijo napake, ki jih nisem mogel odpraviti.
Zato se odločim, da opustim takojšnje vstavljanje člankov neposredno v bazo in se ob spominu na kointegrirano rešitev odločim za uporabo datotek, saj z večnitnim pisanjem v datoteko ni težav.
Habr začne banati za uporabo več kot treh niti.
Še posebej vneti poskusi priti do Habra se lahko končajo s prepovedjo ip za nekaj ur. Uporabiti morate torej samo 3 niti, vendar je že to dobro, saj se čas za ponovitev več kot 100 člankov zmanjša s 26 na 12 sekund.
Omeniti velja, da je ta različica precej nestabilna in prenosi občasno padajo na veliko število člankov.
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)
Tretja verzija. Končno
Med odpravljanjem napak v drugi različici sem odkril, da ima Habr kar naenkrat API, do katerega dostopa mobilna različica spletnega mesta. Nalaga se hitreje kot mobilna različica, saj je samo json, ki ga sploh ni treba razčleniti. Na koncu sem se odločil, da znova napišem svoj scenarij.
Torej, ko sem našel ta povezava API, ga lahko začnete razčlenjevati.
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)
Vsebuje polja, povezana s samim člankom in avtorjem, ki ga je napisal.
API.png
Nisem izpisal celotnega json-a vsakega članka, ampak sem shranil samo polja, ki sem jih potreboval:
id
is_tutorial
čas_objave
Naslov
vsebina
comments_count
lang je jezik, v katerem je članek napisan. Zaenkrat ima le en in ru.
tags_string - vse oznake iz objave
reading_count
Avtor
ocena — ocena članka.
Tako sem z uporabo API-ja zmanjšal čas izvajanja skripte na 8 sekund na 100 url.
Ko smo prenesli potrebne podatke, jih moramo obdelati in vnesti v bazo. Tudi s tem nisem imel nobenih težav:
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
No, tradicionalno lahko končno izvlečete nekaj statističnih podatkov iz podatkov:
Od pričakovanih 490 prenosov je bilo prenesenih le 406 člankov. Izkazalo se je, da je bila več kot polovica (228) člankov na Habréju skrita ali izbrisana.
Celotna baza, ki jo sestavlja skoraj pol milijona člankov, tehta 2.95 GB. V stisnjeni obliki - 495 MB.
Skupno je 37804 ljudi avtorjev Habréja. Opozarjam vas, da so te statistike samo iz objav v živo.
Najbolj produktiven avtor na Habréju - alizar - 8774 člankov.