Alla Habr i en databas

God eftermiddag. Det har gått 2 år sedan det skrevs. sista artikeln om att analysera Habr, och vissa punkter har ändrats.

När jag ville ha en kopia av Habr bestämde jag mig för att skriva en parser som skulle spara allt innehåll från författarna till databasen. Hur det gick till och vilka fel jag stötte på – du kan läsa under klippet.

TLDR- databaslänk

Den första versionen av parsern. En tråd, många problem

Till att börja med bestämde jag mig för att göra en skriptprototyp där artikeln skulle tolkas och placeras i databasen direkt efter nedladdning. Utan att tänka två gånger använde jag sqlite3, eftersom. det var mindre arbetskrävande: inget behov av att ha en lokal server, skapad-såg-raderad och sånt.

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)

Allt är klassiskt - vi använder Beautiful Soup, önskemål och en snabb prototyp är klar. Det är bara…

  • Sidnedladdning finns i en tråd

  • Om du avbryter körningen av skriptet kommer hela databasen att gå ingenstans. När allt kommer omkring utförs commit först efter all analys.
    Naturligtvis kan du göra ändringar i databasen efter varje infogning, men då kommer skriptexekveringstiden att öka avsevärt.

  • Att analysera de första 100 000 artiklarna tog mig 8 timmar.

Därefter hittar jag användarens artikel samintegrerad, som jag läste och hittade några life hacks för att påskynda den här processen:

  • Att använda multithreading snabbar upp nedladdningen ibland.
  • Du kan inte få den fullständiga versionen av habr, utan dess mobilversion.
    Till exempel, om en samintegrerad artikel i desktopversionen väger 378 KB, är den redan 126 KB i mobilversionen.

Andra versionen. Många trådar, tillfälligt förbud från Habr

När jag letade igenom Internet på ämnet multithreading i python valde jag det enklaste alternativet med multiprocessing.dummy, jag märkte att problem uppstod tillsammans med multithreading.

SQLite3 vill inte fungera med mer än en tråd.
Fast check_same_thread=False, men detta fel är inte det enda, när jag försöker infoga i databasen uppstår ibland fel som jag inte kunde lösa.

Därför bestämmer jag mig för att överge omedelbar infogning av artiklar direkt i databasen och, med tanke på den samintegrerade lösningen, bestämmer jag mig för att använda filer, eftersom det inte finns några problem med flertrådsskrivning till en fil.

Habr börjar förbjuda för att använda mer än tre trådar.
Särskilt nitiska försök att ta sig igenom till Habr kan sluta med ett ip-förbud i ett par timmar. Så du måste bara använda 3 trådar, men det här är redan bra, eftersom tiden för att iterera över 100 artiklar minskar från 26 till 12 sekunder.

Det är värt att notera att den här versionen är ganska instabil och nedladdningar faller periodvis av på ett stort antal artiklar.

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)

Tredje versionen. Slutlig

När jag felsökte den andra versionen upptäckte jag att Habr helt plötsligt har ett API som den mobila versionen av webbplatsen kommer åt. Den laddas snabbare än mobilversionen, eftersom det bara är json, som inte ens behöver analyseras. Till slut bestämde jag mig för att skriva om mitt manus igen.

Så, efter att ha hittat denna länk API, du kan börja analysera det.

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)

Den innehåller fält relaterade till både själva artikeln och författaren som skrev den.

API.png

Alla Habr i en databas

Jag dumpade inte hela json för varje artikel, utan sparade bara de fält jag behövde:

  • id
  • is_tutorial
  • tid_publicerad
  • rubricerade
  • innehåll
  • comments_count
  • lang är det språk som artikeln är skriven på. Hittills har det bara en och ru.
  • tags_string - alla taggar från inlägget
  • läsning_antal
  • Författaren
  • poäng — betyg av artikeln.

Med hjälp av API:t minskade jag skriptkörningstiden till 8 sekunder per 100 url.

Efter att vi har laddat ner den data vi behöver behöver vi bearbeta den och lägga in den i databasen. Jag hade inga problem med detta heller:

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)

Statistik

Tja, traditionellt, äntligen, kan du extrahera lite statistik från data:

  • Av de förväntade 490 406 nedladdningarna laddades endast 228 512 artiklar ner. Det visar sig att mer än hälften (261894) av artiklarna om Habré gömdes eller raderades.
  • Hela databasen, som består av nästan en halv miljon artiklar, väger 2.95 GB. I komprimerad form - 495 MB.
  • Totalt är 37804 personer författare till Habré. Jag påminner er om att denna statistik endast är från liveinlägg.
  • Den mest produktiva författaren om Habré - alizar - 8774 artiklar.
  • Högst rankad artikel — 1448 plus
  • Mest läst artikel — 1660841 visningar
  • Mest diskuterade artikeln — 2444 kommentarer

Jo, i form av topparTopp 15 författareAlla Habr i en databas
Topp 15 efter betygAlla Habr i en databas
Topp 15 läsningAlla Habr i en databas
Topp 15 diskuteradeAlla Habr i en databas

Källa: will.com

Lägg en kommentar