大家快來嚐嚐鮮 轉<用 curl 和 scsh 編寫 web 指令碼>(轉)

post0發表於2007-08-11
大家快來嚐嚐鮮 轉(轉)[@more@]

用 curl 和 scsh 編寫 web 指令碼

趙蔚 (zhaoway@public1.ptt.js.cn)

自由程式設計師

2004 年 1 月

簡介

讓我們看看程式設計師是如何泡網的。在本文中我們將要介紹如何使用 curl 這個簡單靈巧的 web 工具加上 scsh 這個基於 scheme 語言的威力強大的 UNIX shell 來編寫各種古怪的 web 指令碼來幫助我們泡網。

一些說明

本文中描述的例子程式都是專門針對南京大學小百合 網上論壇當前的 web 介面來做的。因為這是我最經常去逛的一個 web 論壇嘍。而且據說這個 web 論壇的程式程式碼也是國內高校裡面 bbs 論壇上的 web 介面的一個共同的基礎哦。呵呵。不過我們的程式並不牽扯到伺服器的後臺啦。由於 web 介面都是經常變動的,而且各個網站的 web 介面也千差萬別,所以本文的目的並不是要提供給讀者一個立即可用的泡網程式,而是透過對這個程式的說明,讓讀者瞭解 curl 和 scsh 結合起來編寫簡單的 web 指令碼的方法。

本文也假設讀者對 scheme 程式語言已經有了一定程度的瞭解,至少能看懂 scheme 語言編寫的程式片段。還沒有這個自信的讀者可以從本文結尾列出的參考資料中讀到宋國偉發表在 IBM developerWorks 上的關於 scheme 程式語言入門的文章。也許有的讀者朋友對學習一門新的程式語言不是那樣的熱心。我要對這些讀者朋友說的是,本文中的例子最初是用 Plan 9 作業系統上的 rc shell 來開發的。這已經是一個比 UNIX 系統上標準的 Bourne shell 方便了不少的 shell 程式語言了。可是由於程式不斷增加的複雜程度,以及變來變去的 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 頁面地址。這樣就可以用下面這個命令抓取這個頁面。

bash-2.05b$ curl "/bbsqry?userid=iloveqhq"

接下來的任務就是要在我們的指令碼程式中驅動這個命令,並要在指令碼程式中獲取到這個命令輸出的內容,以準備交給程式的其它部分進行進一步的分析和處理。

在普通 UNIX 作業系統上的 Bourne shell 環境中,比如在 GNU/Linux 作業系統上的 BASH 指令碼程式當中,驅動一個 shell 工具是一件很直接、也很簡單的事情。這是 shell 的長處。基於 scheme 程式語言的 scsh 也自詡為是 UNIX shell 的一種,當然也可以作到方便輕鬆的驅動 shell 工具程式。這一點其實也正是 scsh 區別於其它許多的 scheme 程式語言的實現版本的一個主要的特徵。在用 scsh 編寫的指令碼程式當中,用下面的這個 run/string 語法形式就可以完成這一任務。

(run/string (curl "/bbsqry?userid=iloveqhq"))

這個 curl 命令在一般執行的時候,會在標準錯誤輸出埠列印一些統計資訊。在指令碼程式當中,有的時侯並不需要這樣的統計資訊。在 scsh 中我們可以用下面的語法形式來關閉 curl 的標準錯誤輸出埠。

(run/string (curl "/bbsqry?userid=iloveqhq")

(- 2))

就像在標準的 UNIX 作業系統中一樣,數字 2 表示標準錯誤輸出埠。上面的減號表示要關閉這個埠。

上面命令中出現的長長的 URL 字串,我們可以看出來,從指令碼程式的角度,可以分為三個部分。第一部分 "http: //lilybbs.net" 是小百合站點的 URL 字串,這在整個程式當中都是不變的。第二部分 "/bbsqry?userid=" 是在指令碼程式的這一部分的這個函式里面,每次呼叫都固定不變的內容。第三部分 "iloveqhq" 是根據每次函式呼叫所關心的使用者 id 的不同,每次都要發生變化的內容。我們當然希望用不同的變數來分別表示這三個部分字串。這個要求我們用下面這個語法形式就可以達到。

(define lilybbs "")

(run/string (curl ,(string-append lilybbs

"/bbsqry?userid="

userid))

(- 2))

