Të gjitha Habr në një bazë të dhënash

Mirembrema. Kanë kaluar 2 vjet që kur është shkruar. artikulli i fundit rreth analizimit të Habrit, dhe disa pika kanë ndryshuar.

Kur doja të kisha një kopje të Habr, vendosa të shkruaj një analizues që do të ruante të gjithë përmbajtjen e autorëve në bazën e të dhënave. Si ndodhi dhe cilat gabime kam hasur - mund të lexoni nën prerje.

TLDR- lidhjen e bazës së të dhënave

Versioni i parë i analizuesit. Një fije, shumë probleme

Për të filluar, vendosa të bëj një prototip skripti, në të cilin artikulli do të analizohej menjëherë pas shkarkimit dhe do të vendosej në bazën e të dhënave. Pa u menduar dy herë, përdora sqlite3, sepse. ishte më pak punë intensive: nuk kishte nevojë të kishim një server lokal, të krijuar-dukshëm-fshirë dhe gjëra të tilla.

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)

Gjithçka është klasike - ne përdorim Supë e bukur, kërkesat dhe një prototip i shpejtë është gati. Kjo është vetëm…

  • Shkarkimi i faqes është në një temë

  • Nëse ndërprisni ekzekutimin e skriptit, atëherë e gjithë baza e të dhënave nuk do të shkojë askund. Në fund të fundit, angazhimi kryhet vetëm pas të gjitha analizave.
    Sigurisht, mund të kryeni ndryshime në bazën e të dhënave pas çdo futjeje, por më pas koha e ekzekutimit të skriptit do të rritet ndjeshëm.

  • Analiza e 100 artikujve të parë më mori 000 orë.

Më pas gjej artikullin e përdoruesit të bashkëintegruara, të cilin e lexova dhe gjeta disa mënyra për të përshpejtuar këtë proces:

  • Përdorimi i multithreading shpejton shkarkimin herë pas here.
  • Ju mund të merrni jo versionin e plotë të habr, por versionin e tij celular.
    Për shembull, nëse një artikull i bashkëintegruar në versionin desktop peshon 378 KB, atëherë në versionin celular është tashmë 126 KB.

Versioni i dytë. Shumë fije, ndalim i përkohshëm nga Habr

Kur kërkuam internetin për temën e multithreading në python, zgjodha opsionin më të thjeshtë me multiprocessing.dummy, vura re se problemet u shfaqën së bashku me multithreading.

SQLite3 nuk dëshiron të punojë me më shumë se një thread.
fikse check_same_thread=False, por ky gabim nuk është i vetmi, kur përpiqem të fus në bazën e të dhënave, ndonjëherë ndodhin gabime që nuk mund t'i zgjidhja.

Prandaj, vendos të braktis futjen e menjëhershme të artikujve direkt në bazën e të dhënave dhe, duke kujtuar zgjidhjen e bashkëintegruar, vendos të përdor skedarë, sepse nuk ka probleme me shkrimin me shumë fije në një skedar.

Habr fillon të ndalojë për përdorimin e më shumë se tre temave.
Përpjekjet veçanërisht të zellshme për të kaluar në Habr mund të përfundojnë me një ndalim ip për disa orë. Pra, duhet të përdorni vetëm 3 tema, por kjo tashmë është mirë, pasi koha për të përsëritur mbi 100 artikuj reduktohet nga 26 në 12 sekonda.

Vlen të përmendet se ky version është mjaft i paqëndrueshëm dhe shkarkimet bien periodikisht në një numër të madh artikujsh.

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)

Versioni i tretë. Final

Gjatë korrigjimit të versionit të dytë, zbulova se Habr, papritmas, ka një API që akseson versioni celular i faqes. Ai ngarkohet më shpejt se versioni celular, pasi është thjesht json, i cili as nuk ka nevojë të analizohet. Në fund, vendosa të rishkruaj sërish skenarin tim.

Pra, pasi gjeti kjo lidhje API, mund të filloni ta analizoni atë.

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)

Ai përmban fusha që lidhen si me vetë artikullin ashtu edhe me autorin që e ka shkruar atë.

API.png

Të gjitha Habr në një bazë të dhënash

Nuk hodha json të plotë të secilit artikull, por ruajta vetëm fushat që më duheshin:

  • id
  • është_tutorial
  • koha_publikuar
  • titull
  • përmbajtje
  • numërimi i komenteve
  • lang është gjuha në të cilën është shkruar artikulli. Deri më tani, ajo ka vetëm en dhe ru.
  • tags_string - të gjitha etiketat nga postimi
  • leximi_numërimi
  • autor
  • rezultati - vlerësimi i artikullit.

Kështu, duke përdorur API-në, e reduktova kohën e ekzekutimit të skriptit në 8 sekonda për 100 url.

Pasi të kemi shkarkuar të dhënat që na duhen, duhet t'i përpunojmë dhe t'i fusim në bazën e të dhënave. Nuk kam pasur asnjë problem as me këtë:

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

Statistikë

Epo, tradicionalisht, më në fund, mund të nxirrni disa statistika nga të dhënat:

  • Nga 490 shkarkime të pritura, u shkarkuan vetëm 406 artikuj. Rezulton se më shumë se gjysma (228) e artikujve në Habré ishin fshehur ose fshirë.
  • E gjithë baza e të dhënave, e përbërë nga pothuajse gjysmë milioni artikuj, peshon 2.95 GB. Në formë të ngjeshur - 495 MB.
  • Në total, 37804 persona janë autorë të Habré. Ju kujtoj se këto statistika janë vetëm nga postimet e drejtpërdrejta.
  • Autori më produktiv në Habré - alizar - 8774 artikuj.
  • Artikull me vlerësim më të lartë — 1448 pluse
  • Artikulli më i lexuar – 1660841 shikime
  • Artikulli më i diskutuar — 2444 komente

Epo, në formën e majave15 autorët më të mirëTë gjitha Habr në një bazë të dhënash
Top 15 sipas vlerësimitTë gjitha Habr në një bazë të dhënash
Top 15 të lexuaraTë gjitha Habr në një bazë të dhënash
Top 15 të diskutuarTë gjitha Habr në një bazë të dhënash

Burimi: www.habr.com

Shto një koment