Bon après-midi. Cela fait 2 ans qu'il a été écrit. dernier article sur l'analyse Habr, et certains points ont changé.
Lorsque j'ai voulu avoir une copie de Habr, j'ai décidé d'écrire un parseur qui enregistrerait tout le contenu des auteurs dans la base de données. Comment cela s'est passé et quelles erreurs j'ai rencontrées - vous pouvez lire sous la coupe.
La première version de l'analyseur. Un fil, beaucoup de problèmes
Pour commencer, j'ai décidé de faire un prototype de script, dans lequel l'article serait analysé immédiatement après le téléchargement et placé dans la base de données. Sans réfléchir à deux fois, j'ai utilisé sqlite3, parce que. c'était moins laborieux : pas besoin d'avoir un serveur local, créé-semblé-supprimé et des trucs comme ça.
un_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)
Tout est classique - nous utilisons Beautiful Soup, demandes et un prototype rapide est prêt. C'est juste…
Le téléchargement de la page est dans un fil
Si vous interrompez l'exécution du script, toute la base de données n'ira nulle part. Après tout, la validation n'est effectuée qu'après toute l'analyse.
Bien sûr, vous pouvez valider les modifications apportées à la base de données après chaque insertion, mais le temps d'exécution du script augmentera considérablement.
L'analyse des 100 000 premiers articles m'a pris 8 heures.
Ensuite, je trouve l'article de l'utilisateur cointégré, que j'ai lu et trouvé quelques astuces pour accélérer ce processus :
L'utilisation du multithreading accélère parfois le téléchargement.
Vous pouvez obtenir non pas la version complète du habr, mais sa version mobile.
Par exemple, si un article cointégré dans la version desktop pèse 378 Ko, alors dans la version mobile il fait déjà 126 Ko.
Deuxième version. Beaucoup de discussions, interdiction temporaire de Habr
Lorsque j'ai parcouru Internet sur le sujet du multithreading en python, j'ai choisi l'option la plus simple avec multiprocessing.dummy, j'ai remarqué que des problèmes apparaissaient avec le multithreading.
SQLite3 ne veut pas travailler avec plus d'un thread.
fixé check_same_thread=False, mais cette erreur n'est pas la seule, lors d'une tentative d'insertion dans la base de données, des erreurs se produisent parfois que je n'ai pas pu résoudre.
Par conséquent, je décide d'abandonner l'insertion instantanée d'articles directement dans la base de données et, en me souvenant de la solution cointégrée, je décide d'utiliser des fichiers, car il n'y a pas de problèmes d'écriture multithread dans un fichier.
Habr commence à interdire l'utilisation de plus de trois threads.
Les tentatives particulièrement zélées pour atteindre Habr peuvent se terminer par une interdiction IP pendant quelques heures. Il faut donc n'utiliser que 3 threads, mais c'est déjà bien, donc le temps pour itérer sur 100 articles est réduit de 26 à 12 secondes.
Il convient de noter que cette version est plutôt instable et que le téléchargement tombe périodiquement sur un grand nombre d'articles.
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)
Troisième version. Final
En déboguant la deuxième version, j'ai découvert que Habr, tout à coup, avait une API à laquelle la version mobile du site accède. Il se charge plus rapidement que la version mobile, car il ne s'agit que de json, qui n'a même pas besoin d'être analysé. En fin de compte, j'ai décidé de réécrire mon script à nouveau.
Ainsi, ayant trouvé ce lien API, vous pouvez commencer à l'analyser.
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)
Il contient des champs liés à la fois à l'article lui-même et à l'auteur qui l'a écrit.
API.png
Je n'ai pas vidé le json complet de chaque article, mais j'ai enregistré uniquement les champs dont j'avais besoin :
id
est_tutoriel
heure_publié
titre
contenu
commentaires_count
lang est la langue dans laquelle l'article est écrit. Jusqu'à présent, il n'a que en et ru.
tags_string - tous les tags de la publication
nombre_de_lectures
auteur
score - évaluation de l'article.
Ainsi, en utilisant l'API, j'ai réduit le temps d'exécution du script à 8 secondes pour 100 url.
Après avoir téléchargé les données dont nous avons besoin, nous devons les traiter et les entrer dans la base de données. Je n'ai pas eu de problème avec ça non plus :
analyseur.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)
Statistiques
Eh bien, traditionnellement, enfin, vous pouvez extraire quelques statistiques des données :
Sur les 490 406 téléchargements attendus, seuls 228 512 articles ont été téléchargés. Il s'avère que plus de la moitié (261894) des articles sur Habré ont été masqués ou supprimés.
L'ensemble de la base de données, composée de près d'un demi-million d'articles, pèse 2.95 Go. Sous forme compressée - 495 Mo.
Au total, 37804 personnes sont les auteurs de Habré. Je vous rappelle que ces statistiques ne proviennent que des publications en direct.
L'auteur le plus productif sur Habré - Alizar - 8774 articles.