位元詩人 [Selenium] 程式設計教學:如何抓取台股大盤指數歷史資料

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

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

大盤指數本身雖然不像 ETF 般可以直接購買,但大盤指數可以反映目前台灣股市的整體景氣變化,故仍有一定參考性。台股證交所內建的 CSV 下載連結只能取得單月份的數據,對於中長期的分析來說不太方便。本文實作一個可以爬取中長期大盤指數的爬蟲,透過本爬蟲,可以自動化取得連續數年的大盤指數,對於後續的分析比較方便。

同樣地,我們先拆解抓取大盤指數所進行的動作:

  • 前往大盤指數歷史數據網站
  • 選取特定年份
  • 選取特定月份
  • 按下查詢按鈕
  • (需用爬蟲) 抓取該月份的歷史數據
  • (需用爬蟲) 重新選取另一個年份、月份後再查詢

由此可知,如果這個動作用純手動,要抓取跨月資料時會有困難;這時候,就很適合用爬蟲來減少不必要的手工。

由於這個程式碼略長,我們將完整的程式碼放在這裡,除了可以追蹤程式碼,這段程式碼也是一個立即可用的命令列工具,只要台股證交所網站不改版就可以繼續使用。接下來,我們會拆解這段程式碼,供有興趣學習實作的讀者參考。

引入相關的套件:

import csv
import os
import sys
import time
import datetime

from selenium import webdriver

設置時距:

validDurations = ['YTD', '1Y', '3Y', '5Y', '10Y', 'Max']
duration = 'YTD'

原本證交所上的網頁並沒有中長期時距的概念,這段時距是我們自行加上去的。讀者若有需要也可自行加入其他的時距。

設置相關時間點:

now = datetime.datetime.now()

year = None
month = now.month

if duration == 'YTD':
    year = now.year
    month = 1
elif duration == '1Y':
    year = now.year - 1
elif duration == '3Y':
    year = now.year - 3
elif duration == '5Y':
    year = now.year - 5
elif duration == '10Y':
    year = now.year - 10
elif duration == 'Max':
    year = 88 + 1911
    month = 1

我們會有兩個日期點,一個是目前的日期 now 物件,一個是目標日期 yearmonth,我們的迴圈需要這兩個日期點判斷迴圈結束的時機。

建立使用 Chrome 的 web driver:

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

前往大盤指數所在的頁面:

# Go to TAIEX page
driver.get("http://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.html")

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

頁面需要暫停數秒,待網頁載入完成。

抓取查詢按鈕:

queryBtn = driver.find_element_by_css_selector(".main a")

現在不用急著按下查詢鈕,待會會在適當的時間按下查詢鈕。

準備進入這隻爬蟲最重要的部分,按月份爬取大盤指數歷史數據:

data = []
isEnd = False
currYear = year
currMonth = month

# Select the initial year.
ys = driver.find_elements_by_css_selector("select[name=\"yy\"] option")
for y in ys:
    if y.get_attribute("value") == str(currYear):
        y.click()
        time.sleep(2)
        break

while not isEnd:
    # Run the crawler here.

我們在這裡從時距的過去日期 (past date) 開始爬,所以 currYearcurrMonth 會以過去日期來設置。為什麼要從過去日期開始爬?因為我們想要歷史資料的日期是順向的,這樣就不用爬取後再把數據反向。順向的數據對於數據的視覺化來說會比較方便,因為習慣上圖表的左方代表先前日期的交易數據。

我們在這裡會先預選一次年份,這樣之後再跑迴圈時不同每個月重新選取年份。這是筆者邊寫程式邊觀察爬蟲的動作所得的結論,不一定適用在所有網站,也不要死記這個手法。

由於這個迴圈比較大,我們先把實作的部分移除,只看邏輯的部分:

while not isEnd:   
    if currYear < now.year:
        if currMonth <= 12:
            # Crawl the website.

            currMonth += 1
        else:
            currMonth = 1
            currYear += 1

            # Crawl the website.
    else:
        if currMonth <= now.month:
            # Crawl the website.

            currMonth += 1
        else:
            isEnd = True

