Kõik Habr ühes andmebaasis

Tere päevast. Selle kirjutamisest on möödas 2 aastat. viimane artikkel Habri sõelumise kohta ja mõned punktid on muutunud.

Kui tahtsin saada Habri koopiat, otsustasin kirjutada parseri, mis salvestaks kogu autorite sisu andmebaasi. Kuidas see juhtus ja milliseid vigu kohtasin - saate lugeda lõike alt.

TLDR- andmebaasi link

Parseri esimene versioon. Üks teema, palju probleeme

Alustuseks otsustasin teha skripti prototüübi, milles artikkel sõelutakse ja paigutatakse kohe pärast allalaadimist andmebaasi. Kaks korda mõtlemata kasutasin sqlite3, sest. see oli vähem töömahukas: pole vaja kohalikku serverit, loodud-vaadatud-kustutatud ja muud taolist.

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)

Kõik on klassikaline – kasutame Kaunist Suppi, palveid ja kiire prototüüp ongi valmis. See on lihtsalt…

  • Lehe allalaadimine toimub ühes lõimes

  • Kui katkestate skripti täitmise, ei kao kogu andmebaas kuhugi. Kinnitus sooritatakse ju alles pärast kogu parsimist.
    Loomulikult saate pärast iga sisestamist andmebaasi muudatusi teha, kuid siis pikeneb skripti täitmise aeg oluliselt.

  • Esimese 100 000 artikli sõelumine võttis mul 8 tundi.

Järgmisena leian kasutaja artikli kointegreeritud, mida lugesin ja leidsin selle protsessi kiirendamiseks mõned elunäpud:

  • Mitme lõime kasutamine kiirendab mõnikord allalaadimist.
  • Saate hankida mitte habri täisversiooni, vaid selle mobiiliversiooni.
    Näiteks kui lauaarvuti versioonis kaalub kointegreeritud artikkel 378 KB, siis mobiiliversioonis on see juba 126 KB.

Teine versioon. Palju niite, ajutine keeld Habr

Kui ma pythonis mitme lõimestamise teemal Internetti uurisin, valisin multiprocessing.dummyga lihtsaima variandi, märkasin, et koos mitmelõimega tekkisid probleemid.

SQLite3 ei taha töötada rohkem kui ühe lõimega.
fikseeritud check_same_thread=False, kuid see viga pole ainuke, andmebaasi sisestamisel tuleb vahel ette vigu, mida ei osanud lahendada.

Seetõttu otsustan loobuda artiklite kohesest sisestamisest otse andmebaasi ja kointegreeritud lahendust meenutades otsustan kasutada faile, sest mitme lõimega faili kirjutamisega probleeme ei teki.

Habr hakkab keelama rohkem kui kolme lõime kasutamise eest.
Eriti innukad katsed Habrile läbi saada võivad lõppeda paaritunnise ip-keeluga. Seega peate kasutama ainult 3 lõime, kuid see on juba hea, nii et üle 100 artikli kordamise aeg väheneb 26 sekundilt 12 sekundile.

Väärib märkimist, et see versioon on üsna ebastabiilne ja allalaadimine langeb perioodiliselt paljudele artiklitele.

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)

Kolmas versioon. Lõplik

Teise versiooni silumisel avastasin, et Habril on äkki API, millele saidi mobiiliversioon juurde pääseb. See laaditakse kiiremini kui mobiiliversioon, kuna see on lihtsalt json, mida pole vaja isegi sõeluda. Lõpuks otsustasin oma stsenaariumi uuesti ümber kirjutada.

Niisiis, olles leidnud see link API, võite alustada selle sõelumist.

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)

See sisaldab välju, mis on seotud nii artikli enda kui ka selle kirjutanud autoriga.

API.png

Kõik Habr ühes andmebaasis

Ma ei tühjendanud iga artikli täielikku JSON-i, vaid salvestasin ainult vajalikud väljad:

  • id
  • is_tutorial
  • avaldamisaeg
  • pealkiri
  • sisu
  • kommentaaride_arv
  • lang on keel, milles artikkel on kirjutatud. Siiani on sellel ainult en ja ru.
  • tags_string – kõik postituse sildid
  • lugemiste_arv
  • autor
  • skoor — artikli hinnang.

Seega API-d kasutades vähendasin skripti täitmise aega 8 sekundini 100 URL-i kohta.

Kui oleme vajalikud andmed alla laadinud, peame need töötlema ja andmebaasi sisestama. Mul polnud ka sellega probleeme:

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

Noh, traditsiooniliselt saate lõpuks andmetest statistikat välja võtta:

  • Eeldatavast 490 406 allalaadimisest laaditi alla vaid 228 512 artiklit. Selgub, et üle poole (261894) Habrét puudutavatest artiklitest olid peidetud või kustutatud.
  • Kogu ligi poolest miljonist artiklist koosnev andmebaas kaalub 2.95 GB. Tihendatud kujul - 495 MB.
  • Kokku on Habré autoriteks 37804 inimest. Tuletan meelde, et see statistika on ainult reaalajas postitustest.
  • Habré kõige produktiivsem autor - alizar - 8774 artiklit.
  • Kõrgeimalt hinnatud artikkel — 1448 plussi
  • Enim loetud artikkel — 1660841 vaatamist
  • Enim arutatud artikkel — 2444 kommentaari

Noh, topside kujul15 parimat autoritKõik Habr ühes andmebaasis
15 parimat reitingu järgiKõik Habr ühes andmebaasis
15 parimat lugemistKõik Habr ühes andmebaasis
15 parimat arutatudKõik Habr ühes andmebaasis

Allikas: www.habr.com

Lisa kommentaar