Alle Habr in een databasis

Goeie middag. Dit is 2 jaar sedert ek dit geskryf het laaste artikel oor Habr-ontleding, en sommige dinge het verander.

Toe ek 'n kopie van Habr wou hê, het ek besluit om 'n ontleder te skryf wat al die inhoud van die outeurs in 'n databasis sou stoor. Hoe dit gebeur het en watter foute ek teëgekom het - jy kan lees onder die snit.

TL;DR - skakel na databasis

Eerste weergawe van die ontleder. Een draad, baie probleme

Om mee te begin, het ek besluit om 'n prototipe van 'n skrif te maak waarin die artikel onmiddellik na aflaai ontleed en in die databasis geplaas sou word. Sonder om twee keer te dink, het ek sqlite3 gebruik, want... dit was minder arbeidsintensief: jy hoef nie 'n plaaslike bediener te hê, skep, kyk, uitvee en sulke goed nie.

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 volgens die klassieke - ons gebruik Pragtige sop, versoeke en die vinnige prototipe is gereed. Dis net...

  • Die bladsy word in een draad afgelaai

  • As jy die uitvoering van die skrip onderbreek, sal die hele databasis nêrens heen gaan nie. Die commit word immers eers na al die ontleding uitgevoer.
    Natuurlik kan jy veranderinge aan die databasis aanbring na elke invoeging, maar dan sal die skripuitvoeringstyd aansienlik toeneem.

  • Om die eerste 100 000 artikels te ontleed het my 8 uur geneem.

Toe vind ek die gebruiker se artikel saamgevoeg, wat ek gelees het en verskeie life hacks gevind het om hierdie proses te bespoedig:

  • Die gebruik van multithreading versnel die aflaai aansienlik.
  • Jy kan nie die volledige weergawe van Habr ontvang nie, maar die mobiele weergawe daarvan.
    Byvoorbeeld, as 'n saamgevoegde artikel in die rekenaarweergawe 378 KB weeg, dan is dit in die mobiele weergawe reeds 126 KB.

Tweede weergawe. Baie drade, tydelike verbod van Habr

Toe ek die internet deursoek het oor die onderwerp van multithreading in python en die eenvoudigste opsie met multiprocessing.dummy gekies het, het ek opgemerk dat probleme saam met multithreading verskyn het.

SQLite3 wil nie met meer as een draad werk nie.
Vaste check_same_thread=False, maar hierdie fout is nie die enigste een nie; wanneer ek probeer om in die databasis in te voeg, ontstaan ​​soms foute wat ek nie kon oplos nie.

Daarom besluit ek om die onmiddellike invoeging van artikels direk in die databasis te laat vaar en, met die onthou van die saam-geïntegreerde oplossing, besluit ek om lêers te gebruik, aangesien daar geen probleme met multi-threaded skryf na 'n lêer is nie.

Habr begin verbied vir die gebruik van meer as drie drade.
Veral ywerige pogings om Habr te bereik, kan 'n paar uur lank 'n IP-verbod tot gevolg hê. U hoef dus slegs 3 drade te gebruik, maar dit is reeds goed, aangesien die tyd om deur 100 artikels te sorteer van 26 tot 12 sekondes verminder word.

Dit is opmerklik dat hierdie weergawe redelik onstabiel is, en die aflaai van 'n groot aantal artikels misluk periodiek.

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

Terwyl ek die tweede weergawe ontfout het, het ek ontdek dat Habr skielik 'n API het wat deur die mobiele weergawe van die webwerf verkry word. Dit laai vinniger as die mobiele weergawe, aangesien dit net json is, wat nie eers ontleed hoef te word nie. Op die ou end het ek besluit om my draaiboek weer oor te skryf.

So, ontdek hierdie skakel API, jy kan dit begin ontleed.

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)

Dit bevat velde wat verband hou met beide die artikel self en die skrywer wat dit geskryf het.

API.png

Alle Habr in een databasis

Ek het nie die volledige json van elke artikel gestort nie, maar het slegs die velde gestoor wat ek nodig gehad het:

  • id
  • is_tutoriaal
  • tyd_gepubliseer
  • titel
  • inhoud
  • kommentaar_tel
  • lang is die taal waarin die artikel geskryf is. Tot dusver bevat dit net en en ru.
  • tags_string — alle merkers van die pos
  • lees_telling
  • skrywer
  • telling — artikelgradering.

Dus, deur die API te gebruik, het ek die skrifuitvoeringstyd tot 8 sekondes per 100 url verminder.

Nadat ons die data wat ons benodig afgelaai het, moet ons dit verwerk en in die databasis invoer. Daar was ook geen probleme hiermee nie:

ontleder.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)

statistieke

Wel, tradisioneel, uiteindelik, kan u 'n paar statistieke uit die data onttrek:

  • Van die verwagte 490 406 is slegs 228 512 artikels afgelaai. Dit blyk dat meer as die helfte (261894) van die artikels oor Habré weggesteek of uitgevee is.
  • Die hele databasis, wat uit byna 'n halfmiljoen artikels bestaan, weeg 2.95 GB. In saamgeperste vorm - 495 MB.
  • In totaal is daar 37804 XNUMX skrywers op Habré. Laat ek jou daaraan herinner dat dit slegs statistieke van regstreekse plasings is.
  • Die mees produktiewe skrywer op Habré - alizar — 8774 artikels.
  • Top gegradeerde artikel - 1448 pluspunte
  • Mees gelese artikel — 1660841 kyke
  • Mees gepraat oor artikel — 2444 opmerkings

Wel, in die vorm van toppeTop 15 skrywersAlle Habr in een databasis
Top 15 volgens graderingAlle Habr in een databasis
Top 15 geleesAlle Habr in een databasis
Top 15 bespreekAlle Habr in een databasis

Bron: will.com

Voeg 'n opmerking