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 2022Lubię 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 get23url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"4res = get(url)56print(res.status_code) # 2007print(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 get2from bs4 import BeautifulSoup34url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"5res = get(url)6soup = BeautifulSoup(res.text, "html.parser")78print(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 get2from bs4 import BeautifulSoup34url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"5res = get(url)6soup = BeautifulSoup(res.text, "html.parser")7books = soup.select(".authorAllBooks__single")89print(books) # Lista książek10print(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> / 106 </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 get2from bs4 import BeautifulSoup34url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"5res = get(url)6soup = BeautifulSoup(res.text, "html.parser")7books = soup.select(".authorAllBooks__single")89for 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_string1516 print(float(average_rating)) # Float np. 8.317 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 get2from bs4 import BeautifulSoup34url = "https://lubimyczytac.pl/ksiazki/k/107/literatura-popularnonaukowa"5res = get(url)6soup = BeautifulSoup(res.text, "html.parser")7books = soup.select(".authorAllBooks__single")89for 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_string15 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 get2from bs4 import BeautifulSoup34base_url = "https://lubimyczytac.pl"5url = f"{base_url}/ksiazki/k/107/literatura-popularnonaukowa"6average_rating_threshold = 8.07ratings_threshold = 200089res = get(url)10soup = BeautifulSoup(res.text, "html.parser")11books = soup.select(".authorAllBooks__single")1213best_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)2021 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}"}3031 best_books.append(best_book)3233print(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 get2from bs4 import BeautifulSoup34base_url = "https://lubimyczytac.pl"5average_rating_threshold = 8.06rating_threshold = 20007start_page = 18stop_page = 11910best_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")1617 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)2324 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)3435print(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 itemgetter34# ...56subject = "popularnonaukowe"78# ...910best_books = [] # Tu śą najlepsze książki1112# ...1314file = 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)2122file.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: popularnonaukowe23- [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
- Sapiens. Od zwierząt do bogów
autorstwa Yuval Noah Harari | 8.3⭐ 9123 ocen - Sztuka obsługi penisa
autorstwa Przemysław Pilarski,Andrzej Gryżewski | 8.5⭐ 3333 ocen - Krótka historia prawie wszystkiego
autorstwa Bill Bryson | 8.2⭐ 2123 ocen
Ostateczny skrypt
Ostateczna wersja naszego skryptu wygląda tak:
PYTHON
1from bs4 import BeautifulSoup2from operator import itemgetter3from requests import get45def 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")89 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")1112 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")2122 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)2930 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)4041 return best_books4243 except Exception as err:44 print(f"There was a problem during scraping books: {err}")4546def 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()6061best_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.