Ves Habr v eni bazi podatkov

Dober večer. Minili sta 2 leti odkar je bila napisana. zadnji članek o razčlenjevanju Habra in nekatere točke so se spremenile.

Ko sem želel imeti kopijo Habra, sem se odločil, da napišem razčlenjevalec, ki bi shranil vso vsebino avtorjev v bazo. Kako se je to zgodilo in na katere napake sem naletel - si lahko preberete pod rezom.

TLDR- povezava do baze podatkov

Prva različica razčlenjevalnika. Ena nit, veliko težav

Za začetek sem se odločil narediti prototip skripte, v kateri bi članek takoj po prenosu razčlenil in dal v bazo. Brez dvakratnega razmišljanja sem uporabil sqlite3, ker. bilo je manj delovno intenzivno: ni potrebe po lokalnem strežniku, ustvarjenem-pogledanem-izbrisanem in podobnih stvareh.

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)

Vse je klasično - uporabljamo Beautiful Soup, zahtevamo in hitri prototip je pripravljen. To je samo …

  • Prenos strani je v eni niti

  • Če prekinete izvajanje skripta, potem celotna baza podatkov ne bo šla nikamor. Navsezadnje se potrditev izvede šele po celotnem razčlenjevanju.
    Seveda lahko po vsakem vstavljanju potrdite spremembe v bazo podatkov, vendar se bo takrat čas izvajanja skripta znatno povečal.

  • Razčlenjevanje prvih 100 člankov mi je vzelo 000 ur.

Nato najdem članek uporabnika kointegriran, ki sem ga prebral in našel nekaj življenjskih trikov za pospešitev tega procesa:

  • Uporaba večnitnosti včasih pospeši prenos.
  • Ne morete dobiti polne različice habra, ampak njegovo mobilno različico.
    Na primer, če kointegrirani članek v namizni različici tehta 378 KB, potem je v mobilni različici že 126 KB.

Druga različica. Veliko tem, začasna prepoved dostopa do Habra

Ko sem brskal po internetu na temo večnitnosti v pythonu in sem izbral najpreprostejšo možnost z multiprocessing.dummy, sem opazil, da se težave pojavljajo skupaj z večnitnostjo.

SQLite3 ne želi delati z več kot eno nitjo.
fiksno check_same_thread=False, vendar ta napaka ni edina, pri poskusu vstavljanja v bazo se včasih pojavijo napake, ki jih nisem mogel odpraviti.

Zato se odločim, da opustim takojšnje vstavljanje člankov neposredno v bazo in se ob spominu na kointegrirano rešitev odločim za uporabo datotek, saj z večnitnim pisanjem v datoteko ni težav.

Habr začne banati za uporabo več kot treh niti.
Še posebej vneti poskusi priti do Habra se lahko končajo s prepovedjo ip za nekaj ur. Uporabiti morate torej samo 3 niti, vendar je že to dobro, saj se čas za ponovitev več kot 100 člankov zmanjša s 26 na 12 sekund.

Omeniti velja, da je ta različica precej nestabilna in prenosi občasno padajo na veliko število člankov.

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)

Tretja verzija. Končno

Med odpravljanjem napak v drugi različici sem odkril, da ima Habr kar naenkrat API, do katerega dostopa mobilna različica spletnega mesta. Nalaga se hitreje kot mobilna različica, saj je samo json, ki ga sploh ni treba razčleniti. Na koncu sem se odločil, da znova napišem svoj scenarij.

Torej, ko sem našel ta povezava API, ga lahko začnete razčlenjevati.

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)

Vsebuje polja, povezana s samim člankom in avtorjem, ki ga je napisal.

API.png

Ves Habr v eni bazi podatkov

Nisem izpisal celotnega json-a vsakega članka, ampak sem shranil samo polja, ki sem jih potreboval:

  • id
  • is_tutorial
  • čas_objave
  • Naslov
  • vsebina
  • comments_count
  • lang je jezik, v katerem je članek napisan. Zaenkrat ima le en in ru.
  • tags_string - vse oznake iz objave
  • reading_count
  • Avtor
  • ocena — ocena članka.

Tako sem z uporabo API-ja zmanjšal čas izvajanja skripte na 8 sekund na 100 url.

Ko smo prenesli potrebne podatke, jih moramo obdelati in vnesti v bazo. Tudi s tem nisem imel nobenih težav:

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)

statistika

No, tradicionalno lahko končno izvlečete nekaj statističnih podatkov iz podatkov:

  • Od pričakovanih 490 prenosov je bilo prenesenih le 406 člankov. Izkazalo se je, da je bila več kot polovica (228) člankov na Habréju skrita ali izbrisana.
  • Celotna baza, ki jo sestavlja skoraj pol milijona člankov, tehta 2.95 GB. V stisnjeni obliki - 495 MB.
  • Skupno je 37804 ljudi avtorjev Habréja. Opozarjam vas, da so te statistike samo iz objav v živo.
  • Najbolj produktiven avtor na Habréju - alizar - 8774 člankov.
  • Najbolje ocenjen članek — 1448 plusov
  • Najbolj bran članek — 1660841 ogledov
  • Najbolj obravnavan članek — 2444 komentarjev

No, v obliki vrhov15 najboljših avtorjevVes Habr v eni bazi podatkov
Top 15 po oceniVes Habr v eni bazi podatkov
Top 15 branjaVes Habr v eni bazi podatkov
15 najboljših obravnavanihVes Habr v eni bazi podatkov

Vir: www.habr.com

Dodaj komentar