透過Python開啟外部指令進行爬蟲(以下載台灣期貨交易所的公開資料為例)


為了方便、大量與快速取得資料,爬蟲不可或缺,本次來介紹如何使用python進行爬蟲,並且使用外部指令進行主要功能實現。

前言

一般我們普遍認為開源節流是重要的金錢觀,為了維護我們的資產,錢要進得來,同時花出去的錢也要能夠獲得控制。事實上,開源與節流這兩件事誰更重要,當然是開源,開源意味著資產增加,但節流,再怎麼節省,錢包還是這麼大。但或許是因為人心,我們比起獲利,會更害怕損失,於是節流反而對我們來說卻是重點。

另一方面,存錢也是一個很重要的節流方式,但仔細想想,這些錢如果不去利用,其實就是一種浪費。怎麼說呢?如果這些錢能夠透過各種方式,比如投資,讓錢生財,自然也是一種開源的方式。那當然像是投資這種事會有風險,所以它才會有獲利,不是嗎?

開源節流、開源節流,為何要一起講,我認為最好的解釋就是在開源的同時,我們要想著如何節流;反過來說,在我們想著如何節流的時候,我們也要保持考慮如何開源。根據自己的情況、環境與條件,最大限度的提高獲利,並將損失降低至最小。這在程式人眼中看來,自然是一個最佳化問題。

本篇並不討論如何分析這樣子的最佳化問題,所以並不提供任何策略與方向(投資請衡量自身情況,風險與獲利自行承擔),只著重在如何取得用來分析的資料,畢竟沒有資料,也無法開始。無論是人或是機器,都需要感知外界的環境變化與資訊,經過深思熟慮後,才能做出一個好的應對方式。


台灣期貨交易所

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

Python

python程式語言以其開源為後盾,輔以簡便與易上手的性質,使我們能夠在較短的時間內實現許多應用。無論是在可視化、機器學習、影像處理、嵌入式等等領域與議題上都有它的蹤跡,且非常熱門,有著許多對應的模組與套件能夠使用。

本篇希望透過Python程式語言,輕鬆實作並將程式上會用到的相關概念進行介紹。

註: 本篇運作python的環境主要使用的是3.6版。不同python版本與套件版本之間的相容性不同,本篇並不做詳細探討。

外部指令

事實上,Python的爬蟲套件有很多,在連線請求方面,從最簡便的requesturllib,到能夠模擬人、瀏覽器與網頁的互動行為的selenium。在網頁文件解析方面,也有lxmlBeautiful Soup等等,此外,方才提及的selenium也能夠解析網頁,可謂一手包辦。

但有些時候,我們可能會希望在一套系統或一支完整功能的程式中,額外引用到其他的軟體或package,它可能有支援terminal(in Unix-like environments and systems)或command line window(in Windows)的指令與對應的參數輸入,方便我們執行相關功能。這時,我們可以另外調用該外部指令,整個過程透過同一程式語言去呼叫與執行,不用額外自行單獨執行。

一般而言,一個外部指令可能長得像下圖這樣:

外部指令可以帶有輸入參數或使用預設值,直接透過純指令輸入視窗進行呼叫與執行。

欲實現的想法

由於我們的目標並非要解析網頁文件,我們只要能夠透過下載連結進行目標檔案的下載即可,所以我們並不需要用到可對網頁元素進行解析的套件(或對網頁文件進行字串處理)。另外,我們想要介紹如何使用外部指令來達成網址拜訪與下載的功能,所以事實上,我們不去使用python的爬蟲套件。

我們會使用python內建模組subprocess來實現呼叫外部指令的功能,而外部的軟體我們選用Wget,作為網頁資訊獲取的媒介,能夠達成相同或類似的功能的軟體或專案也有cURL

GNU Wget and What’s GNU