注意到上面的語法形式中出現在 string-append 括號前面的逗號。之所以需要這個逗號,是因為 run/string 並不是一個普通的 scheme 語言的函式,而是一個特殊的語法形式。在 run/string 這個語法形式裡面如果想要呼叫 scheme 語言中的函式和變數的話,就需要在相應的表示式前面加上一個逗號才行。

上面的語法形式把 curl 命令的輸出內容抓取到一個字串裡面,這樣以後就可以在 scheme 程式的其它部分進一步的分析和處理這個字串的內容了。不過我們並不是對這個字串裡面全部的內容都感興趣的。我們只關心這個字串裡面說明這個 id 所代表的使用者究竟是“目前在站上”還是“目前不在站上”的這個部分。

我們可以用 scheme 語言自帶的字串處理函式來分析這方面的內容。我們也可以像編寫 shell 指令碼程式所通常習慣做的那樣,把這個任務交給 grep 這個 UNIX 系統上標準的 shell 命令來做。在 scsh 裡面要這樣做的話,可以用下面這樣的一個語法形式。

(run/string (| (curl ,url)

(grep -m 1 -n "目前在站上")))

上面的豎槓符號就像在標準的 UNIX shell 環境中一樣,表示兩個 shell 命令之間的一個管道聯絡。上面 grep 命令的引數 -m 1 表示只要一出現後面指定的字串,就中止命令的繼續執行。引數 -n 表示我們希望 grep 在輸出的結果前面增加列印一個行號。這個行號就說明如果後面指定的字串在管道中出現的話,它究竟是出現在那一行上面。我們為什麼需要行號資訊,這在下面的一小節就可以看出來。

防止欺騙

在小百合上用來顯示使用者 id 線上資訊的 web 頁面允許使用者自己輸入一個簽名檔。有些使用者喜歡用這些簽名檔來開各種各樣的玩笑。我們前面希望用檢查一個特定的字串“目前在站上”是否出現在這個頁面當中,來判斷一個使用者 id 是否線上。這樣的話,如果使用者在簽名檔中輸入了這個字串的話,我們前面的程式就會始終認為這個使用者 id 線上或者不線上。要避免這樣被欺騙,我們判斷一個使用者 id 是否線上的函式就不得不寫成下面這個樣子。

(define (user-online? userid)

(let* ((url (string-append lilybbs "/bbsqry?userid=" userid))

(html (run/string (curl ,url) (- 2)))

(online (run/string (| (echo ,html)

(grep -m 1 -n "目前在站上")))))

(and (< 0 (string-length online))

(let ((offline (run/string (| (echo ,html)

(grep -m 1 -n "目前不在站上")))))

(or (= 0 (string-length offline))

(let ((online (grep-line-number online))

(offline (grep-line-number offline)))

(< online offline)))))))

發出通知

當指令碼程式發現我們的好友 id 上線以後,指令碼程式應該能夠給我們發出通知。在 GNOME 桌面環境下,我們可以用 zenity 這個 shell 命令在桌面上顯示一個 GTK+ 的圖形使用者介面的對話方塊來提醒我們:已經有好友 id 登入小百合了。我們如果在這個時候也登入小百合的話,就可以和好友聯絡上了。這件事情可以用下面的這個語法形式來做到。

(run (zenity --info --title ,lilybbs --text ,info-text))

上面是用的 run 而不是 run/string 這個語法形式。這是因為我們在這裡並不關心 zenity 這個 shell 命令的返回結果。上面的語法形式中出現的逗號的用處,我們在前面已經說過了。

如果我們對一個普通的 GTK+ 對話方塊還不能夠感到滿意,比如說,我們希望能在好友 id 上線的時候,聽到我們的計算機音響裡面播放出來一段美妙的音樂。我們就可以用下面的這個表示式來做到這一點。

(run (mplayer ,(string-append "some-short-music-for-" ,userid ".mp3")))

這樣就可以根據不同的好友使用者 id 播放不同的 mp3 音樂片段。當然,能夠這樣做的前提是你的 GNU/Linux 系統上裝有 mplayer 這個媒體播放軟體。

關於完成這個簡單任務的完整的程式程式碼,可以在本文末尾列出的下載檔案中得到。這裡就不再贅述了。下面進入我們的簡單任務之二:面向 web 論壇的 scheme 直譯器。

簡單任務之二

