Барлық Habr бір дерекқорда

Қайырлы күн. Жазылғанына 2 жыл болды. соңғы мақала Хабрды талдау туралы және кейбір тармақтар өзгерді.

Мен Habr көшірмесін алғым келгенде, мен авторлардың барлық мазмұнын дерекқорға сақтайтын талдаушы жазуды шештім. Бұл қалай болды және қандай қателіктерге тап болдым - сіз кесудің астынан оқи аласыз.

TLDR- дерекқорға сілтеме

Талдаушының бірінші нұсқасы. Бір жіп, көп мәселе

Бастау үшін мен сценарийдің прототипін жасауды шештім, онда мақала жүктеп алғаннан кейін бірден деректер базасына талданады және орналастырылады. Екі рет ойланбастан, мен sqlite3 қолдандым, өйткені. бұл аз еңбекті қажет етті: жергілікті сервердің болуы, жасалған-көрінетін-жойылған және сол сияқты нәрселердің болуының қажеті жоқ.

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)

Барлығы классикалық - біз әдемі сорпаны қолданамыз, сұраныстар және жылдам прототип дайын. Бұл жай ғана…

  • Бетті жүктеу бір ағында

  • Егер сіз сценарийдің орындалуын тоқтатсаңыз, онда бүкіл дерекқор ешқайда кетпейді. Өйткені, міндеттеме барлық талдаудан кейін ғана орындалады.
    Әрине, әрбір кірістіруден кейін дерекқорға өзгерістер енгізуге болады, бірақ содан кейін сценарийді орындау уақыты айтарлықтай артады.

  • Алғашқы 100 000 мақаланы талдау маған 8 сағатты алды.

Содан кейін мен пайдаланушының мақаласын табамын коинтеграцияланған, мен оны оқып, осы процесті жылдамдату үшін бірнеше лайфхактарды таптым:

  • Көп ағынды пайдалану кейде жүктеп алуды жылдамдатады.
  • Сіз хабрдың толық нұсқасын емес, оның мобильді нұсқасын ала аласыз.
    Мысалы, жұмыс үстелі нұсқасындағы коинтеграцияланған мақаланың салмағы 378 КБ болса, мобильді нұсқада ол қазірдің өзінде 126 Кбайт.

Екінші нұсқа. Көптеген жіптер, Хабрдан уақытша тыйым салу

Мен интернетті python тіліндегі көп ағынды тақырыбын зерттегенде, мен multiprocessing.dummy арқылы ең қарапайым нұсқаны таңдадым, мен көп ағынмен қатар проблемалардың пайда болғанын байқадым.

SQLite3 бірнеше ағынмен жұмыс істегісі келмейді.
тұрақты check_same_thread=False, бірақ бұл қате жалғыз емес, дерекқорға енгізу әрекеті кезінде кейде мен шеше алмаған қателер пайда болады.

Сондықтан мен мақалаларды тікелей дерекқорға лезде енгізуден бас тартуды шештім және коинтеграцияланған шешімді еске түсіріп, файлдарды пайдалануды шештім, себебі файлға көп ағынды жазуда ешқандай проблемалар жоқ.

Хабр үш ағыннан артық пайдаланғаны үшін тыйым сала бастайды.
Әсіресе Хабрға өтуге деген құлшыныс әрекеттері бірнеше сағатқа IP тыйымымен аяқталуы мүмкін. Сондықтан сіз тек 3 ағынды пайдалануыңыз керек, бірақ бұл жақсы, өйткені 100-ден астам мақаланы қайталау уақыты 26 секундтан 12 секундқа дейін қысқарды.

Айта кету керек, бұл нұсқа өте тұрақсыз және жүктеп алулар мезгіл-мезгіл көптеген мақалаларға түседі.

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)

Үшінші нұсқа. Финал

Екінші нұсқаны жөндеу кезінде мен Habr-те кенеттен сайттың мобильді нұсқасы кіретін API бар екенін білдім. Ол мобильді нұсқаға қарағанда жылдамырақ жүктеледі, себебі бұл жай ғана json, оны талдаудың қажеті жоқ. Соңында мен сценарийімді қайта жазуды шештім.

Сонымен, тапты бұл сілтеме API, оны талдауды бастауға болады.

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)

Онда мақаланың өзіне де, оны жазған авторға да қатысты өрістер бар.

API.png

Барлық Habr бір дерекқорда

Мен әр мақаланың толық json файлын тастамадым, бірақ маған қажет өрістерді ғана сақтадым:

  • id
  • оқулық
  • уақыты_жарияланды
  • тақырып
  • мазмұны
  • пікірлер_саны
  • lang – мақала жазылатын тіл. Әзірге оның тек en және ru бар.
  • tags_string - жазбадағы барлық тегтер
  • оқу_саны
  • автор
  • балл — мақала рейтингі.

Осылайша, API көмегімен сценарийді орындау уақытын 8 url үшін 100 секундқа дейін қысқарттым.

Бізге қажетті деректерді жүктеп алғаннан кейін біз оны өңдеп, мәліметтер базасына енгізуіміз керек. Менде де бұл мәселе болған жоқ:

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)

Статистика

Дәстүрлі түрде, сайып келгенде, деректерден кейбір статистиканы алуға болады:

  • Күтілетін 490 406 жүктеудің тек 228 512 мақаласы ғана жүктелді. Хабре туралы мақалалардың жартысынан көбі (261894) жасырылған немесе жойылған.
  • Жарты миллионға жуық мақаладан тұратын бүкіл дерекқордың салмағы 2.95 ГБ. Қысылған түрде - 495 МБ.
  • Барлығы 37804 адам Хабренің авторлары. Естеріңізге сала кетейін, бұл статистика тек тікелей эфирдегі посттардан алынған.
  • Хабредегі ең өнімді автор - ализар - 8774 мақала.
  • Жоғары бағаланған мақала — 1448 плюс
  • Ең көп оқылатын мақала — 1660841 қаралды
  • Ең көп талқыланған мақала — 2444 пікір

Жақсы, шыңдар түріндеҮздік 15 авторБарлық Habr бір дерекқорда
Рейтинг бойынша үздік 15Барлық Habr бір дерекқорда
Үздік 15 оқылғанБарлық Habr бір дерекқорда
Талқыланған 15 топБарлық Habr бір дерекқорда

Ақпарат көзі: www.habr.com

пікір қалдыру