22 sierpnia 2022 10 min. czytania

Ssszukaj książek jak programista

Lubię czytać książki i spędzam sporo czasu na ich poszukiwaniu. Dlatego zautomatyzowałem ten proces ze skryptem w języku Python do web scrapingu.

Zaktualizowano: 22 sierpnia 2022
Ręka z lupą sprawdzająca klawiaturę laptopa
Zdjęcie autorstwa Agence Olloweb

Lubię czytać książki. Różne gatunki wypełniają mój regał: ekonomia, psychologia, filozofia, biografie, literatura faktu, a w szczególności książki naukowe. Bonobo z okładki książki Fransa de Walla spogląda na wybitną twarz Einsteina. Zanim jednak cokolwiek tam postawię, dużo szukam. Potrafię spędzić dosłownie godzinę, zanim kliknę “kup” w księgarni internetowej. Poleceń szukam wszędzie: pytając znajomych, nieznajomych, przeglądając forum Quora, Medium i oczywiście - Lubimyczytać. Przeszukiwanie sieci jest fajne, ale każda godzina spędzona na szukaniu, zabiera czas od właściwego czytania. Dlatego, jak prawdziwy programista, spędźmy kolejne godziny automatyzując ten proces!

Zanim napiszę pierwszą linię kodu, muszę zaznaczyć - nie jestem programistą Pythona. Skończyłem jakieś kursy o języku Python, data science i przeczytałem książkę, ale to wszystko. Ten post jest dla mnie okazją do nauki. Możesz uczyć się ze mną. Trochę wiedzy o sieci i języku Python jest wymagane, aby kontynuować.

Plan

Zacznijmy od planu. Co chcemy osiągnąć? Chcemy listę najlepszych książek na konkretny temat lub z konkretnej kategorii. Lubimyczytać ma ogromny katalog książek, dlatego użyjemy go. Wybieranie “najlepszych” książek będzie intersubiektywne - wykorzystamy istniejące oceny użytkowników. To nie jest tak, że znalezione książki będą najlepsze dla każdego. Ale istnieje spora szansa, że nam się spodobają. Zawierzymy mądrości tłumu. Pomyślmy o krokach, które musimy podjąć.

  • Odwiedzić stronę Lubimyczytać.
  • Zażądać listy książek z konkretnej kategorii.
  • Iterować po wielu stronach.
  • Przetworzyć odpowiedź dla każdego adresu URL.
  • Znaleźć elementy DOM ze średnią ocen i liczbą ocen.
  • Stworzyć listę z najlepszymi książkami.
  • Ustalić subiektywne zasady dodawania książek do lisy.
  • Zapisać listę do jakiegoś pliku.

Środowisko

Będę używał Ubuntu, gdzie język Python jest preinstalowany. Jeżeli używasz innego systemu operacyjnego, tu jest link do pobrania. W tym projekcie będę używał Pythona 3.8.10. Będę używał także zewnętrznych modułów. Aby uniknąć problemów z wersjami, stworzymy wirtualne środowisko (dla systemu Windows, kroki mogą się trochę różnić).

🔴 🟡 🟢
python3 -m venv best-books

Będzie ono zawierało konkretne wersje interpretera Python i innych modułów. Po stworzeniu wirtualnego środowiska, musimy je aktywować.

🔴 🟡 🟢
source best-books/bin/activate

Ta komenda umieści pliki wykonywalne pip i python w ścieżce PATH powłoki. Aby sprawdzić czy działa, możesz wpisać:

🔴 🟡 🟢
which python

Powinna wyświetlić się ścieżka:

🔴 🟡 🟢
.../best-books/bin/python

Teraz jesteśmy gotowi, żeby napisssać pierwsze linie kodu. Dobra, nie użyję tego żartu ponownie. Zwłaszcza, że nazwa języka pochodzi od komedii “Latający Cyrk Monty Pythona”, a nie od węża.

Zewnętrzne moduły