南京大學小百合 上的 CompLang 版是一個專門討論程式語言的理論與實踐的版面。對於各種程式語言的學習與實踐對於這個版面上的討論來說,當然是十分的重要的啦。在討論版上發表的帖子裡面附上可以執行的程式程式碼片段以及執行的結果,這對於這個版面來說,就是一個非常有用的功能了。我們的第二個簡單任務就是在這個方向上開一個小頭,開發一個以版面上的文章為輸入和輸出的 scheme 程式語言的直譯器。

這個 scheme 語言的直譯器在小百合的 CompLang 版面上讀取特定標題的帖子,把帖子中的 scheme 程式程式碼片段提取出來,交給一個在本地後臺執行的真正的 scheme 直譯器來執行。然後再把執行得到的結果作為一個新的帖子,發表在小百合上的 CompLang 版面上。

讀取輸入帖子

第一步要完成的任務,就是把 CompLang 版面上的帖子標題都讀出來。首先開啟一個 web 瀏覽器,訪問到這個顯示 CompLang 版面帖子標題的這個 web 頁面。人工看一下這個頁面的 HTML 程式碼的細節到底是怎麼樣的。很快,我們就注意到,用下面這個 scsh 語法形式就可以提取到每個帖子標題的相關 HTML 程式碼片段。

(run/strings (| (curl ,(string-append lilybbs "/bbsdoc?board=CompLang"))

(grep "bbscon?board=")))

注意到上面的 run/strings 是複數,而不是 run/string 的單數。這兩個語法形式的不同在於,前者把 shell 命令的輸出資料中的每一行都作為一個單獨的 scheme 語言中的字串資料返回給程式的其餘部分,而後者則把所有的輸出資料,不分行就當作一個整個的 scheme 語言中的字串資料返回給程式的其餘部分。我們在這裡因為要把每一行所代表的不同的帖子標題的 HTML 程式碼區別開來,所以用的是複數的形式。

正規表示式

這樣我們就得到了每個帖子標題的 HTML 程式碼。接下來的任務就是用正規表示式解析這一行 HTML 程式碼,把裡面的相關的內容都提取出來。在 scsh 當中,用 rx 開頭的語法形式就表示一個正規表示式。下面我們就來看一看我們要用到的正規表示式的例子。

(rx (/ "09azAZ"))

上面的表示式表示正好有一個或者是 0 到 9 的阿拉伯數字或者是小寫的或者是大寫的一個英文字母。開頭的斜槓符號就表示一個“區段選擇”的意思。需要指出的是,只有在rx 涵蓋的語法形式裡面,這些特殊含義才發生效果。在 scsh 指令碼程式的其它部分,這些特殊字元是沒有這裡所說的特殊效果的。

(rx (** 2 12 (/ "09azAZ")))

上面的這個正規表示式表示 0 到 9 的阿拉伯數字和不區分大小寫的英文字母正好出現 2 到 12 遍。由不少於兩個並且不多於十二個的阿拉伯數字和英文字母組成的字串正好就是小百合對使用者 id 的要求。

