Добар дан. Прошле су 2 године откако је написан. последњи чланак о рашчлањивању Хабра, а неке тачке су се промениле.
Када сам желео да имам копију Хабра, одлучио сам да напишем парсер који ће сав садржај аутора сачувати у бази података. Како се то догодило и на које грешке сам наишао - можете прочитати испод реза.
За почетак, одлучио сам да направим прототип скрипте у којем би чланак био рашчлањен и смештен у базу података одмах по преузимању. Без размишљања, користио сам склите3, јер. био је мање радно интензиван: није било потребе да имате локални сервер, креирали-изгледали-избрисали и слично.
оне_тхреад.пи
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 сати.
Затим пронађем чланак корисника коинтегрисано, који сам прочитао и пронашао неколико животних хакова да убрзам овај процес:
Коришћење вишенитног рада понекад убрзава преузимање.
Можете добити не пуну верзију хабра, већ његову мобилну верзију.
На пример, ако коинтегрисани чланак у десктоп верзији тежи 378 КБ, онда је у мобилној верзији већ 126 КБ.
Друга верзија. Много тема, привремена забрана са Хабра
Када сам претражио интернет на тему вишенитности у Питхон-у, изабрао сам најједноставнију опцију са мултипроцессинг.думми, приметио сам да се проблеми појављују заједно са вишенитношћу.
СКЛите3 не жели да ради са више од једне нити.
фиксно check_same_thread=False, али ова грешка није једина, при покушају убацивања у базу понекад се јављају грешке које нисам могао да решим.
Стога одлучујем да напустим тренутно уметање чланака директно у базу података и, сећајући се коинтегрисаног решења, одлучујем да користим датотеке, јер нема проблема са вишенитним писањем у датотеку.
Хабр почиње да забрањује коришћење више од три нити.
Нарочито ревни покушаји да се пробије до Хабра могу се завршити забраном ип-а на неколико сати. Дакле, морате да користите само 3 теме, али ово је већ добро, пошто је време за понављање преко 100 чланака смањено са 26 на 12 секунди.
Вреди напоменути да је ова верзија прилично нестабилна, а преузимања повремено опадају на великом броју чланака.
асинц_в1.пи
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)
Трећа верзија. Коначни
Док сам отклањао грешке у другој верзији, открио сам да Хабр, одједном, има АПИ коме приступа мобилна верзија сајта. Учитава се брже од мобилне верзије, пошто је то само јсон, који чак и не треба да се анализира. На крају сам одлучио да поново напишем свој сценарио.
Дакле, пронашавши овај линк АПИ, можете почети да га анализирате.
асинц_в2.пи
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)
Садржи поља која се односе и на сам чланак и на аутора који га је написао.
АПИ.пнг
Нисам избацио пун јсон сваког чланка, већ сам сачувао само поља која су ми потребна:
id
ис_туториал
тиме_публисхед
наслов
садржина
цомментс_цоунт
ланг је језик на коме је чланак написан. До сада има само ен и ру.
тагс_стринг - све ознаке из поста
реад_цоунт
аутор
оцена — оцена чланка.
Тако сам, користећи АПИ, смањио време извршавања скрипте на 8 секунди на 100 урл-а.
Након што смо преузели податке који су нам потребни, потребно је да их обрадимо и унесемо у базу података. Ни са овим нисам имао проблема:
парсер.пи
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 ГБ. У компримованом облику - 495 МБ.
Укупно 37804 особе су аутори Хабреа. Подсећам вас да су ове статистике само из објава уживо.
Најпродуктивнији аутор на Хабреу - ализар - 8774 чланака.