Alle Habr yn ien databank

Goeiemiddei. It is 2 jier lyn dat ik it skreau lêste artikel oer Habr parsing, en guon dingen binne feroare.

Doe't ik in kopy fan Habr ha woe, besleat ik in parser te skriuwen dy't alle ynhâld fan 'e auteurs yn in databank bewarje soe. Hoe't it barde en hokker flaters ik tsjinkaam - kinne jo lêze ûnder de besuniging.

TL;DR - keppeling nei databank

Earste ferzje fan de parser. Ien tried, in protte problemen

Om te begjinnen, haw ik besletten om in prototype fan in skript te meitsjen wêryn, fuortendaliks by it downloaden, it artikel parseard wurde soe en yn 'e databank pleatst wurde. Sûnder twa kear nei te tinken brûkte ik sqlite3, om't ... it wie minder arbeidsintensyf: jo hoege gjin lokale server te hawwen, oanmeitsje, sjen, wiskje en soksoarte dingen.

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)

Alles is neffens de klassiken - wy brûke Beautiful Soup, oanfragen en it rappe prototype is klear. Dat is gewoan...

  • De side wurdt yn ien tried downloade

  • As jo ​​​​de útfiering fan it skript ûnderbrekke, dan sil de folsleine databank nearne gean. Ommers, de commit wurdt útfierd pas nei alle parsing.
    Fansels kinne jo nei elke ynfoegje feroarings yn 'e databank tajaan, mar dan sil de skriptútfieringstiid signifikant tanimme.

  • It parsearjen fan de earste 100 artikels naam my 000 oeren.

Dan fyn ik it artikel fan de brûker koïntegrearre, dy't ik haw lêzen en ferskate libbenshacks fûn om dit proses te fersnellen:

  • It brûken fan multithreading fersnelt it downloaden signifikant.
  • Jo kinne net de folsleine ferzje fan Habr ûntfange, mar de mobile ferzje.
    Bygelyks, as in cointegrated artikel yn 'e buroblêd ferzje waacht 378 KB, dan yn' e mobile ferzje is it al 126 KB.

Twadde ferzje. In protte triedden, tydlik ferbod fan Habr

Doe't ik it ynternet opsocht oer it ûnderwerp fan multithreading yn python en keas de ienfâldichste opsje mei multiprocessing.dummy, Ik fernaam dat problemen ferskynde tegearre mei multithreading.

SQLite3 wol net wurkje mei mear as ien tried.
Fêst check_same_thread=False, mar dizze flater is net de ienige; by it besykjen fan ynfoegje yn 'e databank, komme soms flaters dy't ik net oplosse koe.

Dêrom beslute ik de direkte ynfoegje fan artikels direkt yn 'e databank te ferlitten en, oantinken oan' e cointegrated oplossing, beslút ik bestannen te brûken, om't d'r gjin problemen binne mei multi-threaded skriuwen nei in bestân.

Habr begjint te ferbieden foar it brûken fan mear as trije triedden.
Benammen iverige besykjen om Habr te berikken kinne resultearje yn in IP-ferbod foar in pear oeren. Sa moatte jo mar 3 triedden brûke, mar dit is al goed, om't de tiid om 100 artikels te sortearjen wurdt fermindere fan 26 nei 12 sekonden.

It is de muoite wurdich opskriuwen dat dizze ferzje is frij ynstabyl, en de download periodyk mislearret op in grut oantal artikels.

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)

Tredde ferzje. Finale

By it debuggen fan 'e twadde ferzje ûntduts ik dat Habr ynienen in API hat dy't tagonklik is troch de mobile ferzje fan' e side. It laadt rapper dan de mobile ferzje, om't it gewoan json is, dy't net iens hoecht te wurde parseard. Uteinlik besleat ik myn skript nochris te herskriuwen.

Dus, ûntdutsen dizze keppeling API, jo kinne it begjinne te parsearjen.

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)

It befettet fjilden yn ferbân mei sawol it artikel sels as de skriuwer dy't it skreau.

API.png

Alle Habr yn ien databank

Ik dumpte de folsleine json fan elk artikel net, mar bewarre allinich de fjilden dy't ik nedich wie:

  • id
  • is_tutorial
  • tiid_publisearre
  • titel
  • ynhâld
  • comments_count
  • lang is de taal wêryn it artikel skreaun is. Oant no ta befettet it allinnich en en ru.
  • tags_string - alle tags fan 'e post
  • lêzen_count
  • skriuwer
  • skoare - artikel wurdearring.

Sa, mei help fan de API, Ik redusearre de skript útfiering tiid nei 8 sekonden per 100 url.

Nei't wy de gegevens hawwe downloade dy't wy nedich binne, moatte wy it ferwurkje en yn 'e databank ynfiere. D'r wiene ek gjin problemen mei dit:

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)

Статистика

No, tradysjoneel kinne jo úteinlik wat statistiken út 'e gegevens ekstrahearje:

  • Fan de ferwachte 490 waarden mar 406 artikels ynladen. It docht bliken dat mear as de helte (228) fan de artikels oer Habré ferburgen of wiske is.
  • De hiele databank, besteande út hast in heal miljoen artikels, waacht 2.95 GB. Yn komprimearre foarm - 495 MB.
  • Yn totaal binne der 37804 auteurs op Habré. Lit my jo herinnerje dat dit allinich statistiken binne fan live berjochten.
  • De meast produktive auteur op Habré - alizar - 8774 artikels.
  • Top rated artikel - 1448 pluses
  • Meast lêzen artikel - 1660841 views
  • Meast praat oer artikel - 2444 opmerkingen

No, yn 'e foarm fan topsTop 15 skriuwersAlle Habr yn ien databank
Top 15 troch wurdearringAlle Habr yn ien databank
Top 15 lêzenAlle Habr yn ien databank
Top 15 besprutsenAlle Habr yn ien databank

Boarne: www.habr.com

Add a comment