2024年6月30日 星期日

Python 學習筆記 : 網頁爬蟲實戰 (十六) books.toscrape.com 的書籍分頁資料

前兩篇的分頁資料網站 (104 人力銀行, NBA 球員資料) 都是 Javascript 動態產生的, 因此必須利用 Selenium 來抓. 本篇則要來抓一個靜態的分頁網站 : books.toscrape.com 的書籍列表網站, 這應該是專門設計給爬蟲學習者的測試網站. 

本系列之前的筆記參考 : 



一. 檢視目標網頁 :  

目標網址如下 : 





此網站上總共有 1000 本的書籍資料, 以分頁的方式呈現, 每頁有 20 本書, 每四本排成一列, 每頁有五列書籍, 一頁有 20 本書, 故總共分成 50 頁, 在每頁最底下中央可以看到 Page x of 50 的分頁訊息 : 




按網頁右下角的 "next" 鈕可切換到下一個分頁, 觀察網址列, 每一個分頁的 URL 格式為 :

https://books.toscrape.com/catalogue/page-x.html   

其中 x 為分頁編號, 首頁 https://books.toscrape.com/ 顯示的其實是第一頁, 所以這 50 頁的網址為 :

https://books.toscrape.com/catalogue/page-1.html 
https://books.toscrape.com/catalogue/page-2.html 
...
https://books.toscrape.com/catalogue/page-50.html 

用 Quick Javascript Switcher 檢查發現即使關閉 Javascript 網頁內容仍在, 可見這不是 Javascript 動態生成的網頁, 而是一個靜態網頁, 只要用 requests 與 BeautifulSoup 即可擷取. 

每一本書的圖片底下有書名與價格, 本篇的任務是擷取這 50 頁共 1000 本書的書名與價格並將其存入 csv 檔中. 


二. 用 requests 與 BeautifulSoup 擷取目標網頁 :  

在 Chrome 按 F12 開啟開發人員工具視窗切到 Element 頁籤, 然後將滑鼠移到書籍圖片上, 按滑鼠右鍵點選 "檢查" 就會在 Element 頁籤中顯示該書的 HTML 碼位置 :




可見每一本書都用 li 清單項目表示, 其內容都放在 class="product_pod" 的 article 元素裡面, 書名可從其內 h3 元素下 a 元素的 title 屬性取得 : 

<a href="a-light-in-the-attic_1000/index.html" title="A Light in the Attic">A Light in the ...</a>

而價格則可從 div 下具有 class="price_color" 的 p 元素取得 : 

<p class="price_color">£51.77</p>




先匯入套件與類別 :

>>> import requests   
>>> from bs4 import BeautifulSoup    

先擷取第一頁的書 : 

>>> url='https://books.toscrape.com/catalogue/page-1.html'    
>>> res=requests.get(url)    
>>> res.encoding    
'ISO-8859-1'
>>> soup=BeautifulSoup(res.text, 'lxml')    

可見網頁使用的編碼方式是俗稱 Latin-1 的 ISO-8859-1.

先從 a 元素的 title 屬性取得書名, 這些超連結可以用 CSS 選擇器 'article > h3 > a' 透過 select() 方法來選取, 它會傳回所有被選取之書目 a 元素之 Tag 物件 :

>>> links=soup.select('article > h3 > a')   
>>> len(links)     
20

可見確實是每頁 20 本書的超連結, 呼叫 Tag 物件的 get() 方法來取得 title 屬性值 : 

>>> links[0].get('title')   
'A Light in the Attic'
>>> links[1].get('title')     
'Tipping the Velvet'
>>> links[19].get('title')   
"It's Only the Himalayas"

這個選擇器可以在 Element 頁籤中點選 a 元素後按滑鼠右鍵, 選取 "Copy/Copyselector" 取得 :




#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > h3 > a

但不需要使用完整的選擇器, 只要最後面的 'article > h3 > a' 即足以定位這些超連結.

接下來是擷取書價, 同樣方式取得 p 元素選擇器 :

#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.product_price > p.price_color

但只需要用 'article > div.product_price > p.price_color' 即可定位這些 p 元素 :

可以用串列生成式從 a 元素 Tag 物件串列中取出書名組成 20 本書的書名串列 :

