Lahat ng Habr sa isang database

Magandang hapon. 2 years na ang nakalipas simula ng isulat ito. huling artikulo tungkol sa pag-parse ng Habr, at nagbago ang ilang punto.

Nang gusto kong magkaroon ng kopya ng Habr, nagpasya akong magsulat ng parser na magse-save ng lahat ng nilalaman ng mga may-akda sa database. Paano ito nangyari at kung anong mga error ang naranasan ko - maaari mong basahin sa ilalim ng hiwa.

TLDR- link sa database

Ang unang bersyon ng parser. Isang thread, maraming problema

Upang magsimula sa, nagpasya akong gumawa ng isang script prototype, kung saan ang artikulo ay ma-parse kaagad sa pag-download at ilagay sa database. Nang hindi nag-iisip ng dalawang beses, gumamit ako ng sqlite3, dahil. ito ay hindi gaanong labor-intensive: hindi na kailangang magkaroon ng isang lokal na server, nilikha-mukhang-tinanggal at mga bagay-bagay tulad na.

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)

Classic ang lahat - gumagamit kami ng Beautiful Soup, mga kahilingan at handa na ang isang mabilis na prototype. yun lang…

  • Ang pag-download ng pahina ay nasa isang thread

  • Kung maabala mo ang pagpapatupad ng script, kung gayon ang buong database ay mapupunta kahit saan. Pagkatapos ng lahat, ang commit ay isinasagawa lamang pagkatapos ng lahat ng pag-parse.
    Siyempre, maaari kang gumawa ng mga pagbabago sa database pagkatapos ng bawat pagpapasok, ngunit pagkatapos ay ang oras ng pagpapatupad ng script ay tataas nang malaki.

  • Ang pag-parse sa unang 100 artikulo ay inabot ako ng 000 oras.

Susunod na mahanap ko ang artikulo ng gumagamit pinagsama-sama, na binasa ko at nakahanap ako ng ilang life hack para mapabilis ang prosesong ito:

  • Ang paggamit ng multithreading ay nagpapabilis sa pag-download minsan.
  • Maaari mong makuha hindi ang buong bersyon ng habr, ngunit ang mobile na bersyon nito.
    Halimbawa, kung ang isang pinagsama-samang artikulo sa desktop na bersyon ay tumitimbang ng 378 KB, sa mobile na bersyon ito ay 126 KB na.

Pangalawang bersyon. Maraming mga thread, pansamantalang pagbabawal mula sa Habr

Nang magsaliksik ako sa Internet sa paksa ng multithreading sa python, pinili ko ang pinakasimpleng opsyon sa multiprocessing.dummy, napansin kong may mga problemang lumitaw kasama ng multithreading.

Ang SQLite3 ay hindi gustong gumana sa higit sa isang thread.
nakapirming check_same_thread=False, ngunit ang error na ito ay hindi lamang isa, kapag sinusubukang ipasok sa database, minsan nangyayari ang mga error na hindi ko malutas.

Samakatuwid, nagpasya akong iwanan ang agarang pagpasok ng mga artikulo nang direkta sa database at, naaalala ang cointegrated na solusyon, nagpasya akong gumamit ng mga file, dahil walang mga problema sa multi-threaded na pagsulat sa isang file.

Nagsimulang mag-ban si Habr para sa paggamit ng higit sa tatlong mga thread.
Lalo na ang masigasig na pagtatangka na makapunta sa Habr ay maaaring mauwi sa isang ip ban sa loob ng ilang oras. Kaya kailangan mong gumamit lamang ng 3 mga thread, ngunit ito ay mabuti na, dahil ang oras upang umulit sa higit sa 100 mga artikulo ay nabawasan mula 26 hanggang 12 segundo.

Ito ay nagkakahalaga na tandaan na ang bersyon na ito ay medyo hindi matatag, at ang mga pag-download ay pana-panahong nahuhulog sa isang malaking bilang ng 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)

Pangatlong bersyon. Pangwakas

Habang nagde-debug sa pangalawang bersyon, natuklasan ko na ang Habr, bigla-bigla, ay may API na ina-access ng mobile na bersyon ng site. Mas mabilis itong naglo-load kaysa sa mobile na bersyon, dahil json lang ito, na hindi na kailangang i-parse. Sa huli, nagpasya akong muling isulat muli ang aking script.

Kaya, nahanap ang link na ito API, maaari mong simulan ang pag-parse nito.

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)

Naglalaman ito ng mga patlang na nauugnay sa artikulo mismo at sa may-akda na sumulat nito.

API.png

Lahat ng Habr sa isang database

Hindi ko itinapon ang buong json ng bawat artikulo, ngunit nai-save lamang ang mga patlang na kailangan ko:

  • id
  • ay_tutorial
  • time_published
  • pamagat
  • nilalaman
  • comments_count
  • lang ay ang wika kung saan nakasulat ang artikulo. Sa ngayon, mayroon lamang itong en at ru.
  • tags_string - lahat ng mga tag mula sa post
  • reading_count
  • may-akda
  • marka — rating ng artikulo.

Kaya, gamit ang API, binawasan ko ang oras ng pagpapatupad ng script sa 8 segundo bawat 100 url.

Pagkatapos naming ma-download ang data na kailangan namin, kailangan naming iproseso ito at ipasok ito sa database. Wala rin akong problema dito:

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

Well, ayon sa kaugalian, sa wakas, maaari mong kunin ang ilang mga istatistika mula sa data:

  • Sa inaasahang 490 na pag-download, 406 na artikulo lamang ang na-download. Lumalabas na higit sa kalahati (228) ng mga artikulo sa Habré ang itinago o tinanggal.
  • Ang buong database, na binubuo ng halos kalahating milyong artikulo, ay tumitimbang ng 2.95 GB. Sa compressed form - 495 MB.
  • Sa kabuuan, 37804 katao ang may-akda ng Habré. Ipinapaalala ko sa iyo na ang mga istatistikang ito ay mula lamang sa mga live na post.
  • Ang pinaka-produktibong may-akda sa Habré - alizar - 8774 na mga artikulo.
  • Top rated na artikulo — 1448 plus
  • Karamihan sa nabasang artikulo — 1660841 view
  • Pinaka-tinalakay na Artikulo — 2444 komento

Well, sa anyo ng mga topsNangungunang 15 may-akdaLahat ng Habr sa isang database
Nangungunang 15 ayon sa ratingLahat ng Habr sa isang database
Top 15 basahinLahat ng Habr sa isang database
Top 15 TinalakayLahat ng Habr sa isang database

Pinagmulan: www.habr.com

Magdagdag ng komento