Przeszukałem sieć i znalazłem sporo narzędzi do web scrapingu dla języka Python. Znalazłem na przykład Scrapy - popularny, potężny i wydajny framework do budowania pająków, które będą przeszukiwały sieć. Brzmi fajnie, ale dla naszego prostego skryptu byłaby to przesada. Krzywa uczenia się jest podobno stroma. Dlatego wykorzystamy dwie prostsze biblioteki. Wykorzystamy bibliotekę requests do wykonywania żądań HTTP. Nazwa może nie jest ekscytująca, ale niech cię to nie zwiedzie - to jest jedna z najpopularniejszych bibliotek Pythona. Zainstalujmy ją.

🔴 🟡 🟢
pip install requests

Po otrzymaniu odpowiedzi, musimy wydobyć potrzebne dane ze strony internetowej. BeautifulSoup pomoże nam w tym. Jest to biblioteka parsująca - pomaga wyłuskać dane z plików XML i HTML. No i nazwa jest bardziej fantazyjna. Współczesne strony chyba faktycznie mogą być piękną zupą składającą się plików JavaScript, HTML i CSS.

🔴 🟡 🟢
pip install beautifulsoup4

Skrypt Pythona do web scrapingu

Najpierw musimy zaimportować metodę get z biblioteki requests.

PYTHON
1from requests import get

Zażądajmy strony Lubimyczytać z książkami popularnonaukowymi, aby sprawdzić czy działa.

PYTHON
1from requests import get
2
3url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"
4res = get(url)
5
6print(res.status_code) # 200
7print(res.text) # Zawartość strony
🔴 🟡 🟢
python3 scrape-books.py

Jeżeli wyświetliłeś zawartość, zauważyłeś dużo rzeczy w terminalu. Teraz musimy zaimportować bibliotekę BeautifulSoup, aby to przetworzyć.

PYTHON
1from requests import get
2from bs4 import BeautifulSoup
3
4url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"
5res = get(url)
6soup = BeautifulSoup(res.text, "html.parser")
7
8print(soup.prettify()) # Sformatowany HTML

Teraz dane wyjściowe z naszego terminala są bardziej czytelne. Książki mają klasę authorAllBooks__single. Możemy ją wykorzystać, aby złapać je wszystkie.

PYTHON
1from requests import get
2from bs4 import BeautifulSoup
3
4url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"
5res = get(url)
6soup = BeautifulSoup(res.text, "html.parser")
7books = soup.select(".authorAllBooks__single")
8
9print(books) # Lista książek
10print(len(books)) # 30

Musimy znaleźć elementy DOM zawierające informacje o ocenach i ich liczbie.

HTML
1<div class="listLibrary__rating">
2 <span class="listLibrary__ratingText"> Średnia ocen: </span>
3 <div class="listLibrary__ratingStars">
4 <span class="listLibrary__ratingStarsImg icon-icon-star-full"></span>
5 <span class="listLibrary__ratingStarsNumber"> 8,3</span> / 10
6 </div>
7</div>
8<div class="listLibrary__ratingAll">3 ocen</div>

Elementy, których szukamy mają klasy listLibrary__ratingStarsNumber i listLibrary__ratingAll. Wykorzystując je możemy wydobyć oceny z książek.

PYTHON
1from requests import get
2from bs4 import BeautifulSoup
3
4url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"
5res = get(url)
6soup = BeautifulSoup(res.text, "html.parser")
7books = soup.select(".authorAllBooks__single")
8
9for book in books:
10 average_rating = book.select_one(
11 ".listLibrary__ratingStarsNumber").get_text(strip=True).replace(",", ".")
12 ratings_string = book.select_one(
13 ".listLibrary__ratingAll").get_text(strip=True).split()[0]
14 ratings = 0 if ratings_string == "ocen" else ratings_string
15
16 print(float(average_rating)) # Float np. 8.3
17 print(int(ratings)) # Int np. 3

Sporo się tu dzieje, dlatego zatrzymajmy się na chwilę. Z każdej książki na stronie chcemy wydobyć informację o średniej i liczbie ocen. Na początek wybieramy span ze średnią ocen i wyciągamy z niego tekst. Zamieniamy przecinki na kropki, żeby móc zmienić typ danych na float. Następnie wybieramy element z informacją o liczbie ocen. Dzielimy ten tekst wykorzystując spacje i wybieramy z listy pierwszy element. Prawie zawsze znajduje się w nim liczba ocen, ale czasem jej brakuje. Dlatego wykorzystałem operator trójskładnikowy, żeby uwzględnić ten przypadek. Mogłem wykorzystać regex albo iterację przez tekst, aby wydobyć te informacje zamiast polegać na pozycjach, ale nie chciałem komplikować tego fragmentu. Wyciągnąłem także informacje o autorze, tytule i link.

