Allt Habr í einum gagnagrunni

Góðan daginn. Það eru 2 ár síðan hún var skrifuð. síðustu grein um að flokka Habr, og sumir punktar hafa breyst.

Þegar ég vildi eignast eintak af Habr ákvað ég að skrifa parser sem myndi vista allt efni höfunda í gagnagrunninn. Hvernig það gerðist og hvaða villur ég rakst á - þú getur lesið undir klippunni.

TLDR- gagnagrunnstengil

Fyrsta útgáfan af parsernum. Einn þráður, mörg vandamál

Til að byrja með ákvað ég að búa til frumgerð handrits þar sem greinin yrði flokkuð og sett í gagnagrunninn strax við niðurhal. Án þess að hugsa mig tvisvar um notaði ég sqlite3, vegna þess að. það var minna vinnufrekt: engin þörf á að hafa staðbundinn netþjón, búið til-útlit-eytt og svoleiðis.

einn_þráður.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)

Allt er klassískt - við notum fallega súpu, beiðnir og fljótleg frumgerð er tilbúin. Það er bara…

  • Síðu niðurhal er í einum þræði

  • Ef þú truflar framkvæmd handritsins mun allur gagnagrunnurinn hvergi fara. Þegar öllu er á botninn hvolft er commit aðeins framkvæmt eftir alla þáttunina.
    Auðvitað geturðu framkvæmt breytingar á gagnagrunninum eftir hverja innsetningu, en þá eykst keyrslutími handrits verulega.

  • Að greina fyrstu 100 greinarnar tók mig 000 klukkustundir.

Næst finn ég grein notandans samþætt, sem ég las og fann nokkur lífshakk til að flýta fyrir þessu ferli:

  • Notkun multithreading flýtir stundum fyrir niðurhali.
  • Þú getur ekki fengið fulla útgáfu af habr, heldur farsímaútgáfu hans.
    Til dæmis, ef samþætt grein í skrifborðsútgáfunni vegur 378 KB, þá er hún þegar 126 KB í farsímaútgáfunni.

Önnur útgáfa. Margir þræðir, bráðabirgðabann frá Habr

Þegar ég leitaði á netinu um efni fjölþráða í python, valdi ég einfaldasta kostinn með multiprocessing.dummy, ég tók eftir því að vandamál komu upp samhliða fjölþráðum.

SQLite3 vill ekki vinna með fleiri en einum þræði.
fastur check_same_thread=False, en þessi villa er ekki sú eina, þegar reynt er að setja inn í gagnagrunninn koma stundum upp villur sem ég gat ekki leyst.

Þess vegna ákveð ég að hætta að setja greinar strax inn í gagnagrunninn og, eftir samþættu lausnina, ákveð ég að nota skrár, vegna þess að það eru engin vandamál með fjölþráða ritun í skrá.

Habr byrjar að banna fyrir að nota fleiri en þrjá þræði.
Sérstaklega vandaðar tilraunir til að komast í gegnum Habr geta endað með ip-banni í nokkra klukkutíma. Svo þú þarft að nota aðeins 3 þræði, en þetta er nú þegar gott, þar sem tíminn til að endurtaka yfir 100 greinar er styttur úr 26 í 12 sekúndur.

Þess má geta að þessi útgáfa er frekar óstöðug og niðurhal fellur reglulega niður á miklum fjölda greina.

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)

Þriðja útgáfa. Úrslitaleikur

Þegar ég kembi í seinni útgáfunni uppgötvaði ég að Habr, allt í einu, er með API sem farsímaútgáfan af síðunni nálgast. Það hleðst hraðar en farsímaútgáfan, þar sem það er bara json, sem þarf ekki einu sinni að flokka. Á endanum ákvað ég að endurskrifa handritið mitt aftur.

Svo, að hafa fundið þessi tengill API, þú getur byrjað að flokka það.

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)

Hún inniheldur svið sem tengjast bæði greininni sjálfri og höfundinum sem skrifaði hana.

API.png

Allt Habr í einum gagnagrunni

Ég varpaði ekki öllu json af hverri grein, heldur vistaði aðeins reiti sem ég þurfti:

  • id
  • is_tutorial
  • tími_birt
  • titill
  • efni
  • comments_count
  • lang er tungumálið sem greinin er skrifuð á. Hingað til hefur það aðeins en og ru.
  • tags_string - öll merki úr færslunni
  • lestur_talning
  • Höfundur
  • stig - einkunn greina.

Þannig, með því að nota API, minnkaði ég keyrslutíma handritsins í 8 sekúndur á hverja 100 vefslóð.

Eftir að við höfum hlaðið niður gögnunum sem við þurfum þurfum við að vinna úr þeim og slá inn í gagnagrunninn. Ég átti heldur ekki í neinum vandræðum með þetta:

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)

Tölfræði

Jæja, venjulega, loksins, geturðu dregið nokkrar tölfræði úr gögnunum:

  • Af væntanlegum 490 niðurhalum var aðeins 406 greinum hlaðið niður. Í ljós kemur að meira en helmingur (228) greinanna á Habré var falinn eða eytt.
  • Allur gagnagrunnurinn, sem samanstendur af tæpri hálfri milljón greina, vegur 2.95 GB. Í þjöppuðu formi - 495 MB.
  • Alls eru 37804 höfundar Habré. Ég minni á að þessi tölfræði er aðeins úr beinni færslu.
  • Afkastamesti höfundurinn á Habré - alizar - 8774 greinar.
  • Grein með hæstu einkunn — 1448 plúsar
  • Mest lesna grein – 1660841 skoðanir
  • Mest rædda greinin — 2444 athugasemdir

Jæja, í formi toppaTop 15 höfundarAllt Habr í einum gagnagrunni
Topp 15 eftir einkunnAllt Habr í einum gagnagrunni
Topp 15 lesiðAllt Habr í einum gagnagrunni
Topp 15 ræddAllt Habr í einum gagnagrunni

Heimild: www.habr.com

Bæta við athugasemd