Arratsalde on. 2 urte pasa dira idatzi zenetik. azken artikulua Habr analizatzeari buruz, eta puntu batzuk aldatu dira.
Habr-en kopia bat izan nahi nuenean, egileen eduki guztia datu-basean gordeko zuen analizatzaile bat idaztea erabaki nuen. Nola gertatu zen eta zer akats aurkitu ditudan - ebaki azpian irakur dezakezu.
Analizatzailearen lehen bertsioa. Hari bat, arazo asko
Hasteko, script-prototipo bat egitea erabaki nuen, non artikulua deskargatu eta datu-basean berehala aztertuko zen. Bi aldiz pentsatu gabe, sqlite3 erabili nuen, zeren. lan gutxiago zen: ez zen zerbitzari lokal bat eduki behar, sortu-itxura-ezabatu eta horrelakoak.
hari_bat.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)
Dena klasikoa da - Beautiful Soup erabiltzen dugu, eskaerak eta prototipo azkar bat prest dago. Hori besterik ez da…
Orria deskargatu hari batean dago
Scriptaren exekuzioa eteten baduzu, datu-base osoa ez da inora joango. Azken finean, konpromezua analisi guztiaren ondoren bakarrik egiten da.
Jakina, datu-basean aldaketak egin ditzakezu txertatze bakoitzaren ondoren, baina gero script-a exekutatzeko denbora nabarmen handituko da.
Lehenengo 100 artikuluak analizatzeak 000 ordu behar izan nituen.
Jarraian erabiltzailearen artikulua aurkitzen dut bateratu, irakurri eta prozesu hau bizkortzeko bizitza hack batzuk aurkitu nituen:
Hari anitzeko erabilerak deskarga bizkortzen du batzuetan.
Ez habr-aren bertsio osoa lor dezakezu, mugikorreko bertsioa baizik.
Adibidez, mahaigaineko bertsioan bateratutako artikulu batek 378 KB pisatzen badu, mugikorreko bertsioan dagoeneko 126 KB da.
Bigarren bertsioa. Hari asko, aldi baterako debekua Habr
Python-en multithreading gaiari buruz Interneten arakatu nuenean, multiprocessing.dummy-rekin aukerarik errazena aukeratu nuen, multithreading-arekin batera arazoak agertzen zirela ohartu nintzen.
SQLite3-k ez du hari bat baino gehiagorekin lan egin nahi.
finkoa check_same_thread=False, baina errore hau ez da bakarra, datu-basean txertatzen saiatzean, batzuetan konpondu ezin izan ditudan akatsak gertatzen dira.
Hori dela eta, artikuluen berehalako datu-basean zuzenean txertatzeari uztea erabakitzen dut eta, kointegratutako irtenbidea gogoratuz, fitxategiak erabiltzea erabakitzen dut, hari anitzeko fitxategi batean idazteko arazorik ez dagoelako.
Habr hiru hari baino gehiago erabiltzea debekatzen hasten da.
Bereziki Habr-era iristeko saiakera sutsuak ordu pare baterako ip debekuarekin amai daiteke. Beraz, 3 hari bakarrik erabili behar dituzu, baina hori ona da dagoeneko, 100 artikulu baino gehiago errepikatzeko denbora 26 segundotik 12ra murrizten baita.
Azpimarratzekoa da bertsio hau nahiko ezegonkorra dela eta deskargak aldizka artikulu ugaritan jaisten direla.
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)
Hirugarren bertsioa. Finala
Bigarren bertsioa arazketan, Habr-ek, bat-batean, webgunearen mugikorreko bertsioak atzitzen duen API bat duela ikusi nuen. Mugikorreko bertsioa baino azkarrago kargatzen da, json besterik ez baita, analizatu beharrik ere ez duena. Azkenean, nire gidoia berriro idaztea erabaki nuen.
Beraz, aurkitu ondoren lotura hau APIa, analizatzen has zaitezke.
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)
Artikuluarekin eta idatzi duen egilearekin lotutako eremuak ditu.
API.png
Ez dut artikulu bakoitzaren json osoa bota, baina behar nituen eremuak bakarrik gorde ditut:
id
da_tutoriala
denbora_argitaratua
izenburua
edukia
iruzkinak_zenbaketa
lang artikulua idazten den hizkuntza da. Orain arte, en eta ru baino ez ditu.
tags_string - mezuaren etiketa guztiak
irakurketa_zenbaketa
Egileak
puntuazioa — artikuluaren balorazioa.
Horrela, APIa erabiliz, scriptaren exekuzio denbora 8 segundora murriztu nuen 100 url bakoitzeko.
Behar ditugun datuak deskargatu ondoren, prozesatu eta datu-basean sartu behar ditugu. Honekin ere ez nuen arazorik izan:
analizatzailea.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)
estatistikak
Tira, tradizionalki, azkenik, datuetatik estatistika batzuk atera ditzakezu:
Espero diren 490 deskargaretatik 406 artikulu baino ez ziren deskargatu. Ematen da Habré-ri buruzko artikuluen erdia baino gehiago (228) ezkutatu edo ezabatu egin zirela.
Datu-base osoak, ia milioi erdi artikuluz osatua, 2.95 GB pisatzen du. Forma konprimituan - 495 MB.
Guztira, 37804 pertsona dira Habréren egileak. Gogorarazten dizut estatistika hauek zuzeneko mezuetatik soilik daudela.
Habré-ri buruzko egilerik emankorrena - alizar - 8774 artikulu.