用 curl 和 scsh 编写 web 脚本

 
用 curl 和 scsh 编写 web 脚本
内容:
简介
一些说明
简单任务之一
判断用户是否在线
防止欺骗
发出通知
简单任务之二
读取输入帖子
正则表达式
匹配
面向对象
用帖子作为输入和输出
用 elk scheme 做沙盘
登录和注销
小甜饼
发帖子
结语
致谢
文件下载
参考资料
关于作者
在 Linux 专区还有:
教程
工具与产品
代码与组件
文章

赵蔚 (zhaoway@public1.ptt.js.cn)
自由程序员
2004 年 1 月

简介

让我们看看程序员是如何泡网的。在本文中我们将要介绍如何使用 curl 这个简单灵巧的 web 工具加上 scsh 这个基于 scheme 语言的威力强大的 UNIX shell 来编写各种古怪的 web 脚本来帮助我们泡网。

一些说明

本文中描述的例子程序都是专门针对南京大学小百合 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 程序语言了。可是由于程序不断增加的复杂程度,以及变来变去的 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 "http://lilybbs.net/bbsqry?userid=iloveqhq"

接下来的任务就是要在我们的脚本程序中驱动这个命令,并要在脚本程序中获取到这个命令输出的内容,以准备交给程序的其它部分进行进一步的分析和处理。

在普通 UNIX 操作系统上的 Bourne shell 环境中,比如在 GNU/Linux 操作系统上的 BASH 脚本程序当中,驱动一个 shell 工具是一件很直接、也很简单的事情。这是 shell 的长处。基于 scheme 程序语言的 scsh 也自诩为是 UNIX shell 的一种,当然也可以作到方便轻松的驱动 shell 工具程序。这一点其实也正是 scsh 区别于其它许多的 scheme 程序语言的实现版本的一个主要的特征。在用 scsh 编写的脚本程序当中,用下面的这个 run/string 语法形式就可以完成这一任务。


(run/string (curl "http://lilybbs.net/bbsqry?userid=iloveqhq"))

这个 curl 命令在一般运行的时候,会在标准错误输出端口打印一些统计信息。在脚本程序当中,有的时侯并不需要这样的统计信息。在 scsh 中我们可以用下面的语法形式来关闭 curl 的标准错误输出端口。


(run/string (curl "http://lilybbs.net/bbsqry?userid=iloveqhq")
	    (- 2))

就像在标准的 UNIX 操作系统中一样,数字 2 表示标准错误输出端口。上面的减号表示要关闭这个端口。

上面命令中出现的长长的 URL 字符串,我们可以看出来,从脚本程序的角度,可以分为三个部分。第一部分 "http://lilybbs.net" 是小百合站点的 URL 字符串,这在整个程序当中都是不变的。第二部分 "/bbsqry?userid=" 是在脚本程序的这一部分的这个函数里面,每次调用都固定不变的内容。第三部分 "iloveqhq" 是根据每次函数调用所关心的用户 id 的不同,每次都要发生变化的内容。我们当然希望用不同的变量来分别表示这三个部分字符串。这个要求我们用下面这个语法形式就可以达到。


(define lilybbs "http://lilybbs.net")

(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 解释器。

简单任务之二

南京大学小百合 http://lilybbs.net 上的 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 (: "<tr><td>" ,num
		  "<td>" (+ whitespace)
		  "<td><a href=bbsqry?userid="
		  ,userid ">"
		  ,userid "</a><td><nobr>"
		  ,time "<td><a href="
		  ,url ">"
		  ,sub "</a>(<font style='font-size:12px; color:#"
		  (| "f00000" "008080")
		  "'>" ,size "</font>)"
		  "<td><font color="
		  (| "red" "black")
		  ">" ,num
		  "</font>")))
		  

上面这最后一个正则表达式如果用基于字符串的、传统的 POSIX 的方式写出来,恐怕谁都会受不了的吧。

匹配

有了正则表达式,我们就可以用它匹配指定的字符串。这主要是通过 regexp-search 这个函数来完成的。

(regexp-search 正则表达式 字符串)

