Sav Habr u jednoj bazi podataka

Dobar dan. Prošlo je 2 godine otkako je napisano. posljednji članak o raščlanjivanju Habra, a neke točke su se promijenile.

Kad sam htio imati primjerak Habra, odlučio sam napisati parser koji će sav sadržaj autora spremati u bazu podataka. Kako se to dogodilo i na koje sam greške nailazio - možete pročitati pod rezom.

TLDR- veza baze podataka

Prva verzija parsera. Jedna nit, mnogo problema

Za početak sam odlučio napraviti prototip skripte u kojoj bi se članak analizirao i stavljao u bazu odmah po preuzimanju. Bez razmišljanja sam upotrijebio sqlite3, jer. bilo je manje radno intenzivno: nema potrebe za lokalnim poslužiteljem, kreirano-izgledano-izbrisano i takve stvari.

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

Sve je klasično - koristimo Beautiful Soup, zahtjeve i brzi prototip je spreman. To je samo…

  • Preuzimanje stranice je u jednoj temi

  • Ako prekinete izvođenje skripte, cijela baza podataka neće nikamo otići. Na kraju krajeva, commit se izvodi tek nakon svih analiza.
    Naravno, možete unijeti promjene u bazu podataka nakon svakog umetanja, ali tada će se vrijeme izvršenja skripte značajno povećati.

  • Za analiziranje prvih 100 članaka trebalo mi je 000 sati.

Zatim pronalazim korisnikov članak kointegrirani, koji sam pročitao i pronašao nekoliko životnih trikova za ubrzanje ovog procesa:

  • Korištenje multithreadinga ponekad ubrzava preuzimanje.
  • Ne možete dobiti punu verziju habra, već njegovu mobilnu verziju.
    Na primjer, ako kointegrirani članak u verziji za stolno računalo teži 378 KB, tada je u mobilnoj verziji već 126 KB.

Druga verzija. Mnogo tema, privremena zabrana s Habra

Kad sam pretražio internet na temu multithreadinga u pythonu, odabrao sam najjednostavniju opciju s multiprocessing.dummy, primijetio sam da se problemi pojavljuju zajedno s multithreadingom.

SQLite3 ne želi raditi s više od jedne niti.
fiksni check_same_thread=False, ali ova greška nije jedina, pri pokušaju ubacivanja u bazu ponekad se pojave greške koje nisam uspio riješiti.

Stoga sam odlučio napustiti trenutno umetanje članaka izravno u bazu podataka i, sjećajući se kointegriranog rješenja, odlučio sam koristiti datoteke, jer nema problema s višenitnim pisanjem u datoteku.

Habr počinje banovati za korištenje više od tri niti.
Posebno revni pokušaji pristupa Habru mogu završiti zabranom ip-a na nekoliko sati. Dakle, morate koristiti samo 3 niti, ali to je već dobro, jer je vrijeme za ponavljanje preko 100 članaka smanjeno sa 26 na 12 sekundi.

Vrijedno je napomenuti da je ova verzija prilično nestabilna, a preuzimanja povremeno padaju na velikom broju članaka.

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ća verzija. Konačna

Dok sam otklanjao pogreške u drugoj verziji, otkrio sam da Habr, odjednom, ima API kojem pristupa mobilna verzija stranice. Učitava se brže od mobilne verzije, jer je to samo json, koji ne treba niti parsirati. Na kraju sam odlučio ponovno napisati svoj scenarij.

Dakle, pronašavši ovaj link API, možete ga početi analizirati.

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)

Sadrži polja koja se odnose i na sam članak i na autora koji ga je napisao.

API.png

Sav Habr u jednoj bazi podataka

Nisam izbacio puni json svakog članka, već sam spremio samo polja koja su mi bila potrebna:

  • id
  • je_uputa
  • vrijeme_objavljeno
  • naslov
  • sadržaj
  • broj_komentara
  • lang je jezik na kojem je članak napisan. Do sada ima samo en i ru.
  • tags_string - sve oznake iz objave
  • broj_čitanja
  • autor
  • score — ocjena članka.

Stoga sam pomoću API-ja smanjio vrijeme izvršavanja skripte na 8 sekundi po 100 url-ova.

Nakon što smo preuzeli podatke koji su nam potrebni potrebno ih je obraditi i unijeti u bazu. Ni sa ovim nisam imao problema:

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

Pa, tradicionalno, konačno, možete izvući neke statistike iz podataka:

  • Od očekivanih 490 preuzimanja, preuzeto je samo 406 članaka. Ispostavilo se da je više od polovice (228) članaka na Habréu skriveno ili obrisano.
  • Cijela baza, koja se sastoji od gotovo pola milijuna članaka, teška je 2.95 GB. U komprimiranom obliku - 495 MB.
  • Ukupno, 37804 ljudi su autori Habréa. Podsjećam vas da su ove statistike samo iz objava uživo.
  • Najproduktivniji autor na Habréu - alizar - 8774 članaka.
  • Najbolje ocijenjeni članak — 1448 pluseva
  • Najčitaniji članak — 1660841 pregleda
  • Članak o kojem se najviše raspravljalo — 2444 komentara

Pa, u obliku vrhova15 najboljih autoraSav Habr u jednoj bazi podataka
Top 15 po ocjeniSav Habr u jednoj bazi podataka
Top 15 pročitanihSav Habr u jednoj bazi podataka
Top 15 o kojima se raspravljaloSav Habr u jednoj bazi podataka

Izvor: www.habr.com

Dodajte komentar