Pob Habr mewn un gronfa ddata

Prynhawn Da. Mae 2 flynedd ers iddo gael ei ysgrifennu. erthygl olaf am ddosrannu Habr, ac mae rhai pwyntiau wedi newid.

Pan oeddwn am gael copi o Habr, penderfynais ysgrifennu parser a fyddai'n arbed holl gynnwys yr awduron i'r gronfa ddata. Sut y digwyddodd a pha wallau y deuthum ar eu traws - gallwch ddarllen o dan y toriad.

TLDR- cyswllt cronfa ddata

Fersiwn gyntaf y parser. Un llinyn, llawer o broblemau

I ddechrau, penderfynais wneud prototeip sgript, lle byddai'r erthygl yn cael ei dosrannu ar unwaith ar ôl ei lawrlwytho a'i roi yn y gronfa ddata. Heb feddwl ddwywaith, defnyddiais sqlite3, oherwydd. roedd yn llai llafurddwys: dim angen cael gweinydd lleol, wedi'i greu-edrych-wedi'i ddileu a phethau felly.

un_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)

Mae popeth yn glasurol - rydym yn defnyddio Beautiful Soup, ceisiadau ac mae prototeip cyflym yn barod. Dyna dim ond…

  • Mae lawrlwytho tudalen mewn un edefyn

  • Os byddwch yn torri ar draws gweithrediad y sgript, yna ni fydd y gronfa ddata gyfan yn mynd i unman. Wedi'r cyfan, dim ond ar ôl yr holl ddosrannu y cyflawnir yr ymrwymiad.
    Wrth gwrs, gallwch chi ymrwymo newidiadau i'r gronfa ddata ar ôl pob mewnosodiad, ond yna bydd yr amser gweithredu sgript yn cynyddu'n sylweddol.

  • Cymerodd 100 awr i mi ddosrannu'r 000 o erthyglau cyntaf.

Nesaf dwi'n dod o hyd i erthygl y defnyddiwr cyfun, a ddarllenais a dod o hyd i ychydig o haciau bywyd i gyflymu'r broses hon:

  • Mae defnyddio multithreading yn cyflymu lawrlwytho ar adegau.
  • Ni allwch gael y fersiwn lawn o'r habr, ond ei fersiwn symudol.
    Er enghraifft, os yw erthygl gyfun yn y fersiwn bwrdd gwaith yn pwyso 378 KB, yna yn y fersiwn symudol mae eisoes yn 126 KB.

Ail fersiwn. Llawer o edafedd, gwaharddiad dros dro o Habr

Pan sgwriais y Rhyngrwyd ar bwnc multithreading yn python, dewisais yr opsiwn symlaf gyda multiprocessing.dummy, sylwais fod problemau'n ymddangos ynghyd â multithreading.

Nid yw SQLite3 eisiau gweithio gyda mwy nag un edefyn.
sefydlog check_same_thread=False, ond nid y gwall hwn yw'r unig un, wrth geisio mewnosod yn y gronfa ddata, mae gwallau weithiau'n digwydd na allwn eu datrys.

Felly, penderfynaf roi'r gorau i fewnosod erthyglau ar unwaith yn uniongyrchol i'r gronfa ddata ac, gan gofio'r datrysiad cyfunol, penderfynaf ddefnyddio ffeiliau, oherwydd nid oes unrhyw broblemau gydag ysgrifennu aml-edau i ffeil.

Mae Habr yn dechrau gwahardd am ddefnyddio mwy na thair edefyn.
Gall ymdrechion arbennig o selog i fynd drwodd i Habr arwain at waharddiad ip am ychydig oriau. Felly dim ond 3 edafedd y mae'n rhaid i chi eu defnyddio, ond mae hyn eisoes yn dda, gan fod yr amser i ailadrodd dros 100 o erthyglau yn cael ei leihau o 26 i 12 eiliad.

Mae'n werth nodi bod y fersiwn hon braidd yn ansefydlog, ac mae llwytho i lawr o bryd i'w gilydd yn disgyn i ffwrdd ar nifer fawr o erthyglau.

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)

Trydydd fersiwn. Diwedd

Wrth ddadfygio'r ail fersiwn, darganfyddais fod gan Habr, yn sydyn iawn, API y mae fersiwn symudol y wefan yn ei gyrchu. Mae'n llwytho'n gyflymach na'r fersiwn symudol, gan mai json yn unig ydyw, nad oes angen ei ddosrannu hyd yn oed. Yn y diwedd, penderfynais ailysgrifennu fy sgript eto.

Felly, ar ôl darganfod y ddolen hon API, gallwch chi ddechrau ei ddosrannu.

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)

Mae'n cynnwys meysydd sy'n ymwneud â'r erthygl ei hun ac â'r awdur a'i hysgrifennodd.

API.png

Pob Habr mewn un gronfa ddata

Wnes i ddim dympio json llawn pob erthygl, ond dim ond y meysydd oedd eu hangen arna i a arbedais:

  • id
  • is_tiwtorial
  • amser_cyhoeddedig
  • Teitl
  • cynnwys
  • sylwadau_cyfrif
  • lang yw'r iaith y mae'r erthygl wedi'i hysgrifennu ynddi. Hyd yn hyn, dim ond en a ru sydd ganddo.
  • tags_string - pob tag o'r post
  • darllen_cyfrif
  • awdur
  • sgôr — gradd yr erthygl.

Felly, gan ddefnyddio'r API, fe wnes i leihau'r amser gweithredu sgript i 8 eiliad fesul 100 url.

Ar ôl i ni lawrlwytho'r data sydd ei angen arnom, mae angen i ni ei brosesu a'i roi yn y gronfa ddata. Doedd gen i ddim problemau gyda hyn chwaith:

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)

Ystadegau

Wel, yn draddodiadol, yn olaf, gallwch dynnu rhai ystadegau o'r data:

  • O'r 490 o lawrlwythiadau disgwyliedig, dim ond 406 o erthyglau a lawrlwythwyd. Mae'n ymddangos bod mwy na hanner (228) yr erthyglau ar Habré wedi'u cuddio neu eu dileu.
  • Mae'r gronfa ddata gyfan, sy'n cynnwys bron i hanner miliwn o erthyglau, yn pwyso 2.95 GB. Ar ffurf gywasgedig - 495 MB.
  • Mae cyfanswm o 37804 o bobl yn awduron Habré. Fe'ch atgoffaf mai dim ond o bostiadau byw y daw'r ystadegau hyn.
  • Yr awdur mwyaf cynhyrchiol ar Habré - alizar — 8774 o erthyglau.
  • Erthygl o'r radd flaenaf — 1448 plws
  • Yr erthygl a ddarllenodd y rhan fwyaf — 1660841 golygfeydd
  • Erthygl a Drafodir Fwyaf — 2444 o sylwadau

Wel, ar ffurf topiau15 awdur gorauPob Habr mewn un gronfa ddata
Y 15 uchaf yn ôl sgôrPob Habr mewn un gronfa ddata
Y 15 uchaf yn darllenPob Habr mewn un gronfa ddata
Y 15 Uchaf a drafodwydPob Habr mewn un gronfa ddata

Ffynhonnell: hab.com

Ychwanegu sylw