PYTHON
1from requests import get
2from bs4 import BeautifulSoup
3
4url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"
5res = get(url)
6soup = BeautifulSoup(res.text, "html.parser")
7books = soup.select(".authorAllBooks__single")
8
9for book in books:
10 average_rating = book.select_one(
11 ".listLibrary__ratingStarsNumber").get_text(strip=True).replace(",", ".")
12 ratings_string = book.select_one(
13 ".listLibrary__ratingAll").get_text(strip=True).split()[0]
14 ratings = 0 if ratings_string == "ocen" else ratings_string
15 title = book.select_one(
16 ".authorAllBooks__singleTextTitle").get_text(strip=True)
17 author = book.select_one(
18 ".authorAllBooks__singleTextAuthor").get_text(strip=True)
19 link = book.select_one(
20 ".authorAllBooks__singleTextTitle")["href"]
21 print(title, author) # Tytuł, autor dla każdej książki

Teraz w końcu możemy wybrać najlepsze książki. Bądźmy wybredni - książka, aby być na naszej liście musi mieć średnią powyżej 8.0 i liczbę ocen powyżej 2000. Możemy zmodyfikować zmienne: average_rating_threshold i average_threshold, aby poluzować warunki. Przed pętlą stworzyłem listę best_books. Informacje o książce są do niej dodane jeżeli oba warunki zostały spełnione.

PYTHON
1from requests import get
2from bs4 import BeautifulSoup
3
4base_url = "https://lubimyczytac.pl"
5url = f"{base_url}/ksiazki/k/107/literatura-popularnonaukowa"
6average_rating_threshold = 8.0
7ratings_threshold = 2000
8
9res = get(url)
10soup = BeautifulSoup(res.text, "html.parser")
11books = soup.select(".authorAllBooks__single")
12
13best_books = []
14for book in books:
15 average_rating = float(book.select_one(
16 ".listLibrary__ratingStarsNumber").get_text(strip=True).replace(",", "."))
17 ratings_string = book.select_one(
18 ".listLibrary__ratingAll").get_text(strip=True).split()[0]
19 ratings = int(0 if ratings_string == "ocen" else ratings_string)
20
21 if average_rating > average_rating_threshold and ratings > ratings_threshold:
22 title = book.select_one(
23 ".authorAllBooks__singleTextTitle").get_text(strip=True)
24 author = book.select_one(
25 ".authorAllBooks__singleTextAuthor").get_text(strip=True)
26 link = book.select_one(
27 ".authorAllBooks__singleTextTitle")["href"]
28 best_book = {"author": author, "title": title,
29 "average_rating": average_rating, "ratings": ratings, "link": f"{base_url}{link}"}
30
31 best_books.append(best_book)
32
33print(best_books) # Lista książek ze średnią powyżej 8.0 i ponad 2000 ocen

Udało nam się “zeskrobać” książki z jednej strony. Zróbmy coś nawet lepszego - przeszukajmy dziesięć stron!

PYTHON
1from requests import get
2from bs4 import BeautifulSoup
3
4base_url = "https://lubimyczytac.pl"
5average_rating_threshold = 8.0
6rating_threshold = 2000
7start_page = 1
8stop_page = 11
9
10best_books = []
11for page in range(start_page, stop_page):
12 url = f"{base_url}/katalog?page={page}&listId=booksFilteredList&category[]=68&rating[]=0&rating[]=10&publishedYear[]=1200&publishedYear[]=2022&catalogSortBy=ratings-desc&paginatorType=Standard"
13 res = get(url)
14 soup = BeautifulSoup(res.text, "html.parser")
15 books = soup.select(".authorAllBooks__single")
16
17 for book in books:
18 average_rating = float(book.select_one(
19 ".listLibrary__ratingStarsNumber").get_text(strip=True).replace(",", "."))
20 ratings_string = book.select_one(
21 ".listLibrary__ratingAll").get_text(strip=True).split()[0]
22 ratings = int(0 if ratings_string == "ocen" else ratings_string)
23
24 if average_rating > average_rating_threshold and ratings > rating_threshold:
25 title = book.select_one(
26 ".authorAllBooks__singleTextTitle").get_text(strip=True)
27 author = book.select_one(
28 ".authorAllBooks__singleTextAuthor").get_text(strip=True)
29 link = book.select_one(
30 ".authorAllBooks__singleTextTitle")["href"]
31 best_book = {"author": author, "title": title,
32 "average_rating": average_rating, "ratings": ratings, "link": f"{base_url}{link}"}
33 best_books.append(best_book)
34
35print(best_books)

