Labdien. Ir pagājuši 2 gadi kopš tā uzrakstīšanas. pēdējais raksts par Habr parsēšanu, un daži punkti ir mainīti.
Kad vēlējos iegūt Habr kopiju, nolēmu uzrakstīt parsētāju, kas visu autoru saturu saglabātu datu bāzē. Kā tas notika un ar kādām kļūdām es saskāros - varat lasīt zem griezuma.
Pirmā parsētāja versija. Viens pavediens, daudz problēmu
Sākumā nolēmu izveidot skripta prototipu, kurā raksts uzreiz pēc lejupielādes tiktu parsēts un ievietots datu bāzē. Divreiz nedomājot izmantoju sqlite3, jo. tas bija mazāk darbietilpīgs: nav nepieciešams lokāls serveris, izveidots-izskatījās-dzēsts un tamlīdzīgi.
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)
Viss ir klasisks - izmantojam Skaisto zupu, lūgumi un ātrais prototips gatavs. Tas ir tikai…
Lapas lejupielāde notiek vienā pavedienā
Ja pārtraucat skripta izpildi, visa datu bāze nekur nepazudīs. Galu galā apņemšanās tiek veikta tikai pēc visas parsēšanas.
Protams, jūs varat veikt izmaiņas datu bāzē pēc katras ievietošanas, taču tad skripta izpildes laiks ievērojami palielināsies.
Pirmo 100 000 rakstu analīze man prasīja 8 stundas.
Tālāk es atrodu lietotāja rakstu kointegrēti, kuru izlasīju un atradu dažus dzīves veidus, lai paātrinātu šo procesu:
Daudzpavedienu izmantošana paātrina lejupielādi reizēm.
Jūs varat iegūt nevis pilno habr versiju, bet gan tās mobilo versiju.
Piemēram, ja kointegrētais raksts darbvirsmas versijā sver 378 KB, tad mobilajā versijā tas jau ir 126 KB.
Otrā versija. Daudzi pavedieni, pagaidu aizliegums no Habr
Kad es pārlūkoju internetu par daudzpavedienu tēmu python, es izvēlējos vienkāršāko opciju ar multiprocessing.dummy, es pamanīju, ka problēmas parādījās kopā ar vairāku pavedienu izmantošanu.
SQLite3 nevēlas strādāt ar vairāk nekā vienu pavedienu.
fiksēts check_same_thread=False, bet šī kļūda nav vienīgā, mēģinot ievietot datubāzē, dažkārt gadās kļūdas, kuras nevarēju atrisināt.
Tāpēc nolemju atteikties no rakstu tūlītējas ievietošanas tieši datu bāzē un, atceroties kointegrēto risinājumu, nolemju izmantot failus, jo ar vairākpavedienu ierakstīšanu failā nav problēmu.
Habr sāk aizliegt lietot vairāk nekā trīs pavedienus.
Īpaši dedzīgi mēģinājumi tikt cauri Habram var beigties ar ip banu uz pāris stundām. Tātad jums ir jāizmanto tikai 3 pavedieni, bet tas jau ir labi, jo vairāk nekā 100 rakstu atkārtošanas laiks tiek samazināts no 26 līdz 12 sekundēm.
Ir vērts atzīmēt, ka šī versija ir diezgan nestabila, un daudzu rakstu lejupielādes periodiski samazinās.
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šā versija. Fināls
Atkļūdojot otro versiju, es atklāju, ka Habram pēkšņi ir API, kurai piekļūst vietnes mobilā versija. Tas tiek ielādēts ātrāk nekā mobilā versija, jo tas ir tikai JSON, kas pat nav jāparsē. Galu galā es nolēmu vēlreiz pārrakstīt savu scenāriju.
Tātad, atradis šo saiti API, varat sākt to parsēt.
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)
Tajā ir lauki, kas saistīti gan ar pašu rakstu, gan ar autoru, kurš to uzrakstījis.
API.png
Es neizmetu katra raksta pilnu JSON, bet saglabāju tikai vajadzīgos laukus:
id
ir_pamācība
publicēšanas laiks
virsraksts
saturs
komentāru_skaits
lang ir valoda, kurā raksts ir uzrakstīts. Līdz šim tam ir tikai en un ru.
tags_string — visas ziņas atzīmes
lasīšanas_skaits
autors
punktu skaits — raksta vērtējums.
Tādējādi, izmantojot API, es samazināju skripta izpildes laiku līdz 8 sekundēm uz 100 url.
Kad esam lejupielādējuši nepieciešamos datus, tie ir jāapstrādā un jāievada datu bāzē. Man arī nebija nekādu problēmu ar šo:
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
Nu, tradicionāli, visbeidzot, jūs varat iegūt statistiku no datiem:
No paredzamajām 490 406 lejupielādēm tika lejupielādēti tikai 228 512 raksti. Izrādās, ka vairāk nekā puse (261894) rakstu par Habrē tika paslēpti vai dzēsti.
Visa datubāze, kas sastāv no gandrīz pusmiljona rakstu, sver 2.95 GB. Saspiestā formā - 495 MB.
Kopumā Habrē autori ir 37804 cilvēki. Atgādinu, ka šī statistika ir tikai no tiešraides ierakstiem.
Produktīvākais autors par Habrē - alizārs - 8774 raksti.