如果不发生匹配,就会返回表示“假”的 #f 这个布尔值。如果发生匹配了,则会返回一个 match 类型的数据。这个类型的数据里面包括了关于具体匹配的子字符串的具体内容。这些内容可以用 match:substring 等一些函数提取出来。

(match:substring match-data index)

零号索引表示整个的正则表达式匹配到的子字符串。其它的索引则表示正则表达式中出现的 submatch 的部分。我们还是用上面最后的那个 re 正则表达式来说明。这一次我们给它加上 submatch 的信息。


		  (define re (rx (: "<tr><td>" (submatch ,num)
		  "<td>" (+ whitespace)
		  "<td><a href=bbsqry?userid="
		  ,userid ">"
		  (submatch ,userid) "</a><td><nobr>"
		  (submatch ,time) "<td><a href="
		  (submatch ,url) ">"
		  (submatch ,sub)
		  "</a>(<font style='font-size:12px; color:#"
		  (| "f00000" "008080")
		  "'>" ,size "</font>)"
		  "<td><font color="
		  (| "red" "black") ">"
		  ,num "</font>")))
		  

在 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 (: "<tr><td>" (submatch ,num) "<td>" (+ whitespace)
		    "<td><a href=bbsqry?userid=" ,userid ">"
		    (submatch ,userid) "</a><td><nobr>"
		    (submatch ,time) "<td><a href=" (submatch ,url) ">"
		    (submatch ,sub) "</a>(<font style='font-size:12px; color:#"
		    (| "f00000" "008080") "'>" ,size "</font>)"
		    "<td><font color=" (| "red" "black") ">" ,num "</font>"))))
    (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 (: #/newline "iloveqhq: elk" (* whitespace) #/newline)))
	 (kle (rx (: #/newline "iloveqhq: kle" (* whitespace) #/newline)))
	 (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 "<script>document.cookie='utmp"))))
	 (re (rx (: "<script>document.cookie='"
		    (submatch (: "utmp"
				 (| "num" "key" "userid")
				 "=" (+ (~ #/'))))
		    "'</script>")))
	 (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" http://lilybbs.net

发送 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 语言的沙盘环境,这也是非常有意思的一个话题。如果有机会的话,在这个方面,作者还有一些更加有趣的内容可以说一说。

致谢

南京大学小百合 <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 语言的小程序例子。里面包括有本文中详细说明的这个脚本程序的完整程序代码。

参考资料

scsh <http://www.scsh.net> 是一个基于 scheme 语言的威力强大的 UNIX shell 环境。在 IBM developerWorks 的 Linux 专区中有下面几篇文章专门介绍了 scheme 程序语言以及 scsh 这个实现版本。

<http://www-900.ibm.com/developerWorks/cn/linux/l-scheme/part1/index.shtml> <http://www-900.ibm.com/developerWorks/cn/linux/l-scheme/part1/index.shtml> 宋国伟的两篇文章一步步的带领读者学习 scheme 语言程序设计的基础部分。这两篇文章捎带讲到了 scheme 语言的 guile 这个版本。这是自由软件基金会的 GNU 工程的一个项目。在 guile 中也有一个 scsh 的实现。

<http://www-900.ibm.com/developerWorks/cn/linux/l-scheme/part1/index.shtml> 赵蔚的一篇文章给出了 scheme 编程语言的一个概括的介绍。

<http://www-900.ibm.com/developerWorks/cn/linux/l-scheme/part1/index.shtml> 赵蔚的另一篇文章介绍了如何使用 scsh 进行 UNIX 系统编程。

curl <http://curl.haxx.se> 的主页上列有关于这个小巧而强大的 web 工具的详细文档。

elk <http://sam.zoy.org/projects/elk/> <http://www-rn.informatik.uni-bremen.de/software/elk/> 这是本文中涉及到的另一个 scheme 语言的实现。本文中开发的面向 web 论坛的 scheme 解释器就是用的 elk 做驱动的。它的特点是比较小巧,启动速度比较快。很方便嵌入到 C 和 C++ 语言的程序当中去。

关于作者

赵蔚是一名生活在南京的自由程序员。他的网络日记 <http://advogato.org/person/zhaoway> 记载了各种杂七杂八的胡思乱想。在程序以外,赵蔚是一名不负责任的白日做梦者和业余水平的数学爱好者。



到页首



 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值