Alle Habr in één database

Goedemiddag. Het is 2 jaar geleden dat het is geschreven. laatste artikel over het ontleden van Habr, en sommige punten zijn veranderd.

Toen ik een kopie van Habr wilde hebben, besloot ik een parser te schrijven die alle inhoud van de auteurs in de database zou opslaan. Hoe het gebeurde en welke fouten ik tegenkwam - lees je onder de snit.

TLDR- database koppeling

De eerste versie van de parser. Eén topic, veel problemen

Om te beginnen besloot ik een scriptprototype te maken, waarin het artikel direct na het downloaden zou worden geparseerd en in de database zou worden geplaatst. Zonder er twee keer over na te denken, gebruikte ik sqlite3, omdat. het was minder arbeidsintensief: geen behoefte aan een lokale server, gemaakt-zag-verwijderd en dat soort dingen.

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)

Alles is klassiek - we gebruiken Beautiful Soup, aanvragen en een snel prototype is klaar. Dat is gewoon…

  • Het downloaden van pagina's gebeurt in één thread

  • Als u de uitvoering van het script onderbreekt, gaat de hele database nergens heen. De commit wordt immers pas uitgevoerd na al het parsen.
    Natuurlijk kunt u na elke invoeging wijzigingen in de database doorvoeren, maar dan neemt de uitvoeringstijd van het script aanzienlijk toe.

  • Het ontleden van de eerste 100 artikelen kostte me 000 uur.

Vervolgens vind ik het artikel van de gebruiker gecoïntegreerd, die ik las en een paar life-hacks vond om dit proces te versnellen:

  • Het gebruik van multithreading versnelt het downloaden soms.
  • U kunt niet de volledige versie van de habr krijgen, maar de mobiele versie.
    Als een meegeïntegreerd artikel in de desktopversie bijvoorbeeld 378 KB weegt, dan is dat in de mobiele versie al 126 KB.

Tweede versie. Veel discussies, tijdelijke ban van Habr

Toen ik het internet doorzocht over het onderwerp multithreading in python, koos ik de eenvoudigste optie met multiprocessing.dummy, ik merkte dat er problemen optraden samen met multithreading.

SQLite3 wil niet met meer dan één thread werken.
vast check_same_thread=False, maar deze fout is niet de enige, wanneer ik probeer in de database in te voegen, treden er soms fouten op die ik niet kon oplossen.

Daarom besluit ik af te zien van het rechtstreeks invoegen van artikelen in de database en, denkend aan de gecoïntegreerde oplossing, besluit ik bestanden te gebruiken, omdat er geen problemen zijn met multi-threaded schrijven naar een bestand.

Habr begint met bannen voor het gebruik van meer dan drie threads.
Vooral ijverige pogingen om door te dringen tot Habr kunnen eindigen met een ip-ban voor een paar uur. Je hoeft dus maar 3 threads te gebruiken, maar dit is al goed, aangezien de tijd om meer dan 100 artikelen te herhalen wordt teruggebracht van 26 naar 12 seconden.

Het is vermeldenswaard dat deze versie nogal onstabiel is en dat de downloads periodiek op een groot aantal artikelen vallen.

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)

Derde versie. Laatste

Tijdens het debuggen van de tweede versie ontdekte ik dat Habr ineens een API heeft waartoe de mobiele versie van de site toegang heeft. Het laadt sneller dan de mobiele versie, omdat het gewoon json is, dat niet eens hoeft te worden geparseerd. Uiteindelijk besloot ik mijn script opnieuw te herschrijven.

Dus gevonden hebben deze link API, je kunt beginnen met het ontleden.

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)

Het bevat velden die betrekking hebben op zowel het artikel zelf als de auteur die het heeft geschreven.

API.png

Alle Habr in één database

Ik heb niet de volledige json van elk artikel gedumpt, maar alleen de velden opgeslagen die ik nodig had:

  • id
  • is_zelfstudie
  • tijd_gepubliceerd
  • titel
  • content
  • reacties_count
  • lang is de taal waarin het artikel is geschreven. Tot nu toe heeft het alleen en en ru.
  • tags_string - alle tags van het bericht
  • lezen_tellen
  • auteur
  • score — artikelbeoordeling.

Dus met behulp van de API heb ik de uitvoeringstijd van het script teruggebracht tot 8 seconden per 100 url.

Nadat we de gegevens hebben gedownload die we nodig hebben, moeten we deze verwerken en invoeren in de database. Ik had hier ook geen problemen mee:

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)

statistiek

Nou, traditioneel kun je ten slotte wat statistieken uit de gegevens halen:

  • Van de verwachte 490 downloads zijn er slechts 406 artikelen gedownload. Het blijkt dat meer dan de helft (228) van de artikelen over Habré is verborgen of verwijderd.
  • De gehele database, bestaande uit bijna een half miljoen artikelen, weegt 2.95 GB. In gecomprimeerde vorm - 495 MB.
  • In totaal zijn 37804 mensen de auteurs van Habré. Ik herinner je eraan dat deze statistieken alleen afkomstig zijn van live berichten.
  • De meest productieve auteur op Habré - Alizar - 8774 artikelen.
  • Best beoordeeld artikel — 1448 plussen
  • Meest gelezen artikel — 1660841 keer bekeken
  • Meest besproken artikel — 2444 reacties

Nou ja, in de vorm van topjesTop 15 auteursAlle Habr in één database
Top 15 op beoordelingAlle Habr in één database
Top 15 gelezenAlle Habr in één database
Top 15 besprokenAlle Habr in één database

Bron: www.habr.com

Voeg een reactie