Visi Habr vienā datu bāzē

Labdien. Ir pagājuši 2 gadi kopš tā uzrakstīšanas. pēdējais raksts par Habr parsēšanu, un daži punkti ir mainīti.

Kad vēlējos iegūt Habr kopiju, nolēmu uzrakstīt parsētāju, kas visu autoru saturu saglabātu datu bāzē. Kā tas notika un ar kādām kļūdām es saskāros - varat lasīt zem griezuma.

TLDR- datu bāzes saite

Pirmā parsētāja versija. Viens pavediens, daudz problēmu

Sākumā nolēmu izveidot skripta prototipu, kurā raksts uzreiz pēc lejupielādes tiktu parsēts un ievietots datu bāzē. Divreiz nedomājot izmantoju sqlite3, jo. tas bija mazāk darbietilpīgs: nav nepieciešams lokāls serveris, izveidots-izskatījās-dzēsts un tamlīdzīgi.

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)

Viss ir klasisks - izmantojam Skaisto zupu, lūgumi un ātrais prototips gatavs. Tas ir tikai…

  • Lapas lejupielāde notiek vienā pavedienā

  • Ja pārtraucat skripta izpildi, visa datu bāze nekur nepazudīs. Galu galā apņemšanās tiek veikta tikai pēc visas parsēšanas.
    Protams, jūs varat veikt izmaiņas datu bāzē pēc katras ievietošanas, taču tad skripta izpildes laiks ievērojami palielināsies.

  • Pirmo 100 000 rakstu analīze man prasīja 8 stundas.

Tālāk es atrodu lietotāja rakstu kointegrēti, kuru izlasīju un atradu dažus dzīves veidus, lai paātrinātu šo procesu:

  • Daudzpavedienu izmantošana paātrina lejupielādi reizēm.
  • Jūs varat iegūt nevis pilno habr versiju, bet gan tās mobilo versiju.
    Piemēram, ja kointegrētais raksts darbvirsmas versijā sver 378 KB, tad mobilajā versijā tas jau ir 126 KB.

Otrā versija. Daudzi pavedieni, pagaidu aizliegums no Habr

Kad es pārlūkoju internetu par daudzpavedienu tēmu python, es izvēlējos vienkāršāko opciju ar multiprocessing.dummy, es pamanīju, ka problēmas parādījās kopā ar vairāku pavedienu izmantošanu.

SQLite3 nevēlas strādāt ar vairāk nekā vienu pavedienu.
fiksēts check_same_thread=False, bet šī kļūda nav vienīgā, mēģinot ievietot datubāzē, dažkārt gadās kļūdas, kuras nevarēju atrisināt.

Tāpēc nolemju atteikties no rakstu tūlītējas ievietošanas tieši datu bāzē un, atceroties kointegrēto risinājumu, nolemju izmantot failus, jo ar vairākpavedienu ierakstīšanu failā nav problēmu.

Habr sāk aizliegt lietot vairāk nekā trīs pavedienus.
Īpaši dedzīgi mēģinājumi tikt cauri Habram var beigties ar ip banu uz pāris stundām. Tātad jums ir jāizmanto tikai 3 pavedieni, bet tas jau ir labi, jo vairāk nekā 100 rakstu atkārtošanas laiks tiek samazināts no 26 līdz 12 sekundēm.

Ir vērts atzīmēt, ka šī versija ir diezgan nestabila, un daudzu rakstu lejupielādes periodiski samazinās.

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šā versija. Fināls

Atkļūdojot otro versiju, es atklāju, ka Habram pēkšņi ir API, kurai piekļūst vietnes mobilā versija. Tas tiek ielādēts ātrāk nekā mobilā versija, jo tas ir tikai JSON, kas pat nav jāparsē. Galu galā es nolēmu vēlreiz pārrakstīt savu scenāriju.

Tātad, atradis šo saiti API, varat sākt to parsēt.

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)

Tajā ir lauki, kas saistīti gan ar pašu rakstu, gan ar autoru, kurš to uzrakstījis.

API.png

Visi Habr vienā datu bāzē

Es neizmetu katra raksta pilnu JSON, bet saglabāju tikai vajadzīgos laukus:

  • id
  • ir_pamācība
  • publicēšanas laiks
  • virsraksts
  • saturs
  • komentāru_skaits
  • lang ir valoda, kurā raksts ir uzrakstīts. Līdz šim tam ir tikai en un ru.
  • tags_string — visas ziņas atzīmes
  • lasīšanas_skaits
  • autors
  • punktu skaits — raksta vērtējums.

Tādējādi, izmantojot API, es samazināju skripta izpildes laiku līdz 8 sekundēm uz 100 url.

Kad esam lejupielādējuši nepieciešamos datus, tie ir jāapstrādā un jāievada datu bāzē. Man arī nebija nekādu problēmu ar šo:

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

Nu, tradicionāli, visbeidzot, jūs varat iegūt statistiku no datiem:

  • No paredzamajām 490 406 lejupielādēm tika lejupielādēti tikai 228 512 raksti. Izrādās, ka vairāk nekā puse (261894) rakstu par Habrē tika paslēpti vai dzēsti.
  • Visa datubāze, kas sastāv no gandrīz pusmiljona rakstu, sver 2.95 GB. Saspiestā formā - 495 MB.
  • Kopumā Habrē autori ir 37804 cilvēki. Atgādinu, ka šī statistika ir tikai no tiešraides ierakstiem.
  • Produktīvākais autors par Habrē - alizārs - 8774 raksti.
  • Visaugstāk novērtētais raksts — 1448 plusi
  • Lasītākais raksts — 1660841 skatījums
  • Visvairāk apspriestais raksts — 2444 komentāri

Nu topu veidāTop 15 autoriVisi Habr vienā datu bāzē
Top 15 pēc vērtējumaVisi Habr vienā datu bāzē
15 populārākie lasītieVisi Habr vienā datu bāzē
15 populārākie apspriestieVisi Habr vienā datu bāzē

Avots: www.habr.com

Pievieno komentāru