python可以做shell脚本吗?当然可以的。首先介绍一些相关函数:
os.system(command)
这个函数可以调用shell运行命令行command并且返回它的返回值。试一下在python的解释器里输入os.system(”ls -l”),就可以看到”ls”列出了当前目录下的文件。可以说,通过这个函数,python就拥有了shell的所有能力。呵呵。。不过,通常这条命令不需要用到。因为shell常用的那些命令在python中通常有对应而且同样简洁的写法。
shell中最常用的是ls命令,python对应的写法是:os.listdir(dirname),这个函数返回字符串列表,里面是所有的文件名,不过不包含”.”和”..”。如果要遍历整个目录的话就会比较复杂一点。我们等下再说吧。先在解释器里试一下:
>>> os.listdir(”/”)
[’tmp’, ‘misc’, ‘opt’, ‘root’, ‘.autorelabel’, ’sbin’, ’srv’, ‘.autofsck’, ‘mnt’, ‘usr’, ‘var’, ‘etc’, ’selinux’, ‘lib’, ‘net’, ‘lost+found’, ’sys’, ‘media’, ‘dev’, ‘proc’, ‘boot’, ‘home’, ‘bin’]
就像这样,接下去所有命令都可以在python的解释器里直接运行观看结果。
对应于cp命令的是:shutil.copy(src,dest),这个函数有两个参数,参数src是指源文件的名字,参数dest则是目标文件或者目标目录的名字。 如果dest是一个目录名,就会在那个目录下创建一个相同名字的文件。与shutil.copy函数相类似的是shutil.copy2(src,dest),不过copy2还会复制最后存取时间和最后更新时间。
不过,shell的cp命令还可以复制目录,python的shutil.copy却不行,第一个参数只能是一个文件。这怎么办?其实,python还有个shutil.copytree(src,dst[,symlinks]) 。参数多了一个symlinks,它是一个布尔值,如果是True的话就创建符号链接。
移动或者重命名文件和目录呢?估计被聪明的朋友猜到了,shutil.move(src,dst),呵呵。。与mv命令类似,如果src和dst在同一个文件系统上,shutil.move只是简单改一下名字,如果src和dst在不同的文件系统上,shutil.move会先把src复制到dst,然后删除src文件。看到现在,大多数朋友应该已经对python的能力有点眉目了,接下来我就列个表,介绍一下其它的函数:
os.chdir(dirname)
把当前工作目录切换到dirname下
os.getcwd()
返回当前的工作目录路径
os.chroot(dirname)
把dirname作为进程的根目录。和*nix下的chroot命令类似
os.chmod(path,mode)
更改path的权限位。mode可以是以下值(使用or)的组合:
os.S_ISUID
os.S_ISGID
os.S_ENFMT
os.S_ISVTX
os.S_IREAD
os.S_IWRITE
os.S_IEXEC
os.S_IRWXU
os.S_IRUSR
os.S_IWUSR
os.S_IXUSR
os.S_IRWXG
os.S_IRGRP
os.S_IWGRP
os.S_IXGRP
os.S_IRWXO
os.S_IROTH
os.S_IWOTH
os.S_IXOTH
具体它们是什么含义,就不仔细说了,基本上就是R代表读,W代表写,X代表执行权限。USR代表用户,GRP代表组,OTH代表其它。
os.chown(path,uid,gid)
改变文件的属主。uid和gid为-1的时候不改变原来的属主。
os.link(src,dst)
创建硬连接
os.mkdir(path,[mode])
创建目录。mode的意义参见os.chmod(),默认是0777
os.makedirs(path,[mode])
和os.mkdir()类似,不过会先创建不存在的父目录。
os.readlink(path)
返回path这个符号链接所指向的路径
os.remove(path)
删除文件,不能用于删除目录
os.rmdir(path)
删除文件夹,不能用于删除文件
os.symlink(src,dst)
创建符号链接
shutil.rmtree(path[,ignore_errors[,onerror]])
删除文件夹
介绍了这么多,其实只要查一下os和shutil两个模块的文档就有了,呵呵。。真正编写shell脚本的时候还需要注意:
1.环境变量。python的环境变量保存在os.environ这个字典里,可以用普通字典的方法修改它,使用system启动其它程序的时候会自动被继承。比如:
os.environ[”fish”]=”nothing”
不过也要注意,环境变量的值只能是字符串。和shell有些不同的是,python没有export环境变量这个概念。为什么没有呢?因为python没有必要有:-)
2.os.path这个模块里包含了很多关于路径名处理的函数。在shell里路径名处理好像不是很重要,但是在python里经常需要用到。最常用的两个是分离和合并目录名和文件名:
os.path.split(path) -> (dirname,basename)
这个函数会把一个路径分离为两部分,比如:os.path.split(”/foo/bar.dat”)会返回(”/foo”,”bar.dat”)
os.path.join(dirname,basename)
这个函数会把目录名和文件名组合成一个完整的路径名,比如:os.path.join(”/foo”,”bar.dat”)会返回”/foo/bar.dat”。这个函数和os.path.split()刚好相反。
还有这些函数:
os.path.abspath(path)
把path转成绝对路径
os.path.expanduser(path)
把path中包含的”~”和”~user”转换成用户目录
os.path.expandvars(path)
根据环境变量的值替换path中包含的”$name”和”${name}”,比如环境变量FISH=nothing,那os.path.expandvars(”$FISH/abc”)会返回”nothing/abc”
os.path.normpath(path)
去掉path中包含的”.”和”..”
os.path.splitext(path)
把path分离成基本名和扩展名。比如:os.path.splitext(”/foo/bar.tar.bz2″)返回(’/foo/bar.tar’, ‘.bz2′)。要注意它和os.path.split()的区别
3.在os模块有一个很好用的函数叫os.stat()没有介绍,因为os.path模块里包含了一组和它具有同样功能的函数,但是名字更好记一点。
os.path.exists(path)
判断文件或者目录是否存在
os.path.isfile(path)
判断path所指向的是否是一个普通文件,而不是目录
os.path.isdir(path)
判断path所指向的是否是一个目录,而不是普通文件
os.path.islink(path)
判断path所指向的是否是一个符号链接
os.path.ismount(path)
判断path所指向的是否是一个挂接点(mount point)
os.path.getatime(path)
返回path所指向的文件或者目录的最后存取时间。
os.path.getmtime(path)
返回path所指向的文件或者目录的最后修改时间
os.path.getctime(path)
返回path所指向的文件的创建时间
os.path.getsize(path)
返回path所指向的文件的大小
4.应用python编写shell脚本经常要用到os,shutil,glob(正则表达式的文件名),tempfile(临时文件),pwd(操作/etc/passwd文件),grp(操作/etc/group文件),commands(取得一个命令的输出)。前面两个已经基本上介绍完了,后面几个很简单,看一下文档就可以了。
5.sys.argv是一个列表,保存了python程序的命令行参数。其中sys.argv[0]是程序本身的名字。
不能光说不练,接下来我们就编写一个用于复制文件的简单脚本。前两天叫我写脚本的同事有个几万个文件的目录,他想复制这些文件到其它的目录,又不能直接复制目录本身。他试了一下”cp src/* dest/”结果报了一个命令行太长的错误,让我帮他写一个脚本。操起python来:
import sys,os.path,shutil
for f in os.listdir(sys.argv[1]):
shutil.copy(os.path.join(sys.argv[1],f),sys.argv[2])
再试一下linuxapp版里的帖子——把一个文件夹下的所有文件重命名成10001~10999。可以这样写:
import os.path,sys
dirname=sys.argv[1]
i=10001
for f in os.listdir(dirname):
src=os.path.join(dirname,f)
if os.path.isdir(src):
continue
os.rename(src,str(i))
i+=1
一、深度优先搜索
深度优先搜索就是在搜索树的每一层始终先只扩展一个子节点,不断地向纵深前进直到不能再前进(到达叶子节点或受到深度限制)时,才从当前节点返回到上一级节点,沿另一方向又继续前进。这种方法的搜索树是从树根开始一枝一枝逐渐形成的。
深度优先搜索亦称为纵向搜索。由于一个有解的问题树可能含有无穷分枝,深度优先搜索如果误入无穷分枝(即深度无限),则不可能找到目标节点。所以,深度优先搜索策略是不完备的。另外,应用此策略得到的解不一定是最佳解(最短路径)。
深度优先搜索的路径示意图:

三、广度优先搜索
在深度优先搜索算法中,是深度越大的结点越先得到扩展。如果在搜索中把算法改为按结点的层次进行搜索, 本层的结点没有搜索处理完时,不能对下层结点进行处理,即深度越小的结点越先得到扩展,也就是说先产生 的结点先得以扩展处理,这种搜索算法称为广度优先搜索法。
广度优先搜索路径示意图:

Heritrix
Heritrix是一个开源,可扩展的web爬虫项目。Heritrix设计成严格按照robots.txt文件的排除指示和META robots标签。
http://crawler.archive.org/
WebSPHINX
WebSPHINX是一个Java类包和Web爬虫的交互式开发环境。Web爬虫(也叫作机器人或蜘蛛)是可以自动浏览与处理Web页面的程序。WebSPHINX由两部分组成:爬虫工作平台和WebSPHINX类包。
http://www.cs.cmu.edu/~rcm/websphinx/
WebLech
WebLech是一个功能强大的Web站点下载与镜像工具。它支持按功能需求来下载web站点并能够尽可能模仿标准Web浏览器的行为。WebLech有一个功能控制台并采用多线程操作。
http://weblech.sourceforge.net/
Arale
Arale主要为个人使用而设计,而没有像其它爬虫一样是关注于页面索引。Arale能够下载整个web站点或来自web站点的某些资源。Arale还能够把动态页面映射成静态页面。
http://web.tiscali.it/_flat/arale.jsp.html
J-Spider
J-Spider:是一个完全可配置和定制的Web Spider引擎.你可以利用它来检查网站的错误(内在的服务器错误等),网站内外部链接检查,分析网站的结构(可创建一个网站地图),下载整个Web站点,你还可以写一个JSpider插件来扩展你所需要的功能。
http://j-spider.sourceforge.net/
spindle
spindle 是一个构建在Lucene工具包之上的Web索引/搜索工具.它包括一个用于创建索引的HTTP spider和一个用于搜索这些索引的搜索类。spindle项目提供了一组JSP标签库使得那些基于JSP的站点不需要开发任何Java类就能够增加搜 索功能。
http://www.bitmechanic.com/projects/spindle/
Arachnid
Arachnid: 是一个基于Java的web spider框架.它包含一个简单的HTML剖析器能够分析包含HTML内容的输入流.通过实现Arachnid的子类就能够开发一个简单的Web spiders并能够在Web站上的每个页面被解析之后增加几行代码调用。 Arachnid的下载包中包含两个spider应用程序例子用于演示如何使用该框架。
http://arachnid.sourceforge.net/
LARM
LARM能够为Jakarta Lucene搜索引擎框架的用户提供一个纯Java的搜索解决方案。它包含能够为文件,数据库表格建立索引的方法和为Web站点建索引的爬虫。
http://larm.sourceforge.net/
JoBo
JoBo 是一个用于下载整个Web站点的简单工具。它本质是一个Web Spider。与其它下载工具相比较它的主要优势是能够自动填充form(如:自动登录)和使用cookies来处理session。JoBo还有灵活的 下载规则(如:通过网页的URL,大小,MIME类型等)来限制下载。
http://www.matuschek.net/software/jobo/index.html
snoics-reptile
snoics -reptile是用纯Java开发的,用来进行网站镜像抓取的工具,可以使用配制文件中提供的URL入口,把这个网站所有的能用浏览器通过GET的方式 获取到的资源全部抓取到本地,包括网页和各种类型的文件,如:图片、flash、mp3、zip、rar、exe等文件。可以将整个网站完整地下传至硬盘 内,并能保持原有的网站结构精确不变。只需要把抓取下来的网站放到web服务器(如:Apache)中,就可以实现完整的网站镜像。
http://www.blogjava.net/snoics
Web-Harvest
Web-Harvest是一个Java开源Web数据抽取工具。它能够收集指定的Web页面并从这些页面中提取有用的数据。Web-Harvest主要是运用了像XSLT,XQuery,正则表达式等这些技术来实现对text/xml的操作。
http://web-harvest.sourceforge.net
spiderpy
spiderpy是一个基于Python编码的一个开源web爬虫工具,允许用户收集文件和搜索网站,并有一个可配置的界面。
http://pyspider.sourceforge.net/
The Spider Web Network Xoops Mod Team
pider Web Network Xoops Mod是一个Xoops下的模块,完全由PHP语言实现。
http://www.tswn.com/
Fetchgals
Fetchgals是一个基于perl多线程的Web爬虫,通过Tags来搜索色情图片。
https://sourceforge.net/projects/fetchgals
larbin
larbin是个基于C++的web爬虫工具,拥有易于操作的界面,不过只能跑在LINUX下,在一台普通PC下larbin每天可以爬5百万个页面(当然啦,需要拥有良好的网络)
http://larbin.sourceforge.net/index-eng.html
Google Gears是一个开源产品,它能够在离线情况下使用网页应用程序。它的主要功能包括捕捉并支撑组成网页应用程序的资源和代码,具体包括图片、逻辑和外观;Gears的另外一个主要功能是创建网页应用程序能够访问的本地数据库;第三个功能使得开发人员能够通过多核处理器的多线程能力来在后台运行JavaScript。通过这个功能,开发人员能够创建基于网页的应用程序及其桌面版本,并且这两个版本之间能够进行同步。如果没有多线程功能,那么当一个网页应用程序与本地桌面版的应用程序进行同步时,Gears会先冻结应用程序直到同步完成。
同步功能对任何跨两个平台的应用程序来说都是必须具有的功能,但对于用户来说出现任何延迟都是不可接受的。Google开发部经理Brent Taylor表示,希望业内共同努力把这些功能作为所有浏览器的标准。
而Google并不是唯一一间想把网页应用程序延伸到桌面的公司。
Adobe的Apollo也针对同样目标设计。然而,Apollo寄生在桌面并且能够访问到任何本地的文件系统,而Gears只能访问到由它创建的轻量级SQL数据库。但是Adobe高级副主席兼首席软件架构师Kevin Lynch表示,Apollo中将会提供Gears的API。
GNU Wget 1.10.2,非交互式的网络文件下载工具。
用法: wget [选项]... [URL]...
Mandatory arguments to long options are mandatory for short options too.
开始:
-V, --version 显示Wget版本之后退出。
-h, --help 显示本帮助
-b, --background 启动后转到后台运行。
-e, --execute=COMMAND 运行 `.wgetrc'-那样的命令。
日志和输入文件:
-o, --output-file=FILE 将日志记录到文件 FILE.
-a, --append-output=FILE 添加消息到文件 FILE.
-d, --debug 输出大量调试信息。
-q, --quiet 静默模式 (无输出)。
-v, --verbose 显示信息 (默认打开).
-nv, --no-verbose 显示很少信息但不是完全安静.
-i, --input-file=FILE 下载存储在 FILE 文件中的所有URL地址指向的文件。
-F, --force-html 把输入文件当成 HTML.
-B, --base=URL prepends URL to relative links in -F -i file.
下载:
-t, --tries=NUMBER 设定重试次数(0 一直重试)。
--retry-connrefused retry even if connection is refused.
-O, --output-document=FILE 写入文档到 FILE.
-nc, --no-clobber 跳过将要到已存在文件的下载。
-c, --continue 续传下载。
--progress=TYPE select progress gauge type.
-N, --timestamping don't re-retrieve files unless newer than
local.
-S, --server-response 显示服务器的响应。
--spider 什么都不下载
-T, --timeout=SECONDS 把所有超时时间设为SECONDS秒。
--dns-timeout=SECS 把DNS超时时间设为SECS秒。
--connect-timeout=SECS 设定连接超时为 SECS.
--read-timeout=SECS 设定读取超时为 SECS.
-w, --wait=SECONDS wait SECONDS between retrievals.
--waitretry=SECONDS wait 1..SECONDS between retries of a retrieval.
--random-wait wait from 0...2*WAIT secs between retrievals.
-Y, --proxy 显示打开代理
--no-proxy 显示关闭代理
-Q, --quota=NUMBER set retrieval quota to NUMBER.
--bind-address=ADDRESS bind to ADDRESS (hostname or IP) on local host.
--limit-rate=RATE 把下载速度限制为RATE。
--no-dns-cache 禁止查找DNS缓存。
--restrict-file-names=OS restrict chars in file names to ones OS allows.
-4, --inet4-only 仅连接IPv4地址。
-6, --inet6-only 只连接到 IPv6 地址。
--prefer-family=FAMILY connect first to addresses of specified family,
one of IPv6, IPv4, or none.
--user=USER 把ftp和http的用户名设定为UESR。
--password=PASS 把ftp和http的密码设定为PASS。
目录:
-nd,--no-directories 不建立文件夹。
-x,--force-directories 强制建立文件夹。
-nH, --no-host-directories 不创建主机目录。
--protocol-directories use protocol name in directories.
-P, --directory-prefix=PREFIX 将文件保存到 PREFIX/...
--cut-dirs=NUMBER ignore NUMBER remote directory components.
HTTP 选项:
--http-user=USER 将 http用户设为 USER.
--http-password=PASS 将http密码设为 PASS.
--no-cache 不接受服务器缓存的数据.
-E,--html-extension 以'.html'扩展名保存HTML文档。
--ignore-length ignore `Content-Length' header field.
--header=STRING insert STRING among the headers.
--proxy-user=USER 设定 USER 作为代理用户名。
--proxy-password=PASS 设定 PASS 作为代理密码。
--referer=URL include `Referer: URL' header in HTTP request.
--save-headers 保存 HTTP 头到文件。
-U, --user-agent=AGENT identify as AGENT instead of Wget/VERSION.
--no-http-keep-alive disable HTTP keep-alive (persistent connections).
--no-cookies 不使用cookies.
--load-cookies=FILE 在会话前从文件中读取cookies。
--save-cookies=FILE 在会话结束后保存 cookies 到 FILE。
--keep-session-cookies load and save session (non-permanent) cookies.
--post-data=STRING use the POST method; send STRING as the data.
--post-file=FILE use the POST method; send contents of FILE.
HTTPS (SSL/TLS) 选项:
--secure-protocol=PR choose secure protocol, one of auto, SSLv2,
SSLv3, and TLSv1.
--no-check-certificate don't validate the server's certificate.
--certificate=FILE client certificate file.
--certificate-type=TYPE client certificate type, PEM or DER.
--private-key=FILE private key file.
--private-key-type=TYPE private key type, PEM or DER.
--ca-certificate=FILE file with the bundle of CA's.
--ca-directory=DIR directory where hash list of CA's is stored.
--random-file=FILE file with random data for seeding the SSL PRNG.
--egd-file=FILE file naming the EGD socket with random data.
FTP 选项:
--ftp-user=USER 设置ftp用户为 USER.
--ftp-password=PASS 设置ftp密码为 PASS.
--no-remove-listing don't remove `.listing' files.
--no-glob turn off FTP file name globbing.
--no-passive-ftp disable the "passive" transfer mode.
--retr-symlinks when recursing, get linked-to files (not dir).
--preserve-permissions preserve remote file permissions.
递归下载:
-r, --recursive specify recursive download.
-l, --level=NUMBER maximum recursion depth (inf or 0 for infinite).
--delete-after delete files locally after downloading them.
-k, --convert-links make links in downloaded HTML point to local files.
-K, --backup-converted before converting file X, back up as X.orig.
-m, --mirror shortcut for -N -r -l inf --no-remove-listing.
-p, --page-requisites get all images, etc. needed to display HTML page.
--strict-comments turn on strict (SGML) handling of HTML comments.
Recursive accept/reject:
-A, --accept=LIST comma-separated list of accepted extensions.
-R, --reject=LIST comma-separated list of rejected extensions.
-D, --domains=LIST comma-separated list of accepted domains.
--exclude-domains=LIST 被拒绝的域名的用逗号分开的列表。
--follow-ftp follow FTP links from HTML documents.
--follow-tags=LIST comma-separated list of followed HTML tags.
--ignore-tags=LIST comma-separated list of ignored HTML tags.
-H, --span-hosts 当递归时转到陌生的主机。
-L, --relative 只跟随相对链接。
-I, --include-directories=LIST 允许的目录的列表。
-X, --exclude-directories=LIST 不包括的目录的列表。
-np, --no-parent don't ascend to the parent directory.
日志统计分析软件Awstats需要Perl支持,但是Nginx内建的Perl模块目前还并不稳定,经常会出问题,所以还是用FastCGI模式运行Perl比较可靠。下面就谈谈如何在Nginx下配置Perl的FastCGI模式: 首先,安装Perl的FastCGI模块: #wget http://www.cpan.org/modules/by-module/FCGI/FCGI-0.67.tar.gz 其实也可以用这种方法:#perl -MCPAN -e ‘install FCGI’ #!/usr/bin/perl close(PARENT_WR); } 将权限改为可执行,并执行之。 配置nginx.conf,使之支持perl脚本: location ~* .*\.pl$ 编辑awstats.conf fastcgi_pass unix:/tmp/perl_fastcgi.sock; 然后重启nginx:
#tar zxvf FCGI-0.67.tar.gz
#cd FCGI-0.67
# perl Makefile.PL
#make && make install
然后,配置Perl的FastCGI脚本(从网上找到的,未找到原始出处):
use FCGI;
use Socket;
use POSIX qw(setsid);
require ’syscall.ph’;
&daemonize;
END() { } BEGIN() { }
*CORE::GLOBAL::exit = sub { die “fakeexit\nrc=”.shift().”\n”; };
eval q{exit};
if ($@) {
exit unless $@ =~ /^fakeexit/;
};
&main;
sub daemonize() {
chdir ‘/’ or die “Can’t chdir to /: $!”;
defined(my $pid = fork) or die “Can’t fork: $!”;
exit if $pid;
setsid or die “Can’t start a new session: $!”;
umask 0;
}
sub main {
$socket = FCGI::OpenSocket( “/tmp/perl_fastcgi.sock”, 10 );
$request = FCGI::Request( \*STDIN, \*STDOUT, \*STDERR, \%req_params, $socket );
if ($request) { request_loop()};
FCGI::CloseSocket( $socket );
}
sub request_loop {
while( $request->Accept() >= 0 ) {
$stdin_passthrough =”;
$req_len = 0 + $req_params{’CONTENT_LENGTH’};
if (($req_params{’REQUEST_METHOD’} eq ‘POST’) && ($req_len != 0) ){
my $bytes_read = 0;
while ($bytes_read < $req_len) {
my $data = '';
my $bytes = read(STDIN, $data, ($req_len - $bytes_read));
last if ($bytes == 0 || !defined($bytes));
$stdin_passthrough .= $data;
$bytes_read += $bytes;
}
}
if ( (-x $req_params{SCRIPT_FILENAME}) &&
(-s $req_params{SCRIPT_FILENAME}) &&
(-r $req_params{SCRIPT_FILENAME})
){
pipe(CHILD_RD, PARENT_WR);
my $pid = open(KID_TO_READ, "-|");
unless(defined($pid)) {
print("Content-type: text/plain\r\n\r\n");
print "Error: CGI app returned no output - Executing $req_params{SCRIPT_FILENAME} failed !\n";
next;
}
if ($pid > 0) {
close(CHILD_RD);
print PARENT_WR $stdin_passthrough;
close(PARENT_WR);
while(my $s = ) { print $s; }
close KID_TO_READ;
waitpid($pid, 0);
} else {
foreach $key ( keys %req_params){
$ENV{$key} = $req_params{$key};
}
if ($req_params{SCRIPT_FILENAME} =~ /^(.*)\/[^\/]+$/) {
chdir $1;
}
close(STDIN);
syscall(&SYS_dup2, fileno(CHILD_RD), 0);
exec($req_params{SCRIPT_FILENAME});
die(”exec failed”);
}
}
else {
print(”Content-type: text/plain\r\n\r\n”);
print “Error: No such CGI app - $req_params{SCRIPT_FILENAME} may not exist or is not executable by this process.\n”;
}
}
{
include awstats.conf;
}
fastcgi_index awstats.pl;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_read_timeout 60;
#service nginx restart
What Is POE, And Why Should I Use It?
回顾一下我们每天写的程序,可以发现这些程序大部分都有着基本的结构:启动,执行一系列的动作,然后退出。对于那些在用户和他们的数据之间不需要很多交互的应用中,这样的程序都能工作得很好。然后,如果是如果是碰到了复杂的任务,我想你就需要一个更加好的程序框架理解任务的复杂性,完成这个任务。
所以POE(Perl 对象环境)应运而生了。POE是一个构建Perl程序的框架,以更自然地完成那些需要对外部数据作出反应的任务,比如网络通讯或者用户接口。用POE写的程序完全是非线性的;你只需要建立一些小的子程序,并且定义好它们之间如何相互调用,那么POE就会根据程序处理的输入输出,自动在它们之间切换。说到现在,你可能已经头晕了。如果你习惯于看代码去理解,那么看了下面一个小例子后,你就会恍然大悟了。
POE Design
说POE是一个小的操作系统一点都不夸张,它有它自己的内核,进程,进程间通讯(IPC),驱动等等这些。实际上,它是一个简单的系统,构建了一系列的状态信息。下面对组成POE各个部分的一个简单的描述:
States
POE的基本程序块就是状态,它是一些代码,当事件发生时触发执行。例如,输入的数据到达,一个会话过期,一个会话传递了一条信息给另一个会话。POE里的每件事情都是基于接收和处理各种各样的事件。
The Kernel
POE的内核跟操作系统的内核很象:它守护着后台的所有你的进程和数据,安排你的代码的触发。你能够用POE的内核来为你的进程设置警报,控制你的状态队列和其他一些低级服务,当然,大部分时间你不用直接来跟内核交互。
Sessions
POE里的会话相当于一个真的操作系统里的进程。一个会话就是一个POE程序,当它在运行时不停地在状态之间切换。它能够创建子会话,传递信息给其他的会话等等。每个会话都能通过调用 heap 存储会话特有的数据,这些数据在会话中的各个状态都是可以存取的。
POE有一个简单的协作的多任务处理模块;每个会话在同一个OS进程中执行,不需要任何线程和进程。对于这样的会话,你应该特别注意在POE程序中使用模块化的系统调用。
这些就是POE的最基本的部分,但是在我们开始实际的代码前还需要稍微ie解释一些高级的POE的部分。
Drivers
驱动位于POE最低级的I/O层。目前,只有一个驱动包括在POE的安装中-- POE::Driver::SysRW,它读写文件句柄中的数据。不管怎样,我们都不需要直接使用驱动。
Filters
过滤器,在某个方面来说,是非常有用的。过滤器是一个简单的接口,转换数据块从一个格式成另一个格式。例如,POE::Filter::HTTPD 能够在 HTTP 1.0 requests 之间 HTTP::Request 对象互相转换。POE::Filter::Line 能够见数据流转换成序列化数据行。
Wheels
Wheels包含了每天任务的可重复利用的高级逻辑部分。它们是POE封装有用代码的方法。在POE中你需要处理到的Wheels包括事件驱动的输入输出数据和网络连接的创建。Wheels经常使用过滤器和驱动来处理和传递数据。这个描述太过于含糊了,下面的代码将会给出一个例子来说明这个概念。
Components
组件是一个会话,被设计来受控于其它会话。你的会话能够从它们那里来处理命令和接受事件,这点很象真实操作系统中的通过IPC通讯。一个组件的例子就是POE::Component::IRC,一个创建基于POE的IPC客户端接口,或者是POE::Component::Client::HTTP,一个事件驱动的Perl实现的HTTP用户代理。组件同样是POE非常有用的一个部分。
A Simple Example
在这个例子中,我们会建立一个守护进程服务端,它接受TCP连接,同时打印由它的客户端提交的算术问题的答案。当有人连接它的端口31008,它将打印“Hello,client”。客户端能够提交一个算术表达式,以一个新行结束提交,服务端将送回表达式的答案。够简单吧。
写这样一个POE程序跟写一个Unix下的守护进程的传统方法没有什么很大的不同。我们将有一个服务器会话在端口31008监听TCP连接。每当连接到来时,它将创建一个子会话来处理连接。每个子会话都能跟用户交互,然后在连接断开后安静地死掉。所有这些用Perl简单地实现只需要模块化的72行代码就行了。
这个程序简单地以下面的代码开始:
1 #!/usr/bin/perl -w
2 use strict;
3 use Socket qw(inet_ntoa);
4 use POE qw( Wheel::SocketFactory Wheel::ReadWrite
5 Filter::Line Driver::SysRW );
6 use constant PORT => 31008;
这一段我们导入了脚本所需要的模块以及函数,并定义了监听端口的常量值。”use POE;”后面的 qw()语句一次导入了很多POE::模块。
现在开始本文最酷的部分:
7 new POE::Session (
8 _start => \&server_start,
9 _stop => \&server_stop,
10 );
11 $poe_kernel->run();
12 exit;
这样就已经是一个完整的程序了!我建立了服务端会话,告诉POE内核开始执行事件,并且在事件结束后退出。(当没有任何会话需要管理的时候,POE内核就认为事情做完了,但是既然我们已经打算把服务端会话放在一个无穷的循环中,它将不会以这种发式退出)POE在你写"use POE;"h后,自动输出变量$poe_kernel 到你的名字空间中。
new POE::Session 方法调用需要解释一下。当你创建一个会话时,你给内核一个这个会话将接受事件的列表。在上面的代码中,意味着这个新的会话将通过调用&server_start,&server_stop处理_start和_stop事件。任何一个受到的但没有列举到的事件,将会被会话忽略。_Start和_Stop事件对于一个POE会话来说是一个特殊事件。_start事件是当一个会话创建时第一件要做的事情,当会话将要被摧毁时内核就通知这个会话进入_stop状态。
现在,我们已经写了完整的程序,我们还要写当会话运行时要执行的状态代码。接下来,让我们以&server_start开始。当主要的服务端会话被创建,开始执行程序时,它将会被调用。
13 sub server_start {
14 $_[HEAP]->{listener} = new POE::Wheel::SocketFactory
15 ( BindPort => PORT,
16 Reuse => 'yes',
17 SuccessState => \&accept_new_client,
18 FailureState => \&accept_failed
19 );
20 print "SERVER: Started listening on port ", PORT, ".\n";
21 }
这是一个有关POE状态的很好的例子。首先:看到变量$_[HEAP]了吗?POE有一个特殊的方法来传递参数。@_数组是由许多外部的参数打包在一起的。外部参数包括目前内核,会话和状态名的一个引用,堆栈的引用,以及其它一些东西。我们可以用POE导出的特殊常量来作为这个数组索引,比如 HEAP, SESSION, KERNEL, STATE和ARG0~ARG9(用户提供的参数)。跟POE的大部分设计一样,这样子的安排能够最大化后面的兼容性而又不牺牲速度。上面的例子存储了一个 SocketFactory wheel 在键值 listen 下。
POE::Wheel::SocketFactory wheel是POE里最酷的东西之一。你能够用它创建任何一个流socket(UDP socket还不行),而不需要担心细节。上面的代码将创建一个SocketFactory来监听特殊的TCP端口的新连接。当连接建立时,它将调用&accept_new_client来传递一个新的客户端,如果在这其中发生错误了,它将调用&accept_failed,而不是让我们自己来处理错误。这差不多就是用POE在网络中要做的。
我们存储一个wheel在堆栈中,以防止在状态的最后被Perl的垃圾收集机制意外地消灭掉,——这个方法在会话的每个状态中到处存在。现在我们来看&server_stop 状态的代码: 22 sub server_stop {
23 print "SERVER: Stopped.\n";
24 }
这段不需要多解释了。接下来的是我们创建一个新的会话来处理每个新到的连接。 25 sub accept_new_client {
26 my ($socket, $peeraddr, $peerport) = @_[ARG0 .. ARG2];
27 $peeraddr = inet_ntoa($peeraddr);
28 new POE::Session (
29 _start => \&child_start,
30 _stop => \&child_stop,
31 main => [ 'child_input', 'child_done', 'child_error' ],
32 [ $socket, $peeraddr, $peerport ]
33 );
34 print "SERVER: Got connection from $peeraddr:$peerport.\n";
35 }
当客户端成功地建立了跟服务端的连接后,我们的 POE::Wheel::SocketFactory将调用这个子例程。
我们将socket地址转换成人可读的IP地址,并且建立一个新的会话来跟客户端通讯。这跟前面的
POE::Session 构造器很象,但是有几点在接下来要说明。
@_[ARG0 .. ARG2]是($_[ARG0], $_[ARG1], $_[ARG2])的快捷方式。你会在POE程序中
看到很多这样的数组切片。
看31行:好像有点奇怪,其实这是一种很聪明的缩写。它可以用下面的比较长的方法来重写:
new POE::Session (
...
child_input => &main::child_input,
child_done => &main::child_done,
child_error => &main::child_error,
...
);
这是一个方便的做法来写出很多的状态名字,这些名字跟事件的名字是一样的。——你可以传递包名或者
对象来作为键,或者是任何一个有子例程,方法名组成的数组引用。可以参考POE::Session得到更多的信息。
最后,在POE::Session 构造器参数列表最后的数组引用是我们要手动传递给_start状态的参数列表。
如果POE::Wheel::SocketFactory在创建监听端口或者接受连接时产生了问题,下面的会发生:
36 sub accept_failed {
37 my ($function, $error) = @_[ARG0, ARG2];
38 delete $_[HEAP]->{listener};
39 print "SERVER: call to $function() failed: $error.\n";
40 }
打印错误信息是正常的处理方式,但是我们为什么还要从堆栈中删除SocketFactory wheel呢?答案在于
POE管理会话资源的方式。只要会话还能产生和接受事件,那么它就被认为是活动的。如果没有wheel,POE内核
会认为会话已经死了,并收集会话的资源。服务端得到事件的唯一方式就是从SocketFactory wheel,如果
它被消灭了,POE内核会一直等待,直到所有它的子会话结束,再收集所有占用的资源。在这一点上,既然
已经没有剩下的会话需要执行,POE内核将会结束并退出。
所以,基本上来说,这是最通常的结束POE会话的方法:结束所有会话资源,让内核干剩下的工作。现在,
我们谈到子会话的细节。
41 sub child_start {
42 my ($heap, $socket) = @_[HEAP, ARG0];
43 $heap->{readwrite} = new POE::Wheel::ReadWrite
44 ( Handle => $socket,
45 Driver => new POE::Driver::SysRW (),
46 Filter => new POE::Filter::Line (),
47 InputState => 'child_input',
48 ErrorState => 'child_error',
49 );
50 $heap->{readwrite}->put( "Hello, client!" );
51 $heap->{peername} = join ':', @_[ARG1, ARG2];
52 print "CHILD: Connected to $heap->{peername}.\n";
53 }
每当新的子会话被创建来处理最新的连接客户端时上面的代码就会被调用。这里我们要介绍一种新的
POE wheel:ReadWrite wheel,这是一个由事件驱动的处理I/O任务的wheel。我们把它传递给文件
句柄,一个驱动,一个过滤器等等。接着,每当新的数据到达文件句柄时,wheel将发送一个child_input
事件给会话,如果有任何错误发生就会发送一个child_error 事件。
我们理解用这个新的wheel来输出字符串”Hello,client”给socket。最后,我们存储在堆栈中
的客户端地址和端口,并打印成功信息。
我们省略了 child_stop 的状态讨论。它只有一行长度。现在我们直接看主要的child_input 程序。 57 sub child_input {
58 my $data = $_[ARG0];
59 $data =~ tr{0-9+*/()-}{}cd;
60 return unless length $data;
61 my $result = eval $data;
62 chomp $@;
63 $_[HEAP]->{readwrite}->put( $@ || $result );
64 print "CHILD: Got input from peer: \"$data\" = $result.\n";
65 }
当客户端发送给我们一行数据,我们取得里面简单的算术表达式并eval它,然后把结果或者错误信息
发送给客户端。通常情况下,将客户端发送来的不可信任的数据直接就eval是非常危险的,所以我们要确保
在eval前,将字符串中所有的非数学字符全部过滤掉。子会话将一直保持接受数据直到客户端断开。
所有事件驱动的应用都是使用POE的好地方。
源代码:http://www.perl.com/2001/01/poe-math3.pl