All Habr an enger Datebank

Gudde Mëtteg. Et sinn 2 Joer zënter ech et geschriwwen hunn leschten Artikel iwwer Habr Parsing, an e puer Saachen hu geännert.

Wann ech eng Kopie vum Habr wollt hunn, hunn ech decidéiert e Parser ze schreiwen, deen all Inhalt vun den Auteuren an eng Datebank späichert. Wéi et geschitt ass a wéi eng Feeler ech begéint hunn - Dir kënnt ënnert dem Schnëtt liesen.

TL;DR - Link op d'Datebank

Éischt Versioun vum Parser. Ee Fuedem, vill Problemer

Fir unzefänken hunn ech décidéiert e Prototyp vun engem Skript ze maachen, an deem den Artikel direkt beim Download géif parséiert an an d'Datebank gesat ginn. Ouni zweemol ze denken, hunn ech sqlite3 benotzt, well ... et war manner Aarbechtsintensiv: Dir braucht net e lokale Server ze hunn, schafen, kucken, läschen a Saachen wéi dat.

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 ass no de Klassiker - mir benotzen Schéin Zopp, Ufroen an de schnelle Prototyp ass fäerdeg. Dat ass just ...

  • D'Säit gëtt an engem Thread erofgelueden

  • Wann Dir d'Ausféierung vum Skript ënnerbrach, da geet déi ganz Datebank néierens. No allem gëtt d'Verpflichtung nëmmen no all Parsing ausgefouert.
    Natierlech kënnt Dir Ännerunge fir d'Datebank no all Insertion engagéieren, awer da wäert d'Skriptausféierung däitlech eropgoen.

  • Parsing déi éischt 100 Artikelen huet mech 000 Stonnen.

Da fannen ech den Artikel vum Benotzer cointegréiert, déi ech gelies hunn a verschidde Liewenshacks fonnt hunn fir dëse Prozess ze beschleunegen:

  • D'Benotzung vu Multithreading beschleunegt den Download wesentlech.
  • Dir kënnt net déi voll Versioun vum Habr kréien, awer seng mobil Versioun.
    Zum Beispill, wann e kointegréierten Artikel an der Desktop Versioun 378 KB weegt, dann ass et an der mobiler Versioun schonn 126 KB.

Zweet Versioun. Vill Threads, temporäre Verbuet vum Habr

Wann ech den Internet iwwer d'Thema Multithreading am Python duerchgekuckt hunn an déi einfachst Optioun mat Multiprocessing.dummy gewielt hunn, hunn ech gemierkt datt Problemer zesumme mam Multithreading optrieden.

SQLite3 wëll net mat méi wéi engem thread schaffen.
Befestegt check_same_thread=False, awer dëse Feeler ass net deen eenzegen; wann Dir probéiert an d'Datebank ze setzen, entstinn heiansdo Feeler déi ech net konnt léisen.

Dofir décidéieren ech d'Instant Insertion vun Artikelen direkt an d'Datebank opzeginn an, erënnere mech un déi kointegréiert Léisung, décidéieren ech Dateien ze benotzen, well et keng Probleemer mat Multi-threaded Schreiwen op eng Datei sinn.

Habr fänkt un ze verbannen fir méi wéi dräi Threads ze benotzen.
Besonnesch ängschtlech Versuche fir Habr z'erreechen kënnen zu engem IP Verbuet fir e puer Stonnen féieren. Also musst Dir nëmmen 3 Threads benotzen, awer dat ass scho gutt, well d'Zäit fir 100 Artikelen ze sortéieren ass vu 26 op 12 Sekonnen reduzéiert.

Et ass derwäert ze bemierken datt dës Versioun zimmlech onbestänneg ass, an den Download periodesch op enger grousser Zuel vun Artikelen feelt.

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)

Drëtt Versioun. Finall

Beim Debugging vun der zweeter Versioun hunn ech entdeckt datt Habr op eemol eng API huet déi vun der mobiler Versioun vum Site zougänglech ass. Et lued méi séier wéi déi mobil Versioun, well et just json ass, wat net emol muss parséiert ginn. Um Enn hunn ech decidéiert mäi Skript nach eng Kéier ëmzeschreiwen.

Also, entdeckt dëse Link API, Dir kënnt et parséieren ufänken.

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)

Et enthält Felder am Zesummenhang mat dem Artikel selwer an dem Auteur deen et geschriwwen huet.

API.png

All Habr an enger Datebank

Ech hunn net de komplette json vun all Artikel gedumpt, awer nëmmen d'Felder gespäichert déi ech gebraucht hunn:

  • id
  • ass_tutorial
  • Zäit_publizéiert
  • Titel
  • Inhalt
  • comments_count
  • lang ass d'Sprooch an där den Artikel geschriwwen ass. Bis elo enthält et nëmmen en an ru.
  • tags_string - all Tags vum Post
  • liesen_zielen
  • Auteur
  • Score - Artikel Bewäertung.

Also, mat der API, hunn ech d'Skriptausféierungszäit op 8 Sekonnen pro 100 URL reduzéiert.

Nodeems mir d'Donnéeën erofgelueden hunn, déi mir brauchen, musse mir se veraarbecht an an d'Datebank aginn. Et waren och keng Probleemer mat dësem:

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)

Статистика

Gutt, traditionell, endlech, kënnt Dir e puer Statistiken aus den Donnéeën extrahéieren:

  • Vun den erwaarten 490 goufen nëmmen 406 Artikelen erofgelueden. Et stellt sech eraus datt méi wéi d'Halschent (228) vun den Artikelen iwwer Habré verstoppt oder geläscht goufen.
  • Déi ganz Datebank, déi aus bal eng hallef Millioun Artikelen besteet, weegt 2.95 GB. An kompriméierter Form - 495 MB.
  • Am Ganzen sinn et 37804 Auteuren op Habré. Loosst mech Iech drun erënneren datt dës Statistike nëmme vu Live Posts sinn.
  • De produktivsten Auteur op Habré - alizar - 8774 Artikelen.
  • Top bewäert Artikel - 1448 Pluss
  • Meescht gelies Artikel - 1660841 Meenung
  • Am meeschte geschwat Artikel - 2444 Kommentaren

Gutt, an der Form vun TopsTop 15 AuteurenAll Habr an enger Datebank
Top 15 no BewäertungAll Habr an enger Datebank
Top 15 liesenAll Habr an enger Datebank
Top 15 diskutéiertAll Habr an enger Datebank

Source: will.com

Setzt e Commentaire