2012-06-03

用 xsel, sed & crontab 快速擷取網頁特定資料

寫程式的能力差,要做什麼事就不會想到要用寫程式來竟全功。

話說想要把網頁上的大盤資料例行性擷取下來,試試拿來做點什麼。最直接、直觀的做法就是每天到特定網頁去把資料反白複製,存到檔案裡,把不要的字去掉。

這個想法立刻招來高手的諄諄教誨。「自己動手改總有出錯的時候」、「最可靠的還是讓程式去處理」、「完整的資料有利於解決日後任何不測」。為什麼不這樣呢?為什麼不那樣呢?……於是我跟高手說一個故事。
話說張無忌在山洞裡習武有成,來到光明頂正值武林各家精英盡出大打出手之際。他在一旁納悶,明明這人的劍再往前一寸就是致命的一招,明明那人的拳再往左一點就必中對方要害,為什麼每個人都嘎然而止?難道大家在比誰有禮貌嗎?(原著小說《倚天屠龍記》 by 金庸)
「你有聽出來嗎?這是在稱讚你!但是回過頭來請明鑒,我就是不到那個階段,所以想著用這種笨方法!」

總之,高手指點明路,展開程式大法,一步步往盡可能省事的方法、最可能無誤的資料邁進。我想這是個很好的思路進展過程的例子,為了以後可以想得起來,在此把始末筆記一下。

作業系統背景

OS: Debian/Lenny
Kernel: 2.6.57.62
編碼: Big5
Window Manager: fluxbox

做法 1:運用 sed
原始資料:網頁 A。新寫/加寫檔案:tse_filter、資料檔。

本文以 http://www.cnyes.com/twstock/idx_listed/0000T.htm 這個網頁(簡稱「網頁 A」)為例,目的是擷取此頁大盤那一整塊資料裡的數據。

將整個資料區塊反白複製下來存成純文字檔 test.dat ,其內容如下:
資料原始模樣
上市
指數 7106.09 高點 7301.50
漲跌 -195.41 低點 7106.09
昨收 7301.50 總額 794.69億
項目 張數 筆數 均張
委買 7,559,840 1,565,089 4.83
委賣 6,495,225 1,487,531 4.37
成交 2,941,070 698,797 4.21
上櫃
指數 102.72 高點 105.81
漲跌 -3.09 低點 102.58
昨收 105.81 總額 111.51億
項目 張數 筆數 均張
委買 1,285,348 284,434 4.52
委賣 1,382,047 278,883 4.96
成交 433,078 121,685
配合下面這個以 sed 為主、取名為 tse_filter 的 shell-script:
#!/bin/sh
sed \
-e '/項目.*張數.*筆數.*均張/d' \
-e '/上市/d' \
-e '/上櫃/d' \
-e 's/指數[ \t]*/ /' \
-e 's/高點[ \t]*/ /' \
-e 's/漲跌[ \t]*/ /' \
-e 's/低點[ \t]*/ /' \
-e 's/昨收[ \t]*/ /' \
-e 's/總額[ \t]*/ /' \
-e 's/委買[ \t]*/ /' \
-e 's/委賣[ \t]*/ /' \
-e 's/成交[ \t]*/ /' \
-e 's/億//' | tr '\012\015\011' '\040\040\040' | sed \
-e 's/  / /g' \
-e 's/   / /g' \
-e 's/  / /g' \
-e 's/  / /g' \
-e 's/$/\n/g ' \
-e "s/^/`date "+%G%m%d"`/g "
執行
$ tse_filter < test.dat
會得到
20120601 7106.09 7301.50 -195.41 7106.09 7301.50 794.69 7,559,840 1,565,089 4.83 6,495,225 1,487,531 4.37 2,941,070 698,797 4.21 102.72 105.81 -3.09 102.58 105.81 111.51 1,285,348 284,434 4.52 1,382,047 278,883 4.96 433,078 121,685 3.56
如下執行,則會把結果存入檔案:
$ tse_filter < test.dat >> 檔名
但這麼做是無法令人滿足的。雖然如此一來只須在網頁上動一下滑鼠,但還是得動手打一行指令才能完事。

做法 2:運用 xsel、sed 及 Window Manager 的快速鍵
原始資料:網頁 A。新寫/加寫檔案:tse_filter、~/.fluxbox/keys、資料檔。

