Kaikki Habr yhdessä tietokannassa

Hyvää iltapäivää. Siitä on 2 vuotta, kun se kirjoitettiin. viimeinen artikkeli Habrin jäsentämisestä, ja jotkut kohdat ovat muuttuneet.

Kun halusin saada kopion Habrista, päätin kirjoittaa jäsentimen, joka tallentaisi kaiken tekijöiden sisällön tietokantaan. Miten se tapahtui ja mitä virheitä törmäsin - voit lukea leikkauksen alta.

TLDR- tietokantalinkki

Jäsentimen ensimmäinen versio. Yksi lanka, monta ongelmaa

Aluksi päätin tehdä skriptin prototyypin, jossa artikkeli jäsennetään ja sijoitetaan tietokantaan heti latauksen jälkeen. Ajattelematta kahdesti, käytin sqlite3:a, koska. se oli vähemmän työvoimavaltaista: ei tarvinnut olla paikallista palvelinta, luotu-näytti-poistettu ja muuta sellaista.

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)

Kaikki on klassista - käytämme Beautiful Soupia, pyyntöjä ja nopea prototyyppi on valmis. Se on vain…

  • Sivun lataus on yhdessä säikeessä

  • Jos keskeytät komentosarjan suorittamisen, koko tietokanta ei mene minnekään. Loppujen lopuksi sitoutuminen suoritetaan vasta kaikkien jäsennysten jälkeen.
    Tietysti voit tehdä muutoksia tietokantaan jokaisen lisäyksen jälkeen, mutta silloin komentosarjan suoritusaika pitenee huomattavasti.

  • Ensimmäisen 100 000 artikkelin jäsentäminen kesti 8 tuntia.

Seuraavaksi löydän käyttäjän artikkelin kointegroitunut, jonka luin ja löysin muutaman elämänhackin nopeuttamaan tätä prosessia:

  • Monisäikeen käyttö nopeuttaa latausta toisinaan.
  • Et saa habrin täyttä versiota, vaan sen mobiiliversiota.
    Esimerkiksi jos kointegroitu artikkeli painaa työpöytäversiossa 378 kt, niin mobiiliversiossa se on jo 126 kt.

Toinen versio. Monet säikeet, väliaikainen kielto Habr

Kun selasin Internetiä pythonin monisäikeisyyden aiheena, valitsin yksinkertaisimman vaihtoehdon multiprocessing.dummylla, huomasin, että ongelmia ilmaantui monisäikeistyksen mukana.

SQLite3 ei halua toimia useamman kuin yhden säikeen kanssa.
korjattu check_same_thread=False, mutta tämä virhe ei ole ainoa, kun yrität lisätä tietokantaan, joskus tapahtuu virheitä, joita en pystynyt ratkaisemaan.

Siksi päätän luopua artikkelien välittömästä lisäämisestä suoraan tietokantaan ja muistaen yhteisintegroidun ratkaisun päätän käyttää tiedostoja, koska monisäikeisessä tiedostoon kirjoittamisessa ei ole ongelmia.

Habr alkaa kieltää useamman kuin kolmen säikeen käyttämisestä.
Erityisen innokkaat yritykset päästä Habriin voivat päätyä parin tunnin ip-kieltoon. Joten sinun on käytettävä vain 3 säiettä, mutta tämä on jo hyvä, koska yli 100 artikkelin iterointiaika lyhenee 26 sekunnista 12 sekuntiin.

On syytä huomata, että tämä versio on melko epävakaa ja lataukset putoavat ajoittain useissa artikkeleissa.

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)

Kolmas versio. Lopullinen

Toisen version virheenkorjauksen aikana huomasin, että Habrilla on yhtäkkiä API, jota sivuston mobiiliversio käyttää. Se latautuu nopeammin kuin mobiiliversio, koska se on vain json, jota ei tarvitse edes jäsentää. Lopulta päätin kirjoittaa käsikirjoitukseni uudelleen.

Eli löydettyään linkki API, voit aloittaa sen jäsentämisen.

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)

Se sisältää kenttiä, jotka liittyvät sekä itse artikkeliin että sen kirjoittajaan.

API.png

Kaikki Habr yhdessä tietokannassa

En poistanut jokaisen artikkelin täyttä json-tiedostoa, vaan tallensin vain tarvitsemani kentät:

  • id
  • is_tutorial
  • aika_julkaisu
  • otsikko
  • pitoisuus
  • kommenttien_määrä
  • lang on kieli, jolla artikkeli on kirjoitettu. Toistaiseksi sillä on vain en ja ru.
  • tags_string - kaikki viestistä tulevat tunnisteet
  • lukumäärä
  • kirjoittaja
  • pisteet — artikkelin arvio.

Siten API:n avulla lyhensin komentosarjan suoritusaikaa 8 sekuntiin 100 URL-osoitetta kohden.

Kun olemme ladaneet tarvitsemamme tiedot, meidän on käsiteltävä ne ja syötettävä ne tietokantaan. Minulla ei myöskään ollut ongelmia tämän kanssa:

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)

Tilastot

No, perinteisesti, lopulta voit poimia tilastoja tiedoista:

  • Odotetusta 490 406 latauksesta vain 228 512 artikkelia ladattiin. Osoittautuu, että yli puolet (261894) Habrén artikkeleista oli piilotettu tai poistettu.
  • Koko tietokanta, joka koostuu lähes puolesta miljoonasta artikkelista, painaa 2.95 Gt. Pakattuna - 495 Mt.
  • Habrén kirjoittajia on yhteensä 37804 XNUMX henkilöä. Muistutan, että nämä tilastot ovat vain live-postauksista.
  • Habrén tuottavin kirjailija - alizar - 8774 artikkelia.
  • Parhaiten arvioitu artikkeli - 1448 plussaa
  • Luetuin artikkeli — 1660841 katselukertaa
  • Eniten keskusteltu artikkeli - 2444 kommenttia

No, toppien muodossa15 parasta kirjoittajaaKaikki Habr yhdessä tietokannassa
Top 15 arvioiden mukaanKaikki Habr yhdessä tietokannassa
Top 15 luettuaKaikki Habr yhdessä tietokannassa
15 parasta keskusteltuaKaikki Habr yhdessä tietokannassa

Lähde: will.com

Lisää kommentti