>>> book_names=[link.get('title') for link in links]   
>>> book_names   
['A Light in the Attic', 'Tipping the Velvet', 'Soumission', 'Sharp Objects', 'Sapiens: A Brief History of Humankind', 'The Requiem Red', 'The Dirty Little Secrets of Getting Your Dream Job', 'The Coming Woman: A Novel Based on the Life of the Infamous Feminist, Victoria Woodhull', 'The Boys in the Boat: Nine Americans and Their Epic Quest for Gold at the 1936 Berlin Olympics', 'The Black Maria', 'Starving Hearts (Triangular Trade Trilogy, #1)', "Shakespeare's Sonnets", 'Set Me Free', "Scott Pilgrim's Precious Little Life (Scott Pilgrim #1)", 'Rip it Up and Start Again', 'Our Band Could Be Your Life: Scenes from the American Indie Underground, 1981-1991', 'Olio', 'Mesaerion: The Best Science Fiction Stories 1800-1849', 'Libertarianism for Beginners', "It's Only the Himalayas"]

接下來同樣利用選擇器從 p 元素取得書價資訊 :

>>> prices=soup.select('article > div.product_price > p.price_color')    
>>> len(p_prices)    
20   
>>> prices[0].text   
'£51.77'   
>>> prices[1].text   
'£53.74'

由於英鎊符號在轉成 utf-8 時會出現怪碼, 可用正規式取出書價的數值部分即可 :

>>> import re  
>>> re.findall(r'\d+\.\d+', prices[0].text)   
['51.77']
>>> re.findall(r'\d+\.\d+', prices[0].text)[0]   
'51.77'   
>>> float(re.findall(r'\d+\.\d+', prices[0].text)[0])   
51.77

同樣使用串列生成式從 p 元素的 Tag 物件串列中取得書價組成串列 :

>>> book_prices=[float(re.findall(r'\d+\.\d+', price.text)[0]) for price in prices]   
>>> book_prices     
[51.77, 53.74, 50.1, 47.82, 54.23, 22.65, 33.34, 17.93, 22.6, 52.15, 13.99, 20.66, 17.46, 52.29, 35.02, 57.25, 23.88, 37.59, 51.33, 45.17]

以上我們已經得到 a 元素 Tag 物件串列 links (可取得書名) 與 p 元素 Tag 物件串列 (可取得書價), 我們可以用 zip 函式將此二組 Tag 物件串列合組成 zip 物件, 以便能在迴圈中走訪 (書名, 書價) 對, 關於 zip() 用法參考 :


>>> zipped=zip(book_names, book_prices)  # 將書名與書價串列組成 zip 對
>>> type(zipped)  
<class 'zip'>

然後走訪此 zip 物件, 並使用串列生成式分別從兩組 Tag 物件串列對中取出書名與書價組成 tuple 串列 data, 這樣就能將書名與書價綁在 tuple 中了, 方便寫入 csv 檔中 : 

>>> data=[(z[0], z[1]) for z in zipped]       # 將被 zip 的書名與書價組成 tuple list      
>>> data[0]   
('A Light in the Attic', '51.77')
>>> data[1]   
('Tipping the Velvet', '53.74')

然後匯入 csv 套件直接將此 tuple list 寫入 csv 檔即可 :

>>> with open('books_page_1.csv', 'w', newline='', encoding='utf-8') as f:  
    writer=csv.writer(f)   
    writer.writerows(data)   

用 Excel 開啟 books_page_1.csv 檔 : 




可見已將第一頁的書名與書價資訊成功寫入 csv 檔了. 

接下來只要用迴圈去逐頁抓取書名與價格並寫入 csv 檔即可, 此網站雖然已知有 50 頁, 但網站可能在既有結構下增加書籍數量, 分頁數可能因此而增加, 因此為了增強爬蟲程式的強固性, 可以先抓首頁 (其實就是第一頁) 底下的分頁數來確定迴圈要跑幾圈, 這個分頁標示是放在 class="current" 的 li 元素內 :




在 Element 頁籤中按 Ctrl+F 搜尋 "current" 發現只有一個, 因此可以用 select_one() 來尋找它 :


>>> url='https://books.toscrape.com/'    
>>> res=requests.get(url)    
>>> soup=BeautifulSoup(res.text, 'lxml')    
>>> page_li=soup.select_one('.current')
>>> page_li
<li class="current">
            
                Page 1 of 50
            
            </li>