使用 xsel,配合 Window Manager 的快速鍵,以及上述 script,只要將欲複製的區塊反白後按個快速鍵,就處理好資料,並且存入檔案。(各家 Window Manager 的快速鍵格式或有不同,這裡用的是 fluxbox。)
Mod4 s :ExecCommand bash -c "xsel | tcs -f utf -t big5 | tse_filter >> 資料檔檔名"
上式是加寫在 Window Manager 快速鍵設定檔 ~/.fluxbox/keys 裡的快速鍵設定,意思是,將所要的區塊反白後,按下窗戶加 s 鍵,會執行:
  1. 呼叫 bash。以 xsel 指令複製該區塊到 buffer。
  2. 以 tcs 指令將文字編碼從 utf8 轉為 big5。
  3. 經由上述 script tse_filter 處理後,儲存到檔案裡。
但這麼做是無法令人滿足的。雖然如此一來只須在網頁上反白按一下快速鍵就自動把資料處理完畢寫入檔案,但不知執行結果是否無誤(高手)怎麼睡得著,而想查看執行結果卻還要自己動手,怎麼能算完事。

為了儲存之後可以立刻檢查一下是否正確,修改上式如下:
Mod4 s :ExecCommand bash -c "xsel | tcs -f utf -t big5 | tse_filter >> 資料檔檔名 && crxvt -e bash -c "tail 資料檔檔名 | less"
"&&" 後所增加的部分,意思如下:
  1. 呼叫 big5 的視窗及 bash 備用。
  2. 秀出資料檔最後部分。
  3. 透過 less 來看。
但這麼做是無法令人滿足的。雖然如此一來連查核都不必自己動手叫檔案,已經做到在網頁上反白按下快速鍵就存好資料而且自動跳出視窗顯示執行結果以供查核,但還是得要在一開始先叫出瀏覽器再連上網頁才行。

做法 3:運用 sed 及 crontab
原始資料:網頁 B。新寫/加寫檔案:tse_get、crontab -e、資料檔。

事情進行到這裡,跟一開始打算的做法已經天差地遠,但高手還在一直碎碎念,要是電腦定時做這件事,人都不必動手就好了。那我的內心口白是,我也知道呀。不過這的確是很好的「願景」,似乎手開始不聽惰性使喚。

事情進行到這裡,由於寫程式的感覺有點出現,就稍微認真去找原本不想認為可以找得到的該區塊原始碼到底在哪裡。因為這件事非得用到這個不可。這一整段故事裡,我的貢獻只有這個。

總之因為心神較為收束,原本覺得無論如何不會找到的東西,很快找到了。

來到這個網頁 http://traderoom.cnyes.com/tse/quote2FB.aspx?code=TS(簡稱「網頁 B」),可以得到有這些數據的原始碼。於是得出以下這個新的 shell-script,取名為 tse_get:
#!/bin/sh
SOURCE="http://traderoom.cnyes.com/tse/quote2FB.aspx?code=TSE"
TSE_ORG="/tmp/tse_org.html"
TSE_ORG_BIG5="/tmp/tse_org_big5.html"
TSE_DATA="/tmp/tse_data.html"
wget -U opera $SOURCE -O $TSE_ORG 2> /dev/null
tcs -f utf -t big5 $TSE_ORG > $TSE_ORG_BIG5 2> /dev/null
sed -n -e "s/^.*\(上市.*table>\).*$/\1/p" $TSE_ORG_BIG5 > $TSE_DATA 2> /dev/null
sed < $TSE_DATA \
-e 's/<[^<>]*>/ /g' \
-e 's/項目//g' \
-e 's/張數//g' \
-e 's/筆數//g' \
-e 's/均張//g' \
-e 's/上市//' \
-e 's/上櫃//' \
-e 's/指數[ \t]*/ /g' \
-e 's/高點[ \t]*/ /g' \
-e 's/漲跌[ \t]*/ /g' \
-e 's/低點[ \t]*/ /g' \
-e 's/昨收[ \t]*/ /g' \
-e 's/總額[ \t]*/ /g' \
-e 's/委買[ \t]*/ /g' \
-e 's/委賣[ \t]*/ /g' \
-e 's/成交[ \t]*/ /g' \
-e 's/億//g' | tr '\012\015\011' '\040\040\040' | sed \
-e 's/          / /g' \
-e 's/  / /g' \
-e 's/     / /g' \
-e 's/    / /g' \
-e 's/   / /g' \
-e 's/  / /g' \
-e 's/$/\n/g ' \
-e "s/^/`date "+%G%m%d"`/g "
exit $?
這個 script 的作用是:
  1. 以 wget 把網頁抓下來後,以 tcs 轉碼。
  2. 把所需資料以 sed 抓出來。
  3. 以 sed 留下要留住的數據。
