Добар ден. Поминаа 2 години откако е напишано. последната статија за парсирање на Хабр, а некои точки се променети.
Кога сакав да имам копија од Habr, решив да напишам парсер кој ќе ја зачува целата содржина на авторите во базата на податоци. Како се случи и со какви грешки наидов - можете да прочитате под сечењето.
Првата верзија на парсерот. Една нишка, многу проблеми
За почеток, решив да направам прототип на скрипта во која статијата ќе биде анализирана и ставена во базата веднаш по преземањето. Без да размислам двапати, користев sqlite3, затоа што. тоа беше помалку трудоинтензивно: нема потреба да се има локален сервер, создаден-изгледа-избришан и слични работи.
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)
Сè е класично - користиме Убава супа, барања и брз прототип е подготвен. Тоа е само…
Преземањето на страницата е во една нишка
Ако го прекинете извршувањето на скриптата, тогаш целата база на податоци нема да оди никаде. На крајот на краиштата, извршувањето се врши само по целото парсирање.
Се разбира, можете да ги поправите промените во базата на податоци по секое вметнување, но тогаш времето на извршување на скриптата значително ќе се зголеми.
За анализа на првите 100 статии ми требаа 000 часа.
Следно ја наоѓам статијата на корисникот коинтегрирани, што го прочитав и најдов неколку лајф хакови за да го забрзам овој процес:
Користењето на повеќенишки го забрзува преземањето на моменти.
Можете да ја добиете не целосната верзија на habr, туку нејзината мобилна верзија.
На пример, ако коинтегрираната статија во десктоп верзијата тежи 378 KB, тогаш во мобилната верзија веќе е 126 KB.
Втора верзија. Многу теми, привремена забрана од Хабр
Кога пребарував на Интернет на тема мултинишки во python, ја избрав наједноставната опција со multiprocessing.dummy, забележав дека се појавија проблеми заедно со мултинишки.
SQLite3 не сака да работи со повеќе од една нишка.
фиксна check_same_thread=False, но оваа грешка не е единствената, при обидот да се вметне во базата на податоци, понекогаш се појавуваат грешки кои не можев да ги решам.
Затоа, одлучувам да го напуштам инстантното вметнување статии директно во базата на податоци и, сеќавајќи се на коинтегрираното решение, одлучувам да користам датотеки, бидејќи нема проблеми со пишување со повеќе нишки во датотека.
Хабр започнува со забрана за користење повеќе од три нишки.
Особено ревносните обиди да се стигне до Хабр може да завршат со забрана за IP за неколку часа. Значи, треба да користите само 3 нишки, но ова е веќе добро, бидејќи времето за повторување на над 100 статии е намалено од 26 на 12 секунди.
Вреди да се напомене дека оваа верзија е прилично нестабилна и преземањата периодично паѓаат на голем број написи.
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)
Трета верзија. Конечно
Додека ја дебагирав втората верзија, открив дека Habr, одеднаш, има API до кој пристапува мобилната верзија на страницата. Се вчитува побрзо од мобилната верзија, бидејќи е само json, што дури и не треба да се анализира. На крајот, решив повторно да го напишам моето сценарио.
Значи, откако најдов овој линк API, можете да започнете да го анализирате.
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)
Содржи полиња поврзани и со самата статија и со авторот кој ја напишал.
API.png
Не го исфрлив целиот json од секоја статија, туку ги зачував само полињата што ми беа потребни:
id
is_tutorial
време_објавено
Наслов
содржина
коментари_број
lang е јазикот на кој е напишана статијата. Засега има само en и ru.
tags_string - сите ознаки од објавата
читање_број
авторот
резултат - рејтинг на статијата.
Така, користејќи го API, го намалив времето на извршување на скриптата на 8 секунди на 100 URL.
Откако ќе ги преземеме податоците што ни се потребни, треба да ги обработиме и да ги внесеме во базата на податоци. И јас немав проблеми со ова:
анализатор.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)
статистика
Па, традиционално, конечно, можете да извлечете некоја статистика од податоците:
Од очекуваните 490 преземања, преземени се само 406 статии. Излегува дека повеќе од половина (228) од написите на Хабре биле скриени или избришани.
Целата база на податоци, која се состои од речиси половина милион статии, тежи 2.95 GB. Во компресирана форма - 495 MB.
Вкупно, 37804 луѓе се автори на Хабре. Потсетувам дека овие статистики се само од објави во живо.
Најпродуктивниот автор на Хабре - ализар - 8774 статии.