美思 [Selenium] 程式設計教學:如何到 OFX 抓外匯保證金交易資料

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

免責聲明:我們盡力確保本文的正確性,但本文不代表任何投資的建議,我們也無法擔保因使用本文的內容所造成的任何損失。如對本文內容有疑問,請詢問財經相關的專家。

在本文中,我們會撰寫一個爬取 OFX 提供的外匯保證金的歷史終盤匯率的頁面以抓取相關資料的爬蟲,由於這個頁面沒有下載 CSV 資料的連結,我們也會展示如何自行生成 CSV 檔案。要注意 OFX 提供的匯率是外匯保證金 (forex) 的貨幣對,和一般在銀行的外匯不同。

以下是相對應的動作:

  1. 前往 OFX 的歷史匯率頁面
  2. 點選基礎匯率 (base currency) 的箭頭
  3. 選擇我們想要的基礎匯率,像 EUR
  4. 再點選基礎匯率的箭頭以關閉該選項框
  5. 點選目標匯率 (target currency) 的箭頭
  6. 選擇我們想要的目標匯率,像 USD
  7. 再點選目標匯率的箭頭以關閉該選項框
  8. 選擇時距 (durtion) 的箭頭
  9. 選擇我們想要的時距,像是 5 年
  10. 再選擇時距的箭頭以關閉該選項框
  11. 按下送出 (submit) 按鈕
  12. (需使用爬蟲) 抓取歷史匯率資料並寫入 CSV 檔案中

同樣地,請讀者手動操作一次,體會一下這個過程;Selenium 程式就像一個口令一個動作的士兵,指令對才會執行正確的動作。另外,由於該頁面是動態產生的,直接使用 requests 抓取會失敗。

由於完整的程式碼偏長,我們將整個程式碼放在這裡;本文會簡化並拆解這個範例。

一開始,先載入相關的模組:

import csv
import time

from selenium import webdriver

設置好相關的參數:

baseCurrency = "EUR"
targetCurrency = "USD"
targetDuration = "Last 5 years"
downloadPath = os.path.dirname(os.path.abspath(__file__))

我們為了簡化範例,我們將參數寫死。實際在撰寫這類程式時,可以用寫命令列程式的手法將其參數化,就可以創造出一個實用小工具。

使用 Chrome 瀏覽器:

# Use headless mode
options = Options()
options.add_argument('--headless')
options.add_argument('--disable-gpu')

# Create a new instance of the Chrome driver
driver = webdriver.Chrome(chrome_options=options)

我們在這裡使用 headless 模式,在這個模式下,不會有 Chrome 的 UI 畫面,只會在背景跑 Chrome。一開始在撰寫程式時不用馬上使用此模式,因為在 headless 模式下無法觀察該程式實際的動作。可在整個程式都寫完後再改為此模式即可。

前往 OFX 的歷史匯率頁面:

# Go to OFX Historical Data page
driver.get("https://www.ofx.com/en-us/forex-news/historical-exchange-rates/")

# Wait the page to fresh.
time.sleep(10)

同樣地,我們會暫停程式數秒,等頁面刷新。

接著,選取基礎匯率:

# Select base arrow.
baseArrow = driver.find_element_by_css_selector(
    ".historical-rates--camparison--base .select2 .selection span .select2-selection__arrow")
baseArrow.click()

time.sleep(2)  # Simulate idling.

# Click on base currency.
baseOptions = driver.find_elements_by_css_selector(
    ".historical-rates--camparison--base select optgroup option")
for option in baseOptions:
    if baseCurrency in option.text:
        option.click()
        break

time.sleep(1)  # Simulate idling.

baseArrow.click()

time.sleep(1)  # Simulate idling.

實際上,動作拆成三部分。我們先選取基礎匯率的選單的箭頭,展開選單,接著選擇我們想要的基礎匯率,最後再把選單合起來。經筆者實測,如果沒把選單合起來,會蓋掉底下的網頁元素,引發程式的錯誤。

我們用同樣的思維去選我們想要的目標匯率:

# Select target arrow.
targetArrow = driver.find_element_by_css_selector(
    ".historical-rates--camparison--target .select2 .selection span .select2-selection__arrow")
targetArrow.click()

time.sleep(2)  # Simulate idling.

# Click on target currency.
targetOptions = driver.find_elements_by_css_selector(
    ".historical-rates--camparison--target select optgroup option")
for option in targetOptions:
    if targetCurrency in option.text:
        option.click()
        break

time.sleep(1)  # Simulate idling.

targetArrow.click()

time.sleep(1)  # Simulate idling.

接著,我們再用同樣思維去選擇時距:

# Select target period.
periodArrow = driver.find_element_by_css_selector(
    ".historical-rates--period ~ .select2 .selection span .select2-selection__arrow")
periodArrow.click()

time.sleep(2)  # Simulate idling.

# Click on target period.
periodOptions = driver.find_elements_by_css_selector(
    ".historical-rates--period option")
for option in periodOptions:
    if targetDuration in option.text:
        option.click()
        break

time.sleep(1)  # Simulate idling.

periodArrow.click()

time.sleep(1)  # Simulate idling.

按下送出 (submit) 按鈕,即可得到歷史匯率數據:

# Submit for the result.
submitButton = driver.find_element_by_css_selector(".historical-rates--submit")
submitButton.click()

# Wait the page to fresh.
time.sleep(5)

同樣要等待數秒,讓頁面跑完。這時候數據還在頁面上,要用爬蟲抓取下來。

這時候,我們做了一個額外的處理,本程式抓取匯率的平均值,並定出其上下限:

avgRate = float(driver.find_element_by_css_selector(".historical-rates--table--average--value").text)
ratio = 15
upLimit = avgRate * ratio
downLimit = avgRate / ratio

這個動作不是必需的,因筆者在嘗試此程式的過程中,發現有時候該頁面偶爾會有爆高或爆低的不合理匯率,說實在的,筆者無法確認這些數值的真假,但我們會將其剔除。

我們一邊抓取資料,一邊將其寫入 CSV 檔案中:

# Save historical currency data into CSV.
with open("%sto%s.csv" % (baseCurrency, targetCurrency), 'w', newline='') as csvfile:
    csvwriter = csv.writer(csvfile)

    csvwriter.writerow(["Date", "Rate"])

    records = driver.find_elements_by_css_selector(".historical-rates--table--results tr")
    for record in records:
        d = record.find_element_by_css_selector(".historical-rates--table--date").text
        r = float(record.find_element_by_css_selector(".historical-rates--table--rate").text)

        if r >= upLimit or r <= downLimit:
            continue

        csvwriter.writerow([d, r])

我們用 with 區塊來代替 close 函式,可以清楚地展示出我們這個區塊和寫入 CSV 檔案相關。在寫入 CSV 時,要用 newline='' 將換行設為空字串,以免插入額外的空行。我們另外自行寫一個 CSV 的 header,可視需求加入或略去。

在這個程式範例中,我們去除極端值,這並不是必需的,讀者可自行考慮要不要做這個動作。

最後,別忘了關掉瀏覽器:

# Close the browser.
driver.quit()
關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。