MATLAB爬蟲(以下載台灣期交所的公開資料為例)


爬蟲這項技術說實在確實非常重要,也與我們的生活息息相關,現在的網路如此發達,無法想像的資料量也還是在成長著,為了更方便地取得與處理資料,在自動化的議題上,爬蟲便成為了不可或缺的其中一項技術。

台灣期貨交易所

台灣期貨交易所(Taiwan Futures exchange),顧名思義,是在台灣所成立的金融衍生性商品的交易設施,交易商品包含期貨以及選擇權等商品,這兩種都是一種契約。而台灣期貨交易所為了讓交易人能夠快速了解在台灣期貨市場(Futures Market)的動態消息,特別提供了一個官方網站平台(www.taifex.com.tw),上面有台灣期貨交易所相關的公司介紹,包含願景以及交易商品有哪些,還有當前期貨市場上的最新消息。當然,對於想要更進一步了解金融市場的人而言,想必也會特別關注統計資料以及交易資訊了。再來他們也提供了法規規定以及交易規則等等參考檔案,資訊非常完整。

台灣期貨交易所不僅提供交易資訊的查詢,也有提供近期的交易資料下載,其提供的資料也都已經結構化處理過,我們只需要視情況轉換為自己想要的處理格式便可。如果我們想要收集這些資料,以便進行相關的研究與分析(別把自己下載資料拿去交易或相關形式的轉讓等等,避免違法),使用爬蟲技術並讓其自動化是非常方便的。


MATLAB爬蟲

MATLAB相較於Python來說,或許爬蟲相關套件並沒有那麼完備與好用(輕便、容易理解與使用),但在研究上,或想學習爬蟲相關技術的人來說,也是一個很不錯的選擇,畢竟無論使用哪些程式語言進行撰寫,這些技術的運作流程,甚至是套件與函式的使用方法也不會變化太多。但為了講解方便,我們就先以MATLAB為主要語言來進行介紹。

MATLAB提供了幾種比較基本的爬蟲函式,像是urlread()、urlwrite()、webread()、webwrite()、websave()等等。其中urlread()以及urlwrite()屬於較舊的函式。urlread()將網頁內容下載回來,儲存為character vector型態;urlwrite()同樣也是將網頁內容下載下來,不過是以檔案形式儲存。這兩個函式官方當前並不推薦使用,原因主要是因應新的網頁框架設計模式,主要偏好RESTful的網頁服務,相關的請求功能操作也進行更明確的分離與更動,所以另外再推出了webread()、webwrite()以及websave()等函式,而webread()以及webwrite()也分別對應到GET、POST,功能分別是取得資料以及新增資料。而websave()則是將網頁內容存放至檔案中。而這三個函式所支援的weboptions()函式,也是為了給非程式設計專長的人員更容易使用而設計的,可以幫助我們快速設定網路參數。

爬蟲目標

由於台灣期貨交易所提供的下載資料頁面中,大多數的連結給定的格式與網址參數都大同小異,所以本次的爬蟲目標就簡單以每筆成交資料為主,在台灣期貨交易所上有保留了前30天交易日的每筆成交資料。

再來是,要爬的是查詢後的網頁表格資料或是官方已經整理好的資料,基本上我會選後者,畢竟兩者的欄位並無太大的相異,而兩者的資料更新速度也不是即時,若要即時資料,我想還是直接串接券商API可能為佳,要驗證資料事後驗證便可。因此本篇把爬蟲目標再次縮小至直接下載檔案的方式,其實主要就差在需不需要再對html做處理並從中擷取資料而已。


爬蟲程式

一個爬蟲程式主要的流程包含決定爬蟲目標、給定輸出目錄、設定參數、執行爬蟲功能(呼叫爬蟲函式)。此外,也可以進行例外處理以及檔案檢查等等,確保檔案下載無誤。且執行爬蟲程式時,無論是有無設定工作排程,還得考慮提供資料那一方的更新時間,避免下載下來的資料不完全。

