資料抓取與解析
什麼是 API?
現在大家講到 API,好像都會說 API 就是網路上的資料,但 API 這個名詞早在撥接上網之前就已經存在了。API 的全名是「應用程式介面(Application Programming Interface)」,它定義如何與軟體元件互動的規則,讓不同的軟體之間進行通訊,甚至可以讓開發者在不了解其內部運作原理的情況下也能使用該軟體所提供的功能。
用個平常比較常見的例子的話,我們到餐廳吃飯的時候,通常只要跟服務生點餐就行了,至於餐點是怎麼做出來或是服務生有沒有在裡面吐口水我們就不用管太多,只要稍候片刻服務生就會幫我們把餐點送上桌,這裡的服務生就差不多是 API 的角色。
雖然 API 的定義跟現在大家認知的 API 可能有點不同,但基本概念還是差不多的,主要是讓不同的軟體或應用程式之間進行通訊或交換資料。早期的 API 交換格式有好幾種,例如純文字格式或是 XML 格式,現在比較多人使用的格式是 JSON(JavaScript Object Notation),JSON 格式看起來有一點點像 Python 的字典,在 Python 也有內建的模組可以把 JSON 轉成字 典。
不過並不是所有的網站都有提供 API 的義務,有些網站可能只有網頁版的頁面供一般人瀏覽,如果想要取得這些頁面上的資料,我們可能得寫一些程式來取得並解析資料,通常我們又會稱這樣的程式叫做「網頁爬蟲(Web Crawler)」。放心,它不是真的蟲,只是一個比喻而已。
撰寫爬蟲程式的時候請特別注意,如果不是公開的 API 或是有明確授權的資料來源,請勿隨意爬取未經授權的網站資料並拿來做不該做的事。或許你會認為明明就是公開的網站,為什麼不能自己寫程式抓來用?網站公開瀏覽是一回事,你自己另外寫程式爬取又是另一回事,不管什麼用途,特別是要用於商業行為請一定要格外注意,一不小心可能就會吃上官司。
抓取網頁資料
在台灣講到電腦書應該沒有人沒聽過天瓏書局,我常會說天瓏書局就像電腦世界的誠品一樣,各種中、英文電腦書幾乎都能在這裡找到。因為我自己有出過幾本書,所以我會每天都會固定去刷一下排行榜,看看我的書排在第幾名。
這種例行公事如果交給電腦來做應該會輕鬆一些,不過因為網站本身並沒有提供 API 可以取用,我們可以試著寫個程式來抓取天瓏書局的七天熱銷排行榜的資料。
再次溫馨提醒,並不是公開的網站資料就能隨便抓,請注意網站的使用條款,以下內容僅供教學使用。
既然要抓取網頁資料,我們就得先了解網頁的 HTML 是什麼樣的結構,以及我想要抓的資料在 HTML 的哪個位置。現代的瀏覽器功能都很完整,在頁面上點擊滑鼠右鍵就能檢視原始碼,如果我想要抓的是書的排行榜,要先知道存放書的 HTML 元素叫什麼名字:
從原始碼看的出來我想要抓的資料都是在 class 名為 single-book
的 <li>
元素裡。書名以及價格都在這個元素裡面,所以我應該要先想辦法取得這一整排的 <li>
元素,待會再跑個迴圈,應該就差不多能把資料整理出來了。
有網址也知道要抓什麼資料之後,接下來該怎麼做?Python 本身就有內建的 HTTP 模組可以取得網路上的資料,不過有個更方便而且很多人在使用的第三方套件叫做 requests
,可以讓我們更容易地取得網路資料。同樣也是透過 pip 安裝:
$ pip install requests
requests 套件 https://pypi.org/project/requests/
安裝完成後,先來試著取得頁面資料:
import requests
URL = "https://www.tenlong.com.tw/zh_tw/recent_bestselling?range=7"
response = requests.get(URL)
print(response.text)
就這樣,簡單幾行程式就能取得七天熱銷排行榜的 HTML 內容了。不過這資料太雜了,我只想要列出前 30 名的書名及售價,所以再來就是要想辦法解析 HTML 的內容。
如果自己寫迴圈或利用字串的搜尋比對要找到我要的資料,那可能會是個痛苦的過程,還好網路上厲害的善心人士很多,有一個也是很知名的套件叫做 BeautifulSoup
,使用這個套件來解析 HTML 內容,會比我們自己慢慢比對字串簡單很多。
題外話,BeautifulSoup 這個套件的名稱是來自《愛麗絲夢遊仙境》(Alice's Adventures in Wonderland)這本書中。套件的作者本人 Leonard Richardson 曾經解釋過為什麼取這個名字:
Back in 2004 most parsers could only parse well-formed XML and HTML. The poorly-formed stuff you saw on the Web was referred to as "tag soup", and only a web browser could parse it. Beautiful Soup started out as an HTML parser that would take tag soup and make it beautiful, or at least workable.
BeautifulSoup 可以把雜亂的網路資料轉換成結構化資料,就像把各種原料煮成一鍋美味的湯一樣(老實說,我沒有抓到這個煮湯的哏),用它來解析 HTML 可以讓我們更容易地取得我們想要的資料。
要使它之前,得先安裝套件:
$ pip install beautifulsoup4
要注意套件名稱最後面的 4,別漏了,這是因為 BeautifulSoup 分別有支援 Python 2 跟 3 版本,如果 Python 3 的話是安裝 beautifulsoup4
,如果漏了 4,會安裝到支援 Python 2 的版本。
BeautifulSoup https://www.crummy.com/software/BeautifulSoup/
安裝好之後,就可以開始使用 BeautifulSoup 來解析 HTML 內容了:
import requests
from bs4 import BeautifulSoup
URL = "https://www.tenlong.com.tw/zh_tw/recent_bestselling?range=7"
response = requests.get(URL)
soup = BeautifulSoup(response.text, "html.parser")
output = f"{"排行":<5}{"原價":10}{"特價":10}{"書名":20}\n"
for index, book in enumerate(soup.select("li.single-book"), start=1):
title = book.select_one(".title a").text[:15]
price = book.select_one(".pricing")
orig_price, sell_price = price.text.split()
output += f"{index:<5}{orig_price:10}{sell_price:10}{title:20}\n"
print(output)
上面的程式碼應該還算容易理解,比較複雜的應該是在 for
迴圈裡面做的事,裡面的 .select_one()
方法是可以使用 CSS 選取器(Selector)來取得我們想要的元素,這樣就不用自己寫程式去比對了,簡單很多。再透過 F 字串的格式化功能,就可以把資料整理成我們想要的格式:
排行 原價 特價 書名
1 $680 $537 LangChain 開發手冊
2 $650 $507 前端測試指南:策略與實踐
3 $820 $648 超圖解 ESP32 應用實作
4 $680 $537 軟體架構原理|工程方法 (Fu
...略...
27 $500 $390 為你自己學 Git
28 $680 $510 無料 AI:ChatGPT +
29 $850 $663 重構:改善 .NET 與 C#
30 $1,200 $948 史上最強 Python 入門邁
滿好的,我的「為你自己學 Git」竟然還在前 30 名。
《練習》地震資料
台灣是個地震很多的國家,幾乎每天都會有大大小小的地震發生,如果你想要知道最近的地震資訊,交通部中央氣象署有提供地震資料 API。這個 API 提供了最近 30 天的地震資料,而且是以 JSON 格式回傳,很適合我們來練習:
透過 API 資料
氣象署提供的地震資料 API 需要一定的權限才能使用,首先要先到交通部中央氣象署提供的氣象資料開放平臺註冊一個帳號,註冊帳號不需要費用,而且流程應該不會很複雜,完成帳號註冊並登入之後可以在網站取得 API 授權碼,授權碼大概長這樣:
CWA-ECBDCF4D-0372-4E5A-AEBA-09EC8A79BFFB
我們待會使用 API 的時候,需要把這個授權碼一併傳過去才能正常使用。這個授權碼在大家看到這本書的時候應該已經沒用了,大家請到網站上申請自己專屬的授權碼。
氣象資料開放平臺 https://opendata.cwa.gov.tw
接著在網站上的「資料主題」裡應該可以找到「地震海嘯」的資料,點進去之後會有好幾款 API 可使用,這裡我用「顯著有感地震報告」做為範例。
點擊 API 之後,會有個頁面可以看到這個 API 的說明,包括網址以及要傳送的參數,還可以點擊「Try it out」按鈕,在線上試用看看:
在試用的時候,「Authorization」欄位要填入我們的授權碼,底下可以再選擇日期及縣市,然後點擊「Execute」按鈕,就可以看到這個 API 回傳的資料了。
知道 API 的網址以及使用方法後,接著就是準備撰寫 Python 程式來取得資料了。另外,像是授權碼或是金鑰之類的資料不應該直接寫在程式碼裡,實務上常會把這些比較敏感的資料設定在環境變數裡,在開發過程中可以使用 python-dotenv
套件讀取 .env
檔案的內容並設定成環境變數。
為了讓環境乾淨一點,這裡我會使用 venv
來建立虛擬環境,然後安裝抓取網路資料的 requests
以及設定環境變數的 python-dotenv
:
$ mkdir earthquake
$ cd earthquake
$ python -m venv .venv
$ source .venv/bin/activate
$ (.venv) % pip install requests python-dotenv
如果對上面的流程不太熟悉,或是對 venv
不熟的話,可再參閱第二章的環境安裝關於虛擬環境的介紹。
環境變數的設定可以在專案的根目錄下建立一個 .env
檔案,檔案內容如下:
API_KEY=CWA-ECBDCF4D-0372-4E5A-AEBA-09EC8A79BFFB
待會可以透過 python-dotenv
提供的函數來載入這個檔案,取得裡面的 API_KEY
並設定成環境變數。
氣象署的 API 傳送參數的方式是透過 URL 的 Query String 來傳送,我們可以透過 F 字串手工組裝這些參數:
END_POINT = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
URL = f"{END_POINT}?Authorization={API_KEY}&format=JSON&AreaName=花蓮縣"
但這種手工組裝的方式只要參數數量多一點就會變的不好維護,一不小心就會多打或少打幾個字而出錯。我們可以先寫個字典,然後請 Python 內建的 urllib.parse
模組幫我們做轉換:
import os
from urllib.parse import urlencode
API_KEY = os.getenv("API_KEY")
END_POINT = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
params = {
"Authorization": API_KEY,
"format": "JSON",
"AreaName": "花蓮縣",
}
URL = f"{END_POINT}?{urlencode(params)}"
之後如果要調整參數的話就直接調整 params
字典的內容就好。接下來就準備使用 requests
套件來取得 API 的資料:
import requests
response = requests.get(URL, headers={"accept": "application/json"})
data = response.json()
這裡我使用 requests.get()
函數來取得 API 的資料,並按照氣象署 API 的規格在 headers
參數裡指定 accept
的值為 application/json
。資料抓回來後還沒辦法直接使用,需要透過 .json()
方法把它轉換成 JSON 格式的資料。
接下來,就是仔細觀察 API 回傳的資料結構,取出我們需要的資料。這裡我只想列出幾月幾日發生了規模多少的地震列表,最後再用 F 字串簡單做個排版輸出。因為程式碼相對比較簡單,我就直接寫在 api.py
檔案裡了,完整程式碼如下:
import os
import requests
from datetime import datetime
from dotenv import load_dotenv
from urllib.parse import urlencode
load_dotenv()
API_KEY = os.getenv("API_KEY")
END_POINT = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
params = {
"Authorization": API_KEY,
"format": "JSON",
"AreaName": "花蓮縣",
}
URL = f"{END_POINT}?{urlencode(params)}"
response = requests.get(URL, headers={"accept": "application/json"})
data = response.json()
output = f"{"日期":15}震度\n"
for record in reversed(data["records"]["Earthquake"]):
info = record["EarthquakeInfo"]
date = datetime.strptime(info["OriginTime"], "%Y-%m-%d %H:%M:%S")
date_str = date.strftime("%Y/%m/%d")
value = info["EarthquakeMagnitude"]["MagnitudeValue"]
output += f"{date_str:15} {value}\n"
print(output)
執行這個檔案,就可以看到花蓮縣最近的 地震資訊了:
日期 地震規模
2024/05/28 4.5
2024/05/28 4.4
2024/05/29 4.6
2024/05/30 4.0
2024/05/30 5.1
2024/05/30 5.3
2024/05/30 5.0
2024/06/01 5.5
2024/06/01 4.9
2024/06/02 4.8
2024/06/02 4.4
2024/06/06 4.4
2024/06/06 4.0
2024/06/10 4.1
2024/06/12 4.9
2024/06/12 4.3
2024/06/12 4.2
有幾點需要說明:
- 氣象署提供的 API 並非即時資料,而且能查詢的日期範圍有限。
- 氣象署的 API 參數或是回傳的資料格式可能會因為網站更新而有所變動,請大家自行確認。
是說,雖然 API 很方便,但氣象署提供的 API 資料並不是即時資訊,如果要比較即時的資訊的話可能要直接到氣象署的網站,所以接下來,我們就來看看怎麼直接從氣象署的網站取得比較即時的地震資訊。
透過網頁資料
進到氣象署網站,應該可以在選單裡找到地震資訊的頁面,點進頁面會顯示最近發生的地震紀錄。我們前面才介紹過怎麼觀察網頁的 HTML 結構,然後再透過 requests
以及 BeautifulSoup
套件來取得並解析網頁的資料。
但這次會遇到一些 麻煩,如果你直接用 requests
套件來取得網頁的資料,你會發現拿回來的資料並沒有地震資訊。這是因為這個網頁的地震資訊是透過 JavaScript 動態載入的,而 requests
只能取得 HTML 結構,無法取得頁面載入後執行 JavaScript 程式並進行渲染之後的結果。
正因為這個原因,我們會需要使用另一個套件 requests-html
。requests-html
是 requests
的延伸套件,如果只是發送一般的 HTTP 請求,用 requests
就夠了,不過如果需要在發送 HTTP 請求之後還需要做點事的,使用 requests_html
會比較合適,這裡我就要利用這個套件幫我取得頁面執行 JavaScript 程式碼染後的結果。
requests-html https://github.com/psf/requests-html
別忘了要安裝 requests-html
套件:
$ pip install requests-html
完整程式碼如下:
from requests_html import HTMLSession
session = HTMLSession()
URL = "https://www.cwa.gov.tw/V8/C/E/index.html"
response = session.get(URL)
response.html.render(timeout=20)
rows = response.html.find(".eq_list .eq-row")
output = f"{"日期":15}地震規模\n"
for row in reversed(rows):
detail = row.find(".eq-detail", first=True)
date_str = detail.find("span", first=True).text
value = detail.find("ul li")[2].text.replace("地震規模", "")
output += f"{date_str:15} {value}\n"
print(output)
雖然跟 requests
的用法不太一樣,但基本上還是差不多的,這裡的重點在於中間的 .render()
函數,這行程式碼會等待網頁載入並執行 JavaScript 程式碼,然後再取得網頁的 HTML 結構。HTML 拿到手之後,剩下的就只是處理資料的細節了。執行之後,你應該會看到類似這樣的輸出:
日期 地震規模
06/10 04:05 4.1
06/10 07:05 4.0
06/10 11:56 5.1
06/11 00:35 3.3
06/11 09:23 4.0
06/11 21:02 3.4
06/12 05:51 4.9
06/12 20:40 4.3
06/12 20:41 4.2
06/13 05:20 3.5
06/13 07:37 4.1
06/14 02:08 3.9
06/14 02:30 3.2
06/14 04:54 3.0
06/14 05:53 3.7
再次強調,直接抓取網站的頁面並不是正規的做法,除了可能有侵權的問題,也可能會因為網站更新或是 HTML 的結構調整而導致程式無法正常運作。
以上資料來自交通部中央氣象署,完整專案可在我的 GitHub 取得。