Visi Habr vienoje duomenų bazėje

Laba diena. Praėjo 2 metai, kai jį parašiau paskutinis straipsnis apie Habr analizavimą, o kai kurie dalykai pasikeitė.

Kai norėjau turėti Habr kopiją, nusprendžiau parašyti analizatorių, kuris visą autorių turinį išsaugotų duomenų bazėje. Kaip tai atsitiko ir su kokiomis klaidomis susidūriau - galite perskaityti po pjūviu.

TL;DR - nuoroda į duomenų bazę

Pirmoji analizatoriaus versija. Viena tema, daug problemų

Pirmiausia nusprendžiau sukurti scenarijaus prototipą, kuriame iškart po atsisiuntimo straipsnis būtų išanalizuotas ir patalpintas į duomenų bazę. Du kartus negalvodamas panaudojau sqlite3, nes... tai buvo mažiau darbo jėgos: jums nereikia turėti vietinio serverio, kurti, ieškoti, ištrinti ir panašiai.

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)

Viskas pagal klasiką – naudojame Gražią sriubą, prašymus ir greitasis prototipas paruoštas. Tai tik...

  • Puslapis atsisiunčiamas vienoje temoje

  • Jei nutrauksite scenarijaus vykdymą, visa duomenų bazė niekur nedings. Juk įsipareigojimas vykdomas tik po visų analizavimo.
    Žinoma, galite atlikti duomenų bazės pakeitimus po kiekvieno įterpimo, tačiau tada scenarijaus vykdymo laikas žymiai padidės.

  • Pirmųjų 100 000 straipsnių analizė užtruko 8 valandas.

Tada randu vartotojo straipsnį kointegruotas, kurį perskaičiau, ir radau kelis gyvenimo įsilaužimus, kurie pagreitina šį procesą:

  • Kelių gijų naudojimas žymiai pagreitina atsisiuntimą.
  • Galite gauti ne pilną Habr versiją, o jos mobiliąją versiją.
    Pavyzdžiui, jei darbalaukio versijoje kointegruotas straipsnis sveria 378 KB, tai mobiliojoje versijoje jis jau yra 126 KB.

Antroji versija. Daug gijų, laikinas draudimas iš Habr

Kai naršiau internetą daugiasriegio python tema ir pasirinkau paprasčiausią variantą su multiprocessing.dummy, pastebėjau, kad kartu su multithreading atsiranda ir problemų.

SQLite3 nenori dirbti su daugiau nei viena gija.
Fiksuotas check_same_thread=False, tačiau ši klaida nėra vienintelė, bandant įterpti į duomenų bazę kartais iškyla klaidų, kurių negaliu išspręsti.

Todėl nusprendžiu atsisakyti momentinio straipsnių įterpimo tiesiai į duomenų bazę ir, prisimindamas kointegruotą sprendimą, nusprendžiu naudoti failus, nes nekyla problemų su kelių gijų įrašymu į failą.

Habras pradeda drausti naudoti daugiau nei tris gijas.
Ypač uolūs bandymai pasiekti Habrą gali sukelti IP uždraudimą kelioms valandoms. Taigi jūs turite naudoti tik 3 gijas, bet tai jau gerai, nes 100 straipsnių rūšiavimo laikas sutrumpėja nuo 26 iki 12 sekundžių.

Verta paminėti, kad ši versija yra gana nestabili, o daugelio straipsnių atsisiuntimas periodiškai nepavyksta.

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)

Trečia versija. Galutinis

Derindamas antrąją versiją sužinojau, kad Habras staiga turi API, kurią pasiekia mobilioji svetainės versija. Jis įkeliamas greičiau nei mobilioji versija, nes tai tik json, kurio net nereikia analizuoti. Galiausiai nusprendžiau dar kartą perrašyti savo scenarijų.

Taigi, atradęs ši nuoroda API, galite pradėti ją analizuoti.

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)

Jame yra laukai, susiję tiek su pačiu straipsniu, tiek su jį parašiusiu autoriumi.

API.png

Visi Habr vienoje duomenų bazėje

Neišverčiau viso kiekvieno straipsnio Json, bet išsaugojau tik tuos laukus, kurių man reikėjo:

  • id
  • is_tutorial
  • paskelbimo laikas
  • pavadinimas
  • turinys
  • komentarų_skaičius
  • lang yra kalba, kuria parašytas straipsnis. Kol kas jame yra tik en ir ru.
  • tags_string – visos įrašo žymos
  • skaitymo_skaičius
  • autorius
  • balas — straipsnio įvertinimas.

Taigi, naudodamas API, scenarijaus vykdymo laiką sumažinau iki 8 sekundžių 100 URL.

Atsisiuntę mums reikalingus duomenis, turime juos apdoroti ir įvesti į duomenų bazę. Su tuo taip pat nebuvo jokių problemų:

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

Na, pagaliau tradiciškai galite išgauti statistiką iš duomenų:

  • Iš numatytų 490 406 buvo atsisiųsta tik 228 512 straipsnių. Pasirodo, daugiau nei pusė (261894) straipsnių apie Habré buvo paslėpti arba ištrinti.
  • Visa duomenų bazė, kurią sudaro beveik pusė milijono straipsnių, sveria 2.95 GB. Suglaudinta forma - 495 MB.
  • Iš viso Habré yra 37804 XNUMX autoriai. Priminsiu, kad tai statistika tik iš tiesioginių įrašų.
  • Produktyviausias Habré autorius - alizar — 8774 straipsniai.
  • Geriausiai įvertintas straipsnis — 1448 pliusai
  • Labiausiai skaitomas straipsnis — 1660841 peržiūrų
  • Daugiausia kalbama apie straipsnį — 2444 komentarai

Na, viršūnių pavidalu15 geriausių autoriųVisi Habr vienoje duomenų bazėje
15 geriausių pagal įvertinimąVisi Habr vienoje duomenų bazėje
15 populiariausių skaitytųVisi Habr vienoje duomenų bazėje
15 geriausių aptartųVisi Habr vienoje duomenų bazėje

Šaltinis: www.habr.com

Добавить комментарий