Tanan nga Habr sa usa ka database

Maayong hapon. 2 ka tuig na ang milabay sukad kini gisulat. katapusan nga artikulo mahitungod sa pag-parse sa Habr, ug ang pipila ka mga punto nausab.

Sa diha nga gusto ko nga adunay usa ka kopya sa Habr, nakahukom ko sa pagsulat sa usa ka parser nga makaluwas sa tanan nga mga sulod sa mga tagsulat ngadto sa database. Giunsa kini nahitabo ug unsa nga mga sayup ang akong nasugatan - mahimo nimong basahon ubos sa pagputol.

TLDR- link sa database

Ang unang bersyon sa parser. Usa ka thread, daghang problema

Sa pagsugod, nakahukom ko nga maghimo ug script prototype, diin ang artikulo ma-parse dayon sa pag-download ug ibutang sa database. Sa walay paghunahuna sa makaduha, gigamit nako ang sqlite3, tungod kay. dili kaayo labor-intensive: dili kinahanglan nga adunay usa ka lokal nga server, gibuhat-tan-awon-natangtang ug mga butang nga ingon niana.

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)

Ang tanan klasiko - gigamit namon ang Matahum nga Sabaw, mga hangyo ug usa ka dali nga prototype andam na. Mao ra…

  • Ang pag-download sa panid anaa sa usa ka thread

  • Kung gibalda nimo ang pagpatuman sa script, nan ang tibuuk nga database dili moadto bisan diin. Pagkahuman, ang commit gihimo lamang pagkahuman sa tanan nga pag-parse.
    Siyempre, mahimo nimong buhaton ang mga pagbag-o sa database pagkahuman sa matag pagsal-ot, apan unya ang oras sa pagpatuman sa script modaghan pag-ayo.

  • Ang pag-parse sa unang 100 ka artikulo mikuha ug 000 ka oras.

Sunod nakit-an nako ang artikulo sa tiggamit gihiusa, nga akong gibasa ug nakit-an ang pipila ka mga hack sa kinabuhi aron mapadali kini nga proseso:

  • Ang paggamit sa multithreading makapadali sa pag-download usahay.
  • Dili nimo makuha ang tibuuk nga bersyon sa habr, apan ang mobile nga bersyon niini.
    Pananglitan, kung ang usa ka cointegrated nga artikulo sa desktop nga bersyon adunay gibug-aton nga 378 KB, nan sa mobile nga bersyon kini 126 KB na.

Ikaduha nga bersyon. Daghang mga thread, temporaryo nga pagdili gikan sa Habr

Sa dihang gisuhid nako ang Internet sa hilisgutan sa multithreading sa python, gipili nako ang pinakasimple nga opsyon sa multiprocessing.dummy, akong namatikdan nga ang mga problema nagpakita uban sa multithreading.

SQLite3 dili gusto sa pagtrabaho uban sa labaw pa kay sa usa ka thread.
naayo check_same_thread=False, apan kini nga sayop dili lamang ang usa, sa diha nga naningkamot sa sal-ot ngadto sa database, mga sayop usahay mahitabo nga dili nako masulbad.

Busa, nakahukom ko nga biyaan ang instant insertion sa mga artikulo direkta ngadto sa database ug, sa paghinumdom sa cointegrated nga solusyon, nakahukom ko nga gamiton ang mga file, tungod kay walay mga problema sa multi-threaded nga pagsulat sa usa ka file.

Ang Habr nagsugod sa pagdili sa paggamit sa labaw sa tulo ka mga hilo.
Ilabi na ang madasigon nga mga pagsulay nga makaagi sa Habr mahimong matapos sa usa ka pagdili sa ip sulod sa pipila ka oras. Busa kinahanglan ka nga mogamit lamang sa 3 nga mga hilo, apan kini maayo na, tungod kay ang oras sa pag-uli sa sobra sa 100 nga mga artikulo gikunhoran gikan sa 26 ngadto sa 12 ka segundo.

Angay nga matikdan nga kini nga bersyon medyo dili lig-on, ug ang mga pag-download matag karon ug unya nahulog sa daghang mga artikulo.

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)

Ikatulo nga bersyon. Katapusan

Samtang nag-debug sa ikaduhang bersyon, akong nadiskobrehan nga ang Habr, sa kalit lang, adunay API nga gi-access sa mobile nga bersyon sa site. Mas paspas ang pag-load kaysa sa mobile nga bersyon, tungod kay json ra kini, nga dili na kinahanglan nga ma-parse. Sa katapusan, nakahukom ko nga isulat pag-usab ang akong script.

Busa, kay nakit-an kini nga sumpay API, mahimo nimong sugdan ang pag-parse niini.

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)

Naglangkob kini sa mga natad nga adunay kalabotan sa artikulo mismo ug sa tagsulat nga nagsulat niini.

API.png

Tanan nga Habr sa usa ka database

Wala nako gilabay ang bug-os nga json sa matag artikulo, apan gitipigan lamang ang mga natad nga akong gikinahanglan:

  • id
  • is_tutorial
  • time_published
  • titulo
  • sulod
  • comments_count
  • lang ang pinulongan diin gisulat ang artikulo. Sa pagkakaron, aduna lang kini en ug ru.
  • tags_string - tanang tag gikan sa post
  • pagbasa_ihap
  • awtor nga
  • score β€” rating sa artikulo.

Sa ingon, gamit ang API, gikunhoran nako ang oras sa pagpatuman sa script ngadto sa 8 segundos kada 100 ka url.

Human namo ma-download ang datos nga among gikinahanglan, kinahanglan namo kining iproseso ug isulod sa database. Wala usab akoy problema niini:

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)

Statistics

Aw, tradisyonal, sa katapusan, mahimo nimong makuha ang pipila ka mga estadistika gikan sa datos:

  • Sa gipaabot nga 490 ka download, 406 lang ka artikulo ang na-download. Mogawas nga kapin sa katunga (228) sa mga artikulo sa HabrΓ© ang gitago o gitangtang.
  • Ang tibuok database, nga gilangkoban sa halos tunga sa milyon nga mga artikulo, may gibug-aton nga 2.95 GB. Sa compressed nga porma - 495 MB.
  • Sa kinatibuk-an, 37804 ka tawo ang tagsulat sa HabrΓ©. Gipahinumdoman ko ikaw nga kini nga mga estadistika gikan lamang sa mga live post.
  • Ang labing produktibo nga tagsulat sa HabrΓ© - alizar - 8774 nga mga artikulo.
  • Top rated nga artikulo β€” 1448 ka dugang
  • Labing nabasa nga artikulo β€” 1660841 mga pagtan-aw
  • Labing Gihisgutan nga Artikulo β€” 2444 mga komento

Aw, sa porma sa mga tumoyTop 15 nga mga awtorTanan nga Habr sa usa ka database
Top 15 pinaagi sa ratingTanan nga Habr sa usa ka database
Top 15 nga gibasaTanan nga Habr sa usa ka database
Top 15 nga GihisgutanTanan nga Habr sa usa ka database

Source: www.habr.com

Idugang sa usa ka comment