爬蟲目標

台灣期貨交易所網站提供資料下載與查詢的地方,在網頁上方項目清單列表中"交易資訊"=>"交易資訊" or "資料下載專區"頁面中。此外,我們也可以看到該頁面會提供資料說明(包含商品代號說明以及相關欄位說明等等)以及不同格式的檔案如何開啟,相當貼心。這邊提醒一下,無論是rpt檔或是csv檔,皆是一種文字檔,可以使用文字編輯器開啟,兩者的檔案編碼皆為Big5。


由於官方主要提供下載的格式皆為zip檔,所壓縮的內容有兩種,rpt檔以及csv檔。所以我們挑選"前30個交易日期貨每筆成交資料"作為目標,下載的是壓縮著csv檔的zip檔。


觀察資料更新時間

資料更新時間很重要,如果忽略了這個部分,可能會導致下載過程中出現問題,包括資料下載不全或資料遺失等等。這邊要注意到的是,除了資料更新的時間,或者說時間範圍、時段,我們也得注意資料更新的次數有幾次,可能因為流程或機制的問題,所以資料更新的時間不只一個,可能一天有好幾次的更新時段。

我們先來觀察台灣期貨交易所中,剛才挑選的目標頁面。我們可能會發現不同時後上去觀察資料更新時間其實不一,以下面這張圖來看,大致分為兩個時段,包括上午以及下午。這個與交易制度有關,關於交易制度的說明可以在網頁上方項目清單"交易制度"=>"交易制度",並於左方選項區塊"盤後交易"=>"盤後交易介紹"中找到。



其中,在第三點交易及部位歸屬原則提到,交易時段主要分為一般交易時段以及盤後交易時段,中間會有休息、停頓的時間。且盤後交易時段會被歸類在次一個一般交易時段的交易中。

這意味著,上午更新的部分屬於盤後交易時段的交易,而這個交易其實還沒完,還有白天的一般交易時段的交易還沒進行完畢,所以下午還會再更新一次。


再仔細觀察下載頁面,由於上午以及下午的更新時段都是一個區間,官方可能在這個區間內更新多次,為了保險,我們設定排程或程式在進行判斷的時候,得確保每一筆資料的完整性,決定是否重新下載覆蓋,或是最後一次更新後才下載完整資料。

網路參數設定

一般而言,網站可能會有Header、隱藏參數、認證、Cookie與緩存(快取)等等要求。即便找到目標網址,也可能在連線過程中被擋下來,或者造成格式不符,請求失敗。此外,也可能會做像是對短時間內多次連線請求的封鎖,避免造成伺服器負擔與惡意攻擊。

而由於本次要下載的資料屬於台灣期貨交易所開放給大眾瀏覽與使用的,並沒有甚麼額外參數要去設定,我們只要透過GET方法進行連線請求就行,並且適當延遲一小段時間(0.5秒或1秒之類的)。

爬蟲目標網址

一般而言,有些網頁元素的屬性,並不能讓使用者直接透過滑鼠右鍵點擊的方式來獲取網址。在這樣的情況下,直接觀察網頁原始碼是最簡單的方式了。在Chrome瀏覽器中,可以使用組合鍵"Ctrl + Shift + I"來叫出"檢查"視窗(也是可以在網頁背景處點擊右鍵,但部分網頁可能會封鎖這種方式)。

在"檢查"視窗中,找到"Elements"頁面可以瀏覽整個網頁程式碼。此外,"檢查"視窗左上角有一個箭頭符號(可以透過"Ctrl + Shift + C"呼叫),是用來選取網頁元素的,選取完成後會自動對應到相應的網頁程式碼位置,相當方便。

那我們就開始使用這個技巧,來找到目標網址,並尋找檔案命名規則~


我們選取了csv檔下載按鈕元素後,會跳到該網頁程式碼位置,我們已經輕鬆地找到了目標網址,並且觀察了其他的下載點後,發現其檔案的命名規則非常規律、簡單,就是以日期作為改動的依據。

CSV檔案格式如下:

https://www.taifex.com.tw/file/taifex/Dailydownload/DailydownloadCSV/Daily_<yyyy>_<mm>_<dd>.zip

我們另外也觀察一下RPT檔的下載連結長怎樣:

https://www.taifex.com.tw/file/taifex/Dailydownload/Dailydownload/Daily_<yyyy>_<mm>_<dd>.zip

其實也就檔案目錄略改一下而已(DailydownloadCSV => Dailydownload)。

撰寫MATLAB爬蟲程式

為了更清楚說明爬蟲的機制,我們先從舊的架構、舊的函式來寫起,就以urlread()、urlwrite()為例。

而因為欲下載之檔案命名規則是以日期為主要依據,我們得解決獲取日期問題,並且將獲取到的日期進行處理,最後轉為字串。

為了方便獲取與計算日期,我們先呼叫能獲取當前日期與時間的函式:

% 格式參考: 03-Aug-2020 16:46:18,假設為今天
datetime('now');

datetime()函式會回傳一個datetime array,用來表示當前時間與日期。

由於台灣期貨交易所提供30天左右的每日每筆成交資料,所以除了今天之外,我們得往回推日期,datatime array的內容正好可以直接透過加減運算來往後或往前推日期。

% 格式參考: 02-Aug-2020 16:46:18,相當於昨天
datetime('now') - 1;
% or datetime('yesterday');

% 格式參考: 04-Aug-2020 16:46:18,相當於明天
datetime('now') + 1;
% or datetime('tomorrow');

而終止條件可以是計算大約30次左右成功下載次數,或是超過一定次數的連續下載失敗次數等等,方法有很多,但無論如何,通常會確保所有資料都能獲取到。這個部分並非本篇的重點,就先不談。


因為目標網址是有一定規則的,又是根據日期,這正好讓我們可以將datetime array進行轉換,變成固定形式的字串。

這我們可以透過datestr()來進行日期格式的轉換,並且傳入設定參數來設定格式。

% 參考格式: 2020_08_03
formatOut = 'yyyy_mm_dd';
date_str = datestr(datetime('now'), formatOut);

而網址有前綴以及後綴,我們宣告並初始化這些變數:


prefix = 'https://www.taifex.com.tw/file/taifex/Dailydownload/DailydownloadCSV/Daily_';
postfix = '.zip';

我們要將以上三個子字串部分進行串接,可以使用join()函式,其預設串接字串時會以空白當作間隔(依版本不同而有差異),我們需要進一步透過delimiter進行設定。

target_url = join([prefix, date_str, postfix], 'delimeter', '');
% or
target_url = [prefix, date_str, postfix];

再來我們要決定下載下來的檔案名稱,就以網址最後的名稱作為檔名吧~這時我們會需要分割字串,只要找到最後一個'/'符號,就能夠將最後的檔名子字串給取出。我們首先使用一個變數紀錄最後一個'/'符號位置。

pre_filename_pos = strfind(target_url, '/');
pre_filename_pos = pre_filename_pos(end);

再來將檔名子字串取出。

output_name = target_url(pre_filename_pos + 1:end);

開始來使用urlread()函式獲取內容吧~

urlread(target_url);
 

疑,怎麼出錯了,它說網路連線或者proxy設定怪怪的?

我們用urlwrite()函式再試試看,由於它的功能是將網頁內容下載至檔案,我們需要再給它檔案名稱。

urlwrite(target_url, output_name);
 

有點奇怪,檢查了一下,網路連線並沒有異常,直接從瀏覽器下載也沒問題,這意味著肯定是這兩個函式有點問題。

再去google搜尋一下,發現原來是憑證認證問題,相關連結附在以下:

首先,urlread()以及urlwrite()對憑證的驗證操作是由在MATLAB中所附的jre所執行,其包含一個keystore用來儲存具有公信力機構認證所發行的鑰匙,所以如果是自己私有的數位簽章將會不被信任(或是正好沒被這些機構所認證)。所以這就清楚明瞭了,為了能夠被信任,我們可以下載或添加新的鑰匙,並加進keystore中。雖然上方所附的網址中,有提供了一個添加新鑰匙功能的程式碼,但這相對比較麻煩,並且也修改到了原設定。

所以這邊我們選擇先不使用urlread()以及urlwrite()函式,畢竟官方也不推薦,我們來試試webread()、webwrite()以及websave()三個函式,看看會出現什麼情況。

但我們要先來介紹憑證這個東西,以便於讓我們更容易了解為何要這些認證。

數位簽章與數位憑證認證機構

之所以需要數位簽章,是因為在網路上的傳播的資料很可能被人竄改,或者仿冒。此外,數位簽章雖然使用到了加密,甚至也添加了Hash Function進行混淆與打亂,但目的並不是不給看訊息,而是用來認證。那這應該是怎麼進行的?

一般而言,在使用非對稱加密的情況下,如果我們希望資料有保密性,不希望被外人看見,我們會使用一對金鑰一組是公鑰,除了自己,大家都能夠拿到,用來加密,這意味著大家都能用這組公鑰進行內容加密;另一組是私鑰,自己要保存好,不能被別人看見,用來解密,所以在不考慮有人刻意破解、竊盜或竊聽的情況下,能夠確保只有自己才能看到內容。

數位簽章則是使用私鑰進行加密,當作自己的簽名,也因為私鑰理應只有自己能知道,代表簽名確實屬於自己親自簽章。而公鑰則公開給大家來進行解密,或者說驗證,如果對應的公鑰能夠解開,或者配對成功(還原回Hash後的結果),而我們也得確保在產生私鑰與公鑰的時候,不會因為公鑰被公開而造成私鑰被猜測或破解出來,不然就會造成任何人都能仿冒簽章。

所以這個過程有主要三個特性與目的:

  1. 確保資料的完整性
  2. 認證是否訊息發布者為給出公鑰的人
  3. 發布訊息者不能否認這個事實,具有不可否認性

然而,即便如此,也可能出現問題,比方說同樣的內容被不同人進行簽章,最後可能不知道真正的發行人,或者說是作者是誰了。勢必需要有一個具有公信力的團體存在,而這個具有公信力的團體被稱為了數位憑證認證機構(Certificate Authority,簡稱為CA)。數位憑證認證機構通常為第三方團體,不受任何主要勢力控制,對公鑰的合法性檢驗負有一定的責任,機構也不只一個,比如說Comodo、(Symantec、GeoTrust、DigiCert)、Thawte、GlobalSign、RapidSSL等等。

這些數位憑證認證機構會對用戶的公鑰進行加密,產生他們的簽名,代表這些內容被他們所認證通過,而他們會公開他們的公鑰用來驗證這些簽名,開放給各位進行訊息的驗證。


RESTful Web Service

webread()、webwrite()以及websave()屬於針對RESTful Web Service進行處理的函式,其功能包裝與參數的調整都較urlread()以及urlwrite()方便許多,能夠更輕易地設定不同的參數。

我們接續著使用webread()來讀取目標網址資料看看~

html_return = webread(target_url);
 

又出錯了,不過它給出的錯誤訊息更為詳細,它很明確地說明認證有問題。此外,這個錯誤其實主要會出現在R2016b版本之後(包含),在此版本之前的驗證方式較為簡單,如只有認證憑證中的CN(CommonName)。

在這個錯誤訊息中,它提到了一條路徑,代表著根憑證(Root Certificate)的位置,根憑證為憑證頒發機構所發布的公鑰憑證。

這個檔案可以直接透過文字檔打開,我們來看看它長甚麼樣子:

也可以透過OpenSSL打開,可以查看更多的訊息,包含簽章機構、加密位元與方法等等,其中我們可以看到Issuer中有一個CN欄位,便是剛才所提到的CommonName:


根據錯誤訊息,可能原因為憑證過期、遺失或者無效,而基本上我們實際打開並無發現有問題的地方,那可能只是簽發單位正好沒認證到而已。那我們就換個根憑證試試。

我們可以透過以下網頁下載其他根憑證:

下載後我們打開來看看有什麼不一樣:




我們回過頭來看,還記得一開始我們有提到webread()、webwrite()以及websave()有一個搭配的函式weboptions(),其將會產生一個weboptions object,可以用來設定網路參數。

一般而言,為了解決上述問題,我們最懶人的方法就是不給它憑證進行認證,這樣它將會無條件通過信任,但這並不怎麼推薦。取而代之的,我們應該嘗試其他可信任機構所發行的憑證。

我們可以如此設定:

options = weboptions('CertificateFilename', 'path/to/root_certificate.pem');
% or
options = weboptions('CertificateFilename', ''); % 不建議

接著再嘗試一次webread():

html_return = webread(target_url, options);

我們取得以下結果:


發現其回傳的資料是一個uint8型態的矩陣,這是怎麼回事?

我們先自行從瀏覽器上下載一個目標檔案下來觀察,透過Notepad++打開,並用其外掛程式HEX-Editor瀏覽:(此程式可以從以下連結下載)



在回傳的uint8矩陣中,我們首先看到80、75、3等等,再觀察一下使用Notepad++開啟的16進制顯示視窗,我們可以發現50、4b、03、etc.正好對應得起來,這代表讀取到的內容組成與zip檔其實無異,程式碼功能是沒問題的。


那如何將uint8矩陣的所有內容寫成一個檔案呢?

我們可以使用fopen()、fwrite()以及fclose(),如下:

fileID = fopen(output_name, 'w');
fwrite(fileID, html_return);
fclose(fileID);

我們可以看到檔案已成功被下載,且經測試能正常開啟,資料並無異常:



再來我們使用webwrite()試試看,其實功能只是從取得資料變為更改資料,從而接收其response而已。

html_return = webwrite(target_url, options);
fileID = fopen(output_name, 'w');
fwrite(fileID, html_return);
fclose(fileID);

由於接受到的資料內容與寫檔步驟並無改變,所以就不多做說明了~


以上兩個方法都需要額外寫檔,有沒有一讀到資料內容就寫檔的功能呢?

答案是有的,就是websave():

output_filename = websave(output_name, target_url, options);

只需要一行便能完成上述的步驟,是不是非常方便~

此外,websave將會回傳儲存完後的檔案名稱,這裡我們使用output_filename去接收。


例外處理

這邊說是例外處理,其實只是想看看如果請求失敗會出現什麼回應而已。

一開始我們提到,除了我們先判斷當前所指的日期是否為正常交易日之外,我們也可以直接請求看看,如果回傳的格式不符或沒回傳資料,我們就可以忽略這筆資料。

我們故意挑一個不存在資料的日期去測試,比如2020.08.02(日),看看會回傳什麼?

使用webread()或webwrite()的回傳結果:


使用websave()的結果:




其實就是一個狀態碼(Status Code)為404的回應網頁而已。

針對這個,我們可以選定某個特定子字串,來代表整個網頁回應。或是如果回傳的檔案不是zip檔,就不考慮等等。

至此,我們完成了一個能夠完整下載目標檔案的爬蟲功能。


總結

本篇我們透過台灣期貨交易所的公開資料,進行詳細的爬蟲介紹與操作,使用的程式語言為MATLAB。此外,我們也提到了網路中的資料如何進行驗證與確保完整,以求網路世界中的安全、穩定秩序。我們也分析了在MATLAB中,所使用的爬蟲相關函式可能的問題,並去解決與解釋它們。


沒有留言:

張貼留言