楊育晟(Peter Yang)

嗨, 我叫育晟, 部落格文章主題包含了程式設計、財務金融及投資...等等,內容多是記錄一些學習的過程和心得,任何想法都歡迎留言一起討論。



Email: ycy.tai@gmail.com
LinkedIn: Peter Yang
Github: ycytai

證交所股票價格爬蟲實作教學-附程式碼(Use Python to collect stock price)

什麼是Python網路爬蟲?

Python 網路爬蟲就是利用撰寫 Python 程式碼去對網路資訊進行擷取,例如蒐集匯率的歷史走勢、熱門議題的輿情...等等

現在網路發展的成熟度相當高,幾乎可以在網路上找到、或者說 google 到大部份需要的資訊,而當找到資訊後,如何彙整、蒐集,甚至是定期的去執行這些工作,其實都能夠透過程式來完成

今天要一步一步從介紹套件、解析資料來實作股價爬蟲,並前往證券交易所爬取股票價格資訊。

爬蟲不是一般人常聽到的字詞,可以想作抓資料的概念,而抓取資料雖然要撰寫程式碼,不過可以將訣竅簡化為下面幾個步驟,後面的實作也會照著這個步驟來解說

  1. 選定要爬取的目標
  2. 解讀與分析資料
  3. 成功爬取一小部份
  4. 觀察資料的異同
  5. 利用迴圈爬取全部

寫程式要完成任務前,都會先導入(import)需要使用的套件,除了資料處理上常用到的 pandas 之外,本次任務也會使用 requests

requests 是寫 Python 爬蟲常使用到的套件,能夠讓程式對網頁送出請求,取得資料。 Python 有許多強大的套件應用在各種不同領域,透過運用這些套件將能更容易、迅速的達成工作目標。

Python爬蟲-證交所股價擷取

1. 選定要爬取的目標

按照前面所說的步驟,來開始今天的實作吧! 先來看一下今天要爬取的目標,網址在這,從網頁中可以看到分類的選單,在下拉的內容中找到選取全部,按下查詢,再點選左邊的 CSV 下載

接著將會得到一個包含當天市場上各類商品、指數、股票…等等全部的資訊,打開後可以看到包山包海,有好多資料,由於剛剛的下拉選單中並沒有單純股票這個選項,所以必須要從這裡頭來萃取股價資訊。

用 Excel 將檔案打開後,可以發現和網頁上的資訊是相同的,這份檔案就是將網頁上的資訊整理成 csv 後下載。(Windows 用戶打開檔案應該沒問題, Mac 可能會碰到中文亂碼 QQ)

而把 csv 拉到接近最下方,可以看到出現股票的資料,像下圖中一樣,看到台泥就是了,大約有快 1000 筆,這就是本次爬蟲任務的爬取目標。

雖然可以透過直接下載 csv 的方式取得資料,不過如果今天要的是一年份的資料,一天一天下載、整理,效率太低,所以讓 Python 來完成剛剛的動作吧!

首先,先回到剛剛的證交所網頁,對網頁按右鍵 inspect (希望你瀏覽器也是用 Chrome),然後下方會出現一個區塊,點選 Network 的分頁。

此時下方區塊會開始記錄執行的動作,我們再點選一次 CSV 下載 ,這時候下方的區塊會多出一個紀錄,這是剛點選下載的動作,點下後旁邊可以看到 Request URL: 的地方,這個網址就是 csv 資料的來源。

在寫這篇教學文時看到的 Request URL 是下面這樣

https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=20230928&type=ALL&response=csv

從 URL 中有看到三個參數,分別為 date, type 以及 response

從字面上就能迅速理解參數, date 為資料日期, type 為資料內容,網址上顯示的 ALL 就是因為前面我們在下拉選單所選的全部 。 至於 response 則更白話,就是下載的資料格式是 csv 啦。

確認爬許目標,也找到資料來源後,就可以來寫 code 囉!

首先先導入套件

import pandas as pd
import requests

接著讓 requests 來執行類似下載的動作。 requests 後方的 get 為對資料來源的請求方式,爬蟲大多時候會碰到的有兩種, getpost

把結果存在 data 這個變數中,然後進入下一個步驟

url = 'https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=20230928&type=ALL&response=csv'
res = requests.get(url)
data = res.text

2. 解讀與分析資料

