Tout Habr dans une seule base de données

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.

TL; DR — lien de base de données

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

Tout Habr dans une seule base de données

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.
  • Article le mieux noté — 1448 plus
  • Article le plus lu — 1660841 vues
  • Article le plus discuté — 2444 commentaires

Eh bien, sous la forme de hauts15 meilleurs auteursTout Habr dans une seule base de données
Top 15 par noteTout Habr dans une seule base de données
Top 15 des lecturesTout Habr dans une seule base de données
Top 15 discutéTout Habr dans une seule base de données

Source: habr.com

Ajouter un commentaire