>>> page_li.text    
'\n            \n                Page 1 of 50\n            \n            '
>>> page_li.text.strip()   
'Page 1 of 50'
>>> pages=int(page_li.text.strip().split('of')[1])    
>>> pages    
50

以上測試之完整程式碼如下 :

import requests   
from bs4 import BeautifulSoup
import re
import csv
import time

url=f'https://books.toscrape.com/'
res=requests.get(url)    
soup=BeautifulSoup(res.text, 'lxml')
page_li=soup.select_one('.current')
pages=int(page_li.text.strip().split('of')[1])
csv_file='books.toscrape.com.csv'
with open(csv_file, 'w+', newline='', encoding='utf-8') as f:
    writer=csv.writer(f)
    for i in range(pages):
        print(f'擷取第 {i + 1} 頁 ... ', end='')
        url=f'https://books.toscrape.com/catalogue/page-{i+1}.html'    
        res=requests.get(url)    
        soup=BeautifulSoup(res.text, 'lxml')
        links=soup.select('article > h3 > a')
        prices=soup.select('article > div.product_price > p.price_color')
        book_names=[link.get('title') for link in links]
        reg=r'\d+\.\d+'
        book_prices=[float(re.findall(reg, price.text)[0]) for price in prices]
        zipped=zip(book_names, book_prices)
        data=[(z[0], z[1]) for z in zipped]
        writer.writerows(data)
        print(f'存檔完成!')
    time.sleep(0.5)

執行結果如下 : 

>>> %Run books_toscrape_1.py   
擷取第 1 頁 ... 存檔完成!
擷取第 2 頁 ... 存檔完成!
擷取第 3 頁 ... 存檔完成!
擷取第 4 頁 ... 存檔完成!
擷取第 5 頁 ... 存檔完成!
擷取第 6 頁 ... 存檔完成!
擷取第 7 頁 ... 存檔完成!
擷取第 8 頁 ... 存檔完成!
擷取第 9 頁 ... 存檔完成!
擷取第 10 頁 ... 存檔完成!
擷取第 11 頁 ... 存檔完成!
擷取第 12 頁 ... 存檔完成!
擷取第 13 頁 ... 存檔完成!
擷取第 14 頁 ... 存檔完成!
擷取第 15 頁 ... 存檔完成!
擷取第 16 頁 ... 存檔完成!
擷取第 17 頁 ... 存檔完成!
擷取第 18 頁 ... 存檔完成!
擷取第 19 頁 ... 存檔完成!
擷取第 20 頁 ... 存檔完成!
擷取第 21 頁 ... 存檔完成!
擷取第 22 頁 ... 存檔完成!
擷取第 23 頁 ... 存檔完成!
擷取第 24 頁 ... 存檔完成!
擷取第 25 頁 ... 存檔完成!
擷取第 26 頁 ... 存檔完成!
擷取第 27 頁 ... 存檔完成!
擷取第 28 頁 ... 存檔完成!
擷取第 29 頁 ... 存檔完成!
擷取第 30 頁 ... 存檔完成!
擷取第 31 頁 ... 存檔完成!
擷取第 32 頁 ... 存檔完成!
擷取第 33 頁 ... 存檔完成!
擷取第 34 頁 ... 存檔完成!
擷取第 35 頁 ... 存檔完成!
擷取第 36 頁 ... 存檔完成!
擷取第 37 頁 ... 存檔完成!
擷取第 38 頁 ... 存檔完成!
擷取第 39 頁 ... 存檔完成!
擷取第 40 頁 ... 存檔完成!
擷取第 41 頁 ... 存檔完成!
擷取第 42 頁 ... 存檔完成!
擷取第 43 頁 ... 存檔完成!
擷取第 44 頁 ... 存檔完成!
擷取第 45 頁 ... 存檔完成!
擷取第 46 頁 ... 存檔完成!
擷取第 47 頁 ... 存檔完成!
擷取第 48 頁 ... 存檔完成!
擷取第 49 頁 ... 存檔完成!
擷取第 50 頁 ... 存檔完成!






完整抓到 50 頁共 1000 本書的書名與書價資料, 大功告成!

沒有留言 :