God eftermiddag. Det har gått 2 år sedan det skrevs. sista artikeln om att analysera Habr, och vissa punkter har ändrats.
När jag ville ha en kopia av Habr bestämde jag mig för att skriva en parser som skulle spara allt innehåll från författarna till databasen. Hur det gick till och vilka fel jag stötte på – du kan läsa under klippet.
Den första versionen av parsern. En tråd, många problem
Till att börja med bestämde jag mig för att göra en skriptprototyp där artikeln skulle tolkas och placeras i databasen direkt efter nedladdning. Utan att tänka två gånger använde jag sqlite3, eftersom. det var mindre arbetskrävande: inget behov av att ha en lokal server, skapad-såg-raderad och sånt.
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)
Allt är klassiskt - vi använder Beautiful Soup, önskemål och en snabb prototyp är klar. Det är bara…
Sidnedladdning finns i en tråd
Om du avbryter körningen av skriptet kommer hela databasen att gå ingenstans. När allt kommer omkring utförs commit först efter all analys.
Naturligtvis kan du göra ändringar i databasen efter varje infogning, men då kommer skriptexekveringstiden att öka avsevärt.
Att analysera de första 100 000 artiklarna tog mig 8 timmar.
Därefter hittar jag användarens artikel samintegrerad, som jag läste och hittade några life hacks för att påskynda den här processen:
Att använda multithreading snabbar upp nedladdningen ibland.
Du kan inte få den fullständiga versionen av habr, utan dess mobilversion.
Till exempel, om en samintegrerad artikel i desktopversionen väger 378 KB, är den redan 126 KB i mobilversionen.
Andra versionen. Många trådar, tillfälligt förbud från Habr
När jag letade igenom Internet på ämnet multithreading i python valde jag det enklaste alternativet med multiprocessing.dummy, jag märkte att problem uppstod tillsammans med multithreading.
SQLite3 vill inte fungera med mer än en tråd.
Fast check_same_thread=False, men detta fel är inte det enda, när jag försöker infoga i databasen uppstår ibland fel som jag inte kunde lösa.
Därför bestämmer jag mig för att överge omedelbar infogning av artiklar direkt i databasen och, med tanke på den samintegrerade lösningen, bestämmer jag mig för att använda filer, eftersom det inte finns några problem med flertrådsskrivning till en fil.
Habr börjar förbjuda för att använda mer än tre trådar.
Särskilt nitiska försök att ta sig igenom till Habr kan sluta med ett ip-förbud i ett par timmar. Så du måste bara använda 3 trådar, men det här är redan bra, eftersom tiden för att iterera över 100 artiklar minskar från 26 till 12 sekunder.
Det är värt att notera att den här versionen är ganska instabil och nedladdningar faller periodvis av på ett stort antal artiklar.
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)
Tredje versionen. Slutlig
När jag felsökte den andra versionen upptäckte jag att Habr helt plötsligt har ett API som den mobila versionen av webbplatsen kommer åt. Den laddas snabbare än mobilversionen, eftersom det bara är json, som inte ens behöver analyseras. Till slut bestämde jag mig för att skriva om mitt manus igen.
Så, efter att ha hittat denna länk API, du kan börja analysera det.
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)
Den innehåller fält relaterade till både själva artikeln och författaren som skrev den.
API.png
Jag dumpade inte hela json för varje artikel, utan sparade bara de fält jag behövde:
id
is_tutorial
tid_publicerad
rubricerade
innehåll
comments_count
lang är det språk som artikeln är skriven på. Hittills har det bara en och ru.
tags_string - alla taggar från inlägget
läsning_antal
Författaren
poäng — betyg av artikeln.
Med hjälp av API:t minskade jag skriptkörningstiden till 8 sekunder per 100 url.
Efter att vi har laddat ner den data vi behöver behöver vi bearbeta den och lägga in den i databasen. Jag hade inga problem med detta heller:
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)
Statistik
Tja, traditionellt, äntligen, kan du extrahera lite statistik från data:
Av de förväntade 490 406 nedladdningarna laddades endast 228 512 artiklar ner. Det visar sig att mer än hälften (261894) av artiklarna om Habré gömdes eller raderades.
Hela databasen, som består av nästan en halv miljon artiklar, väger 2.95 GB. I komprimerad form - 495 MB.
Totalt är 37804 personer författare till Habré. Jag påminner er om att denna statistik endast är från liveinlägg.
Den mest produktiva författaren om Habré - alizar - 8774 artiklar.