[Python網頁爬蟲]如何透過Selenium與 Scrapy 擷取JavaScript動態網頁(上)
當我們剛學會一些技巧之後,就會躍躍欲試的想要對實際的東西大展身手。網路爬蟲的初學者也不例外,當學會一兩招之後,就會隨手找到個網站,對該網頁進行資料的爬取工程。然而往往爬取的成功率只有50%,甚至於更低。有很大的原因是因為爬取失敗的網站本身並非透過伺服器端產生的傳統網頁,而是採用JavaScript在本地端產生的動態網頁。
問題起源
這陣子台灣的疫情嚴峻,對於報導每日染疫人數的網站諸如COVID-19全球疫情地圖中的台灣疫情報告,在這幾個月內獲得更多的關注。或許你興沖沖的想要透過網路爬蟲來取得網站中的疫情相關資料,卻發現無法有效的爬取裡面的內容。
檢視原始檔案仔細一看,才發現該網頁的原始碼裡面並不存在報導疫情人數的資料。為什麼會這樣子?明明透過肉眼可以看到的資料,為什麼不存在於原始檔案裡?感覺似乎有點違反直覺與常識。那是因為該網頁在性質上屬於動態網頁。什麼是動態網頁?
什麼是動態網頁
相對於靜態網頁,「動態網頁」是指網頁的內容是「動態」產生的。我們每次瀏覽網頁的內容,會因為時間、使用者輸入等互動行為,而呈現出不同的結果。
以目前常用的技術來說,動態網頁可以分成兩種:一種是「前端的動態網頁」另一種是「後端的動態網頁」。
前端的動態網頁
它又被稱為「客戶端的動態網頁」。這種類型的網頁,是在客戶端(使用者端)使用的瀏覽器上透過JavaSript建立的動態網頁內容,而不是在伺服器端產生的內容。
前端的動態網頁常常透過AJAX(JavaScript的一種技術)從背景送出HTTP請求來取得產生網頁內容的資料(例如:JSON資料)。或者是使用Angular、React或Vue.js等(也是JavaScript的技術,是一種框架)虛擬DOM產生的網頁。使用這種技術的優點是使用者體驗比較好,使用者會覺得網頁的操作行為就像是在使用一個應用軟體(或者是App)一樣的感覺不會有太多的等待時間,就算是需要等待時間,也會出現一些類似「下載中…」等等的中間過場畫面以補間頁面與頁面之間的銜接點。讓使用者不會覺得在操作網頁的過程中一直不停的在頁面下載、換頁顯示、重新整理等等不適感。
後端的動態網頁
這種模式又可以稱為「伺服器端的動態網頁」。它是在伺服器端(Web Server)透過諸如PHP、JSP、ASP.NET等腳本語言產生的網頁內容,再傳遞到使用者的瀏覽器上。
對於網路爬蟲來說,它所關心的動態網頁是指透過JavaScript產生的HTML網頁內容。由於這些網頁內容是無法直接透過對瀏覽器「按右鍵>檢視原始檔案」的方式來查看的。因此無法透過Beautiful Soup來爬取,對Scrapy來說也是一樣,無法直接擷取資料。對此,Selenium扮演了重要的角色。
如何擷取動態網頁
由於Selenium被設計用來進行自動化測試,它會模擬使用者的動作,啟動真實瀏覽器來進行網站的自動化操作。不論是登入網站或滾動scrollbar,藉此可以取得瀏覽器即時產生的網頁內容,並能夠執行JavaScript程式碼,取得網站執行JavaScript程式產生的HTML標籤及其所修改的DOM元素。透過Selenium可以幫助我們擷取動態內容也可以對網站上的表單進行互動。這些就是Selenium與BeautifulSoup最大不同的地方。
接下來,我們會先試著透過Selenium來擷取JavaScript動態網頁並在後半段透過與Scrapy的結合,來使用Scrapy框架擷取JavaScript動態網頁。
Selenium 擷取動態網頁
目標網站
擷取網頁需要有目標。這次要擷取的目標網站為COVID-19全球疫情地圖中的台灣疫情報告。該網站的網址是:
https://covid-19.nchc.org.tw/dt_005-covidTable_taiwan.php
本文要示範的是擷取頁面中「COVID-19台灣最新病例、檢驗統計」部分的資料。
如果您透過按右鍵「檢視原始碼」的方式(如紅色區塊)瀏覽網頁的HTML原始碼:
就會發現這裡面的<table>
標籤裡面完全沒有資料,只有包覆在資料外面的標題(<thead>
),與結尾(<tfoot>
)。因此,我們可以確認目標網站的這個部分正是透過JavaScript產生的動態網頁資料。
<h3>COVID-19台灣最新病例、檢驗統計 </h3>
<span style="font-size: 0.9em;"></span><br>
<span style="font-size: 0.9em;"><font color="#0000ff"><b>整批資料下載方式:</b></font> 提供連結下載CSV及JSON格式資料 <a href="<https://covid-19.nchc.org.tw/api.php?tableID=4048>"><font color="#0000ff">Link</font></a></span><br>
<div id="resize_wrapper">
<table id="myTable03" class="display nowrap mycustomfontsize">
<thead>
<th>id</th><th>通報日</th>
<th>(A) 今日法定傳染病通報</th>
<th>(B) 今日擴大監測送驗</th>
<th>(A+B) 今日合計送驗</th>
<th>(C) 合計送驗(七天移動平均)</th>
<th>今日新增確診</th>
<th>(D) 新增確診(七天移動平均)</th>
<th>(D/C) 檢驗確診率</th>
</thead>
<tfoot>
<th>id</th><th>通報日</th>
<th>(A) 今日法定傳染病通報</th>
<th>(B) 今日擴大監測送驗</th>
<th>(A+B) 今日合計送驗</th>
<th>(C) 合計送驗(七天移動平均)</th>
<th>今日新增確診</th>
<th>(D) 新增確診(七天移動平均)</th>
<th>(D/C) 檢驗確診率</th>
</tfoot>
</table>
</div>
<br><br>
安裝套件
關於Selenium與Pandas套件的安裝方式,請參考下面這幾篇的說明,不在此贅述:
- 關於selenium套件的安裝,可參考「用Python控制Chrome瀏覽器 — Selenium初體驗 」
- 關於pandas套件的安裝,可參考「[Python資料科學]pandas基礎介紹-進入資料科學的領域 」
匯入套件
在此,除了匯入Selenium套件外,還要匯入pandas與time套件。
匯入pandas套件是為了將擷取到的資料進行搜集與儲存工作 ,而匯入time套件則是為了要讓Selenium的行為可以依照我們所希望的進行等待、停頓。
from selenium import webdriver
import pandas as pd
import time
載入webdriver
匯入selenium之後,就可以使用 webdriver.Chrome()
建立一個Google Chrome瀏覽器物件(在此我們要透過操作Google Chrome瀏覽器來進行資料爬取,如果要使用其他的瀏覽器進行爬取,就需要安裝不同的driver),並且儲存在變數driver中。
path = '/Users/path_to_driver/driver/chromedriver'
driver = webdriver.Chrome(path)
使用 webdriver.Chrome()
物件的get方法進入目標網頁。
driver.get(web)
放大頁面範圍
使用driver
中的 maximize_window()
方法,可以將螢幕擴展到最大的寬度。這樣子做可以完整的取得頁面上的資訊。
driver.maximize_window()
暫停擷取活動
當我們執行網頁擷取時,需要讓網頁下載到瀏覽器。如果速度太快,有時候無法正確地取得資料。我們可以透過暫停程式幾秒鐘的執行,待網頁資料全部下載後,再執行後續的處理。因此下面設定10秒的等待。
time.sleep(10)
分析目標元素
觀察原始檔案後,我們發現想要擷取的資料位於Table裡面。在table元素中,包含了很多次重複的<tr>標籤元素(如綠色區塊),而所有需要的資料都在<tr>標籤裡面(如紅色區塊,裡面有所有我們想要的資料)。因此,我們只要針對<tr>標籤裡面的資料進行截取就可以了。
爬取目標資料
我們可以使用 find_elements_by_xpath
鎖定<tr>
標籤的位置。並且指定給一個變數datas。
datas = driver.find_elements_by_xpath('//*[@id="myTable03"]/tbody/tr')
找尋目標資料元素
在每一個tr元素包覆的區塊中,找出「目標資料」的元素,並且分別賦予變數來儲存。
包括通報日(date)、今日新增確診(confirmed)以及新增確診(七天移動平均)(sevendays)。
使用XPATH找到目標資料元素
由於通報日(date)位於<tr>
標籤中的第一個<td>元素裡面,因此我們可以採用下面方式取得:
date = data.find_element_by_xpath('./td[1]').text
而今日新增確診(confirmed)與新增確診(七天移動平均)(sevendays)則分別位於<tr>
標籤中的第六與第七個<td>元素裡面。則可以採用下面方式取得:
confirmed = data.find_element_by_xpath('./td[6]').text
sevendays = data.find_element_by_xpath('./td[7]').text
搜集目標資料元素的內容
還需要建立三個串列來搜集等一下取得目標資料的所有內容:
date_data = []
confirmed_data = []
sevendays_data =[]
for迴圈取資料
再透過for迴圈取得資料,並且使用append將取得的資料分別放入前面的串列中。
for data in datas:
date = data.find_element_by_xpath('./td[1]').text
confirmed = data.find_element_by_xpath('./td[6]').text
sevendays = data.find_element_by_xpath('./td[7]').text
date_data.append(date)
confirmed_data.append(confirmed)
sevendays_data.append(sevendays)
關閉網頁
取得資料之後,使用quit方法關閉chrome瀏覽器。
driver.quit()
資料存檔
最後,要把擷取到的資料存起來。在這裡,我們要將資料存入CSV檔案中。存擋的步驟如下:
- 建立字典
- 建立DataFrame
- 儲存到CSV檔案
建立字典
建立一個字典。並且將串列中搜集到的資料date_data、confirmed_data與sevendays_data存到字典中,其格式如下:
{'date':date_data,'confirmed':confirmed_data,'sevendays':sevendays_data}
建立DataFrame並儲存到CSV檔案
透過Pandas建立DataFrame物件指定為變數df_covid,並且把前面的字典傳入。然後再透過to_csv的方式,將DataFrame存到CSV檔案裡面(covid19_taiwan.csv
),並且移除index值(index=False
)。
df_covid = pd.DataFrame({
'date':date_data,'confirmed':confirmed_data,'sevendays':sevendays_data,
})df_covid.to_csv('covid19_taiwan.csv', index=False)
完整程式碼
from selenium import webdriver
import pandas as pd
import timepath = '/Users/path_to_driver/driver/chromedriver'
driver = webdriver.Chrome(path)
web = '<https://covid-19.nchc.org.tw/dt_005-covidTable_taiwan.php>'driver.get(web)
driver.maximize_window()time.sleep(10)
datas = driver.find_elements_by_xpath('//*[@id="myTable03"]/tbody/tr')date_data = []
confirmed_data = []
sevendays_data =[]for data in datas:
date = data.find_element_by_xpath('./td[1]').text
confirmed = data.find_element_by_xpath('./td[6]').text
sevendays = data.find_element_by_xpath('./td[7]').text
print(date,confirmed,sevendays)
date_data.append(date)
confirmed_data.append(confirmed)
sevendays_data.append(sevendays)
driver.quit()df_covid = pd.DataFrame({
'date':date_data,'confirmed':confirmed_data,'sevendays':sevendays_data,
})df_covid.to_csv('covid19_taiwan.csv', index=False)
執行測試
完成上述的各項步驟後,就可以執行程式。看看結果CSV檔案(covid19_taiwan.csv
)是否出現在電腦裡。以下是畫面的節錄。
以上是透過Selenium在JavaScript產生的動態網頁中擷取資料的方式。接下來,我們要在Scrapy中添加Selenium來進行截取資料。