Alle Habr i én database

God eftermiddag. Det er 2 år siden den blev skrevet. sidste artikel om at parse Habr, og nogle punkter er ændret.

Da jeg ville have en kopi af Habr, besluttede jeg at skrive en parser, der ville gemme alt indholdet af forfatterne i databasen. Hvordan det skete og hvilke fejl jeg stødte på – du kan læse under klippet.

TLDR- database link

Den første version af parseren. Én tråd, mange problemer

Til at begynde med besluttede jeg at lave en script-prototype, hvor artiklen ville blive parset og placeret i databasen umiddelbart efter download. Uden at tænke to gange brugte jeg sqlite3, fordi. det var mindre arbejdskrævende: ingen grund til at have en lokal server, oprettet-så-slettet og sådan noget.

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)

Alt er klassisk - vi bruger Smuk suppe, forespørgsler og en hurtig prototype er klar. Det er bare…

  • Sidedownload er i én tråd

  • Hvis du afbryder udførelsen af ​​scriptet, vil hele databasen gå ingen vegne. Når alt kommer til alt, udføres commit først efter al parsing.
    Selvfølgelig kan du foretage ændringer i databasen efter hver indsættelse, men så vil scriptudførelsestiden stige betydeligt.

  • At analysere de første 100 artikler tog mig 000 timer.

Dernæst finder jeg brugerens artikel kointegreret, som jeg læste og fandt et par life hacks for at fremskynde denne proces:

  • Brug af multithreading fremskynder downloading til tider.
  • Du kan ikke få den fulde version af habr, men dens mobile version.
    For eksempel, hvis en cointegrated artikel i desktopversionen vejer 378 KB, så er den i mobilversionen allerede 126 KB.

Anden version. Mange tråde, midlertidigt forbud fra Habr

Da jeg gennemsøgte internettet om emnet multithreading i python, valgte jeg den enkleste mulighed med multiprocessing.dummy, jeg bemærkede, at der dukkede problemer op sammen med multithreading.

SQLite3 ønsker ikke at arbejde med mere end én tråd.
fast check_same_thread=False, men denne fejl er ikke den eneste, når jeg forsøger at indsætte i databasen, opstår der nogle gange fejl, som jeg ikke kunne løse.

Derfor beslutter jeg mig for at opgive den øjeblikkelige indsættelse af artikler direkte i databasen, og husker den kointegrerede løsning, beslutter jeg at bruge filer, fordi der ikke er problemer med multi-threaded skrivning til en fil.

Habr begynder at banne for at bruge mere end tre tråde.
Særligt nidkære forsøg på at komme igennem til Habr kan ende med et ip-forbud i et par timer. Så du skal kun bruge 3 tråde, men det er allerede godt, da tiden til at iterere over 100 artikler er reduceret fra 26 til 12 sekunder.

Det er værd at bemærke, at denne version er ret ustabil, og downloads falder med jævne mellemrum af på et stort antal artikler.

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 version. Finale

Mens jeg fejlede den anden version, opdagede jeg, at Habr pludselig har en API, som mobilversionen af ​​webstedet tilgår. Den indlæses hurtigere end mobilversionen, da det kun er json, som ikke engang skal parses. Til sidst besluttede jeg mig for at omskrive mit manuskript igen.

Altså efter at have fundet dette link API, kan du begynde at parse 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 indeholder felter relateret både til selve artiklen og til forfatteren, der har skrevet den.

API.png

Alle Habr i én database

Jeg dumpede ikke den fulde json for hver artikel, men gemte kun de felter, jeg havde brug for:

  • id
  • is_tutorial
  • tid_udgivet
  • titel
  • indhold
  • comments_count
  • lang er det sprog, artiklen er skrevet på. Indtil videre har det kun en og ru.
  • tags_string - alle tags fra indlægget
  • læsning_antal
  • forfatter
  • score — artiklens vurdering.

Ved at bruge API'et reducerede jeg scriptudførelsestiden til 8 sekunder pr. 100 url.

Efter at vi har downloadet de data, vi skal bruge, skal vi behandle dem og indtaste dem i databasen. Jeg havde heller ingen problemer med dette:

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)

Statistikker

Nå, traditionelt, endelig, kan du udtrække nogle statistikker fra dataene:

  • Af de forventede 490 downloads blev kun 406 artikler downloadet. Det viser sig, at mere end halvdelen (228) af artiklerne om Habré blev skjult eller slettet.
  • Hele databasen, der består af næsten en halv million artikler, vejer 2.95 GB. I komprimeret form - 495 MB.
  • I alt er 37804 personer forfatterne til Habré. Jeg minder dig om, at disse statistikker kun er fra liveopslag.
  • Den mest produktive forfatter på Habré - alizar - 8774 artikler.
  • Top bedømte artikel — 1448 plusser
  • Mest læste artikel — 1660841 visninger
  • Mest diskuterede artikel — 2444 kommentarer

Nå, i form af toppeTop 15 forfattereAlle Habr i én database
Top 15 efter vurderingAlle Habr i én database
Top 15 læstAlle Habr i én database
Top 15 diskuteretAlle Habr i én database

Kilde: www.habr.com

Tilføj en kommentar