Tüm Habr'lar tek bir veritabanında

Tünaydın. Bunu yazmayalı 2 yıl oldu son makale Habr ayrıştırma hakkında ve bazı şeyler değişti.

Habr'ın bir kopyasına sahip olmak istediğimde, yazarların tüm içeriğini bir veritabanına kaydedecek bir ayrıştırıcı yazmaya karar verdim. Nasıl oldu ve hangi hatalarla karşılaştım - kesimin altında okuyabilirsiniz.

TL; DR — veritabanına bağlantı

Ayrıştırıcının ilk sürümü. Tek konu, birçok sorun

Başlangıç ​​olarak, indirildikten hemen sonra makalenin ayrıştırılıp veritabanına yerleştirileceği bir komut dosyasının prototipini yapmaya karar verdim. Hiç düşünmeden sqlite3'ü kullandım çünkü... daha az emek gerektiriyordu: yerel bir sunucuya sahip olmanıza, oluşturmanıza, bakmanıza, silmenize ve bunun gibi şeylere ihtiyacınız yok.

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)

Her şey klasiklere göre - Güzel Çorba kullanıyoruz, istekler var ve hızlı prototip hazır. Bu sadece...

  • Sayfa tek bir başlıkta indirildi

  • Komut dosyasının yürütülmesini keserseniz, veritabanının tamamı hiçbir yere gitmez. Sonuçta, taahhüt ancak tüm ayrıştırma sonrasında gerçekleştirilir.
    Tabii ki, her eklemeden sonra veritabanına değişiklik yapabilirsiniz, ancak bu durumda komut dosyasının yürütme süresi önemli ölçüde artacaktır.

  • İlk 100 makaleyi ayrıştırmak 000 saatimi aldı.

Sonra kullanıcının makalesini buluyorum eşbütünleşikBunu okudum ve bu süreci hızlandırmak için birkaç hayat kurtarıcı tüyo buldum:

  • Çoklu iş parçacığı kullanmak indirme işlemini önemli ölçüde hızlandırır.
  • Habr'ın tam sürümünü değil, mobil sürümünü alabilirsiniz.
    Örneğin, eş bütünleşik bir makalenin masaüstü sürümünde ağırlığı 378 KB ise, mobil sürümde zaten 126 KB'dir.

İkinci versiyon. Birçok konu var, Habr'dan geçici yasak

Python'da çoklu iş parçacığı kullanımı konusunda interneti araştırdığımda ve multiprocessing.dummy ile en basit seçeneği seçtiğimde, çoklu iş parçacığıyla birlikte sorunların da ortaya çıktığını fark ettim.

SQLite3 birden fazla iş parçacığıyla çalışmak istemiyor.
Sabit check_same_thread=Falseama tek hata bu değil, veritabanına eklemeye çalışırken bazen çözemediğim hatalar çıkıyor.

Bu nedenle, makalelerin doğrudan veritabanına anında eklenmesinden vazgeçmeye karar veriyorum ve eş bütünleşik çözümü hatırlayarak, bir dosyaya çok iş parçacıklı yazmada herhangi bir sorun olmadığı için dosyaları kullanmaya karar veriyorum.

Habr üçten fazla konu kullandığı için yasaklamaya başladı.
Özellikle Habr'a ulaşmaya yönelik gayretli girişimler, birkaç saatliğine IP yasağıyla sonuçlanabilir. Yani yalnızca 3 konu kullanmanız gerekiyor, ancak bu zaten iyi, çünkü 100 makaleyi sıralamak için gereken süre 26 saniyeden 12 saniyeye düştü.

Bu sürümün oldukça kararsız olduğunu ve çok sayıda makalenin indirilmesinin periyodik olarak başarısız olduğunu belirtmekte fayda var.

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)

Üçüncü versiyon. Final

İkinci versiyonda hata ayıklarken Habr'ın birdenbire sitenin mobil versiyonundan erişilen bir API'ye sahip olduğunu keşfettim. Mobil sürümden daha hızlı yüklenir çünkü yalnızca json olduğundan ayrıştırılmasına bile gerek yoktur. Sonunda senaryomu yeniden yazmaya karar verdim.

Yani keşfettikten sonra Bu linki API'yi ayrıştırmaya başlayabilirsiniz.

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)

Hem makalenin kendisi hem de onu yazan yazarla ilgili alanları içerir.

API.png

Tüm Habr'lar tek bir veritabanında

Her makalenin json dosyasının tamamını atmadım, yalnızca ihtiyacım olan alanları kaydettim:

  • id
  • is_tutorial
  • time_published
  • başlık
  • içerik
  • yorum_sayısı
  • lang, makalenin yazıldığı dildir. Şu ana kadar sadece en ve ru içeriyor.
  • tags_string — gönderideki tüm etiketler
  • okuma_sayımı
  • yazar
  • puan — makale derecelendirmesi.

Böylece API'yi kullanarak komut dosyası yürütme süresini 8 URL başına 100 saniyeye düşürdüm.

İhtiyacımız olan verileri indirdikten sonra işleyip veritabanına girmemiz gerekiyor. Bunda da hiçbir sorun yoktu:

ayrıştırıcı.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)

istatistik

Nihayet geleneksel olarak verilerden bazı istatistikler çıkarabilirsiniz:

  • Beklenen 490 makalenin yalnızca 406'si indirildi. Habré hakkındaki makalelerin yarısından fazlasının (228) gizlendiği veya silindiği ortaya çıktı.
  • Yarım milyona yakın makaleden oluşan veri tabanının tamamı 2.95 GB ağırlığındadır. Sıkıştırılmış biçimde - 495 MB.
  • Toplamda Habré'de 37804 yazar bulunmaktadır. Bunların yalnızca canlı yayınlardan elde edilen istatistikler olduğunu hatırlatmama izin verin.
  • Habré'nin en üretken yazarı - Alizar — 8774 makale.
  • En çok oy alan makale — 1448 artı
  • En çok okunan makale — 1660841 görüntüleme
  • En çok konuşulan makale — 2444 yorum

Peki, üstler şeklindeEn iyi 15 yazarTüm Habr'lar tek bir veritabanında
Derecelendirmeye göre ilk 15Tüm Habr'lar tek bir veritabanında
İlk 15 okunanTüm Habr'lar tek bir veritabanında
Tartışılan İlk 15Tüm Habr'lar tek bir veritabanında

Kaynak: habr.com

Yorum ekle