Zmodyfikowałem adres URL aby zawierał aktualną stronę. Iterowałem przez strony używając funkcji range. Po znalezieniu książek pozostał ostatni krok - zapisanie ich do pliku. Poniżej pętli iterującej przez strony, dodałem fragment kodu, który zapisuje najlepsze książki do pliku markdown.

PYTHON
1# ...
2from operator import itemgetter
3
4# ...
5
6subject = "popularnonaukowe"
7
8# ...
9
10best_books = [] # Tu śą najlepsze książki
11
12# ...
13
14file = open(f"najlepsze-ksiazki-{subject}.md", "w")
15file.write(f"## Najlepsze książki z kategorii: {subject}\n")
16for book in best_books:
17 title, author, average_rating, ratings, link = itemgetter(
18 "title", "author", "average_rating", "ratings", "link")(book)
19 list_item = f'- [{title}]({link})<br />autorstwa {author} | <small title="Średnia ocen">{average_rating}⭐</small> <small>{ratings} ocen</small>\n'
20 file.write(list_item)
21
22file.close()

Wykorzystałem wbudowane funkcje języka Python: open, write i close, aby zapisać najlepsze książki do pliku. Pierwsza linia w pliku to nagłówek h2 z informacją o kategorii. itemgetter to funkcja, która pozwala w elegancki sposób wydobyć dane ze słownika. Następnie użyłem tych danych, aby stworzyć element listy dla każdej książki. Znajduję się tam trochę znaczników HTML, aby wyglądało to ładniej. No i to wszystko - mamy najlepsze książki w pliku. Tu jest jego zawartość...

