Alle Habr i én database

God ettermiddag. Det er 2 år siden det ble skrevet. siste artikkel om å analysere Habr, og noen punkter har endret seg.

Da jeg ønsket å ha en kopi av Habr, bestemte jeg meg for å skrive en parser som ville lagre alt innholdet til forfatterne i databasen. Hvordan det skjedde og hvilke feil jeg møtte – kan du lese under kuttet.

TLDR- databasekobling

Den første versjonen av parseren. En tråd, mange problemer

Til å begynne med bestemte jeg meg for å lage en skriptprototype der artikkelen ville bli analysert og plassert i databasen umiddelbart etter nedlasting. Uten å tenke to ganger brukte jeg sqlite3, fordi. det var mindre arbeidskrevende: du trenger ikke å ha en lokal server, opprettet-så-slettet og sånt.

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 bruker Beautiful Soup, forespørsler og en rask prototype er klar. Det er bare…

  • Sidenedlasting er i én tråd

  • Hvis du avbryter kjøringen av skriptet, vil hele databasen gå ingensteds. Tross alt utføres commit først etter all parsing.
    Selvfølgelig kan du foreta endringer i databasen etter hver innsetting, men da vil skriptutførelsestiden øke betydelig.

  • Å analysere de første 100 000 artiklene tok meg 8 timer.

Deretter finner jeg brukerens artikkel kointegrert, som jeg leste og fant noen life hacks for å fremskynde denne prosessen:

  • Ved å bruke multithreading øker nedlastingen til tider.
  • Du kan ikke få fullversjonen av habr, men mobilversjonen.
    For eksempel, hvis en kointegrert artikkel i desktopversjonen veier 378 KB, er den allerede 126 KB i mobilversjonen.

Andre versjon. Mange tråder, midlertidig forbud fra Habr

Da jeg saumfart på Internett om temaet multithreading i python, valgte jeg det enkleste alternativet med multiprocessing.dummy, jeg la merke til at problemer dukket opp sammen med multithreading.

SQLite3 ønsker ikke å jobbe med mer enn én tråd.
Fikset check_same_thread=False, men denne feilen er ikke den eneste, når jeg prøver å sette inn i databasen, oppstår det noen ganger feil som jeg ikke kunne løse.

Derfor bestemmer jeg meg for å forlate øyeblikkelig innsetting av artikler direkte i databasen, og husker den kointegrerte løsningen, og bestemmer meg for å bruke filer, fordi det ikke er noen problemer med flertrådsskriving til en fil.

Habr begynner å utestenge for bruk av mer enn tre tråder.
Spesielt ivrige forsøk på å komme seg gjennom til Habr kan ende opp med ip-forbud i et par timer. Så du må bare bruke 3 tråder, men dette er allerede bra, siden tiden for å iterere over 100 artikler reduseres fra 26 til 12 sekunder.

Det er verdt å merke seg at denne versjonen er ganske ustabil, og nedlastinger faller med jevne mellomrom av på et stort antall 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 versjon. Endelig

Mens jeg feilsøkte den andre versjonen, oppdaget jeg at Habr plutselig har et API som mobilversjonen av nettstedet får tilgang til. Den laster raskere enn mobilversjonen, siden det bare er json, som ikke engang trenger å analyseres. Til slutt bestemte jeg meg for å skrive om manuset mitt igjen.

Så, etter å ha funnet denne linken API, kan du begynne å analysere 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 inneholder felt knyttet både til selve artikkelen og til forfatteren som skrev den.

API.png

Alle Habr i én database

Jeg dumpet ikke hele json-en for hver artikkel, men lagret bare feltene jeg trengte:

  • id
  • is_tutorial
  • tid_publisert
  • tittel
  • innhold
  • comments_count
  • lang er språket artikkelen er skrevet på. Så langt har det bare en og ru.
  • tags_string - alle tagger fra innlegget
  • lese_antall
  • forfatter
  • score — artikkelvurdering.

Ved å bruke API reduserte jeg skriptutførelsestiden til 8 sekunder per 100 url.

Etter at vi har lastet ned dataene vi trenger, må vi behandle dem og legge dem inn i databasen. Jeg hadde 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)

Statistikk

Vel, tradisjonelt, endelig, kan du trekke ut litt statistikk fra dataene:

  • Av de forventede 490 406 nedlastingene ble bare 228 512 artikler lastet ned. Det viser seg at mer enn halvparten (261894) av artiklene på Habré ble skjult eller slettet.
  • Hele databasen, som består av nesten en halv million artikler, veier 2.95 GB. I komprimert form - 495 MB.
  • Totalt er 37804 personer forfatterne av Habré. Jeg minner om at denne statistikken kun er fra live-innlegg.
  • Den mest produktive forfatteren på Habré - alizar - 8774 artikler.
  • Topprangerte artikkel — 1448 plusser
  • Mest leste artikkelen — 1660841 visninger
  • Mest diskutert artikkel — 2444 kommentarer

Vel, i form av topperTopp 15 forfattereAlle Habr i én database
Topp 15 etter vurderingAlle Habr i én database
Topp 15 lestAlle Habr i én database
Topp 15 diskutertAlle Habr i én database

Kilde: www.habr.com

Legg til en kommentar