data 的資料類型是 str,來偷看一下抓到的資料內容是不是符合預期

data[0:500]

得到結果

'"112年09月28日 價格指數(臺灣證券交易所)"\r\n"指數","收盤指數","漲跌(+/-)","漲跌點數","漲跌百分比(%)","特殊處理註記",\r\n"寶島股價指數","18,734.41","+","49.77","0.27","",\r\n"發行量加權股價指數","16,353.74","+","43.38","0.27","",\r\n"臺灣公司治理100指數","9,128.66","+","19.97","0.22","",\r\n"臺灣50指數","12,059.05","+","27.29","0.23","",\r\n"臺灣50權重上限30%指數","11,402.72","+","27.00","0.24","",\r\n"臺灣中型100指數","16,739.40","+","42.01","0.25","",\r\n"臺灣資訊科技指數","22,409.60","+","71.22","0.32","",\r\n"臺灣發達指數","9,604.56","+","4.72","0.05","",\r\n"臺灣高股息指數","8,073.05","+","36.32","0.45","",\r'

看起來和 csv 裡的內容相同,符合預期。

很多人剛開始可能會被資料雜亂的模樣給嚇到,但仔細一看會發現,如果要整理還是有跡可循的,可以看到有很多不斷出現的 \n ,這符號其實就代表著換行的意思。

所以能先用split()這個函數來將資料分解,split 的用法就是把資料依指定的元素來切割

for d in data[0:500].split('\n'):
    print(d)

印出的結果會像這樣,突然變得像在 Excel 裡看到的模樣,一排一排的往下顯示,看起來是不是親民多了。

"112年09月28日 價格指數(臺灣證券交易所)"
"指數","收盤指數","漲跌(+/-)","漲跌點數","漲跌百分比(%)","特殊處理註記",
"寶島股價指數","18,734.41","+","49.77","0.27","",
"發行量加權股價指數","16,353.74","+","43.38","0.27","",
"臺灣公司治理100指數","9,128.66","+","19.97","0.22","",
"臺灣50指數","12,059.05","+","27.29","0.23","",
"臺灣50權重上限30%指數","11,402.72","+","27.00","0.24","",
"臺灣中型100指數","16,739.40","+","42.01","0.25","",
"臺灣資訊科技指數","22,409.60","+","71.22","0.32","",
"臺灣發達指數","9,604.56","+","4.72","0.05","",
"臺灣高股息指數","8,073.05","+","36.32","0.45","",

這時候對資料的概念馬上就從一堆看似亂碼的樣子變成了打開 csv 檔後出現的模樣,也就是說..想抓取的股票資訊在這串資料的下半部份

3. 成功爬取一小部份

此時可以快速地想辦法先抓取其中一小部份來映證看看,股票資料是不是真的在這串資料下頭。剛剛只是把資料印出來,現在將split()完的結果儲存起來。

s_data = data.split('\n')

經過了split()的處理後,資料的型態是list,可以透過偷懶的 index 找尋股票價格資料是否在裡頭。

s_data[-1000]

印出的資料會像下方這樣

'"1232","大統益","29,681","133","4,202,864","142.00","142.00","141.00","141.50","-","0.50","141.50","1","142.00","6","23.16",\r' 

當執行上面的程式碼後,可以發現股票資料確實也在裡頭! 股票資料確實如我們原本在 csv 檔中看到的,在整堆資料的下頭。

因此,前面的所有動作,就如同直接從網站上將檔案下載下來

4. 觀察資料的異同

不過,不可能慢慢的一檔一檔股票的抓,要把股票資訊從整堆CSV中萃取出來,並且整理成乾淨、整齊的模樣,因此觀察看看,股票資料和其它要忽略的資訊,有甚麼不一樣的地方。

目標資料的長度類型,明顯和 csv 上方指數資料不同,也就是說,可以透過資料長度來過濾掉上方的資料。

再把 csv 往下拉可以看到股票的資料是和上面的權證資料連在一起的,表頭也是共用的,如此一來,剛剛用的資料長度並沒有辦法讓過濾掉這些權證、ETF的資料

接著就需要比較細節的去觀察,可以看到第一個證券代號的儲存格,只要非股票的資料,前頭都會有個 = 的符號

這個特別之處,就能夠讓幫助過濾掉不要的資料,已經很接近完成囉! 加油!

5. 利用迴圈爬取全部