MARKDOWN
1## Najlepsze książki z kategorii: popularnonaukowe
2
3- [Sapiens. Od zwierząt do bogów](https://lubimyczytac.pl/ksiazka/4988781/sapiens-od-zwierzat-do-bogow)<br />autorstwa Yuval Noah Harari | <small title="Średnia ocen">8.3⭐</small> <small>9123 ocen</small>
4- [Sztuka obsługi penisa](https://lubimyczytac.pl/ksiazka/4818570/sztuka-obslugi-penisa)<br />autorstwa Przemysław Pilarski,Andrzej Gryżewski | <small title="Średnia ocen">8.5⭐</small> <small>3333 ocen</small>
5- [Krótka historia prawie wszystkiego](https://lubimyczytac.pl/ksiazka/3749344/krotka-historia-prawie-wszystkiego)<br />autorstwa Bill Bryson | <small title="Średnia ocen">8.2⭐</small> <small>2123 ocen</small>

...i to jak się wyświetla:

Najlepsze książki z kategorii: popularnonaukowe

Ostateczny skrypt

Ostateczna wersja naszego skryptu wygląda tak:

PYTHON
1from bs4 import BeautifulSoup
2from operator import itemgetter
3from requests import get
4
5def scrape_books(start_page=1, stop_page=11, average_rating_threshold=8.0, ratings_threshold=2000):
6 if not (isinstance(start_page, int) and isinstance(stop_page, int) and isinstance(average_rating_threshold, float) and isinstance(ratings_threshold, int)):
7 raise TypeError("Incompatible types of arguments")
8
9 if not (start_page > 0 and stop_page > 0 and stop_page > start_page and 10.0 >= average_rating_threshold >= 0.0 and ratings_threshold > 0):
10 raise TypeError("Incompatible values of arguments")
11
12 try:
13 base_url = "https://lubimyczytac.pl"
14 best_books = []
15 for page in range(start_page, stop_page):
16 url = f"{base_url}/katalog?page={page}&listId=booksFilteredList&category[]=68&rating[]=0&rating[]=10&publishedYear[]=1200&publishedYear[]=2022&catalogSortBy=ratings-desc&paginatorType=Standard"
17 res = get(url)
18 res.raise_for_status()
19 soup = BeautifulSoup(res.text, "html.parser")
20 books = soup.select(".authorAllBooks__single")
21
22 for book in books:
23 average_rating = float(book.select_one(
24 ".listLibrary__ratingStarsNumber").get_text(strip=True).replace(",", "."))
25 ratings_string = book.select_one(
26 ".listLibrary__ratingAll").get_text(strip=True).split()[0]
27 ratings = int(0 if ratings_string ==
28 "ocen" else ratings_string)
29
30 if average_rating > average_rating_threshold and ratings > ratings_threshold:
31 title = book.select_one(
32 ".authorAllBooks__singleTextTitle").get_text(strip=True)
33 author = book.select_one(
34 ".authorAllBooks__singleTextAuthor").get_text(strip=True)
35 link = book.select_one(
36 ".authorAllBooks__singleTextTitle")["href"]
37 best_book = {"author": author, "title": title,
38 "average_rating": average_rating, "ratings": ratings, "link": f"{base_url}{link}"}
39 best_books.append(best_book)
40
41 return best_books
42
43 except Exception as err:
44 print(f"There was a problem during scraping books: {err}")
45
46def save_books(book_list=[], subject="subject"):
47 if not (isinstance(book_list, list) and isinstance(subject, str)):
48 raise TypeError("Incompatible types of arguments")
49 if len(book_list) > 0:
50 file = open(f"najlepsze-ksiazki-{subject}.md", "w")
51 try:
52 file.write(f"## Najlepsze książki z kategorii: {subject}\n")
53 for book in book_list:
54 title, author, average_rating, ratings, link = itemgetter(
55 "title", "author", "average_rating", "ratings", "link")(book)
56 list_item = f'- [{title}]({link})<br />autorstwa {author} | <small title="Średnia ocen">{average_rating}⭐</small> <small>{ratings} ocen</small>\n'
57 file.write(list_item)
58 finally:
59 file.close()
60
61best_books = scrape_books()
62save_books(best_books, "popularnonaukowe")

Zrobiłem małą refaktoryzację. Umieściłem kod odpowiedzialny za scraping i zapisywanie w oddzielnych funkcjach. Mają domyślne parametry, dlatego nie musisz podawać ich wszystkich. Możesz wywołać te funkcje wiele razy dla różnych kategorii. Dodałem też podstawową obsługę błędów.

Newsletter, który rozpala ciekawość💡

Subskrybuj mój newsletter, aby otrzymywać comiesięczną dawkę:

  • Nowości, przykładów, inspiracji ze świata front-end, programowania i designu
  • Teorii naukowych i sceptycyzmu
  • Moich ulubionych źródeł, idei, narzędzi i innych interesujących linków
Nie jestem nigeryjskim księciem, aby oferować ci okazje. Nie wysyłam spamu. Anuluj kiedy chcesz.

Pozostań ciekawy. Przeczytaj więcej

Render 3D ludzkiego mózgu na gradientowym tle15 sierpnia 202310 min. czytania

Wprowadzenie do AI

Zamiast wskawiać na hype train ChatGPT, nauczmy się najpierw podstaw AI.

Czytaj wpis
Cztery filary betonowego budynku pod błękitnym niebem13 października 202210 min. czytania

Programowanie obiektowe w języku JavaScript

Programowanie obiektowe jest podstawą wielu języków programowania. Dlatego zapoznamy się z tym paradygmatem, umieścimy go w kontekście i wykorzystamy w praktyce.

Czytaj wpis
Wiele białych filarów w dwóch rzędach30 marca 20237 min. czytania

Programowanie obiektowe w języku TypeScript

Programowanie obiektowe jest podstawą wielu języków programowania. Dlatego zapoznamy się ze składnią OOP w języku TypeScript i porównamy ją z JavaScriptem.

Czytaj wpis