Habr guztiak datu-base batean

Arratsalde on. 2 urte pasa dira idatzi zenetik. azken artikulua Habr analizatzeari buruz, eta puntu batzuk aldatu dira.

Habr-en kopia bat izan nahi nuenean, egileen eduki guztia datu-basean gordeko zuen analizatzaile bat idaztea erabaki nuen. Nola gertatu zen eta zer akats aurkitu ditudan - ebaki azpian irakur dezakezu.

TLDR- datu-basearen esteka

Analizatzailearen lehen bertsioa. Hari bat, arazo asko

Hasteko, script-prototipo bat egitea erabaki nuen, non artikulua deskargatu eta datu-basean berehala aztertuko zen. Bi aldiz pentsatu gabe, sqlite3 erabili nuen, zeren. lan gutxiago zen: ez zen zerbitzari lokal bat eduki behar, sortu-itxura-ezabatu eta horrelakoak.

hari_bat.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)

Dena klasikoa da - Beautiful Soup erabiltzen dugu, eskaerak eta prototipo azkar bat prest dago. Hori besterik ez da…

  • Orria deskargatu hari batean dago

  • Scriptaren exekuzioa eteten baduzu, datu-base osoa ez da inora joango. Azken finean, konpromezua analisi guztiaren ondoren bakarrik egiten da.
    Jakina, datu-basean aldaketak egin ditzakezu txertatze bakoitzaren ondoren, baina gero script-a exekutatzeko denbora nabarmen handituko da.

  • Lehenengo 100 artikuluak analizatzeak 000 ordu behar izan nituen.

Jarraian erabiltzailearen artikulua aurkitzen dut bateratu, irakurri eta prozesu hau bizkortzeko bizitza hack batzuk aurkitu nituen:

  • Hari anitzeko erabilerak deskarga bizkortzen du batzuetan.
  • Ez habr-aren bertsio osoa lor dezakezu, mugikorreko bertsioa baizik.
    Adibidez, mahaigaineko bertsioan bateratutako artikulu batek 378 KB pisatzen badu, mugikorreko bertsioan dagoeneko 126 KB da.

Bigarren bertsioa. Hari asko, aldi baterako debekua Habr

Python-en multithreading gaiari buruz Interneten arakatu nuenean, multiprocessing.dummy-rekin aukerarik errazena aukeratu nuen, multithreading-arekin batera arazoak agertzen zirela ohartu nintzen.

SQLite3-k ez du hari bat baino gehiagorekin lan egin nahi.
finkoa check_same_thread=False, baina errore hau ez da bakarra, datu-basean txertatzen saiatzean, batzuetan konpondu ezin izan ditudan akatsak gertatzen dira.

Hori dela eta, artikuluen berehalako datu-basean zuzenean txertatzeari uztea erabakitzen dut eta, kointegratutako irtenbidea gogoratuz, fitxategiak erabiltzea erabakitzen dut, hari anitzeko fitxategi batean idazteko arazorik ez dagoelako.

Habr hiru hari baino gehiago erabiltzea debekatzen hasten da.
Bereziki Habr-era iristeko saiakera sutsuak ordu pare baterako ip debekuarekin amai daiteke. Beraz, 3 hari bakarrik erabili behar dituzu, baina hori ona da dagoeneko, 100 artikulu baino gehiago errepikatzeko denbora 26 segundotik 12ra murrizten baita.

Azpimarratzekoa da bertsio hau nahiko ezegonkorra dela eta deskargak aldizka artikulu ugaritan jaisten direla.

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)

Hirugarren bertsioa. Finala

Bigarren bertsioa arazketan, Habr-ek, bat-batean, webgunearen mugikorreko bertsioak atzitzen duen API bat duela ikusi nuen. Mugikorreko bertsioa baino azkarrago kargatzen da, json besterik ez baita, analizatu beharrik ere ez duena. Azkenean, nire gidoia berriro idaztea erabaki nuen.

Beraz, aurkitu ondoren lotura hau APIa, analizatzen has zaitezke.

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)

Artikuluarekin eta idatzi duen egilearekin lotutako eremuak ditu.

API.png

Habr guztiak datu-base batean

Ez dut artikulu bakoitzaren json osoa bota, baina behar nituen eremuak bakarrik gorde ditut:

  • id
  • da_tutoriala
  • denbora_argitaratua
  • izenburua
  • edukia
  • iruzkinak_zenbaketa
  • lang artikulua idazten den hizkuntza da. Orain arte, en eta ru baino ez ditu.
  • tags_string - mezuaren etiketa guztiak
  • irakurketa_zenbaketa
  • Egileak
  • puntuazioa — artikuluaren balorazioa.

Horrela, APIa erabiliz, scriptaren exekuzio denbora 8 segundora murriztu nuen 100 url bakoitzeko.

Behar ditugun datuak deskargatu ondoren, prozesatu eta datu-basean sartu behar ditugu. Honekin ere ez nuen arazorik izan:

analizatzailea.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)

estatistikak

Tira, tradizionalki, azkenik, datuetatik estatistika batzuk atera ditzakezu:

  • Espero diren 490 deskargaretatik 406 artikulu baino ez ziren deskargatu. Ematen da Habré-ri buruzko artikuluen erdia baino gehiago (228) ezkutatu edo ezabatu egin zirela.
  • Datu-base osoak, ia milioi erdi artikuluz osatua, 2.95 GB pisatzen du. Forma konprimituan - 495 MB.
  • Guztira, 37804 pertsona dira Habréren egileak. Gogorarazten dizut estatistika hauek zuzeneko mezuetatik soilik daudela.
  • Habré-ri buruzko egilerik emankorrena - alizar - 8774 artikulu.
  • Baloratutako artikulua — 1448 plus
  • Gehien irakurritako artikulua — 1660841 ikustaldi
  • Gehien eztabaidatutako artikulua — 2444 iruzkin

Tira, goiko formanTop 15 egileHabr guztiak datu-base batean
Top 15 puntuazioa araberaHabr guztiak datu-base batean
Top 15 irakurriHabr guztiak datu-base batean
Top 15 EztabaidaHabr guztiak datu-base batean

Iturria: www.habr.com

Gehitu iruzkin berria