Весь Хабр в одной базе

от автора

Добрый день. Прошло уже 2 года с момента написания последней статьи про парсинг Хабра, и некоторые моменты изменились.

Когда я захотел иметь у себя копию хабра, я решил написать парсер, который бы сохранил весь контент авторов в базу данных. Как это вышло и с какими ошибками я встретился — можете прочитать под катом.

TL;DR — ссылка на базу данных

Первая версия парсера. Один поток, много проблем

Для начала, я решил сделать прототип скрипта, в котором бы сразу при скачивании статья парсилась и помещалась в базу данных. Недолго думав, использовал 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)

Всё по классике — используем Beautiful Soup, requests и быстрый прототип готов. Вот только…

  • Скачивание страницы идет в один поток

  • Если оборвать выполнение скрипта, то вся база уйдет в никуда. Ведь выполнение коммита только после всего парсинга.
    Конечно, можно закреплять изменения в базе после каждой вставки, но тогда и время выполнения скрипта увеличится в разы.

  • Парсинг первых 100 000 статей у меня занял 8 часов.

Дальше я нахожу статью пользователя cointegrated, которую я прочитал и нашел несколько лайфхаков, позволяющих ускорить сей процесс:

  • Использование многопоточности ускоряет скачивание в разы.
  • Можно получать не полную версию хабра, а его мобильную версию.
    Например, если статья cointegrated в десктопной версии весит 378 Кб, то в мобильной уже 126 Кб.

Вторая версия. Много потоков, временный бан от Хабра

Когда я прошерстил интернет на тему многопоточности в python, выбрал наиболее простой вариант с multiprocessing.dummy, то я заметил, что вместе с многопоточностью появились проблемы.

SQLite3 не хочет работать с более чем одним потоком.
Фиксится check_same_thread=False, но эта ошибка не единственная, при попытке вставки в базу иногда возникают ошибки, которые я так и не смог решить.

Поэтому я решаю отказаться от мгновенной вставки статей сразу в базу и, вспоминая решение cointegrated, решаю использовать файлы, т.к никаких проблем с многопоточной записью в файл нет.

Хабр начинает банить за использование более чем трех потоков.
Особо рьяные попытки достучаться до Хабра могут закончится баном 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)

Третья версия. Финальная

Отлаживая вторую версию, я обнаружил, что у Хабра, внезапно, есть API, к которому обращается мобильная версия сайта. Загружается оно быстрее, чем мобильная версия, так как это просто json, который даже парсить особо не нужно. В итоге я решил заново переписать мой скрипт.

Итак, обнаружив по этой ссылке API, можно приступать к его парсингу.

API.png

В нем присутствует поля, относящиеся как к самой статье, так и к автору, который её написал.

Я не стал дампить полный json каждой статьи, а сохранял лишь нужные мне поля:

  • id
  • is_tutorial
  • time_published
  • title
  • content
  • comments_count
  • lang — язык, на котором написана статья. Пока что в ней только en и ru.
  • tags_string — все теги из поста
  • reading_count
  • author
  • score — рейтинг статьи.

Таким образом, используя API, я уменьшил время выполнения скрипта до 8 секунд на 100 url.

После того, как мы скачали нужные нам данные, нужно их обработать и внести в базу данных. С этим тоже не возникло проблем:

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) 

Статистика

Ну и традиционно, напоследок можно извлечь немного статистики из данных:

  • Из ожидаемых 490 406 было скачано лишь 228 512 статей. Получается, что более половины(261894) статей на хабре было скрыто или удалено.
  • Вся база, состоящая из почти полумиллиона статей, весит 2.95 Гб. В сжатом виде — 495 Мб.
  • Всего на Хабре авторами являются 37804 человек. Напоминаю, что это статистика только из живых постов.
  • Самый продуктивный автор на Хабре — alizar — 8774 статьи.
  • Статья с самым большим рейтингом — 1448 плюсов
  • Самая читаемая статья — 1660841 просмотров
  • Самая обсуждаемая статья — 2444 комментария

Ну и в виде топов

Топ 15 авторов

Топ 15 по рейтингу

Топ 15 читаемых

Топ 15 обсуждаемых

ссылка на оригинал статьи https://habr.com/ru/post/490820/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *