一些說明
本文中描述的例子程序都是專門針對南京大學小百合 http://lilybbs.net/ 網上論壇當前的 web 界面來做的。因為這是我最經常去逛的一個 web 論壇嘍。而且據說這個 web 論壇的程序代碼也是國內高校里面 bbs 論壇上的 web 界面的一個共同的基礎哦。呵呵。不過我們的程序并不牽扯到服務器的后臺啦。由于 web 界面都是經常變動的,而且各個網站的 web 界面也千差萬別,所以本文的目的并不是要提供給讀者一個立即可用的泡網程序,而是通過對這個程序的說明,讓讀者了解 curl 和 scsh 結合起來編寫簡單的 web 腳本的方法。
本文也假設讀者對 scheme 程序語言已經有了一定程度的了解,至少能看懂 scheme 語言編寫的程序片段。還沒有這個自信的讀者可以從本文結尾列出的參考資料中讀到宋國偉發表在 IBM developerWorks 上的關于 scheme 程序語言入門的文章。也許有的讀者朋友對學習一門新的程序語言不是那樣的熱心。我要對這些讀者朋友說的是,本文中的例子最初是用 Plan 9 操作系統上的 rc shell 來開發的。這已經是一個比 UNIX 系統上標準的 Bourne shell 方便了不少的 shell 程序語言了??墒怯捎诔绦虿粩嘣黾拥膹碗s程度,以及變來變去的 web 界面對于程序靈活程度的高要求,再加上作者期望的更近一步的發展,程序不得不從 rc shell 移植到了基于 scheme 語言的 scsh 上面。從另一個方面來說,scheme 語言其實是一門很容易學習的程序語言,而且你學了以后,肯定會感覺到不同的。
正文部分主要是講述兩個例子。第一個例子比較簡單。是一個監視好友上線情況的小程序。第二個例子復雜一些,也更加有趣一些。是一個用 web 論壇上的帖子來作為輸入和輸出、并且加上了一點簡單的安全限制的 scheme 語言解釋器。由于時間和精力的限制,沒有那么多時間泡網哇,所以這里介紹的這個版本只是一個功能十分有限的半成品。不過已經可以完成在 scheme 語言的 r5rs 標準中要求的絕大部分的內容了。
簡單任務之一
南京大學的小百合 bbs 和其它許多高校 bbs 一樣,可以讓用戶設置自己的好友。當用戶的好友上線的時候,系統就會通過自動更新了的 web 頁面通知用戶,用戶就可以和自己的好友在網上交流了??墒俏覀儾荒芤淮蜷_計算機,就總是盯著瀏覽器查看自己的好友上線沒有哇。我們的第一個簡單的任務就是用一個腳本程序自動監視好友上線情況,當好友上線以后,就立即發出通知給用戶。這樣用戶就可以打開 web 瀏覽器登錄 bbs 和好友聊天啦。
判斷用戶是否在線
在 web 論壇上不同的用戶用不同的 id 來標識。一個 id 就是一個簡短的字符串。當這個 id 登錄 web 論壇以后,小百合這個 web 論壇就會在這個 id 的相關信息頁面上顯示一句話,說明這個用戶“目前在站上”。如果這個 id 注銷了這次登錄,這個頁面上相應的一句話就變成了這個用戶“目前不在站上”。我們的第一個任務,就是根據我們的好友列表,把屬于好友 id 的這個 web 頁面給抓下來。把這個頁面抓下來之后,我們就可以對它進行詳細的分析,并采取進一步的動作了。
這一步任務主要是由 curl 來完成。這是一個工作在 UNIX shell 上的工具程序。它可以接收好多不同的命令行參數,根據這些命令行參數內容的不同,就可以完成不同的任務。最常見的命令行參數就是一個 URL 字符串,標識出我們想要抓取的 web 頁面的完整的網絡地址。這樣 curl 就會把這個頁面抓取下來,送到自己的標準輸出端口打印出來。我們可以用 web 瀏覽器手工找到小百合上用來顯示 id 在線信息的 web 頁面地址。這樣就可以用下面這個命令抓取這個頁面。
|
接下來的任務就是要在我們的腳本程序中驅動這個命令,并要在腳本程序中獲取到這個命令輸出的內容,以準備交給程序的其它部分進行進一步的分析和處理。
在普通 UNIX 操作系統上的 Bourne shell 環境中,比如在 GNU/Linux 操作系統上的 BASH 腳本程序當中,驅動一個 shell 工具是一件很直接、也很簡單的事情。這是 shell 的長處?;?scheme 程序語言的 scsh 也自詡為是 UNIX shell 的一種,當然也可以作到方便輕松的驅動 shell 工具程序。這一點其實也正是 scsh 區別于其它許多的 scheme 程序語言的實現版本的一個主要的特征。在用 scsh 編寫的腳本程序當中,用下面的這個 run/string 語法形式就可以完成這一任務。
|
這個 curl 命令在一般運行的時候,會在標準錯誤輸出端口打印一些統計信息。在腳本程序當中,有的時侯并不需要這樣的統計信息。在 scsh 中我們可以用下面的語法形式來關閉 curl 的標準錯誤輸出端口。
|
就像在標準的 UNIX 操作系統中一樣,數字 2 表示標準錯誤輸出端口。上面的減號表示要關閉這個端口。
上面命令中出現的長長的 URL 字符串,我們可以看出來,從腳本程序的角度,可以分為三個部分。第一部分 "http://lilybbs.net" 是小百合站點的 URL 字符串,這在整個程序當中都是不變的。第二部分 "/bbsqry?userid=" 是在腳本程序的這一部分的這個函數里面,每次調用都固定不變的內容。第三部分 "iloveqhq" 是根據每次函數調用所關心的用戶 id 的不同,每次都要發生變化的內容。我們當然希望用不同的變量來分別表示這三個部分字符串。這個要求我們用下面這個語法形式就可以達到。
|
注意到上面的語法形式中出現在 string-append 括號前面的逗號。之所以需要這個逗號,是因為 run/string 并不是一個普通的 scheme 語言的函數,而是一個特殊的語法形式。在 run/string 這個語法形式里面如果想要調用 scheme 語言中的函數和變量的話,就需要在相應的表達式前面加上一個逗號才行。
上面的語法形式把 curl 命令的輸出內容抓取到一個字符串里面,這樣以后就可以在 scheme 程序的其它部分進一步的分析和處理這個字符串的內容了。不過我們并不是對這個字符串里面全部的內容都感興趣的。我們只關心這個字符串里面說明這個 id 所代表的用戶究竟是“目前在站上”還是“目前不在站上”的這個部分。
我們可以用 scheme 語言自帶的字符串處理函數來分析這方面的內容。我們也可以像編寫 shell 腳本程序所通常習慣做的那樣,把這個任務交給 grep 這個 UNIX 系統上標準的 shell 命令來做。在 scsh 里面要這樣做的話,可以用下面這樣的一個語法形式。
|
上面的豎杠符號就像在標準的 UNIX shell 環境中一樣,表示兩個 shell 命令之間的一個管道聯系。上面 grep 命令的參數 -m 1 表示只要一出現后面指定的字符串,就中止命令的繼續運行。參數 -n 表示我們希望 grep 在輸出的結果前面增加打印一個行號。這個行號就說明如果后面指定的字符串在管道中出現的話,它究竟是出現在那一行上面。我們為什么需要行號信息,這在下面的一小節就可以看出來。
防止欺騙
在小百合上用來顯示用戶 id 在線信息的 web 頁面允許用戶自己輸入一個簽名檔。有些用戶喜歡用這些簽名檔來開各種各樣的玩笑。我們前面希望用檢查一個特定的字符串“目前在站上”是否出現在這個頁面當中,來判斷一個用戶 id 是否在線。這樣的話,如果用戶在簽名檔中輸入了這個字符串的話,我們前面的程序就會始終認為這個用戶 id 在線或者不在線。要避免這樣被欺騙,我們判斷一個用戶 id 是否在線的函數就不得不寫成下面這個樣子。
|
發出通知
當腳本程序發現我們的好友 id 上線以后,腳本程序應該能夠給我們發出通知。在 GNOME 桌面環境下,我們可以用 zenity 這個 shell 命令在桌面上顯示一個 GTK+ 的圖形用戶界面的對話框來提醒我們:已經有好友 id 登錄小百合了。我們如果在這個時候也登錄小百合的話,就可以和好友聯系上了。這件事情可以用下面的這個語法形式來做到。
|
上面是用的 run 而不是 run/string 這個語法形式。這是因為我們在這里并不關心 zenity 這個 shell 命令的返回結果。上面的語法形式中出現的逗號的用處,我們在前面已經說過了。
如果我們對一個普通的 GTK+ 對話框還不能夠感到滿意,比如說,我們希望能在好友 id 上線的時候,聽到我們的計算機音箱里面播放出來一段美妙的音樂。我們就可以用下面的這個表達式來做到這一點。
|
這樣就可以根據不同的好友用戶 id 播放不同的 mp3 音樂片段。當然,能夠這樣做的前提是你的 GNU/Linux 系統上裝有 mplayer 這個媒體播放軟件。
關于完成這個簡單任務的完整的程序代碼,可以在本文末尾列出的下載文件中得到。這里就不再贅述了。下面進入我們的簡單任務之二:面向 web 論壇的 scheme 解釋器。
簡單任務之二
南京大學小百合 http://lilybbs.net/ 上的 CompLang 版是一個專門討論程序語言的理論與實踐的版面。對于各種程序語言的學習與實踐對于這個版面上的討論來說,當然是十分的重要的啦。在討論版上發表的帖子里面附上可以執行的程序代碼片段以及執行的結果,這對于這個版面來說,就是一個非常有用的功能了。我們的第二個簡單任務就是在這個方向上開一個小頭,開發一個以版面上的文章為輸入和輸出的 scheme 程序語言的解釋器。
這個 scheme 語言的解釋器在小百合的 CompLang 版面上讀取特定標題的帖子,把帖子中的 scheme 程序代碼片段提取出來,交給一個在本地后臺運行的真正的 scheme 解釋器來運行。然后再把運行得到的結果作為一個新的帖子,發表在小百合上的 CompLang 版面上。
讀取輸入帖子
第一步要完成的任務,就是把 CompLang 版面上的帖子標題都讀出來。首先打開一個 web 瀏覽器,訪問到這個顯示 CompLang 版面帖子標題的這個 web 頁面。人工看一下這個頁面的 HTML 代碼的細節到底是怎么樣的。很快,我們就注意到,用下面這個 scsh 語法形式就可以提取到每個帖子標題的相關 HTML 代碼片段。
|
注意到上面的 run/strings 是復數,而不是 run/string 的單數。這兩個語法形式的不同在于,前者把 shell 命令的輸出數據中的每一行都作為一個單獨的 scheme 語言中的字符串數據返回給程序的其余部分,而后者則把所有的輸出數據,不分行就當作一個整個的 scheme 語言中的字符串數據返回給程序的其余部分。我們在這里因為要把每一行所代表的不同的帖子標題的 HTML 代碼區別開來,所以用的是復數的形式。
正則表達式
這樣我們就得到了每個帖子標題的 HTML 代碼。接下來的任務就是用正則表達式解析這一行 HTML 代碼,把里面的相關的內容都提取出來。在 scsh 當中,用 rx 開頭的語法形式就表示一個正則表達式。下面我們就來看一看我們要用到的正則表達式的例子。
|
上面的表達式表示正好有一個或者是 0 到 9 的阿拉伯數字或者是小寫的或者是大寫的一個英文字母。開頭的斜杠符號就表示一個“區段選擇”的意思。需要指出的是,只有在rx 涵蓋的語法形式里面,這些特殊含義才發生效果。在 scsh 腳本程序的其它部分,這些特殊字符是沒有這里所說的特殊效果的。
|
上面的這個正則表達式表示 0 到 9 的阿拉伯數字和不區分大小寫的英文字母正好出現 2 到 12 遍。由不少于兩個并且不多于十二個的阿拉伯數字和英文字母組成的字符串正好就是小百合對用戶 id 的要求。
|
在 scheme 語言中 #\_ 表示下劃線這個字母符號。上面的這個正則表達式就表示正好有一個數字、英文字母、或者下劃線符號。在這個正則表達式開頭的豎杠符號,就表示一個“或者”的意思。在這里我們再次看到,這個豎杠只有在 rx 的語法形式里面,才表示“或者”這個意思。在 run/string 等語法形式里面,豎杠表示的是 shell 管道的意思。這兩個意思是萬全不相干的。
|
上面這個正則表達式可以近似說明版面的英文名稱。表示出現了一個由兩個到十八個下劃線、阿拉伯數字或者英文字母等字符組成的字符串。
|
上面的波浪號表示否定。這個正則表達式表示的意思就是正好有一個不是小于號的任意一個字符。
|
在 rx 語法形式中的加號表示后面的正則表達式會匹配一次或者多次。單個星號表示其后的正則表達式會匹配零次或者多次。兩個星號連在一起,后面再跟兩個正整數,這樣的形式我們已經在前面看到過了,這就表示其后的正則表達式會匹配不少于第一個整數次,同時又不多于第二個整數次。上面的正則表達式的意思就是一個或者多個不是小于號的字符組成的字符串。這個正則表達式在分析 HTML 代碼的時候是很有用、也很方便的。
|
上面的這個正則表達式稍微長了一點。它分為六個部分。最一開頭的冒號,表示這個正則表達式是由這六個部分按順序組合起來的,其中的每一個部分都要正好匹配一次。第一部分的字符串 "bbscon?board=" 就匹配它自己。第二部分開頭的一個逗號表示 scsh 會把這一部分作為一個變量或者一小段 scheme 函數來解釋運行,運行得到的結果,必須是一個 rx 開頭的語法形式。其它的部分就沒有什么新的內容了。這個例子就可以讓我們看出來一點 scsh 里面的這種 scheme 語法風格的正則表達式,比起傳統的基于字符串的 POSIX 的正則表達式來說,可以有一個更加清晰的邏輯結構。這一點我們從下面的例子里面可以看的更加清楚。
|
上面這最后一個正則表達式如果用基于字符串的、傳統的 POSIX 的方式寫出來,恐怕誰都會受不了的吧。
匹配
有了正則表達式,我們就可以用它匹配指定的字符串。這主要是通過 regexp-search 這個函數來完成的。
(regexp-search 正則表達式 字符串)
如果不發生匹配,就會返回表示“假”的 #f 這個布爾值。如果發生匹配了,則會返回一個 match 類型的數據。這個類型的數據里面包括了關于具體匹配的子字符串的具體內容。這些內容可以用 match:substring 等一些函數提取出來。
(match:substring match-data index)
零號索引表示整個的正則表達式匹配到的子字符串。其它的索引則表示正則表達式中出現的 submatch 的部分。我們還是用上面最后的那個 re 正則表達式來說明。這一次我們給它加上 submatch 的信息。
|
在 match:substring 等一系列函數中,索引零表示整個正則表達式匹配到的內容,索引從一往后就表示在上面從左到右一個接一個依次出現的 submatch 所涵蓋的正則表達式上發生的匹配。
|
上面這個函數運行起來,返回的就是由索引 index 所指明的那個 submatch 所匹配到的子字符串。關于 match-data 我們前面已經講到過,是由 regexp-search 所找到的數據。
下面我們看到的就是由 HTML 代碼,經由正則表達式的匹配,找到帖子的標題、發帖者、發帖時間、以及帖子詳細網址的完整的 scsh 函數的程序代碼。
|
面向對象
上面的這個函數如果找到了我們關心的數據,返回的就是下面這樣的一個 lambda 函數。
|
這個 lambda 函數可以接受一個調用參數,這個調用參數的效果,就相當于給這個 lambda 函數發了一個短消息。根據這個短消息的不同,這個 lambda 函數返回不同的結果。這就有點像是面向對象編程里面一個對象的效果。上面的這個技巧也就是在函數式編程語言里面模擬面向對象編程的一個簡單的方法。當然,要真正的做到在函數式編程里面模擬面向對象編程,還是要做多得多的工作的。
用帖子作為輸入和輸出
在這一部分,我們只是做一個簡單的設計??紤]到減輕整個系統的運行負擔,這包括小百合的服務器端以及我們本地的運行程序,我們只搜索處理論壇上最新發表的標題以“○ iloveqhq: ”為開頭的帖子。我們的回復帖子也規定以“○ iloveqhq Re: ”為標題。相關的程序代碼片段列在下面。這個設計當然不是很好。但是更好的設計只有在有相當數量的用戶加入進來測試,并提供足夠多的反饋信息以后才有可能達到。所以目前暫時就先這樣吧。^_^
|
帖子中的內容有普通文本,也有 scheme 程序代碼,我們在這里也只是做一個頭腦簡單的設計,假設帖子中只能出現一段 scheme 程序代碼。這段代碼的開頭第一行必須是“iloveqhq: elk”內容不多也不少。結尾的一行必須是“iloveqhq: kle” 內容也必須是恰恰好。這樣的設計當然也不是很好。在以后的版本中應該會有更好的設計出現的。下面列出的就是提取帖子中 scheme 程序代碼的主要函數。
|
用 elk scheme 做沙盤
從帖子中得到 scheme 程序代碼以后,我們就可以把這段代碼喂給一個 scheme 程序解釋器,讓它運行這段代碼,并且把返回信息傳遞給我們。然后我們就可以用這段返回信息作出一個回復的帖子,張貼到小百合的版面上去。
這里面需要考慮一個安全問題。因為從理論上說,小百合上的任意一個用戶都可以在帖子中嵌入任意的 scheme 代碼片段。我們用 curl 把網上這個我們并不了解詳細內容的代碼片段抓回到本地機器上,交給運行在本地機器的后臺的一個 scheme 解釋器去執行,肯定要考慮到安全的問題。
我們解決這個安全問題的一個簡單辦法,就是做一個 scheme 語言的沙盤環境。我們用 elk scheme 來設置這個環境。
|
在這里做的事情其實就是把 elk scheme 當中涉及到輸入和輸出的大部分函數都給屏蔽掉。這樣一來,網上下載下來的不安全的代碼就不會對本地系統造成任何過分的破壞了。除了輸入和輸出以外,我們也要把 elk scheme 中的模塊加載的部分也給注銷掉。這個理由也是顯然的。
這個安全屏障當然是很簡單的。只能防止一些最惡劣的破壞。在一些更加細致的方面,并沒有做到周密的考慮。因為我們在這里只是說明一個例子而已,所以就沒有必要在這個雖然困難,但卻是枝節的問題上耗費腦筋了。
登錄和注銷
從 scheme 程序語言的沙盤環境得到程序的輸出以后,我們就可以考慮往小百合的 CompLang 論壇上發帖子,把程序輸出的效果張貼出來。不過發帖子和我們前面遇到過的任務都不相同,需要我們登錄小百合。前面的所有任務都是可以用匿名用戶的身份來完成的,不過發帖子就不行了,小百合的大部分版面都是不允許匿名發帖的。發帖之前,我們首先要登錄小百合系統。小百合的登錄和注銷是用 cookie 來處理的。我們就需要用 curl 來處理這些和 cookie 有關的問題了。
首先是通過一個 web 表格把我們需要用到的登錄用戶 id 和密碼發給小百合的 web 服務器。這一步用下面的 curl 命令就可以做到。
|
curl 命令的 -d 選項,后面跟著 key=value 這樣的字符串,就可以用來向 web 地址發送 web 表格信息。表格被發送給 web 服務器以后,服務器會返回一個頁面,這個頁面里面就包括 cookie 有關的信息。
小甜餅
小百合的 cookie 設置比較奇怪,不是通過 HTTP 協議的信息頭來傳送的,而是通過 JavaScript 來傳送。這樣一來,我們就無法利用 curl 標準的處理 cookie 的辦法了。我們需要自己用 scsh 首先對返回的頁面 HTML 加上 JavaScript 做一些分析處理。這個分析處理還是用前面提到過的正則表達式的方法,把 cookie 信息提取出來。相關的具體的代碼實現列在下面。
|
curl 命令的 -b 選項加上一個字符串參數,就可以向網站發送 cookie 小甜餅。我們從下面的例子可以看出來。另外,我們了解到所謂 cookie 其實就是一個個的鍵和鍵值組成的字符串。
|
發送 cookie 給小百合以注銷先前登錄的用戶 id 的函數片段在下面列出來。
|
發帖子
如何登錄和如何注銷都談過了以后,下面我們就可以在小百合上發帖子了。要注意的一件事情是在把帖子的內容交給 curl 發送到網站上去之前,先要把帖子中的一些特殊的字符按照 HTTP 協議的要求進行編碼轉換。這件事情 curl 是不會代替我們完成的。我們必須自己用 scsh 函數來完成。下面的程序代碼片段就是完成這個工作。
|
有了前面的那么多準備工作,最后發送文章就是一件輕而易舉的事情了。
|
結語
上面用兩個例子說明了用 curl 和 scsh 編寫 web 腳本程序的技術。如果網站提供有標準的 web 服務接口的話,當然會讓我們的任務減輕許多??墒悄壳按蟛糠值木W站,尤其是我們最感興趣的論壇網站都沒有提供適于編程的 web 服務接口,所以如果我們想要對 web 論壇上的一些任務進行自動化處理的話,用 curl 和 scsh 編寫 web 腳本程序的辦法就是非常有吸引力的了。
在本文中的第二個例子里面涉及到的 scheme 語言的沙盤環境,這也是非常有意思的一個話題。如果有機會的話,在這個方面,作者還有一些更加有趣的內容可以說一說。
致謝
南京大學小百合 < http://bbs.nju.edu.cn/> 上的 vt
非常感謝你的 upbbs1 和 upbbs2 兩個腳本程序!見識了你的例子,我才知道有 curl 這么個強大的工具!而且,我又可以在 LinuxUnix 版上貼圖啦!謝謝!
南京大學小百合 < http://bbs.nju.edu.cn/> 上的 xiaoxinpan
感謝你的支持!謝謝!
Fcitx 小企鵝中文輸入法 < http://www.fcitx.org/>
這篇文章是在 GNU/Linux 操作系統上用 Emacs 編輯器加上 Fcitx 小企鵝中文輸入法編輯輸入的。感謝 Fcitx 的開發者們!終于可以在我最喜愛的操作系統上舒舒服服的編輯中文文章啦!謝謝!
文件下載
iloveqhq-991213.tar.bz2; 這個打包文件里面是一些 scheme 語言的小程序例子。里面包括有本文中詳細說明的這個腳本程序的完整程序代碼。
參考資料
關于作者 趙蔚是一名生活在南京的自由程序員。他的網絡日記 < http://advogato.org/person/zhaoway> 記載了各種雜七雜八的胡思亂想。在程序以外,趙蔚是一名不負責任的白日做夢者和業余水平的數學愛好者。 |