Сите 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 часа.

Следно ја наоѓам статијата на корисникот коинтегрирани, што го прочитав и најдов неколку лајф хакови за да го забрзам овој процес:

  • Користењето на повеќенишки го забрзува преземањето на моменти.
  • Можете да ја добиете не целосната верзија на habr, туку нејзината мобилна верзија.
    На пример, ако коинтегрираната статија во десктоп верзијата тежи 378 KB, тогаш во мобилната верзија веќе е 126 KB.

Втора верзија. Многу теми, привремена забрана од Хабр

Кога пребарував на Интернет на тема мултинишки во 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
  • is_tutorial
  • време_објавено
  • Наслов
  • содржина
  • коментари_број
  • lang е јазикот на кој е напишана статијата. Засега има само en и ru.
  • tags_string - сите ознаки од објавата
  • читање_број
  • авторот
  • резултат - рејтинг на статијата.

Така, користејќи го API, го намалив времето на извршување на скриптата на 8 секунди на 100 URL.

Откако ќе ги преземеме податоците што ни се потребни, треба да ги обработиме и да ги внесеме во базата на податоци. И јас немав проблеми со ова:

анализатор.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) од написите на Хабре биле скриени или избришани.
  • Целата база на податоци, која се состои од речиси половина милион статии, тежи 2.95 GB. Во компресирана форма - 495 MB.
  • Вкупно, 37804 луѓе се автори на Хабре. Потсетувам дека овие статистики се само од објави во живо.
  • Најпродуктивниот автор на Хабре - ализар - 8774 статии.
  • Највисоко оценет напис — 1448 плус
  • Најчитана статија — 1660841 прегледи
  • Најдискутирана статија — 2444 коментари

Па, во форма на врвовиТоп 15 авториСите Habr во една база на податоци
Топ 15 по рејтингСите Habr во една база на податоци
Топ 15 прочитаниСите Habr во една база на податоци
Топ 15 дискутираниСите Habr во една база на податоци

Извор: www.habr.com

Додадете коментар