(rx (| #\_ (/ "azAZ09")))

在 scheme 語言中 #\_ 表示下劃線這個字母符號。上面的這個正規表示式就表示正好有一個數字、英文字母、或者下劃線符號。在這個正規表示式開頭的豎槓符號,就表示一個“或者”的意思。在這裡我們再次看到,這個豎槓只有在 rx 的語法形式裡面,才表示“或者”這個意思。在 run/string 等語法形式裡面,豎槓表示的是 shell 管道的意思。這兩個意思是萬全不相干的。

(rx (** 2 18 (| #\_ (/ "azAZ09"))))

上面這個正規表示式可以近似說明版面的英文名稱。表示出現了一個由兩個到十八個下劃線、阿拉伯數字或者英文字母等字元組成的字串。

(rx (~ #

上面的波浪號表示否定。這個正規表示式表示的意思就是正好有一個不是小於號的任意一個字元。

(rx (+ (~ #

在 rx 語法形式中的加號表示後面的正規表示式會匹配一次或者多次。單個星號表示其後的正規表示式會匹配零次或者多次。兩個星號連在一起,後面再跟兩個正整數,這樣的形式我們已經在前面看到過了,這就表示其後的正規表示式會匹配不少於第一個整數次,同時又不多於第二個整數次。上面的正規表示式的意思就是一個或者多個不是小於號的字元組成的字串。這個正規表示式在分析 HTML 程式碼的時候是很有用、也很方便的。

(rx (: "bbscon?board="

,board

"&file="

(+ (~ #&))

"&num="

,num))

上面的這個正規表示式稍微長了一點。它分為六個部分。最一開頭的冒號,表示這個正規表示式是由這六個部分按順序組合起來的,其中的每一個部分都要正好匹配一次。第一部分的字串 "bbscon?board=" 就匹配它自己。第二部分開頭的一個逗號表示 scsh 會把這一部分作為一個變數或者一小段 scheme 函式來解釋執行,執行得到的結果,必須是一個 rx 開頭的語法形式。其它的部分就沒有什麼新的內容了。這個例子就可以讓我們看出來一點 scsh 裡面的這種 scheme 語法風格的正規表示式,比起傳統的基於字串的 POSIX 的正規表示式來說,可以有一個更加清晰的邏輯結構。這一點我們從下面的例子裡面可以看的更加清楚。

(define regexp-userid (rx (** 2 12 (/ "09azAZ"))))

(define regexp-board (rx (** 2 18 (| #\_ (/ "azAZ09")))))

(define regexp-time (rx (+ (~ #

(define regexp-size (rx (+ (~ #

(define regexp-num (rx (+ (/ "09"))))

(define regexp-url (rx (: "bbscon?board="

,board

"&file="

(+ (~ #&))

"&num="

,num)))

(define regexp-sub (rx (+ (~ #

(define re (rx (: "" ,num

"" (+ whitespace)

",userid ">"

,userid "
"

,time ""

,sub "
(" ,size ")"

"">" ,num

"
")))

上面這最後一個正規表示式如果用基於字串的、傳統的 POSIX 的方式寫出來,恐怕誰都會受不了的吧。

匹配

有了正規表示式,我們就可以用它匹配指定的字串。這主要是透過 regexp-search 這個函式來完成的。

(regexp-search 正規表示式 字串)

如果不發生匹配,就會返回表示“假”的 #f 這個布林值。如果發生匹配了,則會返回一個 match 型別的資料。這個型別的資料裡面包括了關於具體匹配的子字串的具體內容。這些內容可以用 match:substring 等一些函式提取出來。

(match:substring match-data index)

零號索引表示整個的正規表示式匹配到的子字串。其它的索引則表示正規表示式中出現的 submatch 的部分。我們還是用上面最後的那個 re 正規表示式來說明。這一次我們給它加上 submatch 的資訊。

(define re (rx (: "" (submatch ,num)

"" (+ whitespace)

",userid ">"

(submatch ,userid) "
"

(submatch ,time) ""

(submatch ,sub)

"
(" ,size ")"

""

,num "
")))

在 match:substring 等一系列函式中,索引零表示整個正規表示式匹配到的內容,索引從一往後就表示在上面從左到右一個接一個依次出現的 submatch 所涵蓋的正規表示式上發生的匹配。

(match:substring match-data index)

上面這個函式執行起來,返回的就是由索引 index 所指明的那個 submatch 所匹配到的子字串。關於 match-data 我們前面已經講到過,是由 regexp-search 所找到的資料。

下面我們看到的就是由 HTML 程式碼,經由正規表示式的匹配,找到帖子的標題、發帖者、發帖時間、以及帖子詳細網址的完整的 scsh 函式的程式程式碼。

(define (html->posts htm)

(let* ((userid (rx (** 2 12 (/ "09azAZ"))))

(board (rx (** 2 18 (| #\_ (/ "azAZ09")))))

(time (rx (+ (~ #(size (rx (+ (~ #(num (rx (+ (/ "09"))))

(url (rx (: "bbscon?board=" ,board "&file=" (+ (~ #&)) "&num=" ,num)))

(sub (rx (+ (~ #(re (rx (: "" (submatch ,num) "" (+ whitespace)

""

(submatch ,userid) "
"

(submatch ,time) ""

(submatch ,sub) "
(" ,size ")"

"" ,num ""))))

(map (lambda (str)

(let* ((mat (regexp-search re str))

(sub (lambda (idx)

(match:substring mat idx))))

(if (not mat) #f (lambda (sym) (case sym

((num) (sub 1))

((userid) (sub 2))

((time) (sub 3))

((url) (sub 4))

((subject) (sub 5)))))))

(run/strings (| (echo ,htm) (grep "bbscon?board="))))))

物件導向

上面的這個函式如果找到了我們關心的資料,返回的就是下面這樣的一個 lambda 函式。

(lambda (sym) (case sym

((num) (sub 1))

((userid) (sub 2))

((time) (sub 3))

((url) (sub 4))

((subject) (sub 5))))

這個 lambda 函式可以接受一個呼叫引數,這個呼叫引數的效果,就相當於給這個 lambda 函式發了一個短訊息。根據這個短訊息的不同,這個 lambda 函式返回不同的結果。這就有點像是物件導向程式設計裡面一個物件的效果。上面的這個技巧也就是在函數語言程式設計語言裡面模擬物件導向程式設計的一個簡單的方法。當然,要真正的做到在函數語言程式設計裡面模擬物件導向程式設計,還是要做多得多的工作的。

用帖子作為輸入和輸出

在這一部分,我們只是做一個簡單的設計。考慮到減輕整個系統的執行負擔,這包括小百合的伺服器端以及我們本地的執行程式,我們只搜尋處理論壇上最新發表的標題以“○ iloveqhq: ”為開頭的帖子。我們的回覆帖子也規定以“○ iloveqhq Re: ”為標題。相關的程式程式碼片段列在下面。這個設計當然不是很好。但是更好的設計只有在有相當數量的使用者加入進來測試,並提供足夠多的反饋資訊以後才有可能達到。所以目前暫時就先這樣吧。 ^_^

(define (get-ask-post)

(let* ((url (string-append lilybbs "/bbsdoc?board=CompLang"))

(htm (run/string (curl ,url)))

(asksub (rx "○ iloveqhq: "))

(anssub (rx "○ iloveqhq Re: ")))

(let lp ((lis (html->posts htm))

(asknum 0)

(askpost #f)

(ansnum 0)

(anspost #f))

(if (null? lis)

(if (> asknum ansnum)

askpost

#f)

(let* ((post (car lis))

(sub (post 'subject))

(num (string->number (post 'num))))

(if (and (> num asknum)

(regexp-search? asksub sub))

(lp (cdr lis) num post ansnum anspost)

(if (and (> num ansnum)

(regexp-search? anssub sub))

(lp (cdr lis) asknum askpost num post)

(lp (cdr lis) asknum askpost ansnum anspost))))))))

帖子中的內容有普通文字,也有 scheme 程式程式碼,我們在這裡也只是做一個頭腦簡單的設計,假設帖子中只能出現一段 scheme 程式程式碼。這段程式碼的開頭第一行必須是“iloveqhq: elk”內容不多也不少。結尾的一行必須是“iloveqhq: kle” 內容也必須是恰恰好。這樣的設計當然也不是很好。在以後的版本中應該會有更好的設計出現的。下面列出的就是提取帖子中 scheme 程式程式碼的主要函式。

(define (string->elk-string str)

(let* ((elk (rx (: # ewline "iloveqhq: elk" (* whitespace) # ewline)))

(kle (rx (: # ewline "iloveqhq: kle" (* whitespace) # ewline)))

(re (rx (: ,elk (submatch (+ any)) ,kle))))

(let lp ((str str)

(res ""))

(let ((mat (regexp-search re str)))

(if (not mat)

res

(lp (substring str (match:end mat 1) (string-length str))

(string-append res (match:substring mat 1))))))))

用 elk scheme 做沙盤

從帖子中得到 scheme 程式程式碼以後,我們就可以把這段程式碼餵給一個 scheme 程式直譯器,讓它執行這段程式碼,並且把返回資訊傳遞給我們。然後我們就可以用這段返回資訊作出一個回覆的帖子,張貼到小百合的版面上去。

這裡面需要考慮一個安全問題。因為從理論上說,小百合上的任意一個使用者都可以在帖子中嵌入任意的 scheme 程式碼片段。我們用 curl 把網上這個我們並不瞭解詳細內容的程式碼片段抓回到本地機器上,交給執行在本地機器的後臺的一個 scheme 直譯器去執行,肯定要考慮到安全的問題。

我們解決這個安全問題的一個簡單辦法,就是做一個 scheme 語言的沙盤環境。我們用 elk scheme 來設定這個環境。

(define (elk-disable)

(let ((nuke (lambda (sym)

(string-append "(define " (symbol->string sym) " #f)")))

(sym '(require

call-with-input-file call-with-output-file

with-input-from-file with-output-to-file

open-input-file open-output-file open-input-output-file

tilde-expand file-exists?

load load-path load-noisily? load-libraries

autoload autoload-notify? dump)))

(concat-string-list (map nuke sym) " ")))

(define (elk-run-string str)

(run/string (| (echo ,(string-append (elk-disable) str))

(elk -l -))))

在這裡做的事情其實就是把 elk scheme 當中涉及到輸入和輸出的大部分函式都給遮蔽掉。這樣一來,網上下載下來的不安全的程式碼就不會對本地系統造成任何過分的破壞了。除了輸入和輸出以外,我們也要把 elk scheme 中的模組載入的部分也給登出掉。這個理由也是顯然的。

這個安全屏障當然是很簡單的。只能防止一些最惡劣的破壞。在一些更加細緻的方面,並沒有做到周密的考慮。因為我們在這裡只是說明一個例子而已,所以就沒有必要在這個雖然困難,但卻是枝節的問題上耗費腦筋了。

登入和登出

從 scheme 程式語言的沙盤環境得到程式的輸出以後,我們就可以考慮往小百合的 CompLang 論壇上發帖子,把程式輸出的效果張貼出來。不過發帖子和我們前面遇到過的任務都不相同,需要我們登入小百合。前面的所有任務都是可以用匿名使用者的身份來完成的,不過發帖子就不行了,小百合的大部分版面都是不允許匿名發帖的。發帖之前,我們首先要登入小百合系統。小百合的登入和登出是用 cookie 來處理的。我們就需要用 curl 來處理這些和 cookie 有關的問題了。

首先是透過一個 web 表格把我們需要用到的登入使用者 id 和密碼發給小百合的 web 伺服器。這一步用下面的 curl 命令就可以做到。

(run/string (curl -d ,(string-append "id=" id)

-d ,(string-append "pw=" pw)

,(string-append lilybbs "/bbslogin?type=2"))

(- 2))

curl 命令的 -d 選項,後面跟著 key=value 這樣的字串,就可以用來向 web 地址傳送 web 表格資訊。表格被髮送給 web 伺服器以後,伺服器會返回一個頁面,這個頁面裡面就包括 cookie 有關的資訊。

小甜餅

小百合的 cookie 設定比較奇怪,不是透過 HTTP 協議的資訊頭來傳送的,而是透過 JavaScript 來傳送。這樣一來,我們就無法利用 curl 標準的處理 cookie 的辦法了。我們需要自己用 scsh 首先對返回的頁面 HTML 加上 JavaScript 做一些分析處理。這個分析處理還是用前面提到過的正規表示式的方法,把 cookie 資訊提取出來。相關的具體的程式碼實現列在下面。

(define (get-login-cookie id pw)

(let* ((url (string-append lilybbs "/bbslogin?type=2"))

(html (run/string (curl -d ,(string-append "id=" id)

-d ,(string-append "pw=" pw)

,url) (- 2)))

(cookie-lines (run/strings (| (echo ,html)

(grep "

(find (lambda (line)

(let ((mat (regexp-search re line)))

(if (not mat)

""

(match:substring mat 1))))))

(concat-string-list (map find cookie-lines) "; ")))

curl 命令的 -b 選項加上一個字串引數,就可以向網站傳送 cookie 小甜餅。我們從下面的例子可以看出來。另外,我們瞭解到所謂 cookie 其實就是一個個的鍵和鍵值組成的字串。

bash-2.05b$ curl -b "key1=value1; key2=value2"

傳送 cookie 給小百合以登出先前登入的使用者 id 的函式片段在下面列出來。

(define (logout-cookie cookie)

(let ((url (string-append lilybbs "/bbslogout")))

(run (curl -b ,cookie ,url) (- 2))))

發帖子

如何登入和如何登出都談過了以後,下面我們就可以在小百合上發帖子了。要注意的一件事情是在把帖子的內容交給 curl 傳送到網站上去之前,先要把帖子中的一些特殊的字元按照 HTTP 協議的要求進行編碼轉換。這件事情 curl 是不會代替我們完成的。我們必須自己用 scsh 函式來完成。下面的程式程式碼片段就是完成這個工作。

(define (url-encode-char ch)

;; Returns the url-encoded equivalent of a character

(cond ((char-ascii=? ch 32) "%20") ; space

((char=? ch #&) "%26") ; ampersand

((char=? ch #?) "%3F") ; question

((char=? ch #{) "%7B") ; open curly

((char=? ch #}) "%7D") ; close curly

((char=? ch #|) "%7C") ; vertical bar

((char=? ch #) "%5C") ; backslash

((char=? ch #/) "%2F") ; slash

((char=? ch #^) "%5E") ; caret

((char=? ch #~) "%7E") ; tilde

((char=? ch #[) "%5B") ; open square

((char=? ch #]) "%5D") ; close square

((char=? ch #`) "%60") ; backtick

((char=? ch #\%) "%25") ; percent

((char=? ch #+) "%2B") ; plus

(else (string ch))))

有了前面的那麼多準備工作,最後傳送文章就是一件輕而易舉的事情了。

(define (post-article board title text)

(let* ((cookie (get-login-cookie my-own-id my-own-pw))

(url (string-append lilybbs "/bbssnd?board=" board))

(post (string-append "title=" (url-encode title)

"&text=" (url-encode text))))

(run (curl -b ,cookie -d ,post ,url) (- 2))

(logout-cookie cookie)))

結語

上面用兩個例子說明了用 curl 和 scsh 編寫 web 指令碼程式的技術。如果網站提供有標準的 web 服務介面的話,當然會讓我們的任務減輕許多。可是目前大部分的網站,尤其是我們最感興趣的論壇網站都沒有提供適於程式設計的 web 服務介面,所以如果我們想要對 web 論壇上的一些任務進行自動化處理的話,用 curl 和 scsh 編寫 web 指令碼程式的辦法就是非常有吸引力的了。

在本文中的第二個例子裡面涉及到的 scheme 語言的沙盤環境,這也是非常有意思的一個話題。如果有機會的話,在這個方面,作者還有一些更加有趣的內容可以說一說。

致謝

南京大學小百合 < 上的 vt

非常感謝你的 upbbs1 和 upbbs2 兩個指令碼程式!見識了你的例子,我才知道有 curl 這麼個強大的工具!而且,我又可以在 LinuxUnix 版上貼圖啦!謝謝!

南京大學小百合 < 上的 xiaoxinpan

感謝你的支援!謝謝!

Fcitx 小企鵝中文輸入法 <

這篇文章是在 GNU/Linux 作業系統上用 Emacs 編輯器加上 Fcitx 小企鵝中文輸入法編輯輸入的。感謝 Fcitx 的開發者們!終於可以在我最喜愛的作業系統上舒舒服服的編輯中文文章啦!謝謝!

檔案下載

iloveqhq-991213.tar.bz2; 這個打包檔案裡面是一些 scheme 語言的小程式例子。裡面包括有本文中詳細說明的這個指令碼程式的完整程式程式碼。

參考資料

scsh < 是一個基於 scheme 語言的威力強大的 UNIX shell 環境。在 IBM developerWorks 的 Linux 專區中有下面幾篇文章專門介紹了 scheme 程式語言以及 scsh 這個實現版本。

宋國偉的兩篇文章一步步的帶領讀者學習 scheme 語言程式設計的基礎部分。這兩篇文章捎帶講到了 scheme 語言的 guile 這個版本。這是自由軟體基金會的 GNU 工程的一個專案。在 guile 中也有一個 scsh 的實現。

趙蔚的一篇文章給出了 scheme 程式語言的一個概括的介紹。

趙蔚的另一篇文章介紹瞭如何使用 scsh 進行 UNIX 系統程式設計。

curl 的主頁上列有關於這個小巧而強大的 web 工具的詳細文件。

elk < < -bremen.de/software/elk/> 這是本文中涉及到的另一個 scheme 語言的實現。本文中開發的面向 web 論壇的 scheme 直譯器就是用的 elk 做驅動的。它的特點是比較小巧,啟動速度比較快。很方便嵌入到 C 和 C++ 語言的程式當中去。

關於作者

趙蔚是一名生活在南京的自由程式設計師。他的網路日記 < 記載了各種雜七雜八的胡思亂想。在程式以外,趙蔚是一名不負責任的白日做夢者和業餘水平的數學愛好者。

http://www-900.ibm.com/developerWorks/cn/linux/l-scheme/part3/index.shtml?ca=dwcn-newsletter-linu

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-944884/,如需轉載,請註明出處,否則將追究法律責任。

大家快來嚐嚐鮮 轉<用 curl 和 scsh 編寫 web 指令碼>(轉)
請登入後發表評論 登入
全部評論

相關文章