2018年8月30日 星期四

用 lynx 加 perl 的 csv 句型從網頁裡的表格產生試算表

如果說 三大 regexp 句型 是 「懶得學 perl 程式語言的系統管理員必學的三句 perl」, 那麼今天要介紹的就是第四重要的 perl 句型: perl -F, -nale 'print join(", ", @F[2,5,6])' 姑且就稱它為 perl 的 csv 句型吧。 它跟文字瀏覽器 lynx 合作, 可以把網頁的表格抓下來變成試算表。 當你不想為了一點小事開啟 calc 時, 就用它們來馴服 .csv 檔吧。

我們拿台積電歷年每季財務報表摘要來練習。 先把網頁原始碼抓回自己的硬碟, 才不會一直製造無謂的網路流量: lynx -source 'https://goodinfo.tw/StockInfo/StockBzPerformance.asp?YEAR_PERIOD=9999&RPT_CAT=M_QUAR_ACC&STOCK_ID=2330' > 2330.html 然後可以用 w3m 2330.htmllynx 2330.html 檢視確認。

要把 html 檔變成人眼可讀的文字檔, 可以這樣下: lynx -dump 2330.html 。 注意它會把超連結變成像是參考文獻一樣, 用方括弧數字來表達; 可是我們並不需要超連結啊。 而且, 因為表格太寬, 螢幕放不下, 所以每一列被斷成兩列。 改這樣下好了: lynx -dump -nonumbers -nolist -width 999 2330.html > 2330.txt

如果頁面太長, 只想擷取中一個表格, 而且從原始碼可以看出如何用 css selector 定位你要的表格, 那麼可以先用 extract.php 做前置處理。 以眼前的例子來說, 可以先 extract.php -w -s 'table.solid_1_padding_4_0_tbl' < 2330.html > table.html

也許你會想說那我們就 extract.php 一路用到底好了。 早些時候我也是這麼想, 畢竟都已經那麼辛苦地寫好 extract.php 了, 當然要像擠柳丁汁一樣把它最後一滴的功能都榨出來。 可以用 extract.php -w -s 'td:nth-child(4)' < table.html | lynx -dump -nonumbers -nolist -stdin 把第四欄 (股價) 抓出來, 其他各欄如法泡製, 然後再想辦法 (用 paste 指令) 把它們橫向黏貼在一起。 我真的這樣做過幾次, 但這太囉嗦了, 尤其是 paste 時也會怕怕的, 擔心會不會錯位。

所以呢, 倒帶兩段, 還是回到 2330.txt 。 我們用 perl 的 csv 句型, 簡單多了。 它預設處理的不是 csv 格式, 而是 「以空白分格的欄位」, 正好符合我們的需求。

  1. 先抓季度名稱、 平均股價、 EPS 三欄就好: perl -nale 'print "$F[0] $F[4] $F[20]" if /^\s*20\d\dQ\d/' 2330.txt 注意欄位從第 0 欄開始數起。 F 是固定的一個內建變數名稱。
  2. 同樣的效果, 換個寫法: perl -nale 'print "@F[0,4,20]" if /^\s*20\d\dQ\d/' 2330.txt 只要是常用的功能, 在 perl 裡總是找得到簡寫的方式。
  3. 連續欄位可以用 .. 簡寫: perl -nale 'print "@F[0..4, 20..22]" if /^\s*20\d\dQ\d/' 2330.txt
  4. 如果列印時想改用逗點作為分隔符號, 產生真正的 csv 檔, 就用 join(): perl -nale 'print join(", ", @F[0..4,20..22]) if /^\s*20\d\dQ\d/' 2330.txt > 2330.csv
  5. 如果是從 csv 檔讀資料, 就加上 -F, 選項: perl -F, -nale 'print "@F[0,4,5]"' 2330.csv 注意: -F, 不能寫在 -nale 後面 (主要是因為 -e 後面馬上要接程式碼)
  6. 甚至可以直接運算。 例如想要驗證一下我所理解 (但幾乎查不到文件) 的近似公式: ROE 大約等於 EPS / BVPS : perl -nale 'printf("$F[0] %.2f\n", $F[20]/$F[22]*100/$F[16]) if /^\s*20\d\dQ\d/' 2330.txt 注意: 一旦改用 printf 而不是 print , 那麼就要自己補上換列字元。

Perl 萬歲! 感恩 Larry Wall 大大, 讚嘆 Larry Wall 大大! 光是學會這個句型, 以後凡是遇到 .csv 檔, 大概就有九成的機會可以省下許多寫程式的時間。 (你可以估計看看用 python 寫相同的功能需要多久。 更別提 java 了。) 懶惰是資訊人應有的美德; 最強的程式, 就是用丟即棄、 看起來像是命令而不像程式的程式。 命令列上的 -nale 選項各自是什麼意思, 其實不需要深究; 但如果你受到強大 perl 的震撼與感動, 可以查看 man perlrun 在裡面按 "/^ *-a" (搜尋 -a 選項的說明)。 註: 以上語法細節不需要記憶。 懶惰是資訊人應有的美德。 有印象它能做哪些事就夠了。 有需要時再搜尋 「perl csv 句型」 回來這裡抄指令去改。

應該有讀者快受不了 貴哥的銅臭味 了。 可是同學, 這篇的重點真的是 perl 跟 csv 而不是股票啊~ 好吧, 那我們就跳脫紅塵俗世, 改拿天上的星星來做最後的應用實例好了。 下一篇我們想研究 太陽系裡所有的天然衛星的軌道特性

wget -O sat.html https://en.wikipedia.org/wiki/List_of_natural_satellites
extract.php -w -s table.sortable < sat.html | lynx -dump -stdin -nolist -nonumbers

以上是概念性、 原始的想法。 但是這個檔案裡有些欄位內含空格 (衛星名稱), 有些欄位內含逗點 (軌道半長軸、 發現者姓氏), 會讓我們數錯欄位。 遇到這麼複雜的表格, 還是先找專業的程式幫我們處理第一步好了: sudo npm install html2csv -ghtml2csv 安裝起來。 執行 html2csv sat.html 會產生很多個 csv 檔, 我們要的是其中的 06.csv。 但是, 到了這個地步才要處理資料欄位裡面的逗號跟空格, 就太晚了, 尤其現在又多了雙引號, 問題更麻煩。 所以要:

  1. 先在 html 檔裡面 (1) 把數字之間的逗點刪掉 (2) 把其他逗點 (及後面跟著的空格) 改成中文全型逗點: perl -pe 's/(\d),(\d)/$1$2/g; s/, */,/g;' sat.html > s.htm
  2. rm -f 0?.csv ; html2csv s.htm
  3. 刪掉數字欄位裡的 「± ...」、 「–...」、 「(r)」、 「~」、 空格; 把其他空格通通改成底線; 同時刪掉文字欄位的雙引號: 略過空白列: perl -pe 's/\(r\)//; s/(±|–)[^,]*//g; s/(\d)\s+,/$1,/g; s/ /_/g; s/["~]//g; ' 06.csv | grep -Pv '^\s*$' > s.csv
  4. 現在終於可以用 perl 的 csv 句型抓出有興趣的欄位: perl -F, -nale 'print join(", ", @F[2..5,10])' s.csv > satellites.csv

然後就可以拿 satellites.csv (略經手工編輯過) 來進行 天體運動研究~ 期待啊~ (咦?)

沒有留言:

張貼留言