Wget(https://www.gnu.org/software/wget/)是在基於GNU(GNU’s Not Unix)專案的理念下的產物,GNU專案目標便是提供一個絕對的、由自由開源軟體組成的且與Unix相容的作業系統。事實上,多數人在使用的Linux作業系統,最初應該是基於GNU的自由開源理念下的人機互動介面,因此,真正完整的名稱應該是GNU/Linux。即便人們習慣簡稱,可能是為了方便或是什麼,但這個簡稱一旦忽略了其最重要的元素,這種誤差,隨著時間與人們之間的流傳,就會導致原先的理念受到阻礙,比如這個作業系統很有可能會變質。因此,即便Linux比GNU/Linux好念,也要記得當提及這個名詞時,也要想到GNU的基本理念。

相關資訊請至以下連結:

而Wget一詞由World Wide Webget構成,其主要用在從網路上擷取資訊與檔案,支援HTTP、HTTPS、FTP與FTPS等網路協定(Internet protocols, IPs),其中HTTP並不支援HTTP/2。而現在也有衍生工具Wget2被提出(https://gitlab.com/gnuwget/wget2),支援HTTP/2,也使用多執行緒(multithreading)的方法,比起Wget,能夠有更快的執行速度。

若想直接調用Wget指令,Wget在Unix-like系統上有內建,但若要在像是Windows系統上執行,需要自行下載安裝,並設定環境變數。

本次我們使用Wget較新的穩定版1.20.3,Source Code可至以下其中之一連結下載:

exe執行檔可以至以下連結下載:

註: 本篇教學環境主要以Windows系統為主。

subprocess模組

subprocess模組可以幫助我們創建新的行程(process),這個process會是一個子行程(child process or subprocess)。其中有兩個較常用的函式,包括run()以及call(),這兩個函式可以執行指定的命令(command),並等待指令完成。可以想成我們將在command line window視窗中手動打指令執行的動作,改為讓程式自動執行那樣。

在官方文件中,在一般情況下比較推薦使用run()的方法來調用child processes,這個run()的方法在python 3.5版本以後才被加入,在此之前主要是使用call()方法。

官方文件參考如以下連結:

爬蟲目標

在先前的文章中,有介紹到了台灣期貨交易所有提供哪些資料,並決定我們最終欲爬蟲的目標,本篇的爬蟲目標也是一樣,所以我們先來回顧當初我們找到的目標網址長得如何。

期貨每筆成交資料:


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

另外,我們使用相同的方法找出選擇權資料。

選擇權每筆成交資料:


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

其他資料的下載方式都類似,只是檔案名稱不同而已,所以我們就以這兩種資料為例。

根據以上兩種網址,我們可以很簡單發現除了前綴與副檔名是固定的之外,只有一個規則,也就是透過日期來決定不同交易日的資料。

開始爬蟲工作

為了有更好的相容性,我們需要把剛才介紹的Wget工具,把32位元與64位元兩種版本都給下載下來,並放置在根目錄底下。如下圖:

這支程式將會偵測運行的作業系統版本,做一個簡單的判斷,根據環境決定使用哪個版本的工具。

並且我們創建一個python的主要執行檔案,以上圖來說,名稱就叫做crawler.py。

首先,我們先在這個crawler.py主程式添加幾行import:

import os
import subprocess
import platform

import datetime
import time

以上添加的函式庫與模組都是內建的,由於我們會對檔案及資料夾進行存取或確認,我們使用os。為了確認運行的作業系統環境,我們使用platform。而subprocess用來創建child processes,並執行外部指令。由於我們會對日期與相關字串進行處理,使用datetime。最後,之所以使用time,是因為我們希望對程序進行短暫暫停,模擬可能的反爬蟲機制,或者盡量不要造成伺服器的負擔。


我們可以宣告一些變數儲存預設的資訊,讓我們等等程式的編寫更直觀與順利。如何解析網址並儲存,每個人都不一樣,這邊就簡單把每一種目標網址拆解成兩部分,包含前綴以及副檔名(後綴),包在tuple結構中,而兩種網址則使用List結構儲存。當然,也是可以視情況使用其他結構(如dict結構)來儲存。

target_url_list = [
('https://www.taifex.com.tw/file/taifex/Dailydownload/DailydownloadCSV/Daily_', '.zip'),
('https://www.taifex.com.tw/file/taifex/Dailydownload/OptionsDailydownloadCSV/OptionsDaily_', '.zip')
]

另外,決定我們的檔案輸出資料夾與位置:

futures_download_dir = "./futures_download"
option_download_dir = "./option_download"

再來是要設定子資料夾:

futures_subdir_list = ['every_strike']
option_subdir_list = ['every_strike']

雖然這邊期貨與選擇權各都只有要下載每筆成交資料,但其實也還有其他資料能夠下載,為了方便未來做擴充,我們使用List結構進行變數指定。


另外,剛剛有提到要偵測運行的作業系統環境:

def get_os_bits():
os_architecture = platform.architecture()
if "Windows" in os_architecture[1]:
return os_architecture[0].replace("bit", "")
else:
print("OS is not Windows...")
exit(0)

這裡我們定義一個函式來實現這一功能,其中呼叫到了platform模組中的architecture()函式,其會回傳一組tuple,回傳作業系統的位元數以及作業系統是什麼。我們可以試著單獨呼叫一次看看其輸出:

print(platform.architecture())
# ('64bit', 'WindowsPE')

可以看到它回傳是64位元,且是Windows預先安裝環境(Windows PE或WinPE),是一個輕量版版本。一般直接購買附有作業系統的套裝電腦或筆電,就是使用這種版本,我們不必自行完整安裝系統,只需做些許設定即可啟用。

而因為我們主要是使用Windows作業系統環境,我們透過if條件式進行篩選,若是Windows系統,便正常回傳其位元數;反之,就輸出提示字串並終止程式。


接下來我們要實作一個函式,其功能便是透過subprocess模組來使用Wget指令。

所以我們要先來看看會使用到怎樣的Wget的指令,包含參數如何設置。

如果一開始下載Wget執行檔時,是下載壓縮檔,或是有另外下載使用手冊(manual),可能是PDF、HTML檔等等的形式,我們可以打開該文件來查看有哪些參數能夠使用,看看這些參數分別對應到哪些功能。

以下附上官方使用手冊的連結:

打開說明文件如下圖所示:

首先我們會設定下載根目錄,如同說明所示,有兩種語法可以進行根目錄的指定,prefix便是根目錄的名稱:

(1)-P prefix
(2)--directory-prefix=prefix

--------------------------------------------------------------------
Set directory prefix to prefix. The directory prefix is the directory where all other files and subdirectories will be saved to, i.e. the top of the retrieval tree. The default is . (the current directory).

考慮到如果所指定的下載網址並不存在或被移除,台灣期貨交易所可能會做重導向的動作,比方說給定一個錯誤資訊頁,甚至回首頁等等。所以只接受成功下載的情況的話,正常情況下至少不會有重導向的情況發生。因此,會用到指定最大重導向次數的設定。相關說明如下:

--max-redirect=number
--------------------------------------------------------------------
Specifies the maximum number of redirections to follow for a resource. The default is 20, which is usually far more than necessary. However, on those occasions where you want to allow more (or fewer), this is the option to use.

我們可以檢查看看若檔案不存在或者不接受存取,台灣期貨交易所網站會做出什麼反饋,在瀏覽器中輸入以下的網址:


https://www.taifex.com.tw/file/taifex/Dailydownload/DailydownloadCSV/Daily_2020_08_29.zip

結果如下:

可以發現會被重新導向至404 Error網頁。


接下來的參數你們可以自行決定是否使用,-nc用來決定下載重複檔案的行為。如果你希望每次下載時,遇到重複的檔案會建立新版本(除了舊檔會保留之外,新檔以添加編號的方式命名),則不需指定-nc。若不想重新下載,一樣會保留舊檔,可以指定-nc。

(1)-nc
(2)--no-clobber

--------------------------------------------------------------------
If a file is downloaded more than once in the same directory, Wget’s behavior depends on a few options, including -nc. In certain cases, the local file will be clobbered, or overwritten, upon repeated download. In other cases it will be preserved.

When running Wget without -N, -nc, -r, or -p, downloading the same file in the same directory will result in the original copy of file being preserved and the second copy being named file.1. If that file is downloaded yet again, the third copy will be named file.2, and so on. (This is also the behavior with -nd, even if -r or -p are in effect.) When -nc is specified, this behavior is suppressed, and Wget will refuse to download newer copies of file. Therefore, ""no-clobber"" is actually a misnomer in this mode---it’s not clobbering that’s prevented (as the numeric suffixes were already preventing clobbering), but rather the multiple version saving that’s prevented.

When running Wget with -r or -p, but without -N, -nd, or -nc, re-downloading a file will result in the new copy simply overwriting the old. Adding -nc will prevent this behavior, instead causing the original version to be preserved and any newer copies on the server to be ignored.

When running Wget with -N, with or without -r or -p, the decision as to whether or not to download a newer copy of a file depends on the local and remote timestamp and size of the file. -nc may not be specified at the same time as -N.

-nv用來決定下載時是否輸出完整資訊,但保持錯誤訊息以及基本資訊。避免輸出訊息太過冗長。

(1)-nv
(2)--no-verbose

--------------------------------------------------------------------
Turn off verbose without being completely quiet (use -q for that), which means that error messages and basic information still get printed.

先來看看實際使用Wget的情況。

首先我們可以在cmd(Command line window)中輸入以下指令:(記得自行修改下載目標,至少要在30個交易日內的資料才有提供下載)


wget -P "./futures_download" --max-redirect 0 -nc "https://www.taifex.com.tw/file/taifex/Dailydownload/DailydownloadCSV/Daily_2020_08_28.zip"

以上指令指定了下載目錄、不接受重導向以及不重新下載重複檔案。

輸出結果如下:

我們另外添加不輸出完整下載資訊的參數-nv試試:


wget -P "./futures_download" --max-redirect 0 -nc -nv "https://www.taifex.com.tw/file/taifex/Dailydownload/DailydownloadCSV/Daily_2020_08_28.zip"

因為才剛下載過相同檔案,所以這步就結果上基本不會發生什麼事,因為也沒出錯,也不會有什麼輸出訊息。

最後,將參數-nc移除,代表我們會重新下載,並建立版本號:


wget -P "./futures_download" --max-redirect 0 -nv "https://www.taifex.com.tw/file/taifex/Dailydownload/DailydownloadCSV/Daily_2020_08_28.zip"

以上兩個步驟的輸出結果如下:




所以我們只需要將會變動的資訊當作輸入參數,並將以上所有概念與工具結合在一起,就能寫出以下的程式碼:

def run_download_script(date: str, download_dir: str, url_tuple: tuple):
os_bits = get_os_bits()

target_format_str = './wget-1.20.3-win{}/wget -P {} --max-redirect 0 -nc -nv {}{}{}'.format(
os_bits, download_dir, url_tuple[0], date, url_tuple[1])

# Older high-level API
# subprocess.call(target_format_str)

# high-level API
subprocess.run(target_format_str)

以上我們定義了一個函式,叫做run_download_script(),其輸入參數包含日期、下載目錄位置以及網址資訊,型態分別是字串、字串以及tuple。

在函式裡面,會先呼叫到先前定義的get_os_bits()函式,取得位元環境,並檢測是不是在Windows作業系統底下操作。

再來是一個指令字串,我們將它儲存在一個變數中方便使用,透過字串的format()函式對變數進行填入,包括使用到哪個版本的Wget工具、下載目錄在哪、目標網址前綴、目標檔案對應日期字串、目標檔案的副檔名。

這個指令字串便可以直接被用在subprocess模組的call()或者run()函式,去執行下載的動作。

呼叫的程式碼大致如以下:

run_download_script('2020_08_27', futures_download_dir, target_url_list[0])

輸出結果如下:

我們可以發現其除了會幫我們自動創建指定目錄,而這個下載目錄是在當下運行程式碼的根目錄底下,不是Wget工具的目錄底下,這個是我們要注意的地方。


最後,因為台灣期交所公布的每筆成交資料可以供存取30個交易日(可能會因為新增跟移除動作,導致能夠存在31天交易日的資料),所以我們另外寫一個函式來完成一次下載全部資料的功能。


def data_download(download_dir: str, target_item: str, target_url_tuple: tuple):
count = 31

# the first variable of <temp_count_list> represents the number of download item checked in one turn.
# the second variable of <temp_count_list> represents date shift.
temp_count_list = [0, 0]
if datetime.datetime.now().strftime("%H%M%S") < "190000":
temp_count_list[1] = 1

dir_ = os.path.join(download_dir, target_item)

os.makedirs(dir_, exist_ok=True)
now_file_len = len(os.listdir(dir_))

date_format = '%Y_%m_%d'
while temp_count_list[0] < count and temp_count_list[1] < count * 2:
date = datetime.datetime.strftime(
datetime.datetime.now() + datetime.timedelta(days=-temp_count_list[1]), date_format)
temp_count_list[1] += 1

if os.path.exists('{}/{}/{}{}{}'.format(
download_dir, target_item, target_url_tuple[0].split('/')[-1], date, target_url_tuple[1])):
print('{} exist'.format(date))
temp_count_list[0] += 1
continue
else:
print(date)
run_download_script(date, dir_, target_url_tuple)

if len(os.listdir(dir_)) > now_file_len:
now_file_len = len(os.listdir(dir_))
temp_count_list[0] += 1

time.sleep(0.5)

這個函式名稱為data_download(),輸入參數有下載根目錄、下載子目錄(對應到資料種類)以及網址資訊,型態分別為字串、字串以及tuple。

而部分較不需外部變動的資訊,我們將其存為local variable,相關變數與其意義如下:

count -> 代表最大下載的交易日資料次數
temp_count_list -> 包含兩個元素,第一個元素代表已下載的資料數,第二個元素代表從執行當天往回推日期的天數
dir -> 儲存最終的下載目錄
now_file_len -> 用來記錄當前已下載的資料數量
date_format -> 儲存該目標檔案對應的日期格式符號(Directives)
date -> 紀錄每輪執行欲代入的日期字串

一般來說,在當天下午7點前資料就會完全更新完畢,所以我們有使用以下程式碼確認是否下載當天資料,基本上是取得當天的時間字串,比對是否小於"190000"字串,是的話就不下載當天資料,反之亦然:

if datetime.datetime.now().strftime("%H%M%S") < "190000":
temp_count_list[1] = 1

而為了避免下載目錄不存在(以本篇例子基本上不會發生,因為Wget指令可以自動幫我們創建資料夾),使用到了os模組中的makedirs()函式,其中輸入參數exist_ok設置為True,代表若存在則忽略,目錄不存在時再創建。

os.makedirs(dir_, exist_ok=True)

再來是While迴圈的條件,根據已下載數量以及天數的位移量決定。當已經下載超過指定數量或者天數的位移量已超過可能的測試範圍(需考慮假日等非交易日。這邊選擇直接將最大可下載數量乘以2,因為在這個31 * 2天內,非交易日一般不會超過交易日)時便停止:

while temp_count_list[0] < count and temp_count_list[1] < count * 2:
pass

在迴圈內,每次執行都會設定當下的日期字串(其中datetime模組中的timedelta()函式可以產生日期差距物件,也就是timedelta物件),並且讓天數位移量加1。

while temp_count_list[0] < count and temp_count_list[1] < count * 2:
date = datetime.datetime.strftime(
datetime.datetime.now() + datetime.timedelta(days=-temp_count_list[1]), date_format)
temp_count_list[1] += 1

在正式下載檔案前,我們可以先檢查檔案是否以下載過,用到了os.path模組中的exists()函式,並輸出相關提示資訊:

while temp_count_list[0] < count and temp_count_list[1] < count * 2:
...

if os.path.exists('{}/{}/{}{}{}'.format(
download_dir, target_item, target_url_tuple[0].split('/')[-1], date, target_url_tuple[1])):
print('{} exist'.format(date))
temp_count_list[0] += 1
continue
else:
print(date)

確認需要下載後,呼叫先前已定義好的run_download_script()函式:

while temp_count_list[0] < count and temp_count_list[1] < count * 2:
...

run_download_script(date, dir_, target_url_tuple)

下載執行完成,檢查資料是否已正確下載成功,並更新資訊:

while temp_count_list[0] < count and temp_count_list[1] < count * 2:
...

if len(os.listdir(dir_)) > now_file_len:
now_file_len = len(os.listdir(dir_))
temp_count_list[0] += 1

最後,暫停程式0.5秒鐘,為雙方緩和一下交流的步調:

while temp_count_list[0] < count and temp_count_list[1] < count * 2:
...

time.sleep(0.5)

執行時,只需呼叫以下程式碼即可:

# 下載期貨資料
data_download(futures_download_dir, futures_subdir_list[0], target_url_list[0])
# 下載選擇權資料
data_download(option_download_dir, option_subdir_list[0], target_url_list[1])

輸出結果如下:



結論

本篇我們介紹了如何使用python進行爬蟲,爬蟲目標為台灣期交所的公開資料,並且使用到了外部的工具與指令去實現實際上的爬蟲工作,python在這邊只負責處理自動化工作而已。

爬蟲工具甚至套件有各式各樣,並且都能達成某些共同或相似的功能,根據需求選擇適當的作法能夠事半功倍。


沒有留言:

張貼留言