從這段程式碼中看得出來,我們的迴圈就是按月遞增,當迴圈遞增到目前日期時,迴圈就中止。

我們來看當目前的年份小於現在年份時的情形:

while not isEnd:   
    if currYear < now.year:
        if currMonth <= 12:
            ms = driver.find_elements_by_css_selector("select[name=\"mm\"] option")

            for m in ms:
                if m.get_attribute("value") == str(currMonth):
                    m.click()
                    time.sleep(2)
                    queryBtn.click()
                    time.sleep(3)

                    items = driver.find_elements_by_css_selector("#report-table_wrapper tbody tr")

                    for item in items:
                        tds = item.find_elements_by_css_selector("td")

                        data.append([td.text for td in tds])
                    break

            currMonth += 1
        else:
            currMonth = 1
            currYear += 1

            # Update the year when one year progresses.
            ys = driver.find_elements_by_css_selector("select[name=\"yy\"] option")

            for y in ys:
                if y.get_attribute("value") == str(currYear):
                    y.click()
                    time.sleep(2)
                    break
    else:
        if currMonth <= now.month:
            # Crawl the website.

            currMonth += 1
        else:
            isEnd = True

在每個月份中,我們選取特定月份並按下查詢鈕。之後用爬蟲抓取資料,將資料加入 data 串列的尾端。在這裡我們不直接將資料寫入 CSV 檔案,因為爬取時間較久,這樣整個開啟檔案的時間會拉得很長,我們先將數據存在記憶體,整個爬完後再將數據寫入 CSV 檔。

當跨到下一個年度時,我們重新選取下一個年份。在這個網頁不需在每個月都選一次年份,可節省一點點操作時間。

接下來看一下當下年份等於目前年份的情形:

while not isEnd:   
    if currYear < now.year:
        if currMonth <= 12:
            # Crawl the website.

            currMonth += 1
        else:
            currMonth = 1
            currYear += 1

            # Crawl the website.
    else:
        if currMonth <= now.month:
            ms = driver.find_elements_by_css_selector("select[name=\"mm\"] option")

            for m in ms:
                if m.get_attribute("value") == str(currMonth):
                    m.click()
                    time.sleep(2)
                    queryBtn.click()
                    time.sleep(3)

                    items = driver.find_elements_by_css_selector("#report-table_wrapper tbody tr")

                    for item in items:
                        tds = item.find_elements_by_css_selector("td")

                        data.append([td.text for td in tds])
                    break

            currMonth += 1
        else:
            isEnd = True

其實爬資料的動作是一樣的,重點是邏輯的部分有變化,我們不會爬完整年份的資料,只會爬到當下月份的資料,因為目前的月份還沒走完,無法取得整年的數據。另外,我們也要在適當的時機結束這個迴圈。

我們設置一些字串,這些字串會用到自動化生成檔名:

def monToStr(m):
    if m < 10:
        return '0' + str(m)
    else:
        return str(m)

pastDateStr = "%d%s" % (year, monToStr(month))
currDateStr = "%d%s" % (now.year, monToStr(now.month))

將歷史數據寫入 CSV 檔:

with open("TAIEX_%sto%s.csv" % (pastDateStr, currDateStr), 'w', newline='') as csvfile:
    csvwriter = csv.writer(csvfile)

    csvwriter.writerow(["Date", "Open", "High", "Low", "Close"])

    for d in data:
        csvwriter.writerow(d)

最後別忘了關掉瀏覽器。

# Close the browser.
driver.quit()

透過這個程式,我們就可以取得中長期的大盤指數歷史數據。由於原本證交所網站沒有中長期時距的概念,我們在程式中自行加入;另外,我們也可以練習如何用 Selenium 取得跨月份的歷史數據。這些都是實作這隻爬蟲時學到的寶貴經驗。

關於作者

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

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