前兩篇的分頁資料網站 (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 頁 ... 存檔完成!
開啟 books.toscrape.com.csv 檔 :
完整抓到 50 頁共 1000 本書的書名與書價資料, 大功告成!
沒有留言 :
張貼留言