接下來要請 crontab 出場,以便讓電腦自行定時工作。
$ crontab -e
Vim 的編輯視窗出現後,加上以下排程:
58 22 * * 1-5 /路徑/tse_get >> /路徑/資料檔 && aplay /路徑/聲音檔.wav && crxvt -display :0.0 -e bash -c "tail /路徑/資料檔 | less"
這個意思是:
  1. 禮拜一到五的 22 點 58 分,執行指定的工作。
  2. 執行 tse_get 之後,寫入檔案。
  3. 發出聲音告知工作之執行。
  4. 叫出一個視窗,以 less 秀出該檔案的最後部分。
至此,記得打開電腦,是唯一要做的事。

這件事至此告一段落,過一陣子就可以畫圖來玩了。不過美中不足的是,要是禮拜一到禮拜五遇到休市,抓下來的檔案日期可以沿用最後一個正常開市的日期就好了,那麼資料檔處理起來可以少些手續。

主程式改寫 2012-06-22

原來的 tse_get 高手說太低路,改成如下。
#!/bin/sh
SOURCE="http://traderoom.cnyes.com/tse/quote2FB.aspx?code=TSE"
TSE_ORG="/tmp/tse_org.html"
TSE_ORG_BIG5="/tmp/tse_org_big5.html"

wget -U opera "$SOURCE" -O "$TSE_ORG" 2> /dev/null
tcs -f utf -t big5 "$TSE_ORG" > "$TSE_ORG_BIG5" 2> /dev/null

grep '上市.*table' "$TSE_ORG_BIG5" | \
sed \
-e 's/<[^<>]*>/ /g' | \
tr -d  '\072-\377/' | \
sed \
-e 's/          / /g' \
-e 's/  / /g' \
-e 's/     / /g' \
-e 's/    / /g' \
-e 's/   / /g' \
-e 's/  / /g' \
-e "s/^/`date "+%G%m%dT%H%M%S"`/g " #| tee -a /home/phasma/sh/tse.dat

exit $?



番外篇:所以,以下這個程式……

但高手告誡,這樣還是無法高枕無憂。雖然只要電腦是開著的,時間一到,就會自動跳出視窗,告知當日執行結果,但是誰可以保證當睡過頭時,機器處於開機狀態?所以,以下這個程式……

是的,事實上還有停電的問題。要解決這些問題,即使有奴隸也不可靠。於是我打算開始打造機器人。在機器人有眉目之前,我準備先弄一個緊急供電設備(UPS),以及智慧型紅外線定時遙控開機裝置。聽說最好還要符合什麼 Smart Energy 2.0 規格。

巴爾札克 〈不知名的傑作〉插圖。
(圖片來源:Wikipedia
那麼我又想講一個故事:
一名畫家天天畫著一幅肖像,重覆地塗塗抹抹,一層又一層,左潤右飾,前減後添,總是達不到想要的完美境界。日復一日,永遠無法完成。最後就這樣死了。(原著小說 〈不知名的傑作〉(Le Chef-d'œuvre inconnu, 1831)by 巴爾札克)
哦,追求完善是無止境的。要求純粹是辛苦的。

(上述故事是二十幾年前看的,雖然到現在還留有震到的印象,但內容只依稀記得一點點。剛剛到 wiki 看故事大綱,果然情節是較為瘋狂的。)

番外篇:sed

最後的 script "tse_get",因為高手很忙,想說高手已經打好底了,自己來試試看。那麼因為很懶惰,所以變成埋頭苦幹。最大的問題在於 sed 在搜尋時,一尋就尋到該行最後,所以如何以簡單的方法把所有不需要的 "<.......>" 部分去掉,而有用的部分還是能留下來呢?因為不會,所以變成這樣:
sed < $TSE_DATA \
-e 's/<[tds\"\/][vhrde\"]>/ /g' \
-e 's/<[tds\"\/].[vhrde\"]>/ /g' \
-e 's/<[tds\"\/]..[vhrden\"]>/ /g' \
-e 's/<[tds\"\/]...[vhrden\"]>/ /g' \
-e 's/<[tds\"\/]....[vhrde\"]>/ /g' \
-e 's/<[tds\"\/!]...........[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]............[vhrde\" -]>//g' \
-e 's/<[tds\"\/!].............[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]..............[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]...............[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!].................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]..................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]...................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]....................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!].....................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]......................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!].......................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!]........................[vhrde\" -]>//g' \
-e 's/<[tds\"\/!].........................[vhrde\" -]>//g' \
-e 's/<.*>//g' \
那麼,雖然它會動了,動得也很正確,不過自己也知道這樣寫很可笑。果然請高手看一下
,高手立刻哭笑不得
(高手抗議說他沒有哭笑不得而是很虔誠地),他用一行就搞定了:
sed < $TSE_DATA \
-e 's/<[^<>]*>/ /g'
也就是 "^" 這個符號可以用來去除不欲使之包括在內的字元。
  

No comments:

Post a Comment