Όλα τα Habr σε μία βάση δεδομένων

Καλό απόγευμα. Έχουν περάσει 2 χρόνια από τότε που γράφτηκε. τελευταίο άρθρο σχετικά με την ανάλυση του Habr, και ορισμένα σημεία έχουν αλλάξει.

Όταν ήθελα να έχω ένα αντίγραφο του Habr, αποφάσισα να γράψω έναν αναλυτή που θα αποθηκεύει όλο το περιεχόμενο των συγγραφέων στη βάση δεδομένων. Πώς συνέβη και ποια σφάλματα αντιμετώπισα - μπορείτε να διαβάσετε κάτω από την περικοπή.

TLDR- σύνδεσμος βάσης δεδομένων

Η πρώτη έκδοση του αναλυτή. Ένα νήμα, πολλά προβλήματα

Αρχικά, αποφάσισα να φτιάξω ένα πρωτότυπο σεναρίου στο οποίο το άρθρο θα αναλύεται και θα τοποθετείται στη βάση δεδομένων αμέσως μετά τη λήψη. Χωρίς να το σκεφτώ δύο φορές, χρησιμοποίησα το sqlite3, γιατί. ήταν λιγότερο εντατική: δεν χρειάζεται να υπάρχει ένας τοπικός διακομιστής, που δημιουργήθηκε-εμφανίστηκε-διαγράφηκε και άλλα παρόμοια.

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)

Όλα είναι κλασικά - χρησιμοποιούμε Beautiful Soup, αιτήματα και ένα γρήγορο πρωτότυπο είναι έτοιμο. Αυτό είναι απλά…

  • Η λήψη της σελίδας είναι σε ένα νήμα

  • Εάν διακόψετε την εκτέλεση του σεναρίου, τότε ολόκληρη η βάση δεδομένων δεν θα πάει πουθενά. Εξάλλου, η δέσμευση εκτελείται μόνο μετά από όλη την ανάλυση.
    Φυσικά, μπορείτε να πραγματοποιήσετε αλλαγές στη βάση δεδομένων μετά από κάθε εισαγωγή, αλλά στη συνέχεια ο χρόνος εκτέλεσης του σεναρίου θα αυξηθεί σημαντικά.

  • Η ανάλυση των πρώτων 100 άρθρων μου πήρε 000 ώρες.

Στη συνέχεια βρίσκω το άρθρο του χρήστη συνολοκληρωμένη, το οποίο διάβασα και βρήκα μερικά life hacks για να επιταχύνω αυτή τη διαδικασία:

  • Η χρήση πολλαπλών νημάτων επιταχύνει τη λήψη κατά καιρούς.
  • Δεν μπορείτε να λάβετε την πλήρη έκδοση του habr, αλλά την έκδοση για κινητά.
    Για παράδειγμα, εάν ένα συνενσωματωμένο άρθρο στην έκδοση για υπολογιστές ζυγίζει 378 KB, τότε στην έκδοση για κινητά είναι ήδη 126 KB.

Δεύτερη έκδοση. Πολλά νήματα, προσωρινή απαγόρευση από το Habr

Όταν έψαξα στο Διαδίκτυο σχετικά με το θέμα του multithreading σε python, επέλεξα την απλούστερη επιλογή με το multiprocessing.dummy, παρατήρησα ότι εμφανίστηκαν προβλήματα μαζί με το multithreading.

Το SQLite3 δεν θέλει να λειτουργήσει με περισσότερα από ένα νήμα.
σταθερός check_same_thread=False, αλλά αυτό το σφάλμα δεν είναι το μόνο, όταν προσπαθείτε να εισαγάγετε στη βάση δεδομένων, μερικές φορές εμφανίζονται σφάλματα που δεν μπορούσα να λύσω.

Ως εκ τούτου, αποφασίζω να εγκαταλείψω την άμεση εισαγωγή άρθρων απευθείας στη βάση δεδομένων και, ενθυμούμενος τη συνενσωματωμένη λύση, αποφασίζω να χρησιμοποιήσω αρχεία, επειδή δεν υπάρχουν προβλήματα με την εγγραφή πολλαπλών νημάτων σε ένα αρχείο.

