# 爬取銀行匯率資訊 ###### tags: `python` `crawler` ## 說明 ### 爬蟲目標 爬取台灣銀行匯率網頁(https://rate.bot.com.tw/xrt?Lang=zh-TW),將所有幣別的匯率存成CSV檔案。 ### 規格 #### 實現步驟 1. 確認網頁可以連線。 2. 檢視網頁原始碼確認有要取得的資訊。 3. 使用BeautifulSoup取得全部網頁。 4. 研究規則並透過BeautifulSoup的各種方法(如:`find()`、`find_all()`等等)來取得目標資料。 #### 會用到的模組 | 模組名稱 | 說明 | | -------- | -------------------------------------- | | bs4 | BeautifulSoup網頁解析模組 | | lxml | 網頁解析器 | | requests | HTTP模組,用來直接透過網路讀取網頁資料 | | csv | 操作CSV檔案格式模組 | | time | 取得時間 | ## 程式碼解說 #### 1. ``` # text屬性為html文件原始碼 html_doc = response.text # 指定lxml作為解析器來建立Beautiful物件 soup = BeautifulSoup(html_doc, "lxml") ``` ##### 解說 `response`物件中的`text`屬性就是我們需要的網頁原始碼,另外存到新的變數`html_doc`中後再交給`BeautifulSoup`來解析。如果不另外存到新的變數,也可以直接形成一行如下: ``` # 指定lxml作為解析器來建立Beautiful物件 soup = BeautifulSoup(response.text, "lxml") ``` #### 2. ``` rate_table = soup.find('table').find('tbody') ``` ##### 解說 取出匯率資訊網頁內的table內的tbody標籤,因為匯率網頁的第一個table就是匯率資訊,所以直接使用`find()`方法找出第一個table標籤即可。 #### 3. ``` rate_table_rows = rate_table.find_all('tr') ``` ##### 解說 因為每一筆匯率都被一個tr標籤包圍,因此透過`find_all()`方法取得每一個tr標籤。 #### 4. ``` if c.attrs['data-table'] == '幣別': ``` ##### 解說 因為每一個欄位(column)內都有data-table屬性來標示該欄位的資料標題(例如:幣別),因此透過該屬性可以針對不同欄位來做特殊處理。 > `attrs`為每個soup的屬性,透過指定的標籤的屬性名稱,可以用來對應的屬性值。 #### 5. ``` last_div = None divs = c.find_all('div') # 取得最後一個div標籤 for last_div in divs:pass ``` ##### 解說 這裡用了一個小技巧,因為幣別的tr標籤內有多個div標籤,而大部分都是我們不要的資訊,剛好最後一個div標籤內有我們需要的幣別(例如:美金USD),所以透過`for-in`迴圈的列舉特性,更新事先宣告好的變數,當一個什麼事都不做的`for-in`迴圈結束後,該變數內就會是最後被列舉(iterator)出來的div標籤。 #### 6. ``` # 取得幣別 data.append(last_div.string.strip()) ... # 存入匯率資訊 data.append(c.getText().strip()) ``` ##### 解說 透過`string`屬性取得的文字因為前後都多了一些「換行」與「空白」字元,所以使用Python`string`物件本身提供的方法`strip()`來去掉。 #### 7. ``` # 以目前時間建立檔名 now = time.localtime() file_name = time.strftime('%Y%m%d_%H%M%S.csv', now) ``` ##### 解說 `time`模組內提供了`strftime()`方法可以用來自訂日期顯示的格式,參數說明如下: | 參數 | 說明 | | ---- | ------------------------------- | | %Y | %Y 四位数的年份表示(000-9999) | | %m | 月份(01-12) | | %d | 月内中的一天(1-31) | | %H | 24小時制的時(0-23) | | %M | 分鐘數(00-59) | | %S | 秒(00-59) | #### 8. ``` # 開啟輸出的 CSV 檔案 with open(file_name, 'w', newline='') as csvfile: ``` ##### 解說 使用 `with` 開啟檔案時,會將開啟的檔案放在 `csvfile` 變數中,但是這個 `csvfile` 只有在這個 `with` 的範圍內才可以使用,離開這個範圍後, `csvfile` 就會自動被關閉,並回收相關的資源,好處就是,避免忘記呼叫`close()`方法而造成檔案使用完畢卻沒有釋放資源,造成記憶體浪費或是檔案無法再次被開啟(open)的問題。 #### 9. ``` writer.writerow(['幣別', '現金買入', '現金賣出', '即期買入', '即期賣出']) writer.writerows(result) ``` ##### 解說 透過Python提供的csv模組的`writer`物件,呼叫`writerow()`方法可以寫入一列,參數為一個`stirng`的`list`,而`writerows()`(注意方法翠後面多了一個s),則需要提供一個`list`的`list`物件,這個`list`內的每一筆資料都是一個`list`,代表一列資料。 ## 完整程式碼 ``` from bs4 import BeautifulSoup import requests import csv import time # 存下全部匯率資料 result = [] # 台灣銀行匯率網址 url = 'https://rate.bot.com.tw/xrt?Lang=zh-TW' # 使用requests物件的get方法把網頁抓下來 response = requests.get(url) # text屬性為html文件原始碼 html_doc = response.text # 指定lxml作為解析器來建立Beautiful物件 soup = BeautifulSoup(html_doc, "lxml") # 找到匯率內容表格 rate_table = soup.find('table').find('tbody') rate_table_rows = rate_table.find_all('tr') for row in rate_table_rows: # 解析每一列的資料 columns = row.find_all('td') # 存放解析後的每一筆資料 data = [] for c in columns: if c.attrs['data-table'] == '幣別': last_div = None divs = c.find_all('div') # 取得最後一個div標籤 for last_div in divs:pass # 取得幣別 data.append(last_div.string.strip()) elif c.getText().find('查詢') != 0 and str(c).find('print_width') > 0 : # 存入匯率資訊 data.append(c.getText().strip()) # 存入已解析完的一個幣別的全部匯率 result.append(tuple(data)) print(result) # 以目前時間建立檔名 now = time.localtime() file_name = time.strftime('%Y%m%d_%H%M%S.csv', now) print('輸出的檔案:', file_name) # 開啟輸出的 CSV 檔案 with open(file_name, 'w', encoding='utf-8', newline='') as csvfile: # 建立 CSV 檔寫入器 writer = csv.writer(csvfile) writer.writerow(['幣別', '現金買入', '現金賣出', '即期買入', '即期賣出']) writer.writerows(result) ``` ## 使用CSS Selector的方式 ```python= from bs4 import BeautifulSoup import requests # 下載網頁用的模組 import csv import time url = 'https://rate.bot.com.tw/xrt?Lang=zh-TW' # 匯率網址 # 下載台灣銀行匯率網頁原始碼 response = requests.get(url) html_doc = response.text # 寫入本地端確認有下載完整 # with open('rate.html', 'w', encoding='utf-8') as rate: # rate.write(html_doc) soup = BeautifulSoup(html_doc, 'html.parser') # print(soup.prettify()) # 格式化下載後的網頁 tbody = soup.find('tbody') all_rates_rows = tbody.find_all('tr') all_rate_data = [] for row in all_rates_rows: rate_data = [] # 解析每一列資料 rate_data.append(row.select_one('.hidden-phone.print_show').string.strip()) rate_data.append(row.select_one('td[data-table="本行現金買入"].print_width').string.strip()) rate_data.append(row.select_one('td[data-table="本行現金賣出"].print_width').string.strip()) rate_data.append(row.select_one('td[data-table="本行即期買入"].print_width').string.strip()) rate_data.append(row.select_one('td[data-table="本行即期賣出"].print_width').string.strip()) all_rate_data.append(rate_data) # 全部資料 print(all_rate_data) # 以目前時間產生檔名 now = time.localtime() file_name = time.strftime('%Y%m%d_%H%M%S.csv', now) print('輸出的檔案名稱:', file_name) with open(file_name, 'w', encoding='utf-8', newline='') as csvfile: csvfile.write('\ufeff') # UTF-8 BOM writer = csv.writer(csvfile) # 建立CSV寫入器 writer.writerow(['幣別', '現金買入', '現金賣出', '即期買入', '即期賣出']) writer.writerows(all_rate_data) ```