當發現資料的異同後,就能夠透過給予限制過濾掉要忽略的資料,並且寫成迴圈印出來。

資料長度的部份,用剛剛找到的股票資料來查看,在 split() 之後,每行股票資料的長度應為 16,等等用 len() 函數來過濾

>>> row = s_data[-1000]
>>> row.split(',')
['"1232', '大統益', '29,681', '133', '4,202,864', '142.00', '142.00', '141.00', '141.50', '-', '0.50', '141.50', '1', '142.00', '6', '23.16",\r']
>>> len(row.split('","'))
16

而用資料內容的話,透過指定每一列資料的第一個元素的第一個符號不等於 = 來過濾掉權證、ETF等資料。綜合上面寫出程式碼如下

output = []
for d in s_data:
    _d = d.split('","')
    length = len(_d)
    symbol = _d[0]
    if length == 16 and not symbol.startswith('='):
        output.append(_d)

output 的前幾筆印出來看看

output[0:3]

結果正確,有表頭跟前兩筆資料。

[['"證券代號', '證券名稱', '成交股數', '成交筆數', '成交金額', '開盤價', '最高價', '最低價', '收盤價', '漲跌(+/-)', '漲跌價差', '最後揭示買價', '最後揭示買量', '最後揭示賣價', '最後揭示賣量', '本益比",\r'], ['"1101', '台泥', '18,375,914', '10,223', '609,037,501', '33.10', '33.30', '33.05', '33.25', '+', '0.20', '33.20', '146', '33.25', '165', '27.25",\r'], ['"1101B', '台泥乙特', '16,381', '17', '771,372', '47.20', '47.20', '46.90', '47.20', ' ', '0.00', '46.95', '2', '47.20', '18', '0.00",\r']]

恭喜! 印出來的結果已經成功萃取出股票的資料了! 如此一來要做的就是將資料整理成乾淨的樣式。

至於整理的話,下方就直接附上程式碼和簡單的解釋,讀者可以專注在瞭解前面爬蟲的邏輯

資料清洗

資料中會有一些元素是不需要的例如 ",\r 之類,所以要把資料清理乾淨後再存起來

# 把爬下來的資料整理乾淨
output = []
for d in s_data:
    _d = d.split('","')
    length = len(_d)
    symbol = _d[0]
  
    if length == 16 and not symbol.startswith('='):
        output.append([
          ele.replace('",\r','').replace('"','') 
          for ele in _d
        ])

接著利用 pandas 把資料整理成 DataFrameDataFrame 是一種 pandas 提供的資料型態,像表格一樣。把整理乾淨的 output 丟給 pd.DataFrame(),然後指定第一筆資料當表頭,指定證券代號那欄作為索引。

df = pd.DataFrame(output[1:], columns=output[0])
df.set_index('證券代號', inplace=True)

最後來把爬到的股價,用 df.to_csv() 來輸出成 csv 檔案吧!

df.to_csv('stock_price_20230928.csv')

如果一切順利,檔案打開後就像下方圖片一樣,乾乾淨淨、整整齊齊的股價資料,終於完成囉!

完整程式碼

雖然爬蟲的過程看似很複雜,但大多可以利用這 5 個簡化過的步驟來解決 ,等到越來越熟悉之後,就會內化在心裡頭,爬的越來越快,程式也會越寫越簡潔。

完整的程式碼如下,我有再改寫稍微改寫一點,這是抓一天的範本,如果要蒐集歷史資料,就逐步地往回抓每個營業日的股價就 OK 哩

import pandas as pd
import requests

# 設定目標日期
target_date = '20230928'

# 把 csv 檔抓下來
url = f'https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date={target_date}&type=ALL&response=csv'
res = requests.get(url)
data = res.text 
 
# 把爬下來的資料整理成需要的格式
s_data = data.split('\n')
output = []
for d in s_data:
    _d = d.split('","')
    length = len(_d)
    symbol = _d[0]
  
    if length == 16 and not symbol.startswith('='):
        output.append([
          ele.replace('",\r','').replace('"','') 
          for ele in _d
        ])
 
# 轉成 DataFrame 並存成 csv 檔
df = pd.DataFrame(output[1:], columns=output[0])
df.set_index('證券代號', inplace=True)
df.to_csv(f'stock_price_{target_date}.csv')
Tags:
# finance
# python
# stock
# 爬蟲