Ο Habr αρχίζει να απαγορεύει τη χρήση περισσότερων από τριών νημάτων.
Ιδιαίτερα έντονες προσπάθειες να περάσετε στο Habr μπορεί να καταλήξουν με απαγόρευση ip για μερικές ώρες. Επομένως, πρέπει να χρησιμοποιήσετε μόνο 3 νήματα, αλλά αυτό είναι ήδη καλό, καθώς ο χρόνος επανάληψης περισσότερων από 100 άρθρων μειώνεται από 26 σε 12 δευτερόλεπτα.

Αξίζει να σημειωθεί ότι αυτή η έκδοση είναι μάλλον ασταθής και οι λήψεις περιοδικά πέφτουν σε μεγάλο αριθμό άρθρων.

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)

Τρίτη έκδοση. Τελικός

Κατά τον εντοπισμό σφαλμάτων της δεύτερης έκδοσης, ανακάλυψα ότι το Habr, ξαφνικά, έχει ένα API στο οποίο έχει πρόσβαση η έκδοση για κινητά του ιστότοπου. Φορτώνει πιο γρήγορα από την έκδοση για κινητά, αφού είναι απλώς json, που δεν χρειάζεται καν ανάλυση. Στο τέλος, αποφάσισα να ξαναγράψω το σενάριό μου.

Έχοντας βρει λοιπόν αυτό το σύνδεσμο API, μπορείτε να ξεκινήσετε την ανάλυση του.

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)

Περιέχει πεδία που σχετίζονται τόσο με το ίδιο το άρθρο όσο και με τον συγγραφέα που το έγραψε.

API.png

Όλα τα Habr σε μία βάση δεδομένων

Δεν έβγαλα το πλήρες json κάθε άρθρου, αλλά αποθήκευσα μόνο τα πεδία που χρειαζόμουν:

  • id
  • is_tutorial
  • time_published
  • τίτλος
  • περιεχόμενο
  • comments_count
  • lang είναι η γλώσσα στην οποία είναι γραμμένο το άρθρο. Μέχρι στιγμής, έχει μόνο en και ru.
  • tags_string - όλες οι ετικέτες από την ανάρτηση
  • reading_count
  • συγγραφέας
  • βαθμολογία — βαθμολογία άρθρου.

Έτσι, χρησιμοποιώντας το API, μείωσα τον χρόνο εκτέλεσης του σεναρίου σε 8 δευτερόλεπτα ανά 100 url.

Αφού κατεβάσουμε τα δεδομένα που χρειαζόμαστε, πρέπει να τα επεξεργαστούμε και να τα εισάγουμε στη βάση δεδομένων. Ούτε με αυτό είχα κανένα πρόβλημα:

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)

Στατιστική

Λοιπόν, παραδοσιακά, τέλος, μπορείτε να εξαγάγετε ορισμένα στατιστικά στοιχεία από τα δεδομένα:

  • Από τις αναμενόμενες 490 λήψεις, λήφθηκαν μόνο 406 άρθρα. Αποδεικνύεται ότι περισσότερα από τα μισά (228) άρθρα σχετικά με το Habré κρύφτηκαν ή διαγράφηκαν.
  • Ολόκληρη η βάση δεδομένων, που αποτελείται από σχεδόν μισό εκατομμύριο άρθρα, ζυγίζει 2.95 GB. Σε συμπιεσμένη μορφή - 495 MB.
  • Συνολικά, 37804 άτομα είναι οι συγγραφείς του Habré. Υπενθυμίζω ότι αυτά τα στατιστικά είναι μόνο από ζωντανές αναρτήσεις.
  • Ο πιο παραγωγικός συγγραφέας στο Habré - αλιζάρ - 8774 άρθρα.
  • Άρθρο με κορυφαία βαθμολογία — 1448 συν
  • Το πιο διαβασμένο άρθρο — 1660841 προβολές
  • Το πιο πολυσυζητημένο άρθρο — 2444 σχόλια

Λοιπόν, με τη μορφή κορυφώνΟι 15 κορυφαίοι συγγραφείςΌλα τα Habr σε μία βάση δεδομένων
Top 15 με βαθμολογίαΌλα τα Habr σε μία βάση δεδομένων
Το Top 15 διαβάστηκεΌλα τα Habr σε μία βάση δεδομένων
Top 15 που συζητήθηκανΌλα τα Habr σε μία βάση δεδομένων

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο