//{{{\n译者/作者:joe jiang\n出处:中国Perl协会 FPC(Foundation of Perlchina)\n原 名:Class::DBI\n作 者:Tony Bowden\n原 文:http://www.perl.com/pub/a/2002/11/27/classdbi.html\n发 表:November 27, 2002\n请保护作者的著作权,维护作者劳动的结晶。\n//}}}\n\n\n最近perl.com上的几篇文章(包括Phasebook设计模式)都讨论了Perl代码和数据库打交道的问题。Terrence Brannon的DBIx::Recordset一文试图展示数据库相关的程序也可以更加简单和易于维护。这篇文章是要用Class::DBI来使得这个努力更进一步。\n\nClass::DBI奖励懒惰和简单。目标是使简单的数据库操作几乎不用编程,同时使困难的变得有可能。对很多简单的数据库应用来说,它使我们完全不用编写SQL,另一方面它也不强迫你用很复杂的数据结构来表示一个复杂查询。如果你确实需要原始SQL的功能或表达能力,它也会适时的给你让路。\n\n最容易了解Class::DBI的方法就是用它来建立一个例子程序。这篇文章里面我要做个工具来分析我的电话帐单。\n\nData::BT::PhoneBill(可在CPAN下载)给我们一个从BT的网站下载电话帐单的方法。有了这个模块和一些最近的通话帐单条目,我们就可以用数据库来存储详细信息以备分析。\n\nClass::DBI的基本概念是数据库中的每个表都有相应的类。尽管每个类都可以自己做连接(数据库)相关的事情,最好还是有个类来把这些事情封装起来。所以我们要建立数据库并为应用程序建立基类:\n//{{{\npackage My::PhoneBill::DBI;\nuse base 'Class::DBI';\n\n__PACKAGE__->set_db('Main', 'dbi:mysql:phonebill', 'u/n', 'p/w');\n\n1;\n//}}}\n我们只是从Class::DBI继承并用'set_db'方法来建立数据库连接。目前这就是我们在这个类里面需要做的事情,下面我们开始建立用于存储通话信息的表:\n//{{{\nCREATE TABLE call (\n callid MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,\n number VARCHAR(20) NOT NULL,\n destination VARCHAR(255) NOT NULL,\n calldate DATE NOT NULL,\n calltime TIME NOT NULL,\n type VARCHAR(50) NOT NULL,\n duration SMALLINT UNSIGNED NOT NULL,\n cost FLOAT(8,1)\n);\n//}}}\n为这个我们要建立相应的类:\n//{{{\npackage My::PhoneBill::Call;\nuse base 'My::PhoneBill::DBI';\n\n__PACKAGE__->table('call');\n__PACKAGE__->columns(All => qw/callid number destination calldate calltime type duration cost/);\n\n1;\n//}}}\n我们从基类来继承连接信息,并声明我们要用的表和它包含的列。现在我们要开始填充表里面的数据了。\n\n我们建立了一个简单的名为"populate_phone_bill"的脚本:\n//{{{\n#!/usr/bin/perl\n\nuse Data::BT::PhoneBill;\nuse My::PhoneBill::Call;\n\nmy $file = shift or die "Need a phone bill file";\nmy $bill = Data::BT::PhoneBill->new($file) or die "Can't parse bill";\n\nwhile (my $call = $bill->next_call) {\n My::PhoneBill::Call->create({\n number => $call->number,\n calldate => $call->date,\n calltime => $call->time,\n destination => $call->destination,\n duration => $call->duration,\n type => $call->type,\n cost => $call->cost,\n });\n}\n//}}}\ncreate()调用执行SQL来为每行数据INSERT行。因为我们在使用Class::DBI而且设置了主键为 AUTO_INCREMENT,我们就不需要为那个列来提供一个值。对于支持序列的数据库来说,我们也可以提醒Class::DBI需要使用哪个序列来为主键提供下一个唯一值。\n\n现在我们已经有了一个填充了通话数据的表,接着要开始查询数据了。下面就要写个简单的脚本来报告与特定号码的通话记录。\n//{{{\n#!/usr/bin/perl\n \nuse My::PhoneBill::Call;\n \nmy $number = shift or die "Usage: $0 ";\n \nmy @calls = My::PhoneBill::Call->search(number => $number);\nmy $total_cost = 0;\n \nforeach my $call (@calls) {\n $total_cost += $call->cost;\n printf "%s %s - %d secs, %.1f pence\sn",\n\n $call->calldate, $call->calltime, $call->duration, $call->cost;\n}\nprintf "Total: %d calls, %d pence\sn", scalar @calls, $total_cost;\n//}}}\n这里看到Class::DBI提供了一个'search'方法给我们用。我们提供一对对的列/值的杂凑来得到所有符合条件的记录。每个记录都是Call对象的一个实例,每个实例也有对应于列名字的存取方法。(这是一个可以调整值的方法,我们可以用来修改记录,但目前我们只关心报表)\n\n有了这个脚本后,如果我们想看看打了报时台几次,就可以运行这个命令\n//{{{\n>perl calls_to 123\n2002-09-17 11:06:00 - 5 secs, 8.5 pence\n2002-10-19 21:20:00 - 8 secs, 8.5 pence\nTotal: 2 calls, 17 pence\n//}}}\n同样的,若我们想看看某天的所有通话,就可以写个'calls_on'脚本:\n//{{{\n#!/usr/bin/perl\n \nuse My::PhoneBill::Call;\n \nmy $date = shift or die "Usage: $0 ";\n \nmy @calls = My::PhoneBill::Call->search(calldate => $date);\nmy $total_cost = 0;\n \nforeach my $call (@calls) {\n $total_cost += $call->cost;\n printf "%s) %s - %d secs, %.1f pence\sn",\n $call->calltime, $call->number, $call->duration, $call->cost;\n}\nprintf "Total: %d calls, %d pence\sn", scalar @calls, $total_cost;\n\n运行这个命令得到结果:\n\n>perl calls_on 2002-10-19\n...\n18:36:00) 028 9024 4222 - 41 secs, 4.2 pence\n21:20:00) 123 - 8 secs, 8.5 pence\n...\nTotal: 7 calls, 92 pence\n//}}}\n就像前面保证的我们可以不用写SQL就能存取数据库。虽然还没有做什么非常复杂的事情,但是这个小例子也可以让我们的生活更加容易。\n!建立一个电话本\n过去我总是对电话号码有很好的记性。但是诺基亚,爱立信这些公司密谋陷害我。我手机里面的内嵌电话本使我对10/11位数字的记忆能力降低了。现在我看到'calls_on'的输出的时候,已经没法知道"028 9024 4222"代表什么。现在我们需要一个存有联系人信息的电话本来解释这些报表中的数字。\n\n第一步要做的是把我们的信息整理的更整洁点。我们将把号码和通话方两列移到"recipient"表,并增加一个名字列。"Destination"这个词也不能很好的表达和号码的关系,更不用说和通话之间的关系了,所以我们要把它改名为"location"。\n//{{{\nCREATE TABLE recipient (\n recipid MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,\n number VARCHAR(20) NOT NULL,\n location VARCHAR(255) NOT NULL,\n name VARCHAR(255),\n KEY (number)\n);\n//}}}\n接着我们建立表相应的类:\n//{{{\npackage My::PhoneBill::Recipient;\nuse base 'My::PhoneBill::DBI';\n \n__PACKAGE__->table('recipient');\n__PACKAGE__->columns(All => qw/recipid number location name/);\n\n1;\n//}}}\n还需要修改Call表的定义:\n//{{{\nCREATE TABLE call (\n callid MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,\n recipient MEDIUMINT UNSIGNED NOT NULL,\n calldate DATE NOT NULL,\n calltime TIME NOT NULL,\n type VARCHAR(50) NOT NULL,\n duration SMALLINT UNSIGNED NOT NULL,\n cost FLOAT(8,1),\n KEY (recipient)\n);\n//}}}\n和相应的类: package My::PhoneBill::Call; use base 'My::PhoneBill::DBI'; __PACKAGE__->table('call'); __PACKAGE__->columns(All => qw/callid recipient calldate calltime type duration cost/); 1; 下面我们要修改填充数据库的脚本:\n//{{{\n#!/usr/bin/perl\nuse Data::BT::PhoneBill;\nuse My::PhoneBill::Call;\nuse My::PhoneBill::Recipient;\n\nmy $file = shift or die "Need a phone bill file";\nmy $bill = Data::BT::PhoneBill->new($file) or die "Can't parse bill";\n\nwhile (my $call = $bill->next_call) {\n my $recipient = My::PhoneBill::Recipient->find_or_create({\n number => $call->number,\n location => $call->destination,\n });\n My::PhoneBill::Call->create({\n recipient => $recipient->id,\n calldate => $call->date,\n calltime => $call->time,\n duration => $call->duration,\n type => $call->type,\n cost => $call->cost,\n });\n}\n//}}}\n这次我们要作的变动是先建立Recipient,这样才可以从Call中建立指向它的链接。但是我们不必为每个通话建立一个新的 Recipient,若我们已经打过电话给某人,那recipient表就会有一个记录。因此我们用find_or_create来取回可能已经存在的条目,或建立一个新条目。\n\n表里面重新填充数据后,我们回到报表脚本。\n\n我们的calls_on脚本现在会失败,因为我们现在不能直接获得通话的'number'了。下面我们得修改成:\n//{{{\n#!/usr/bin/perl\n \nuse My::PhoneBill::Call;\n \nmy $date = shift or die "Usage: $0 ";\n \nmy @calls = My::PhoneBill::Call->search(calldate => $date);\nmy $total_cost = 0;\n \nforeach my $call (@calls) {\n $total_cost += $call->cost;\n printf "%s) %s - %d secs, %.1f pence\sn",\n $call->calltime, $call->recipient, $call->duration, $call->cost;\n}\nprintf "Total: %d calls, %d pence\sn", scalar @calls, $total_cost;\n//}}}\n但是这个脚本的运行结果和我们的期望不同:\n//{{{\n> perl calls_on 2002-10-19\n...\n18:36:00) 67 - 41 secs, 4.2 pence\n21:20:00) 47 - 8 secs, 8.5 pence\n...\nTotal: 7 calls, 92 pence\n//}}}\n我们得到了recipient表中的ID而不是电话号码,ID不过是一个自增的值。\n\n为了使这个值成为有意义的值,我们在Call类中增加下面的行:\n//{{{\n__PACKAGE__->has_a(recipient => 'My::PhoneBill::Recipient');\n//}}}\n这告诉它recipient方法不是简单的输出一个值而是把那个值转化成一个Recipient类的实例。\n\n当然calls_on还是不正确的:\n//{{{\n> perl calls_on 2002-10-19\n...\n18:36:00) My::PhoneBill::Recipient=HASH(0x835b6b8) - 41 secs, 4.2 pence\n21:20:00) My::PhoneBill::Recipient=HASH(0x835a210) - 8 secs, 8.5 pence\n...\nTotal: 7 calls, 92 pence\n//}}}\n但是现在只要做个小修改就可以了:\n//{{{\nprintf "%s) %s - %d secs, %.1f pence\sn",\n $call->calltime, $call->recipient->number, $call->duration, $call->cost;\n//}}}\n现在所有的一切又完美了:\n//{{{\n> perl calls_on 2002-10-19\n...\n18:36:00) 028 9024 4222 - 41 secs, 4.2 pence\n21:20:00) 123 - 8 secs, 8.5 pence\n...\nTotal: 7 calls, 92 pence\n//}}}\ncalls_to脚本需要更多技巧,因为搜索的开始现在是recipent而不是call。\n\n所以我们把搜索的开始改成:\n//{{{\nmy ($recipient) = My::PhoneBill::Recipient->search(number => $number)\n or die "No calls to $number\sn";\n//}}}\n然后我们需要获得打往那个recipient的所有通话。为了实现这个我们需要申明Recipient和Call之间的关系。和在Call类建立的has_a关系不同,recipient表并不存储和call表的每条通话记录相关的信息。对这种情况我们要为Recipient类添加 has_many申明。\n//{{{\n__PACKAGE__->has_many(calls => 'My::PhoneBill::Call');\n//}}}\n这就为Recipient对象实例建立了一个新的名叫calls的方法,调用它会返回用recipient外键相关的所有Call对象。\n\n这样在calls_to脚本里面已经找到了recipient的前提下,我们只要这样:\n//{{{\n my @calls = $recipient->calls;\n//}}}\n这个脚本现在可以和以前一样工作了:\n//{{{\n#!/usr/bin/perl\n \nuse My::PhoneBill::Recipient;\n \nmy $number = shift or die "Usage: $0 ";\n \nmy ($recipient) = My::PhoneBill::Recipient->search(number => $number)\n or die "No calls to $number\sn";\nmy @calls = $recipient->calls;\n \nmy $total_cost = 0;\n \nforeach my $call (@calls) {\n $total_cost += $call->cost;\n printf "%s %s - %d secs, %.1f pence\sn",\n $call->calldate, $call->calltime, $call->duration, $call->cost;\n}\nprintf "Total: %d calls, %d pence\sn", scalar @calls, $total_cost;\n//}}}\n输出也是老样子:\n//{{{\n> perl calls_to 123\n2002-09-17 11:06:00 - 5 secs, 8.5 pence\n2002-10-19 21:20:00 - 8 secs, 8.5 pence\nTotal: 2 calls, 17 pence\n//}}}\n下面我们写个脚本来为地址本里面某个电话号码设置名字:\n//{{{\n#!/usr/bin/perl\n \nuse My::PhoneBill::Recipient;\n \nmy($number, $name) = @ARGV;\ndie "Usage $0 \sn" unless $number and $name;\n \nmy $recip = My::PhoneBill::Recipient->find_or_create({number => $number});\nmy $old_name = $recip->name;\n$recip->name($name);\n$recip->commit;\n \nif ($old_name) {\n print "OK. $number changed from $old_name to $name\sn";\n} else {\n print "OK. $number is $name\sn";\n}\n//}}}\n这使我们可以建立数字和名字间的关联:\n//{{{\n> perl add_phone_number 123 "Speaking Clock"\nOK. 123 is Speaking Clock\n//}}}\n现在只要很小的改动就可以使calls_on脚本输出我们熟知的名字:\n//{{{\nprintf "%s) %s - %d secs, %.1f pence\sn",\n $call->calltime, $call->recipient->name || $call->recipient->number,\n $call->duration, $call->cost;\n\n>perl calls_on 2002-10-19\n...\n18:36:00) 028 9024 4222 - 41 secs, 4.2 pence\n21:20:00) Speaking Clock - 8 secs, 8.5 pence\n...\nTotal: 7 calls, 92 pence\n//}}}\n要让calls_to脚本能同时接受名字或号码参数,我们可以这样:\n//{{{\nmy $recipient = My::PhoneBill::Recipient->search(name => $number)\n || My::PhoneBill::Recipient->search(number => $number)\n || die "No calls to $number\sn";\n//}}}\n然而,一个名字可能对应多个号码。因为我们在标量环境而不是列表环境里调用search方法,我们会得到一个iterator而不是每个Recipient对象。我们需要遍历每个对象来完成工作:\n//{{{\n my @calls;\n while (my $recip = $recipient->next) {\n push @calls, $recip->calls;\n }\n//}}}\n(打印每个号码的小计功能留给读者作练习。)\n//{{{\n>perl calls_to "Speaking Clock"\n2002-09-17 11:06:00 - 5 secs, 8.5 pence\n2002-10-19 21:20:00 - 8 secs, 8.5 pence\nTotal: 2 calls, 17 pence\n//}}}\n!和其他模块协作\n有时我们需要在数据库里存储其他模块需要使用的数据。比如我们通话需要有个不同的日期类型,我们更喜欢使用Date::Simple类型的对象。Class::DBI也使这个目标易于实现。\n\n我们还是在Call类里用has_a来申明这个关系:\n//{{{\n__PACKAGE__->has_a(recipient => 'My::PhoneBill::Recipient');\n__PACKAGE__->has_a(calldate => 'Date::Simple');\n//}}}\n这样我们获取calldate的时候它就自动被展开成Date::Simple对象。这样我们就可以为calls_to的输出设计一个更漂亮的格式:\n//{{{\nprintf "%s %s - %d secs, %.1f pence\sn",\n $call->calldate->format("%d %b"), $call->calltime,\n $call->duration, $call->cost;\n\n> perl calls_to "Speaking Clock"\n17 Sep 11:06:00 - 5 secs, 8.5 pence\n19 Oct 21:20:00 - 8 secs, 8.5 pence\nTotal: 2 calls, 17 pence\n//}}}\nClass::DBI假定任何非Class::DBI类是通过new方法展开,通过stringification来压缩。因为Date:: Simple确实支持这个,我们就不需要再做更多了。若不是这样,例如你想用Time::Piece类而不是Date::Simple类,你就要告诉 Class::DBI如何在内存值和数据库之间进行数据展开和压缩。\n//{{{\n__PACKAGE__->has_a(calldate => 'Time::Piece',\n inflate => sub { Time::Piece->strptime(shift, "%Y-%m-%d") },\n deflate => 'ymd'\n);\n//}}}\n将Time::Piece对象压缩成适合MySQL的ISO日期类型非常容易:你只要调用类的ymd()方法就好了。这样我们就把它序列化成一个字符串。解压就麻烦了,这需要调用一个带两个参数的strptime()方法。这样我们必须使用一个函数引用。这样我们可以告诉strptime用什么格式来分析日期字符串。\n\n用Time::Piece而不是Date::Time需要我们对输出部分的代码如下更改:\n//{{{\nprintf "%s %s - %d secs, %.1f pence\sn",\n $call->calldate->strftime("%d %b"), $call->calltime,\n $call->duration, $call->cost;\n//}}}\n!最常用号码\nBT给我们一个设定10个最常用号码并节省通话费的服务。这就使我们有必要看看那些号码上花了最多的钱。我们假定那些只打过一次的电话号码没必要分析。我们只关心头十个不只一次通话的花费最多的号码。\n\n前面讲过,Class::DBI不是试图用语法来表达任何SQL,因此有的数据没法直接获得。我们还是用最简单的方法。\n\n首先我们为Recipient类增加一个方法来告诉我们与这人通话我们花了多少:\n//{{{\nuse List::Util 'sum';\n\nsub total_spend {\n my $self = shift;\n return sum map $_->cost, $self->calls;\n}\n//}}}\n然后我们就可以写一个top_ten脚本了:\n//{{{\n#!/usr/bin/perl\n \nuse My::PhoneBill::Recipient;\n \nmy @recipients = My::PhoneBill::Recipient->retrieve_all;\nmy @regulars = grep $_->calls > 1, @recipients;\nmy @sorted = sort { $b->total_spend <=> $a->total_spend } @regulars;\n \nforeach my $recip (@sorted[0 .. 9]) {\n printf "%s - %d calls = %d pence\sn",\n $recip->name || $recip->number,\n scalar $recip->calls,\n $recip->total_spend;\n}\n//}}}\n这是很慢的方法,尤其是数据库里有百十个以上的通话记录的时候。主要的开销在于我们总是用方法的返回值来作排序的比较值。用Schwartzian Transform来替换排序会显著的提高性能:\n//{{{\n my @sorted = map $_->[0],\n sort { $b->[1] <=> $a->[1] }\n map [ $_, $_->total_spend ], @regulars;\n//}}}\n在数据库内容显著的增多以前,这个方法就快多了,尤其是你不经常运行这个脚本的情况下。\n\n但是这还不够,我们可以直接用SQL。理所应当的,在为速度优化时你会损失其他的性能,在这个例子里面损失可能是可移植性。现在我们的例子用MySQL,我们会在Recipient.pm里面增加MySQL才支持的查询:\n//{{{\n__PACKAGE__->set_sql(top_ten => qq{\n SELECT recipient.recipid,\n SUM(call.cost) AS spend,\n COUNT(call.callid) AS calls\n FROM recipient, call\n WHERE recipient.recipid = call.recipient\n GROUP BY recipient.recipid\n HAVING calls > 1\n ORDER BY spend DESC\n LIMIT 10\n});\n//}}}\n接着我们可以建立一个返回很多对象的方法:\n//{{{\nsub top_ten {\n my $class = shift;\n my $sth = $class->sql_top_ten;\n $sth->execute;\n return $class->sth_to_objects($sth);\n}\n//}}}\n任何用set_sql设定的SQL都可以用sql_取出来成为一个编译好代执行的DBI语句句柄。所以我们用my $sth = $class->sql_top_ten来取回top_ten。\n\n我们可以就这么干并调用那些传统的DBI命令如fetchrow_array等,也可以更进一步偷懒。既然我们的查询输出的列中有一个是 Recipient的主键,我们就可以把查询结果喂给sth_to_objects,这个Class::DBI的底层方法使得查询可以返回对象实例列表。\n\n这样我们的脚本就变的简单了:\n//{{{\nforeach my $recip (My::PhoneBill::Recipient->top_ten) {\n printf "%s - %d calls = %d pence\sn",\n $recip->name || $recip->number,\n scalar $recip->calls,\n $recip->total_spend;\n}\n//}}}\n如上所示,Class::DBI使得通常的数据库编程变得不值一提(不用写一行SQL代码)。但在你真的需要的时候,也可以很容易的编写你需要的SQL并执行。
[[首页]]
许多网站都是通过 HTTP 认证来限制连接. 当用户请求一个限制页面时, HTTP 服务器回复 “That document is part of a protected ‘realm’ and you can access it only if you re-request it and add some special authorization headers to your request”. ( 你现在请求了一个限制区域 , 如果你需要重新发送一个带有认证信息的 header 才可以连入. )\n\nUnicode.org 的管理员为了防止机器人访问邮件组获取发信人地址,要求先进行 HTTP 认证。用户名和密码是公开的:用户名: unicode-ml 密码: unicode\n\n假设一个限制页面的地址是\n\n@@http://www.unicode.org/mail-arch/unicode-ml/y2002-m08/0067.html@@\n\n如果你在浏览器里请求这个地址,一个新的窗口跳出,显示 “Enter username and password for ‘Unicode-MailList-Archives’ at server ‘www.unicode.org’”。请你输入用户名和密码,就像这样:\n[img[img/burke_auth_snapshot.gif]]\n\n单纯使用 LWP 请求这个网址 :\n//{{{\n use LWP 5.64;\n my $browser = LWP::UserAgent->new;\n\n my $url =\n 'http://www.unicode.org/mail-arch/unicode-ml/y2002-m08/0067.html';\n my $response = $browser->get($url);\n\n die "Error: ", $response->header('WWW-Authenticate') || \n 'Error accessing',\n # ('WWW-Authenticate' is the realm-name)\n "\sn ", $response->status_line, "\sn at $url\sn Aborting" \n unless $response->is_success;\n//}}}\n你将得到以下错误:\n//{{{\n Error: Basic realm="Unicode-MailList-Archives" \n 401 Authorization Required\n at http://www.unicode.org/mail-arch/unicode-ml/y2002-m08/0067.html\n Aborting at auth1.pl line 9. [or wherever]\n//}}}\n这是因为 LWP 不知道 host www.unicode.org 里的 “Unicode-MailList-Archives” 区的用户名和地址。解决这个问题最简单的方法是使用 credentials 方法来提供用户名和密码:\n//{{{\n $browser->credentials(\n 'servername:portnumber',\n 'realm-name',\n 'username' => 'password'\n );\n//}}}\n通常, 端口是 80. credentials 函数要在发请求之前调用.比如:\n//{{{\n $browser->credentials(\n 'reports.mybazouki.com:80',\n 'web_server_usage_reports',\n 'plinky' => 'banjo123'\n );\n//}}}\n我们的 unicode.org 的例子可以写成\n//{{{\n $browser->credentials( # add this to our $browser 's "key ring" \n 'www.unicode.org:80',\n 'Unicode-MailList-Archives',\n 'unicode-ml' => 'unicode'\n );\n//}}}
/*\n''套件:'' zh_CN\n''功能:'' TiddliWiki 界面简体中文化\n''作者:'' Bram Chen, http://ptw.sf.net/\n''版本:'' 2.0.10\n''说明:''\n* Apr 28 2006\n** 修改用词:"巨集" -> "宏"。\n* Mar 23 2006\n** 添加 config.messages.tiddlerLinkTooltip = "%0 - %1, %2";\n** 添加 config.commands.saveTiddler.readOnlyText = "完成";\n** 添加 config.commands.saveTiddler.readOnlyTooltip = "返回正常显示模式";\n** 改变 config.messages 翻译方式。\n* Feb 24 2006\n** 添加 config.messages.unsavedChangesWarning: "注意! 尚未保存变更\sn\sn[确定]存档,或[取消]放弃存档?",\n** 添加 config.messages.confirmExit: "--------------------------------\sn\snTiddlyWiki 以更改内容尚未保存,继续的话将遗失这些更动\sn\sn--------------------------------",\n** 添加 config.messages.saveInstructions: "SaveChanges",\n** 添加 config.commands.editTiddler.readOnlyText = "检视";\n** 添加 config.commands.editTiddler.readOnlyTooltip = "检视本文之原始内容";\n** 添加 config.commands.saveTiddler.readOnlyText = "完成";\n** 添加 config.commands.saveTiddler.readOnlyTooltip = "正常显示模式";\n* Feb 04 2006\n** config.shadowTiddlers.TabMore 添加 TabMoreShadowed\n* Jan 25 2006\n** Reworking config.views\n* Dec 31 2005\n** 添加 config.views.wikified.toolbarJump: {text: "卷页", tooltip: "卷页至其他已开启的文章"},\n** 修改 config.views.wikified.editor.toolbarDelete: {text: "删除", tooltip: "删除文章", warning: "确定删除 '%0'?"},\n** 删除 config.macros.jump.\n** 添加 config.messages.messageClose: {text: "关闭", tooltip: "关闭此讯息"},\n** 修改 config.views.wikified. tag : {...., openTag = "开启标签 '%0'" , ....};\n* Dec 06 2005:\n** 添加 config.macros.tagging.label = "引用标签:";\n** 添加 config.macros.tagging.labelNotTag = "无引用标签";\n** 添加config.macros.tagging.tooltip = "列出标签为 '%0' 的文章";\n** 修改config.macros.search.successMsg = " %0 篇符合条件: %1";\n** 修改config.macros.search.failureMsg = " 无符合条件: %0";\n* Nov 30 2005:\n** 添加 config.views.wikified: {toolbarCloseOthers: {text: "关闭其他", tooltip: "关闭其他文章"},\n** 添加 config.macros.jump.label = "卷页";\n** 添加 config.macros.jump.prompt = "卷页至其他已开启的文章";\n* Nov 23 2005:\n** 修改 config.messages.macroError: "巨集 {{{<<%0>>}}}运行错误",\n** 修改 config.messages.macroErrorDetails: "运行巨集{{{<<%0>>}}}时,发生错误 :\sn%1",\n* Oct 28, 2005:\n** 添加 config.messages.shadowedTiddlerToolTip\n** 添加 config.macros.list.shadowed.prompt\n** 修改 config.views.wikified.tag\n*/\n//{{{\nversion.extensions.zh_CN = {major: 2, minor: 0, revision: 10,\n date: new Date("May 12, 2006"),\n name: "zh_CN",\n type: "Plugin",\n author: "BramChen",\n source: "http://sourceforge.net/project/showfiles.php?group_id=150646"\n};\n\n// // ''Messages''\n\nvar lingo = config.messages;\nlingo.customConfigError = "customConfig 错误 - '%1' - %0";\nlingo.savedSnapshotError = "此 TiddlyWiki 未正确保存,详见 http://www.tiddlywiki.com/#DownloadSoftware";\nlingo.subtitleUnknown = "(未知)";\nlingo.undefinedTiddlerToolTip = "'%0' 尚无内容";\nlingo.shadowedTiddlerToolTip = "'%0' 尚无内容, 但已定义隐藏的缺省值";\nlingo.tiddlerLinkTooltip = "%0 - %1, %2";\nlingo.externalLinkTooltip = "外部链接至 %0";\nlingo.noTags = "未设置标签的文章";\nlingo.notFileUrlError = "须先将此 TiddlyWiki 存至本机文件,才可保存变更";\nlingo.cantSaveError = "此浏览器无法保存变更,建议使用FireFox";\nlingo.invalidFileError = " '%0' 非有效之 TiddlyWiki";\nlingo.backupSaved = "已保存备份";\nlingo.backupFailed = "无法保存备份";\nlingo.rssSaved = "RSS feed 已保存";\nlingo.rssFailed = "无法保存 RSS feed ";\nlingo.emptySaved = "已保存范本";\nlingo.emptyFailed = "无法保存范本";\nlingo.mainSaved = "主要的TiddlyWiki已保存";\nlingo.mainFailed = "无法保存主要 TiddlyWiki. 所作的改变未保存";\nlingo.macroError = "宏 {{{<<}}}%0{{{>>}}} 运行错误";\nlingo.macroErrorDetails = "运行宏 {{{<<}}}%0{{{>>}}} 时,发生错误 :\sn%1";\nlingo.missingMacro = "无此宏";\nlingo.overwriteWarning = "'%0' 已存在,[确定]覆盖之";\nlingo.unsavedChangesWarning = "注意! 尚未保存变更\sn\sn[确定]保存,或[取消]放弃保存?";\nlingo.confirmExit = "--------------------------------\sn\snTiddlyWiki 以更改内容尚未保存,继续的话将遗失这些更动\sn\sn--------------------------------";\nlingo.saveInstructions = "SaveChanges";\nlingo.messageClose.text = "关闭";\nlingo.messageClose.tooltip = "关闭此讯息";\nlingo.dates.months = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"];\nlingo.dates.days = ["日", "一","二", "三", "四", "五", "六"];\nconfig.messages = lingo;\n\n// // ''config.views'' \nlingo =config.views;\nlingo.wikified.tag.labelNoTags = "未设标签";\nlingo.wikified.tag.labelTags = "标签: ";\nlingo.wikified.tag.openTag = "开启标签 '%0'";\nlingo.wikified.tag.tooltip = "显示标签为 '%0' 的文章";\nlingo.wikified.tag.openAllText = "开启以下所有文章";\nlingo.wikified.tag.openAllTooltip = "开启标签为 '%0' 的文章";\nlingo.wikified.tag.popupNone = "仅此文标签为 '%0'";\nlingo.wikified.defaultText = "";\nlingo.wikified.defaultModifier = "(missing)";\nlingo.wikified.shadowModifier = "(shadow)";\nlingo.editor.tagPrompt = "设定标签之间以空白区隔,[[标签含空白时请使用双中括弧]],或点选现有之标签加入";\nlingo.editor.tagChooser.text = "标签";\nlingo.editor.tooltip = "点选现有之标签加至本文章";\nlingo.editor.popupNone = "未设定标签";\nlingo.editor.tagTooltip = "加入标签 '%0'";\nlingo.editor.defaultText = "";\nconfig.views = lingo;\n\n// // ''Macros\nlingo =config.macros;\nlingo.search.label = " 查找";\nlingo.search.prompt = "查找本 Wiki";\nlingo.search.sizeTextbox = 15;\nlingo.search.accessKey = "F";\nlingo.search.successMsg = " %0 篇符合条件: %1";\nlingo.search.failureMsg = " 无符合条件: %0";\nlingo.tagging.label = "引用标签:";\nlingo.tagging.labelNotTag = "无引用标签";\nlingo.tagging.tooltip = "列出标签为 '%0' 的文章";\nlingo.timeline.dateFormat = "YYYY0MM0DD";\nlingo.allTags.tooltip = "显示文章- 标签为'%0'";\nlingo.allTags.noTags = "没有标签的文章";\nlingo.list.all.prompt = "依字母排序";\nlingo.list.missing.prompt = "被引用且内容空白的文章";\nlingo.list.orphans.prompt = "未被引用的文章";\nlingo.list.shadowed.prompt = "这些隐藏的文章已定义缺省内容";\nlingo.closeAll.label = "全部关闭";\nlingo.closeAll.prompt = "关闭所有开启中的 tiddler (编辑中除外)";\nlingo.saveChanges.label = "保存变更";\nlingo.saveChanges.prompt = "保存所有文章,产生新的版本";\nlingo.permaview.label = "引用链接";\nlingo.permaview.prompt = "可存取现有开启之文章的链接位址";\nlingo.newTiddler.label = "添加文章";\nlingo.newTiddler.prompt = "添加 tiddler";\nlingo.newJournal.label = "添加日志";\nlingo.newJournal.prompt = "添加 jounal";\nconfig.macros = lingo;\n\n// // ''Toolbars (Commands)''\nlingo = config.commands;\nlingo.closeTiddler.text = "关闭";\nlingo.closeTiddler.tooltip = "关闭本文";\nlingo.closeOthers.text = "关闭其他";\nlingo.closeOthers.tooltip = "关闭其他文章";\nlingo.editTiddler.text = "编辑";\nlingo.editTiddler.tooltip = "编辑本文";\nlingo.editTiddler.readOnlyText = "检视";\nlingo.editTiddler.readOnlyTooltip = "检视本文之原始内容";\nlingo.saveTiddler.text = "完成";\nlingo.saveTiddler.tooltip = "确定修改";\nlingo.saveTiddler.readOnlyText = "完成";\nlingo.saveTiddler.readOnlyTooltip = "正常显示模式";\nlingo.cancelTiddler.text = "取消";\nlingo.cancelTiddler.tooltip = "取消修改";\nlingo.cancelTiddler.readOnlyText = "完成";\nlingo.cancelTiddler.readOnlyTooltip = "返回正常显示模式";\nlingo.deleteTiddler.text = "删除";\nlingo.deleteTiddler.tooltip = "删除文章";\nlingo.deleteTiddler.warning = "确定删除 '%0'?";\nlingo.permalink.text = "引用连结";\nlingo.permalink.tooltip = "本文引用链接";\nlingo.references.text = "引用";\nlingo.references.tooltip = "引用本文的文章";\nlingo.references.popupNone = "本文未被引用";\nlingo.jump.text = "卷页";\nlingo.jump.tooltip = "卷页至其他已开启的文章";\nconfig.commands = lingo;\n\n// // ''ShadowTiddlers''\nlingo = config.shadowTiddlers;\nlingo.SideBarOptions = "<<search>><<closeAll>><<permaview>><<newTiddler>><<newJournal YYYY0MM0DD>><<saveChanges>><<slider chkSliderOptionsPanel OptionsPanel 偏好设置 '变更 TiddlyWiki 选项'>>";\nlingo.AdvancedOptions = "<<option chkOpenInNewWindow>> 链接开启于新视窗\sn<<option chkSaveEmptyTemplate>> 保存范本\sn<<option chkToggleLinks>> 点击文章使已开启者关闭\sn\sn<<option chkHttpReadOnly>> 隐藏编辑功能 ({{{http:}}})\sn<<option chkForceMinorUpdate>> 修改文章不变更日期时间\sn(确认修改同时按 Shift 键,或只按 Ctrl-Shift-Enter)\sn<<option chkConfirmDelete>> 删除文章前确认\sn\sn编辑模式中显示列数: <<option txtMaxEditRows>>\sn存放备份文件的资料夹: <<option txtBackupFolder>>";\nlingo.OptionsPanel = "这些设置将暂存于浏览器中,\sn请签名<<option txtUserName>>\sn (范例:WikiWord)\sn\sn <<option chkSaveBackups>> [[保存备份]]\sn<<option chkAutoSave>> [[自动保存]]\sn<<option chkGenerateAnRssFeed>> [[产生 RssFeed]]\sn<<option chkRegExpSearch>> [[正规式搜索]]\sn<<option chkCaseSensitiveSearch>> [[区分大小写搜索]]\sn<<option \snchkAnimate>> [[使用动画显示]]\sn\sn AdvancedOptions";\nlingo.SideBarTabs = "<<tabs txtMainTab 最近更新 '依更新日期排序' TabTimeline 全部 '所有文章' TabAll 分类 '依标签分类' TabTags 更多 '其他' TabMore>>";\nlingo.TabMore = "<<tabs txtMoreTab 未完成 '内容空白的文章' TabMoreMissing 未引用 '未被引用的文章' TabMoreOrphansTabMoreOrphans 缺省文章 '缺省的隐藏文章' TabMoreShadowed>>";\nconfig.shadowTiddlers = lingo;\nlingo = null;\n\n// // ''Date formatString''\nDate.prototype.formatString = function(template){\n template = template.replace(/YYYY/g,this.getFullYear() + '年');\n template = template.replace(/YY/g,String.zeroPad(this.getFullYear()-1911,2) + '年');\n template = template.replace(/MMM/g,config.messages.dates.months[this.getMonth()] + '月');\n template = template.replace(/0MM/g,String.zeroPad(this.getMonth()+1,2) + '月');\n template = template.replace(/MM/g,this.getMonth()+1 + '月');\n template = template.replace(/DDD/g,'星期'+config.messages.dates.days[this.getDay()]);\n template = template.replace(/DDth/g,this.getDate()+this.daySuffix());\n template = template.replace(/0DD/g,String.zeroPad(this.getDate(),2)+ '日');\n template = template.replace(/DD/g,this.getDate() + '日');\n template = template.replace(/0hh/g,String.zeroPad(this.getHours(),2));\n template = template.replace(/hh/g,this.getHours());\n template = template.replace(/0mm/g,String.zeroPad(this.getMinutes(),2));\n template = template.replace(/mm/g,this.getMinutes());\n template = template.replace(/0ss/g,String.zeroPad(this.getSeconds(),2));\n template = template.replace(/ss/g,this.getSeconds());\n return template;\n};\n//}}}\n\n//{{{\nversion.extensions.UploadPlugin = {\n major: 3, minor: 4, revision: 1, \n date: new Date(2006,7,19),\n source: 'http://tiddlywiki.bidix.info/#UploadPlugin',\n documentation: 'http://tiddlywiki.bidix.info/#UploadDoc',\n author: 'BidiX (BidiX (at) bidix (dot) info',\n license: '[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D]]',\n coreVersion: '2.0.0',\n browser: 'Firefox 1.5; InternetExplorer 6.0; Safari'\n};\n\nif (!config.lib) config.lib = {};\nif (!config.lib.file) config.lib.file= {\n author: 'BidiX',\n version: {major: 0, minor: 1, revision: 0}, \n date: new Date(2006,3,9)\n};\nconfig.lib.file.dirname = function (filePath) {\n var lastpos;\n if ((lastpos = filePath.lastIndexOf("/")) != -1) {\n return filePath.substring(0, lastpos);\n } else {\n return filePath.substring(0, filePath.lastIndexOf("\s\s"));\n }\n};\nconfig.lib.file.basename = function (filePath) {\n var lastpos;\n if ((lastpos = filePath.lastIndexOf("#")) != -1) \n filePath = filePath.substring(0, lastpos);\n if ((lastpos = filePath.lastIndexOf("/")) != -1) {\n return filePath.substring(lastpos + 1);\n } else\n return filePath.substring(filePath.lastIndexOf("\s\s")+1);\n};\nwindow.basename = function() {return "@@deprecated@@";};\n\nif (!config.lib) config.lib = {};\nif (!config.lib.log) config.lib.log= {\n author: 'BidiX',\n version: {major: 0, minor: 1, revision: 0}, \n date: new Date(2006,3,9)\n};\nconfig.lib.Log = function(tiddlerTitle, logHeader) {\n if (version.major < 2)\n this.tiddler = store.tiddlers[tiddlerTitle];\n else\n this.tiddler = store.getTiddler(tiddlerTitle);\n if (!this.tiddler) {\n this.tiddler = new Tiddler();\n this.tiddler.title = tiddlerTitle;\n this.tiddler.text = "| !date | !user | !location |" + logHeader;\n this.tiddler.created = new Date();\n this.tiddler.modifier = config.options.txtUserName;\n this.tiddler.modified = new Date();\n if (version.major < 2)\n store.tiddlers[tiddlerTitle] = this.tiddler;\n else\n store.addTiddler(this.tiddler);\n }\n return this;\n};\n\nconfig.lib.Log.prototype.newLine = function (line) {\n var now = new Date();\n var newText = "| ";\n newText += now.getDate()+"/"+(now.getMonth()+1)+"/"+now.getFullYear() + " ";\n newText += now.getHours()+":"+now.getMinutes()+":"+now.getSeconds()+" | ";\n newText += config.options.txtUserName + " | ";\n var location = document.location.toString();\n var filename = config.lib.file.basename(location);\n if (!filename) filename = '/';\n newText += "[["+filename+"|"+location + "]] |";\n this.tiddler.text = this.tiddler.text + "\sn" + newText;\n this.addToLine(line);\n};\n\nconfig.lib.Log.prototype.addToLine = function (text) {\n this.tiddler.text = this.tiddler.text + text;\n this.tiddler.modifier = config.options.txtUserName;\n this.tiddler.modified = new Date();\n if (version.major < 2)\n store.tiddlers[this.tiddler.tittle] = this.tiddler;\n else {\n store.addTiddler(this.tiddler);\n story.refreshTiddler(this.tiddler.title);\n store.notify(this.tiddler.title, true);\n }\n if (version.major < 2)\n store.notifyAll(); \n};\n\nif (!config.lib) config.lib = {};\nif (!config.lib.options) config.lib.options = {\n author: 'BidiX',\n version: {major: 0, minor: 1, revision: 0}, \n date: new Date(2006,3,9)\n};\n\nconfig.lib.options.init = function (name, defaultValue) {\n if (!config.options[name]) {\n config.options[name] = defaultValue;\n saveOptionCookie(name);\n }\n};\n\nversion.extensions.PasswordTweak = {\n major: 1, minor: 0, revision: 2, date: new Date(2006,3,11),\n type: 'tweak',\n source: 'http://tiddlywiki.bidix.info/#PasswordTweak'\n};\n\nconfig.macros.option.passwordCheckboxLabel = "Save this password on this computer";\nconfig.macros.option.passwordType = "password"; // password | text\n\nconfig.macros.option.onChangeOption = function(e)\n{\n var opt = this.getAttribute("option");\n var elementType,valueField;\n if(opt) {\n switch(opt.substr(0,3)) {\n case "txt":\n elementType = "input";\n valueField = "value";\n break;\n case "pas":\n elementType = "input";\n valueField = "value";\n break;\n case "chk":\n elementType = "input";\n valueField = "checked";\n break;\n }\n config.options[opt] = this[valueField];\n saveOptionCookie(opt);\n var nodes = document.getElementsByTagName(elementType);\n for(var t=0; t<nodes.length; t++) {\n var optNode = nodes[t].getAttribute("option");\n if (opt == optNode) \n nodes[t][valueField] = this[valueField];\n }\n }\n return(true);\n};\n\nconfig.macros.option.handler = function(place,macroName,params)\n{\n var opt = params[0];\n var size = 15;\n if (params[1])\n size = params[1];\n if(config.options[opt] === undefined) {\n return;}\n var c;\n switch(opt.substr(0,3)) {\n case "txt":\n c = document.createElement("input");\n c.onkeyup = this.onChangeOption;\n c.setAttribute ("option",opt);\n c.size = size;\n c.value = config.options[opt];\n place.appendChild(c);\n break;\n case "pas":\n // input password\n c = document.createElement ("input");\n c.setAttribute("type",config.macros.option.passwordType);\n c.onkeyup = this.onChangeOption;\n c.setAttribute("option",opt);\n c.size = size;\n c.value = config.options[opt];\n place.appendChild(c);\n // checkbox link with this password "save this password on this computer"\n c = document.createElement("input");\n c.setAttribute("type","checkbox");\n c.onclick = this.onChangeOption;\n c.setAttribute("option","chk"+opt);\n place.appendChild(c);\n c.checked = config.options["chk"+opt];\n // text savePasswordCheckboxLabel\n place.appendChild(document.createTextNode(config.macros.option.passwordCheckboxLabel));\n break;\n case "chk":\n c = document.createElement("input");\n c.setAttribute("type","checkbox");\n c.onclick = this.onChangeOption;\n c.setAttribute("option",opt);\n place.appendChild(c);\n c.checked = config.options[opt];\n break;\n }\n};\n\nwindow.loadOptionsCookie_orig_PasswordTweak = window.loadOptionsCookie;\nwindow.loadOptionsCookie = function()\n{\n var cookies = document.cookie.split(";");\n for(var c=0; c<cookies.length; c++) {\n var p = cookies[c].indexOf("=");\n if(p != -1) {\n var name = cookies[c].substr(0,p).trim();\n var value = cookies[c].substr(p+1).trim();\n switch(name.substr(0,3)) {\n case "txt":\n config.options[name] = unescape(value);\n break;\n case "pas":\n config.options[name] = unescape(value);\n break;\n case "chk":\n config.options[name] = value == "true";\n break;\n }\n }\n }\n};\n\nwindow.saveOptionCookie_orig_PasswordTweak = window.saveOptionCookie;\nwindow.saveOptionCookie = function(name)\n{\n var c = name + "=";\n switch(name.substr(0,3)) {\n case "txt":\n c += escape(config.options[name].toString());\n break;\n case "chk":\n c += config.options[name] ? "true" : "false";\n // is there an option link with this chk ?\n if (config.options[name.substr(3)]) {\n saveOptionCookie(name.substr(3));\n }\n break;\n case "pas":\n if (config.options["chk"+name]) {\n c += escape(config.options[name].toString());\n } else {\n c += "";\n }\n break;\n }\n c += "; expires=Fri, 1 Jan 2038 12:00:00 UTC; path=/";\n document.cookie = c;\n};\n\n// define config.options.pasPassword\nif (!config.options.pasPassword) {\n config.options.pasPassword = 'defaultPassword';\n window.saveOptionCookie('pasPassword');\n}\n// since loadCookies is first called befor password definition\n// we need to reload cookies\nwindow.loadOptionsCookie();\n\nconfig.macros.upload = {\n accessKey: "U",\n formName: "UploadPlugin",\n contentType: "text/html;charset=UTF-8",\n defaultStoreScript: "store.php"\n};\n\n// only this two configs need to be translated\nconfig.macros.upload.messages = {\n aboutToUpload: "About to upload TiddlyWiki to %0",\n crossDomain: "Certainly a cross-domain isue: access to an other site isn't allowed",\n errorDownloading: "Error downloading",\n errorUploadingContent: "Error uploading content",\n fileLocked: "Files is locked: You are not allowed to Upload",\n fileNotFound: "file to upload not found",\n fileNotUploaded: "File %0 NOT uploaded",\n mainFileUploaded: "Main TiddlyWiki file uploaded to %0",\n urlParamMissing: "url param missing",\n rssFileNotUploaded: "RssFile %0 NOT uploaded",\n rssFileUploaded: "Rss File uploaded to %0"\n};\n\nconfig.macros.upload.label = {\n promptOption: "Save and Upload this TiddlyWiki with UploadOptions",\n promptParamMacro: "Save and Upload this TiddlyWiki in %0",\n saveLabel: "save to web", \n saveToDisk: "save to disk",\n uploadLabel: "upload" \n};\n\nconfig.macros.upload.handler = function(place,macroName,params){\n // parameters initialization\n var storeUrl = params[0];\n var toFilename = params[1];\n var backupDir = params[2];\n var uploadDir = params[3];\n var username = params[4];\n var password; // for security reason no password as macro parameter\n var label;\n if (document.location.toString().substr(0,4) == "http")\n label = this.label.saveLabel;\n else\n label = this.label.uploadLabel;\n var prompt;\n if (storeUrl) {\n prompt = this.label.promptParamMacro.toString().format([this.dirname(storeUrl)]);\n }\n else {\n prompt = this.label.promptOption;\n }\n createTiddlyButton(place, label, prompt, \n function () {\n config.macros.upload.upload(storeUrl, toFilename, uploadDir, backupDir, username, password); \n return false;}, \n null, null, this.accessKey);\n};\nconfig.macros.upload.UploadLog = function() {\n return new config.lib.Log('UploadLog', " !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |" );\n};\nconfig.macros.upload.UploadLog.prototype = config.lib.Log.prototype;\nconfig.macros.upload.UploadLog.prototype.startUpload = function(storeUrl, toFilename, uploadDir, backupDir) {\n var line = " [[" + config.lib.file.basename(storeUrl) + "|" + storeUrl + "]] | ";\n line += uploadDir + " | " + toFilename + " | " + backupDir + " |";\n this.newLine(line);\n};\nconfig.macros.upload.UploadLog.prototype.endUpload = function() {\n this.addToLine(" Ok |");\n};\nconfig.macros.upload.basename = config.lib.file.basename;\nconfig.macros.upload.dirname = config.lib.file.dirname;\nconfig.macros.upload.upload = function(storeUrl, toFilename, uploadDir, backupDir, username, password)\n{\n // parameters initialization\n storeUrl = (storeUrl ? storeUrl : config.options.txtUploadStoreUrl);\n toFilename = (toFilename ? toFilename : config.options.txtUploadFilename);\n backupDir = (backupDir ? backupDir : config.options.txtUploadBackupDir);\n uploadDir = (uploadDir ? uploadDir : config.options.txtUploadDir);\n username = (username ? username : config.options.txtUploadUserName);\n password = config.options.pasUploadPassword; // for security reason no password as macro parameter\n if (storeUrl === '') {\n storeUrl = config.macros.upload.defaultStoreScript;\n }\n if (config.lib.file.dirname(storeUrl) === '') {\n storeUrl = config.lib.file.dirname(document.location.toString())+'/'+storeUrl;\n }\n if (toFilename === '') {\n toFilename = config.lib.file.basename(document.location.toString());\n }\n\n clearMessage();\n // only for forcing the message to display\n if (version.major < 2)\n store.notifyAll();\n if (!storeUrl) {\n alert(config.macros.upload.messages.urlParamMissing);\n return;\n }\n // Check that file is not locked\n if (window.BidiX && BidiX.GroupAuthoring && BidiX.GroupAuthoring.lock) {\n if (BidiX.GroupAuthoring.lock.isLocked() && !BidiX.GroupAuthoring.lock.isMyLock()) {\n alert(config.macros.upload.messages.fileLocked);\n return;\n }\n }\n \n var log = new this.UploadLog();\n log.startUpload(storeUrl, toFilename, uploadDir, backupDir);\n if (document.location.toString().substr(0,5) == "file:") {\n saveChanges();\n }\n displayMessage(config.macros.upload.messages.aboutToUpload.format([this.dirname(storeUrl)]), this.dirname(storeUrl));\n this.uploadChanges(storeUrl, toFilename, uploadDir, backupDir, username, password);\n if(config.options.chkGenerateAnRssFeed) {\n //var rssContent = convertUnicodeToUTF8(generateRss());\n var rssContent = generateRss();\n var rssPath = toFilename.substr(0,toFilename.lastIndexOf(".")) + ".xml";\n this.uploadContent(rssContent, storeUrl, rssPath, uploadDir, '', username, password, \n function (responseText) {\n if (responseText.substring(0,1) != '0') {\n displayMessage(config.macros.upload.messages.rssFileNotUploaded.format([rssPath]));\n }\n else {\n if (uploadDir) {\n rssPath = uploadDir + "/" + config.macros.upload.basename(rssPath);\n } else {\n rssPath = config.macros.upload.basename(rssPath);\n }\n displayMessage(config.macros.upload.messages.rssFileUploaded.format(\n [config.macros.upload.dirname(storeUrl)+"/"+rssPath]), config.macros.upload.dirname(storeUrl)+"/"+rssPath);\n }\n // for debugging store.php uncomment last line\n //DEBUG alert(responseText);\n });\n }\n return;\n};\n\nconfig.macros.upload.uploadChanges = function(storeUrl, toFilename, uploadDir, backupDir, \n username, password) {\n var original;\n if (document.location.toString().substr(0,4) == "http") {\n original = this.download(storeUrl, toFilename, uploadDir, backupDir, username, password);\n return;\n }\n else {\n // standard way : Local file\n \n original = loadFile(getLocalPath(document.location.toString()));\n if(window.Components) {\n // it's a mozilla browser\n try {\n netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");\n var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]\n .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);\n converter.charset = "UTF-8";\n original = converter.ConvertToUnicode(original);\n }\n catch(e) {\n }\n }\n }\n //DEBUG alert(original);\n this.uploadChangesFrom(original, storeUrl, toFilename, uploadDir, backupDir, \n username, password);\n};\n\nconfig.macros.upload.uploadChangesFrom = function(original, storeUrl, toFilename, uploadDir, backupDir, \n username, password) {\n var startSaveArea = '<div id="' + 'storeArea">'; // Split up into two so that indexOf() of this source doesn't find it\n var endSaveArea = '</d' + 'iv>';\n // Locate the storeArea div's\n var posOpeningDiv = original.indexOf(startSaveArea);\n var posClosingDiv = original.lastIndexOf(endSaveArea);\n if((posOpeningDiv == -1) || (posClosingDiv == -1))\n {\n alert(config.messages.invalidFileError.format([document.location.toString()]));\n return;\n }\n var revised = original.substr(0,posOpeningDiv + startSaveArea.length) + \n allTiddlersAsHtml() + "\sn\st\st" +\n original.substr(posClosingDiv);\n var newSiteTitle;\n if(version.major < 2){\n newSiteTitle = (getElementText("siteTitle") + " - " + getElementText("siteSubtitle")).htmlEncode();\n } else {\n newSiteTitle = (wikifyPlain ("SiteTitle") + " - " + wikifyPlain ("SiteSubtitle")).htmlEncode();\n }\n\n revised = revised.replaceChunk("<title"+">","</title"+">"," " + newSiteTitle + " ");\n revised = revised.replaceChunk("<!--PRE-HEAD-START--"+">","<!--PRE-HEAD-END--"+">","\sn" + store.getTiddlerText("MarkupPreHead","") + "\sn");\n revised = revised.replaceChunk("<!--POST-HEAD-START--"+">","<!--POST-HEAD-END--"+">","\sn" + store.getTiddlerText("MarkupPostHead","") + "\sn");\n revised = revised.replaceChunk("<!--PRE-BODY-START--"+">","<!--PRE-BODY-END--"+">","\sn" + store.getTiddlerText("MarkupPreBody","") + "\sn");\n revised = revised.replaceChunk("<!--POST-BODY-START--"+">","<!--POST-BODY-END--"+">","\sn" + store.getTiddlerText("MarkupPostBody","") + "\sn");\n\n var response = this.uploadContent(revised, storeUrl, toFilename, uploadDir, backupDir, \n username, password, function (responseText) {\n if (responseText.substring(0,1) != '0') {\n alert(responseText);\n displayMessage(config.macros.upload.messages.fileNotUploaded.format([getLocalPath(document.location.toString())]));\n }\n else {\n if (uploadDir !== '') {\n toFilename = uploadDir + "/" + config.macros.upload.basename(toFilename);\n } else {\n toFilename = config.macros.upload.basename(toFilename);\n }\n displayMessage(config.macros.upload.messages.mainFileUploaded.format(\n [config.macros.upload.dirname(storeUrl)+"/"+toFilename]), config.macros.upload.dirname(storeUrl)+"/"+toFilename);\n var log = new config.macros.upload.UploadLog();\n log.endUpload();\n store.setDirty(false);\n // erase local lock\n if (window.BidiX && BidiX.GroupAuthoring && BidiX.GroupAuthoring.lock) {\n BidiX.GroupAuthoring.lock.eraseLock();\n // change mtime with new mtime after upload\n var mtime = responseText.substr(responseText.indexOf("mtime:")+6);\n BidiX.GroupAuthoring.lock.mtime = mtime;\n }\n \n \n }\n // for debugging store.php uncomment last line\n //DEBUG alert(responseText);\n }\n );\n};\n\nconfig.macros.upload.uploadContent = function(content, storeUrl, toFilename, uploadDir, backupDir, \n username, password, callbackFn) {\n var boundary = "---------------------------"+"AaB03x"; \n var request;\n try {\n request = new XMLHttpRequest();\n } \n catch (e) { \n request = new ActiveXObject("Msxml2.XMLHTTP"); \n }\n if (window.netscape){\n try {\n if (document.location.toString().substr(0,4) != "http") {\n netscape.security.PrivilegeManager.enablePrivilege('UniversalBrowserRead');}\n }\n catch (e) {}\n } \n //DEBUG alert("user["+config.options.txtUploadUserName+"] password[" + config.options.pasUploadPassword + "]");\n // compose headers data\n var sheader = "";\n sheader += "--" + boundary + "\sr\snContent-disposition: form-data; name=\s"";\n sheader += config.macros.upload.formName +"\s"\sr\sn\sr\sn";\n sheader += "backupDir="+backupDir\n +";user=" + username \n +";password=" + password\n +";uploaddir=" + uploadDir;\n // add lock attributes to sheader\n if (window.BidiX && BidiX.GroupAuthoring && BidiX.GroupAuthoring.lock) {\n var l = BidiX.GroupAuthoring.lock.myLock;\n sheader += ";lockuser=" + l.user\n + ";mtime=" + l.mtime\n + ";locktime=" + l.locktime;\n }\n sheader += ";;\sr\sn"; \n sheader += "\sr\sn" + "--" + boundary + "\sr\sn";\n sheader += "Content-disposition: form-data; name=\s"userfile\s"; filename=\s""+toFilename+"\s"\sr\sn";\n sheader += "Content-Type: " + config.macros.upload.contentType + "\sr\sn";\n sheader += "Content-Length: " + content.length + "\sr\sn\sr\sn";\n // compose trailer data\n var strailer = new String();\n strailer = "\sr\sn--" + boundary + "--\sr\sn";\n var data;\n data = sheader + content + strailer;\n //request.open("POST", storeUrl, true, username, password);\n try {\n request.open("POST", storeUrl, true); \n }\n catch(e) {\n alert(config.macros.upload.messages.crossDomain + "\snError:" +e);\n exit;\n }\n request.onreadystatechange = function () {\n if (request.readyState == 4) {\n if (request.status == 200)\n callbackFn(request.responseText);\n else\n alert(config.macros.upload.messages.errorUploadingContent + "\snStatus: "+request.status.statusText);\n }\n };\n request.setRequestHeader("Content-Length",data.length);\n request.setRequestHeader("Content-Type","multipart/form-data; boundary="+boundary);\n request.send(data); \n};\n\n\nconfig.macros.upload.download = function(uploadUrl, uploadToFilename, uploadDir, uploadBackupDir, \n username, password) {\n var request;\n try {\n request = new XMLHttpRequest();\n } \n catch (e) { \n request = new ActiveXObject("Msxml2.XMLHTTP"); \n }\n try {\n if (uploadUrl.substr(0,4) == "http") {\n netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");\n }\n else {\n netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");\n }\n } catch (e) { }\n //request.open("GET", document.location.toString(), true, username, password);\n try {\n request.open("GET", document.location.toString(), true);\n }\n catch(e) {\n alert(config.macros.upload.messages.crossDomain + "\snError:" +e);\n exit;\n }\n \n request.onreadystatechange = function () {\n if (request.readyState == 4) {\n if(request.status == 200) {\n config.macros.upload.uploadChangesFrom(request.responseText, uploadUrl, \n uploadToFilename, uploadDir, uploadBackupDir, username, password);\n }\n else\n alert(config.macros.upload.messages.errorDownloading.format(\n [document.location.toString()]) + "\snStatus: "+request.status.statusText);\n }\n };\n request.send(null);\n};\n\nconfig.lib.options.init('txtUploadStoreUrl','store.php');\nconfig.lib.options.init('txtUploadFilename','');\nconfig.lib.options.init('txtUploadDir','');\nconfig.lib.options.init('txtUploadBackupDir','');\nconfig.lib.options.init('txtUploadUserName',config.options.txtUserName);\nconfig.lib.options.init('pasUploadPassword','');\nconfig.shadowTiddlers.UploadDoc = "[[Full Documentation|http://tiddlywiki.bidix.info/l#UploadDoc ]]\sn"; \n\nconfig.macros.saveChanges.label_orig_UploadPlugin = config.macros.saveChanges.label;\nconfig.macros.saveChanges.label = config.macros.upload.label.saveToDisk;\n\nconfig.macros.saveChanges.handler_orig_UploadPlugin = config.macros.saveChanges.handler;\n\nconfig.macros.saveChanges.handler = function(place)\n{\n if ((!readOnly) && (document.location.toString().substr(0,4) != "http"))\n createTiddlyButton(place,this.label,this.prompt,this.onClick,null,null,this.accessKey);\n};\n//}}}
LWP::Simple 在做简单的工作时很方便。但因为不支持 cookies,用户认证,对 HTTP request header (请求标头)的编辑,和 HTTP resonse header(响应标头)的读写(主要是 HTTP 的 error message)。因此,当需要这些特性的时候,就要使用 LWP Class 模型。\n\n在众多的 LWP Class 里,LWP::UserAgent 和 HTTP::Response 是必须理解的。LWP::UserAgent 就像一个虚拟浏览器用来作 request (请求)。HTTP::Response 用来储存 request (请求) 生成的 response(响应)。\n\n最基本的用法是 $response = $browser->get($url), 或写的更完整些 :\n//{{{\n # 程序开始:\n\n use LWP 5.64; # 载入较新版本的 LWP classes \n\n my $browser = LWP::UserAgent->new;\n\n ...\n\n # get request:\n my $url = 'http://freshair.npr.org/dayFA.cfm?todayDate=current';\n\n my $response = $browser->get( $url );\n die "Can't get $url -- ", $response->status_line\n unless $response->is_success;\n\n die "Hey, 我想要 HTML 格式而不是 ", $response->content_type\n unless $response->content_type eq 'text/html';\n # 或者任何其他的 content-type\n\n # 成功的话就对内容处理\n\n if($response->content =~ m/jazz/i) {\n print "Fresh Air 今天在讨论爵士乐!\sn";\n } else {\n print "Fresh Air 今天讨论的和爵士乐一点边都不沾.\sn";\n }\n//}}}\n上面有两个相关的 object: $browser,是 LWP::UserAgent 的一个object。$response 是属于 HTTP::Response 类的一个object。一个程序里只需要一个 $browser object,但是每次发出一个 request,就会得到一个新的 HTTP::Response object。\n\nHTTP::Response object 有以下一些有价值的属性:\n\n* 一个 status code(状态代码值),表示成功或失败。你可以使用 $response->is_success 来检测它。\n* http status line(http 状态描述),观察 $response->status_line 的结果 ( 比如 “404 Not Found” ) 会帮助你理解这个词的意思。\n* MIME content-type(文件类型)通过 $response->content_type 来获得。例如 “text/html”,”image/gif”,”application/xml” 等等。\n* content of the response(响应返回的内容)储存在 $response->content。内容可能是 html 格式。如果是 GIF 格式,$response->content 里是二进制的 GIF 数据。\n* 许多其他 methods 都可以在 HTTP::Response 和它的 superclasses (父 class) HTTP::Message 和 HTTP::Headers 里找到。\n
LWP @@(“Library for WWW in Perl” 的缩写)@@ 是一个由多个模块组成,用来获取网络数据的的模块组。 和很多 Perl 的模块一样。每一个 LWP 模块都自带详细的文档,做为对这个模块的完整介绍。可是面对 LWP 里的众多模块,有时候即使是完成最简单的工作,新手们也常常不知道从那里开始。\n\n要对 LWP 做全面的介绍需要一整本书,很幸运,Perl & LWP 已经出版。而这篇文章向你介绍了最常见的 LWP 用法。
//{{{\n my $url = 'http://freshair.npr.org/dayFA.cfm?todayDate=current'\n\n use LWP::Simple;\n my $content = get $url;\n die "Couldn't get $url" unless defined $content;\n\n # $content 里是网页内容,下面是对此内容作些分析:\n\n if($content =~ m/jazz/i) {\n print "They're talking about jazz today on Fresh Air!\sn";\n } else {\n print "Fresh Air is apparently jazzless today.\sn";\n }\n//}}}\n\n如果要在命令行里运行,getprint 函数非常方便。如果没有发生错误,它会把网页内容输出到 STDOUT,否则将会有错误信息输出到 STDERR。 例如:\n//{{{\n % perl -MLWP::Simple -e "getprint 'http://cpan.org/RECENT'"\n//}}}\n上面的网址指向一个文本文件,列有最近两个星期内 CPAN 更新过的文件。如果想要知道 Acme:: 的模块是否有更新,有就email 自己,你可以把它和 Shell 结合到一起来实现。如下:\n//{{{\n % perl -MLWP::Simple -e "getprint 'http://cpan.org/RECENT'" \s\n | grep "/by-module/Acme" | mail -s "New Acme modules! Joy!" $USER\n//}}}\nLWP::Simple 还有一些非常有用的函数,包括一个运行HEAD请求的函数,用来检查链接是否有效,网页是否更新。另外两个用来保存和镜像网址的函数也值得一提。具体请看 LWP::Simple 的文档或 Perl & LWP 的第二章.\n
//{{{\n译者/作者:qiang\n出处:中国Perl协会 FPC(Foundation of Perlchina)\n作者:Sean M. Burke – Perl & LWP 作者(O’Reilly 出版)\n原名:Web Basics with LWP\n发表:2002 年 2月 28 日\n原文:http://www.perl.com/pub/a/2002/08/20/perlandlwp.html\n请保护作者的著作权,维护作者劳动的结晶。\n//}}}\n[[LWP 简介]]\n[[LWP::Simple 获取网页]]\n[[LWP Class 模型基础]]\n[[添加其他 HTTP 请求 headers]]\n[[使用cookies]]\n[[使用POST提交表格]]\n[[通过GET提交表格]]\n[[URL处理]]\n[[其他浏览器属性]]\n[[写一个有礼貌的机器人]]\n[[使用代理]]\n[[HTTP认证]]\n[[连接 HTTPS URL]]\n[[获取大文件]]\n[[LWP资源]]\n[[与 Sean Burke 讨论后的一些笔记]]
以上只是对 LWP 常用函数的简介, 如果想了解更多关于 LWP 和 LWP 相关的东西, 请参阅以下文档\n\n* LWP::Simple: 提供简易的 get, head, mirror 方法.\n* LWP: libwww-perl 模块的综述\n* HTTP::Response: 发出 LWP 请求后所得到的响应 , $response = $browser->get(...).\n* HTTP::Message and HTTP::Headers: HTTP::Response 很多方法都是来自两者.\n* URI: Class 处理完全和相对网址路径.\n* URI::Escape: 来正确处理和转换 URL 里的不规则字符 (比如 “this & that” 和 “this%20%26%20that” 之间的转换).\n* HTML::Entities: 来正确处理和转换 HTML 里的不规则字符 (比如 “C. & E. Bront??” 和 “C. & E. Bront??” 之间的转换).\n* HTML::TokeParser and HTML::TreeBuilder: Classes 分析 HTML\n* HTML::LinkExtor: Class :在 HTML 找到链接\n* 当然还有我的 Perl & LWP.\n
[[首页]]\n[[LWP的基本使用]]\n[[perl小技巧 文件操作]]\n[[邮件处理]]\n[[Class::DBI]]\n[[Perl命令行应用介绍]]\n[[单行perl]]\nSideBarOptions
//{{{\n译者/作者:qiang\n出处:中国Perl协会 FPC(Foundation of Perlchina)\n作者:Dave Cross\n原名:Perl Command-Line Options\n原文:http://www.perl.com/pub/a/2004/08/09/commandline.html\n发表:August 10, 2004\n请保护作者的著作权,维护作者劳动的结晶。\n//}}}\n\nPerl 有很多命令行参数。通过它可以让你的程序更简练,并且可以写出很多只有一行命令的perl。在这篇文章里我们来了解一些常用的命令行参数。\n安全网参数\n有三个参数我认为可以起到“安全网”的作用,因为它们可以让你避免犯错,特别是当你在使用 Perl 尝试一些特别聪明(或这愚蠢)的想法时, 错误难免会发生。有经验的 Perl 程序员常常使用这三个参数来提前找到错误所在。\n\n-C 是第一个。这个参数编译 Perl 程序但并不真正运行它。由此检查所有语法错误。每次修改 perl 程序之后我都会立刻使用它来找到任何语法错误。\n//{{{\n$ perl -c program.pl\n//}}}\n这保证了程序依然可以编译。很显然,当你输入一小段代码之后立即进行检查,比起一下子输入几百行代码然后开始 debug 要容易很多。\n\n-W 是第二个参数。它会提示你任何潜在的bug。Perl 5.6.0 之后的版本已经用 use warnings; 替换了 -w。你应该使用 use warnings 因为它要比 -w 更灵活。\n\n-T 是第三个参数。它让 perl 出于了 taint 模式中。在这个模式里,Perl 会质疑任何程序外传来的数据。例如,从命令行读取,外部文件里读取 或是 CGI 程序里传来的数据。这些数据在 -T 模式里都会被 Tainted(污染)。\n\nTainted 数据不可以被用来和外部交互。例如 使用在 system 调用和用作 open 的文件名。关于什么数据会被Tainted,请参阅perlsec 文档,那里有一个完整的列表。\n\n要想使用 Tainted 的数据就必须 untaint这个数据。untaint 是通过正则表达式来实现的,关于 taint 本身的内容足够写一篇单独的文章,所以这里我不会太多的讲述 taint 模式。如果你要编写的程序(例如 CGI 程序)需要从从用户那里接受不可知的输入,我推荐使有 taint 模式。\n\n还有一个值得一提的参数是 -d,它将让 Perl 处于 Debugger 模式。这个话题内容非常多,我推荐阅读文档 ‘perldoc perldebug’ 或 Richard Foley 的 Perl Debugger Pocket Reference 一书.\n!Command-Line Programs\n下面的几个 Perl 参数可以让短小的 Perl 程序很容易的在命令行上运行。-e 可以让 Perl 代码在命令行上被编译器直接执行.例如, 我们可以直接在命令行上运行 “Hello World” 程序而不用把它写称 Perl 程序。\n//{{{\n$ perl -e 'print "Hello World\sn"'\n//}}}\n多个 -e 也可以同时使用, 运行顺序根据它出现的位置.\n//{{{\n$ perl -e 'print "Hello ";' -e 'print "World\sn"'\n//}}}\n象所有的 Perl 程序一样, 只有程序的最后一行不需要以 ; 结尾.\n\n虽然你也可以用 -e 来引用模块, 但 -M 让它变得更容易.\n//{{{\n$ perl -MLWP::Simple -e 'print head "http://www.example.com"'\n//}}}\n-M模块名 和 use 模块名 一样。有些模块有默认的模块导入,如果你不想导入它们,你可以使用 -m。-m模块名 和 use module() 一样,关闭了默认的导入。例如下面这个例子, 因为 head 函数是默认导入,而使用 -m 时就不会执行,结果是没有输出。\n//{{{\n$ perl -mLWP::Simple -e 'print head "http://www.example.com"'\n//}}}\n-m 和 -M 有很多方便的语法来帮助你使用它们,你可以在 = 后面列出对 use 的各种参数。\n//{{{\n$ perl -MCGI=:standard -e 'print header'\n//}}}\n在这里,CGI.pm 的 :standard 被引入,header 函数因此可以使用。要引入多个参数可以通过使用引号和逗号。\n//{{{\n$ perl -MCGI='header,start_html' -e 'print header, start_html'\n//}}}\n这里我们引入了 header 和 start_html 函数。\n!Implicit Loops\n-n 和 -p 增加了循环的功能, 使你可以一行一行来处理文件.\n//{{{\n$ perl -n -e 'some code' file1\n//}}}\n这与下面的程序一样.\n//{{{\n LINE:\n while (<>) {\n # your code goes here\n }\n//}}}\n注意: <> 打开命令行里的文件,一行行的读取。每一行将缺省保存在 $_\n//{{{\n$ perl -n -e 'print "$. - $_"' file\n//}}}\n上面的这一行可以写成 LINE: while (<>) { print ”$. – $_” } 输出当前行数 $. 和当前行 $_.\n\n-p 可以让上面的程序变得更容易.-p 会输出 $_ 的内容,就像这样:\n//{{{\n LINE:\n while (<>) {\n # your code goes here\n } continue {\n print or die "-p destination: $!\sn";\n }\n//}}}\ncontinue 在这里保证 print 在每次循环都会被调用。\n\n使用 -p,我们的打印行数程序可以改为\n//{{{\n$ perl -p -e '$_ = "$. - $_"'\n//}}}\n这种情况下我们就不需要要明确地调用 print 函数了,因为 -p 选项已经调用了它。\n\n注意, LINE: 标签可以让我们直接跳到下一个输入记录,而不管你进入了多少层循环。使用 next LINE。\n//{{{\n$ perl -n -e 'next LINE unless /pattern/; print $_'\n//}}}\n当然,也可以这样写:\n//{{{\n$ perl -n -e 'print unless /pattern/'\n//}}}\n在更复杂的情况里, next LINE 可以让你的代码更容易理解。\n\n如果想在循环的前后做些处理,可以使用 BEGIN 或 END block. 下面的这一行代码可以计算 text 文件里的字数。\n//{{{\n$ perl -ne 'END { print $t } @w = /(\sw+)/g; $t += @w' file.txt\n//}}}\n每一行所有匹配的字放入数组 @w,然后把 @w 的元素数目递加到 $t。END block 里的 print 最后输出文件总字数。\n\n还有两个参数可以让这个程序变得更简单。-a 打开自动分离 (split) 模式。空格是缺省的分离号。输入根据分离号被分离然后放入缺省数组 @F。由此,我们可以把上面的程序改写为\n//{{{\n$ perl -ane 'END {print $x} $x += @F' file.txt\n//}}}\n你也可以通过 -F 把缺省的分离号改为你想要的.例如把分离号定为非字符:\n//{{{\n$ perl -F'\sW' -ane 'END {print $x} $x += @F' file.txt\n//}}}\n下面通过 Unix password 文件来介绍一个复杂的例子。Unix password 是文本文件,每一行是一个用户记录,由冒号 :分离。 第?行是用户的登录 shell 路径。我们可以得出每一个不同 shell 路径被多少个用户使用:\n//{{{\n$ perl -F':' -ane '$s{$F[6]}++;' \s\n> -e 'END { print "$_ : $s{$_}" for keys %s }' /etc/passwd\n//}}}\n虽然现在不是一行,但是你可以看出使用参数可以解决什么问题。\n!数据分隔符\n我以前的文章里提到过 $/ 和 $\s—输入,输出分隔号。$/ 用来分隔从文件句柄里读出的数据,缺省 $/ 分隔号是 \sn,这样每次从文件句柄里就会一行行的读取。 $\s 缺省是空字符,用来自动加到要 print 的数据尾端。这就是为什么很多时候 print 都要在末尾加上 \sn。\n\n$/ 和 $\s 可与 -n -p 一起使用。在命令行上相对应为 -0 (零) 和 -l ( 这是 L )。-0 后面可以跟一个16 进制或8进制数值,这个值用来赋给 $/。-00 打开段落模式,-0777 打开slurp 模式 (即可以一次把整个文件读入),这与把 $/ 设为空字符和 undef 一样效果。\n\n单独使用 -l 有两个效果,第一自动 chomp 输入分隔号,第二 把$/ 值付给 $\s ( 这样 print 的时候就会自动在末尾加 \sn )\n\n我个人常常使用 -l 参数, 用来给每一个输出加 \sn. 例如\n//{{{\n$ perl -le 'print "Hello World"'\n//}}}\n!原位编辑\n使用已有的参数我们可以写出很有效的命令行程序. 常见的Unix I/O 重定向:\n//{{{\n$ perl -pe 'some code' < input.txt > output.txt\n//}}}\n这个程序从 input.txt 读取数据, 然后做一些处理再输出到 output.txt. 你当然也可以把输出重定向到同一个文件里. 上面的程序可以通过 -i 参数做的更简单些。-i 把源文件更名然后从这个更名的源文件里读取。最后把处理后的数据写入源文件。如果 -i 后跟有其他字符串,这个字符串与源文件名合成后来生成一个新的文件名。此文件会被用来储存原始文件以免被 -i 参数覆盖。\n\n这个例子把所有 php 字符替换为 perl :\n//{{{\n$ perl -i -pe 's/\sbPHP\sb/Perl/g' file.txt\n//}}}\n程序读取文件的每一行, 然后替换字符, 处理后的数据重新写入( 即覆盖 ) 源文件. 如果不想覆盖源文件, 可以使用\n//{{{\n$perl -i.bak -pe 's/\sbPHP\sb/Perl/g' file.txt\n//}}}\n这里处理过的数据写入 file.txt , file.txt.bak 是源文件的备份.\n!更多信息\nPerl 有大量的命令行参数,这篇文章只是列举了最有用的一小部分。更详细的信息请参考 “perlrun” 文档。
<<search>><<closeAll>><<permaview>><<newTiddler>><<newJournal YYYY0MM0DD>><<saveChanges>><<slider chkSliderOptionsPanel OptionsPanel 偏好设置 '变更 TiddlyWiki 选项'>><<upload>>
my perl language collections
Perl
上面提到的 URI class 提供很多获取和修改 URL 的方法。例如 如果想要知道 url 是什么类型 (http, ftp 等等) 可以使用 $url->schema 来得到,如果要提取网址里的主机名,可以使用 $url->host。不过,可能最有用的是我前面提到的 query_form 方法,以及把相对网址路径(如”../foo.html”)转换成绝对路径(如”http: //www.perl.com/stuff/foo.html”)的 new_abs 方法。例子如下:\n//{{{\n use URI;\n $abs = URI->new_abs($maybe_relative, $base);\n//}}}\n现在回忆一下获取最新 CPAN 模块的那个例子。\n//{{{\n use strict;\n use warnings;\n use LWP 5.64;\n my $browser = LWP::UserAgent->new;\n\n my $url = 'http://www.cpan.org/RECENT.html';\n my $response = $browser->get($url);\n die "Can't get $url -- ", $response->status_line\n unless $response->is_success;\n\n my $html = $response->content;\n while( $html =~ m/chunk86920392chunklt;A HREF=\s"(.*?)\s"/g ) { \n print "$1\sn"; \n }\n//}}}\n输出的结果是\n//{{{\n MIRRORING.FROM\n RECENT\n RECENT.html\n authors/00whois.html\n authors/01mailrc.txt.gz\n authors/id/A/AA/AASSAD/CHECKSUMS\n ... \n//}}}\n你可以使用 URI 模块的 new_abs 方法来得到完全网址路径,修改 while 循环:\n//{{{\n while( $html =~ m/<A HREF=\s"(.*?)\s"/g ) { \n print URI->new_abs( $1, $response->base ) ,"\sn";\n }\n//}}}\n$response->base 方法可以在 HTTP::Message 里找到。它返回的 URL 通常被用来和相对路径合并来得到完全路径。现在得到的结果是\n//{{{\n http://www.cpan.org/MIRRORING.FROM\n http://www.cpan.org/RECENT\n http://www.cpan.org/RECENT.html\n http://www.cpan.org/authors/00whois.html\n http://www.cpan.org/authors/01mailrc.txt.gz\n http://www.cpan.org/authors/id/A/AA/AASSAD/CHECKSUMS\n ...\n//}}}\n请参考 Perl & LWP 的第四章,以得到对 URI objects 更详细的描述。\n\n当然,使用 regexp (正则表达式) 来匹配 url 相对简单,如果情况复杂,需要更强大的匹配工具,可以考虑 HTML 分析模块 HTML::LinkExtor 或 HTML::TokeParser,甚至 HTML::TreeBuilder
| !date | !user | !location | !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |\n| 26/9/2006 1:5:3 | tristones | [[perl.html|http://tristones.viaspeip.com/collections/perl.html]] | [[store.php|http://tristones.viaspeip.com/collections/store.php]] | | perl.html | |
!Options used by UploadPlugin\nUsername: <<option txtUploadUserName>>\nPassword: <<option pasUploadPassword>>\n\nUrl of the UploadService script^^(1)^^: <<option txtUploadStoreUrl 50>>\nRelative Directory where to store the file^^(2)^^: <<option txtUploadDir 50>>\nFilename of the uploaded file^^(3)^^: <<option txtUploadFilename 40>>\nDirectory to backup file on webserver^^(4)^^: <<option txtUploadBackupDir>>\n\n^^(1)^^Mandatory either in UploadOptions or in macro parameter\n^^(2)^^If empty stores in the script directory\n^^(3)^^If empty takes the actual filename\n^^(4)^^If empty existing file with same name on webserver will be overwritten\n\n<<upload>> with these options.\n\n!Upload Macro parameters\n{{{\n<<upload [storeUrl [toFilename [backupDir [uploadDir [username]]]]]>>\n Optional positional parameters can be passed to overwrite \n UploadOptions. \n}}}
//{{{\n译者/作者:SaladJonk\n出处:中国Perl协会 FPC(Foundation of Perlchina)\n翻 译:SaladJonk\n审 校:qiang\n出 处:中国Perl协会 FPC(Foundation of Perlchina)\n原 名:Perl circus:file operation\n作 者:Luke Melia\n发 表:2002\n请保护作者的著作权,维护作者劳动的结晶。\n//}}}\n\n@@找到具有指定特征的文件@@\n//{{{\n$path = "/path/to/dir/";\nopendir DIR, $path;\n@arr1 = readdir DIR;\n@arr2 = grep{-T "$path$_"} @arr1; #text files only\n@arr3 = grep{!-d "$path$_"} @arr1; #no directories\n@arr4 = grep{-s "$path$_" < 1024} @arr1; #less than 1K\n//}}}\n代码解释:假如被测试的目录项是一个文本文件,那么 -T 文件操作符就会返回真。其实针对目录项的测试操作还有很多。(注:文件和目录在系统中都是以目录项的形式来管理的,所以要区别一个目录项指向的是一个文件还是一个目录需要相应的操作符)。注意上面的 readdir 函数返回指定目录下的所有目录项。因为在 grep 函数中对目录项的测试需要文件的完全路径,所以我们把 $PATH(存储了目录项的部分路径) 和 $_(存储了目录项的名字)中的内容联合起来得到文件的完全路径 *对目录进行递归搜索*\n//{{{\nuse File::Find;\n\nfind(\s&handleFind, 'imac:documents:code');\n\nsub handleFind{\n my $foundFile = $File::Find::name;\n print "$foundFile\sn" if ($foundFile =~ /\s.html?$/i);\n}\n//}}}\nRESULT: imac:documents:code:index.html imac:documents:code:perl:example.HTM 运行结果:代码讨论:那些工作于 Unix 系统的 Perl 程序员可以非常简便的利用 UNIX 上提供的工具来完成许多日常的工作,比如递归的列出指定目录下的所有目录项(也就是列出指定目录及指定目录子目录下的所有目录项目)。然而 Perl 的一个最大的特征就是可以运行于很多的平台上。所以如果你碰巧工作在一个非 UNIX 的平台,或者如果你虽工作在 UNIX 平台,但不喜欢使用系统工具写脚本,你可以选择 Perl。要完成这些巧妙的工作,你需要使用 perl 中的 File:Find 模块。当你加载了这个模块的时候,你就可以使用其中的 find 子函数,在调用这个函数的时候,需要带参数:第一个参数是一个函数的引用,这个函数由你自己建立,每次一个文件被找到的时候,它都会运行。接下来的一个参数是一串你想要搜索的路径。我写的这个示例脚本是运行在 Macintosh OS 8.x 系统上的,所以我使用了 Mac 系统的路径分隔符:。如果是在 Windows,你可以用反斜杠,如果是在 Unix 系统则是正斜杠(至于在 Amiga 系统上用什么我就不知道了)。总之,find 函数将会在每次找到一个文件的时候调用你给出的子函数,而且会对子目录进行查找。在我的 handledfind 子函数中,我通过这个模块特定变量 $File::Find::name 来获得每次 find 找到的文件名。然后,就可以对该文件执行任何你想的测试,在上面的例子中,我们输出有 .html 的扩展名文件名。 *文件读操作* *一次读入整个文件内容。*\n//{{{\nopen FH, "< anthem";\n$/ = undef;\n$slurp = ;\nprint $slurp;\n//}}}\n运行结果:一下就显示了所有的文件内容,此刻你应该非常的自豪。:) 代码讨论:尖括号 <> 对文件句柄进行操作,在标量上下文中它将返回文件的下一条记录,在数组上下文中它将返回所有的记录。在默认的情况下,文件中的记录被认为是由换行符分开(例如回车或其他代表新行开始的字符)。你可以重新设定这个默认的分隔符,然后 Perl 将会以你指定的分隔符为准来替代换行符。全局变量 $/ 里存储了输入文件的分隔符,如果你把 $/ 的值设置为 undef ,那么 Perl 将会认为整个文件是一条记录(因为此刻已经没有文件分隔符了)。牢记 $/ 是全局变量,千万不要在脚本的其他地方不经意的改变它,这个错误将很难被发现。你可能会问,我们能否不改变 $/,而采用把文件的所有记录读到一个数组中,然后把数组联合成一个很长的字符串(比如 $slurp = join("",);)的方法实现一次读入文件。当然这也是一个有效的解决办法,但是你会发现它很慢,是否选用它取决你的应用,取决于你是否关心运行速度。 *赋值* *把一个文件句柄赋给另一个文件句柄*\n//{{{\nopen(MYOUT, "> bottle.txt");\n*STDOUT = *MYOUT;\nprint "message";\n//}}}\n运行结果:文本文件 bottle.txt 现在包含 message 字符串。代码讨论:以前可能你配合使用过 Print 函数和文件句柄,但是你是否知道就算你没有使用文件句柄,Perl 也默认你在使用一个称为 STDOUT 的句柄?C 程序员知道 STDOUT 代表标准输出,也就是通常的屏幕,或终端窗口(或者是 CGI 程序的输出端 -浏览器)。在这里我们完成的工作是创建我们自己的文件句柄,它指向一个给定的文件,然后我们做了一件比较鬼的工作,使用 * 前缀把 STDOUT 转换为 typeglob 类型。Typeglob 类型的数据可以有别名,这样一个变量可能会指向另一个其他名字的变量。上面第二行代码使 STDOUT 指向 MYOUT 变量。所以执行 print 操作时的默认输出对象也就成为了我们创建的文件句柄。 *同时向两个文件句柄执行写操作*\n//{{{\nuse IO::Tee; \n$tee = IO::Tee->new(">> debuglog.txt", \s*STDOUT); \nprint $tee "an error ocurred on ".scalar(localtime)."\sn";\n//}}}\n运行结果:an error ocurred on Fri Feb 23 21:44:20 2001 代码讨论:如果,由于种种原因你想要同时向两个位置写入同一个字符串,这和 UNIX 下的 tee 工具的用途一样。即使你不是工作在 Unix 平台上,Perl 也通过 Tee 模块为你提供这个功能。Tee 模块可以在 CPAN 下载,你应该把它安装到 Perl 的 IO 库文件夹中。Tee 模块以 OOP 方式编写,所以使用它之前你应该首先使用它的 new 方法来创建一个 Tee 对象,整个过程需要两个参数,每个参数既可以是代表文件句柄的字符串,也可以是一个对已打开的文件句柄的引用。在上面的例子中,我们用一个字符串来代表一个以附加模式打开的文件句柄,它指向名为 debuglog.txt 的文件,另一个参数是系统内置的文件句柄 STDOUT,整个句柄是系统自动创建的,print 函数默认情况对它进行操作。为了得到一个文件句柄的引用我们需要对一个 typeglob 类型的数据使用反斜杠。Typeglob 可以代表任何已命名的某个变量,不论它是数组,散列还是标量等。使用 * 很有必要,因为文件句柄自己没有前缀符号。new 操作符返回 Tee 类的一个实例对象,然后我们把整个实例赋给 $tee 标量。现在,无论什么时候我们向 $tee 进行写入操作,我们都同时向两个位置进行写操作。 *更多文件操作。。。* *从一个文件的完全路径中找出它的名字*\n//{{{\nuse File::Basename;\n$path = "/docs/sitecircus.com/html/tricks/trick.of.the.week.html";\n$basename = basename($path, ".html");\nprint $basename;\n//}}}\n运行结果:trick.of.the.week 代码讨论:好了,成功了。问题是要找出文件的名字,要不带任何路径前缀,不带任何扩展名。File::Basename 模块可以使这很容易实现,我们只需要把文件的完全路径还有要剔除的扩展名传给它。上面的 path 变量是文件的完全路径,注意文件分隔符是 /,这个字符很特殊,因为它是操作系统的保留字符。这里你不能在文件名里使用系统的分隔符。你应该知道当今流行的操作系统都使用自己独特的文件分隔符: Unix使用 /,Windows 使用 \s,Macintosh 使用 :(顺便说一下,在 Windows 上的 Perl 脚本中,你既可以使用 \s也可以使用 /作为文件分隔符,Perl 的解释器能理解你的意思)。File::Basename,当然,能正确在完全路径中找到文件名,不论时在什么系统下。 *改变文件的所有者*\n//{{{\n($uid, $gid) = (getpwnam($username))[2,3]\n or die "$user not in passwd file";\nchown ($uid, $gid, $file)\n or warn "couldn't chown $file.";\n//}}}\n运行结果:无输出代码讨论:有的时候,你可能知道一个用户名,而你想用这个用户名做些事,比如改变一个文件的所有者。但是不幸的是,Perl 的 chown 命令不能接受用户名作为参数,但是可以接受一对数字:userid 和 groupid。虽然有这些不便之处,Perl 并没有让我们陷入困境,我们可以把用户名作为 getpwnam 函数的参数,获得一个数组,里面包含了用户名对应的 userid 和 groupid,分别对应着数组里的第二和第三个元素。
n : a young person of either sex; "she writes books for children"; "they're just kids"; "`tiddler' is a British term for youngsters" [syn: child, kid, youngster, minor, shaver, nipper, small fry, tike, tyke, fry, nestling]\n//{{{\nfrom:http://dictionary.reference.com/search?q=tiddler&x=0&y=0\n//}}}
在翻译这篇文章的时候,我联系到了文章作者, Sean Burke. 他同意把原文章里需要补充和更新的地方指出. 我与作者也通过 MSN 交流了一下. 这里我把自己对 LWP 的理解和作者的补充写在这里.\n\n * 在文章里提到了使用\n//{{{\n use LWP::ConnCache;\n\n $browser->conn_cache(LWP::ConnCache->new()): \n//}}}\n 这告诉 browser object 使用 HTTP/1.1 “keep-Alive” 特性, 即重复使用先前的 socket 来加快请求速度.\n\n 你也可以在 new LWP::UserAgent 时为 $browser 加上 “keep-Alive” 特性, 如下\n//{{{\n use LWP;\n $browser = new LWP::UserAgent(keep_alive => 1);\n//}}}\n * 不要忘记 response object 的 header 通常有很多非常值得注意的信息, 你可以通过 headers_as_string 和 as_string 函数来得到. 下面是使用 headers_as_string 返回的例子\n//{{{\n use LWP;\n my $br = LWP::UserAgent->new;\n my $resp = $br ->get('http://www.pulse24.com'); \n print $resp->headers_as_string";\n//}}}\n 输出结果:\n//{{{\n Cache-Control: private, max-age=0\n Connection: close\n Date: Sun, 16 Jan 2005 04:18:26 GMT\n Server: Microsoft-IIS/6.0\n Content-Length: 432\n Content-Type: text/html\n Content-Type: text/html; charset=iso8859-1\n Client-Date: Sun, 16 Jan 2005 04:18:09 GMT\n Client-Peer: 207.61.136.40:80\n Client-Response-Num: 1\n REFRESH: 0;URL=http://www.pulse24.com/Front_Page/page.asp\n X-Meta-Robots: noindex\n X-Powered-By: ASP.NET\n//}}} \n\n 你也可以通过 $response->header(‘field’) 来获得想要的特别 header. 象上面的例子, 如果要访问的网页使用了 meta refresh :\n\n@@<META HTTP-EQUIV="REFRESH" CONTENT="0;URL=http://www.pulse24.com/Front_Page/page.asp">@@\n \n 你可以使用 $response->header(‘refresh’) 来拿到 refresh 的 url 地址, 选择是否继续跟进.\n* 有些时候, 浏览器可以正常访问到的地址, LWP 却不行. 一般是因为你的 LWP 的 header, referer , cookie 或 user-agent 等的设定与对方网络服务器允许连入的不同. 为了找到问题所在, 你需要比较浏览器发出的请求和你的 LWP 发出的请求有何不同, 然后修改再尝试. 很多时候这是反反复复的工作. 我最早使用 Ethereal 来监视,抓取数据, 目前使用 Firefox 的 LiveHTTPHeaders 插件. 现在LWP 也自带一个数据分析模块 LWP::DebugFile 来帮助你找到问题.\n* 另外, 文章里提到了 HTTP::Cookies::Netscape , 现在LWP Cookies 模块支持更多浏览器 Mozilla , Safari , Omniweb\n* 很多时候表格与 javascript 一起使用, LWP 没有分析 Javascript 的引擎,所以你必须分析网页源码里 Javascript 来决定怎样处理.\n//{{{\n function Submit()\n {\n ......... \n self.document.location.href="verify.php";\n\n return false;\n }\n\n ........\n\n <form>\n ......\n\n <input type=button value="Submit your page" onClick="javascript:Submit();return false;//">\n//}}}\n 上面的这个例子通过表格提交来触动 javascript 的 submit 函数, 最后调用了 verify.php. 现在你就可以跳过所有的 javascript 而直接对 verify.php 来提交.\n
大部分HTML表格使用HTML POST 向服务器提交数据,在这里你可以这样:\n//{{{\n $response = $browser->post( $url,\n [\n formkey1 => value1, \n formkey2 => value2, \n ...\n ],\n );\n//}}}\n或者你也可以把 HTTP header 也一起发出\n//{{{\n $response = $browser->post( $url,\n [\n formkey1 => value1, \n formkey2 => value2, \n ...\n ],\n headerkey1 => value1, \n headerkey2 => value2, \n );\n//}}}\n下一个例子向 AltaVista 的搜索引擎发送 HTTP POST 请求,然后从HTML里提取出符合匹配的总数。\n//{{{\n use strict;\n use warnings;\n use LWP 5.64;\n my $browser = LWP::UserAgent->new;\n\n my $word = 'tarragon';\n\n my $url = 'http://www.altavista.com/sites/search/web';\n my $response = $browser->post( $url,\n [ 'q' => $word, # the Altavista query string\n 'pg' => 'q', 'avkw' => 'tgz', 'kl' => 'XX',\n ]\n );\n die "$url error: ", $response->status_line\n unless $response->is_success;\n die "Weird content type at $url -- ", $response->content_type\n unless $response->content_type eq 'text/html';\n\n if( $response->content =~ m{AltaVista found ([0-9,]+) results} ) {\n # 从 "AltaVista found 2,345 results" 里匹配出结果\n print "$word: $1\sn";\n } else {\n print "Couldn't find the match-string in the response\sn";\n }\n//}}}
//{{{\n译者/作者:chunzi\n出处:中国Perl协会 FPC(Foundation of Perlchina)\n原名:The Evolution of Perl Email Handling\n作者:Simon Cozens\n原文:http://www.perl.com/pub/a/2004/06/10/email.html\n发表:June 10, 2004\n请保护作者的著作权,维护作者劳动的结晶。\n//}}}\n每天我都要花费大量的时间在电子邮件相关的工作上,或者通过邮件来和其他工作伙伴联系,或者饶有兴致地分析,索引,重新组织以及挖掘邮件内容。很自然的,Perl 协助我做这些事情。\n\n在 CPAN 上有很多现成的模块可以用来处理电子邮件,我们将介绍其中几个主要的。同时我们也将关注由我和 Richard Clamp,Simon Wistow 以及其他伙伴所致力的 Perl 电子邮件项目(Perl Email Project),该项目的目标是提供一系列简单的,有效的,精准的邮件处理模块。\n!邮件消息的处理\n我们从一些比较简单的,用来描绘一封单独邮件,提供对邮件头和邮件体的访问,甚至修改它们的信息的那些模块开始介绍。\n\n所有的这些模块的曾祖父都是 Mail::Internet ,由 Graham Barr 创建,目前 Mark Overmeer 在维护。该模块提供了通过数组(元素为字符串行)或者文件句柄来读取信件内容的构造器,并通过它返回一个描述该信件的 Mail::Internet 对象。在下面的例子中,我们使用变量 $rfc2822 来表示字符串形式的邮件信息内容。\n//{{{\n my $obj = Mail::Internet->new( [ split /\sn/, $rfc2822 ] );\n//}}}\nMail::Internet 从信件中提取构造出一个邮件头对象,并连带邮件体信息。邮件头对象的类为 Mail::Header 。你可以通过该对象获取或者设置邮件头的信息:\n//{{{\n my $subject = $obj->head->get("Subject");\n $obj->head->replace("Subject", "New subject");\n//}}}\n而读取或者编辑邮件体内容的操作,则可以使用 body 方法:\n//{{{\n my $old_body = $obj->body;\n $obj->body("Wasn't worth reading anyway.");\n//}}}\n到现在为止我还没有提到过任何关于 MIME 的东西。对于简单的任务来说,Mail::Internet 确实非常方便,不过它并不完全支持对 MIME 的处理。谢天谢地,MIME::Entity 作为一个为 MIME 而考虑设计的 Mail::Internet 子类,允许你读取 MIME 消息的每一个独立的部分(part):\n//{{{\n my $num_parts = $obj->parts;\n for (0..$num_parts) {\n my $part = $obj->parts($_);\n ...\n }\n//}}}\n如果 Mail::Internet 和 MIME::Entity 都不适合你,你可以试试 Mark Overmeer 自己的 Mail::Message 模块,该模块是令人印象深刻的 Mail::Box 模块中的一部分。Mail::Message 是个极富特色的、功能全面的模块,但这些优点并不总意味着褒扬。\n\nMail::Message 对象通常都是在 Mail::Box 读取一个电子邮件文件夹的时候,在内部构建的。当然它也可以通过 read 方法来读取一封信件:\n//{{{\n $obj = Mail::Message->read($rfc2822);\n//}}}\n就像 Mail::Internet 一样,邮件消息被分割为邮件头和邮件体,而与 Mail::Internet 不同的是,邮件体也是一个对象。我们如此读取邮件头:\n//{{{\n $obj->head->get("Subject");\n//}}}\n或者,如果是 Subject 头信息以及其他常见的邮件头信息,可以如此读取:\n//{{{\n $obj->subject;\n//}}}\n我找不到直接设置头信息的方法,所以最终可能需要这样做:\n//{{{\n $obj->head->delete($header);\n $obj->head->add($header, $_) for @data;\n//}}}\n读取邮件体内容作为字符串形式表达也仅有一点麻烦:\n//{{{\n $obj->decoded->string\n//}}}\n而设置邮件体内容的操作则绝对是恶梦 -- 我们不得不构建一个 Mail::Message::Body 对象来覆盖现有的。\n//{{{\n $obj->body(Mail::Message::Body->new(data => [split /\sn/, $body]));\n//}}}\nMail::Message 处理邮件的时候可能有点慢,也着实难用。它的体系也非常复杂,上面我们所看到的这些操作就已经用到了 16 种类 (Mail::Address, Mail::Box::Parser, Mail::Box::Parser::Perl, Mail::Message, Mail::Message::Body, Mail::Message::Body::File, Mail::Message::Body::Lines, Mail::Message::Body::Multipart, Mail::Message::Body::Nested, Mail::Message::Construct, Mail::Message::Field, Mail::Message::Field::Fast, Mail::Message::Head, Mail::Message::Head::Complete, Mail::Message::Part, 以及 Mail::Reporter)和 4400 多行的代码。尽管它确实拥有很多功能,我还是傻傻的觉得邮件的分析处理应该更为简洁。所以我坐下来决定自己着手编写尽可能简洁的邮件处理函数库,结果就有了 Email::Simple 模块,它的交互界面如下所示:\n//{{{\n my $obj = Email::Simple->new($rfc2822);\n my $subject = $obj->header("Subject");\n $obj->header_set("Subject", "A new subject");\n my $old_body = $obj->body;\n $obj->body_set("A new body\sn");\n print $obj->as_string;\n//}}}\n它做的事情并不多,但却非常简单和高效。如果你需要 MIME 处理,可以使用它的子类 Email::MIME, 该类增加了 parts 方法。\n\n实际上,选择哪一种邮件处理函数库完全取决于你,最终用户,不过并不总是这样的。有许多辅助性的模块,帮助你在更高的应用层上处理邮件信息的,可能要求你提供特定的邮件表达对象。比如最近的 Mail::ListDetector 模块(稍后我们将解析),需要传给它的邮件为 Mail::Internet 对象,因为该对象的操作界面(API)是已知的。而我不想用 Mail::Internet 对象,但我又需要 Mail::ListDetector 的一些功能,那我可以做些什么呢?\n\n为了让用户也能够有这样的选择,我写了一个用于表达上面各个模块操作界面的抽象层,叫做 Email::Abstract 。给出上面任何一种类型的对象,我们都可以说:\n//{{{\n my $subject = Email::Abstract->get_header($obj, "Subject");\n Email::Abstract->set_header($obj, "Subject", "My new subject");\n my $body = Email::Abstract->get_body($obj);\n Email::Abstract->set_body($message, "Hello\snTest message\sn");\n $rfc2822 = Email::Abstract->as_string($obj);\n//}}}\nEmail::Abstract 知道如何在这些主要的邮件表达对象上作相应的操作。它也抽象了构造邮件消息的过程,并允许你通过类方法 cast 来改变邮件消息对象的操作界面:\n//{{{\n my $obj = Email::Abstract->cast($rfc2822, "Mail::Internet");\n//}}}\nmy $mm = Email::Abstract->cast($obj, "Mail::Message"); 这样使得模块的作者得以使用“接口预先未知(interface-agnostic)”的方式来撰写邮件处理函数库。我很感谢 Michael Stevens 立即在 Mail::ListDetector 中使用了 Email::Abstract 。现在我可以将 Email::Simple 对象传递给 Mail::ListDetector 了,而且它工作的非常好。\n\nEmail::Abstract 也给了我们对上面所有这些模块作基准测试(benchmarks)的机会。这里是我使用的测试代码:\n//{{{\n use Email::Abstract;\n my $message = do { local $/; ; };\n my @classes =\n qw(Email::MIME Email::Simple MIME::Entity Mail::Internet Mail::Message);\n\n eval "require $_" or die $@ for @classes;\n\n use Benchmark;\n my %h;\n for my $class (@classes) {\n $h{$class} = sub {\n my $obj = Email::Abstract->cast($message, $class);\n Email::Abstract->get_header($obj, "Subject");\n Email::Abstract->get_body($obj);\n Email::Abstract->set_header($obj, "Subject", "New Subject");\n Email::Abstract->set_body($obj, "A completely new body");\n Email::Abstract->as_string($obj);\n }\n }\n timethese(1000, \s%h);\n\n __DATA__\n ...\n//}}}\n我把一封短小的邮件放到 DATA 部分中,并运行相同的操作一千次:构造一个新的消息对象,读取邮件头,读取邮件体,并将消息内容作为字符串返回。\n//{{{\n Benchmark: timing 1000 iterations of Email::MIME, Email::Simple, \n MIME::Entity, Mail::Internet, Mail::Message...\n Email::MIME: 10 wallclock secs ( 7.97 usr + 0.24 sys = 8.21 CPU) \n @ 121.80/s (n=1000)\n Email::Simple: 9 wallclock secs ( 7.49 usr + 0.05 sys = 7.54 CPU) \n @ 132.63/s (n=1000)\n MIME::Entity: 33 wallclock secs (23.76 usr + 0.35 sys = 24.11 CPU) \n @ 41.48/s (n=1000)\n Mail::Internet: 24 wallclock secs (17.34 usr + 0.30 sys = 17.64 CPU) \n @ 56.69/s (n=1000)\n Mail::Message: 20 wallclock secs (17.12 usr + 0.27 sys = 17.39 CPU) \n @ 57.50/s (n=1000)\n//}}}\nPerl 电子邮件项目确实是成功的:Email::MIME 和 Email::Simple 的运行速度差不多是对手的两倍。然而,我们要强调一点,这里所做的测试都是非常低级的,如果你要做任何比这里看到的更加复杂的操作,你该考虑哪些老的 Mail:: 模块。\n!邮箱的处理\n对于单独信件的处理已经谈了很多了,让我们来看看对一组邮件或者存放邮件的文件夹该如何处理。我们提到过 Mail::Box ,它绝对是处理邮件文件夹的老大,它支持本地和远程的文件夹处理,可以编辑文件夹,以及作相应的排序操作等等。要使用它,我们首先需要 Mail::Box::Manager 模块,它是用来构建 Mail::Box 对象的工厂对象。\n//{{{\n use Mail::Box::Manager\n my $mgr = Mail::Box::Manager->new;\n//}}}\n接下来,我们通过管理器来打开文件夹:\n//{{{\n my $folder = $mgr->open(folder => $folder_file);\n//}}}\n而现在,我们可以获取各个独立的邮件表达对象(Mail::Message):\n//{{{\n for ($folder->messages) {\n print $_->subject,"\sn";\n }\n//}}}\n与此最为相近的,我喜欢用的邮箱管理器还是 Mail::Util 的 read_mbox 函数。把 Unix 中 mbox 文件路径传递给它,然后返回一系列的匿名数组,每个匿名数组都表示一个邮件消息,其元素为该消息的每一行。如此一来,它非常适合 Mail::Internet->new 或者相近的:\n//{{{\n for (read_mbox($folder_file)) {\n my $obj = Mail::Internet->new($_);\n print $_->head->get("Subject"),"\sn";\n }\n//}}}\n这两种做法都非常容易,不过似乎在 Mail::Util 的简洁性和 Mail::Box 的功能上还有些简化的余地,于是电子邮件项目再次停滞下来,这次的焦点集中在 Email::Folder 和 Email::LocalDelivery 上面。 Email::Folder 可以处理 mbox 和 maildir 格式的邮件文件夹,以及计划中更多其他格式,并且它有非常简洁的操作界面:\n//{{{\n my $folder = Email::Folder->new($folder_file);\n for ($folder->messages) {\n print $_->header("Subject"),"\sn";\n }\n//}}}\n默认情况,它返回一系列 Email::Simple 对象用以表达每封邮件,不过这可以通过派生一个子类来改变。例如,如果我们想要原始的 RFC2822 格式的字符串,我们可以这样做:\n//{{{\n package Email::Folder::Raw; use base 'Email::Folder';\n sub bless_message { my ($self, $rfc2822) = @_; return $rfc2822; }\n//}}}\n可能将来我们不用再派生一个子类,然后 bless_message ,而改用 Email::Abstract->cast 来更容易的改变对邮件消息的表达方式。\n\n处理文件夹的另一方面就是如何写数据了。或者说如何本地投递。Email::LocalDelivery 模块的出现是为了辅助 Email::Filter 。问题比听起来要更难些,因为它必须处理锁定,跳开邮件体,以及由 mailbox 和 maildir 等不同格式而引发的问题。而 LocalDelivery 则通过简单的界面把所有这些都隐藏起来:\n//{{{\n Email::LocalDelivery->deliver($rfc2822, @mailboxes);\n//}}}\nEmail::LocalDelivery 和 Email::Folder 都使用了 Email::FolderType 模块来帮助确定是哪种类型的邮件文件夹(通过文件名来判断)。\n!邮件地址的处理\n我们再次从抽象层面回到低级的处理,有大量的模块可用于对邮件地址的处理。我很喜欢老的 Mail::Address 模块。邮件地址可以分割为各种字段,诸如:实际的邮件地址,名称短语,注释信息。例如:\n//{{{\n Example user (Not a real user)\n//}}}\nMail::Address 解析这些邮件地址,并将名称短语和注释分离出来,以便获取各个独立的部分:\n//{{{\n for (Mail::Address->parse($from_line)) {\n print $_->name, "\st", $_->address, "\sn";\n }\n//}}}\n不幸的是,和其他很多邮件模块一样,并不真的那么有用。\n//{{{\n my ($addr) = Mail::Address->parse('"eBay, Inc." ');\n print $addr->name # Inc. eBay\n//}}}\n得到的结果仍然难以让人接受,虽然它比之间的版本所返回的 "Inc Ebay" 要好些。于是 Casey West 加入我们并创造了 Email::Address 模块。它和 Mail::Address 使用一致的交互界面,并且运行地更加快速,差不多两到三倍。(译注:上面的例子中,Email::Address 返回 "eBay, Inc." 。看来在作者眼里,Mail::Address 的作者画蛇添足了。)\n\n还有一件我们经常需要做的事情就是校验邮件地址是否合法。比如,某个用户在站点上注册,我们就需要对他所提供的邮件地址是否能够接收邮件作检查。Email::Valid 模块是在我们这帮叛逆的人冲进来之前,就已有的 Email:: 名字空间的原住民,这个模块就是用来做这件事情的。在它最简约的用法中,我们可以说:\n//{{{\n if (not Email::Valid->address('test@example.com')) {\n die "Not a valid address"\n }\n//}}}\n你也可以打开其他检查的选项,比如确定它的域名拥有一个合法的 MX 记录,修正常见的 AOL 和 Compuserve 的邮件地址的一些错误,如下:\n//{{{\n if (not Email::Valid->address(-address => 'test@example.com',\n -mxcheck => 1)) {\n die "Not a valid address"\n }\n//}}}\n!邮件数据转换\n我们有了自己的信件,接下来会对它们做些什么呢?我发现大多是对邮件进行文本化分析,这里有三个模块可以协助我们:\n\n首先是 Text::Quoted ,它获取邮件体的文本,实际上可以是任何其他文本,然后尝试找出某些引用其他邮件的文本部分,然后将之分离并保存到嵌套的数据结构中。例如,如果我们有\n//{{{\n $message = < foo\n > # Bar\n > baz\n\n quux\n EOF\n//}}}\n然后运行 extract($message) 就会返回如下的数据结构:\n//{{{\n [\n [\n { text => 'foo', quoter => '>', raw => '> foo' },\n [ \n { text => 'Bar', quoter => '> #', raw => '> # Bar' } \n ],\n { text => 'baz', quoter => '>', raw => '> baz' }\n ],\n\n { empty => 1 },\n { text => 'quux', quoter => '', raw => 'quux' }\n ];\n//}}}\n当你显示邮件消息的内容时,准备用不同的颜色来区分不同的引用文本,那么这个模块就帮到你大忙了。类似概念的还有 Text::Original 模块,用于搜寻以原始文件内容开头,没有被引用的部分。它知道如何识别各种类型的属性行,所以有:\n//{{{\n $message = < Why are there so many different mail modules?\n\n There's more than one way to do it! Different modules have different\n focuses, and operate at different levels; some lower, some higher.\n EOF\n//}}}\n那么 first_sentence($message) 将返回 There's more than one way to do it!。Mariachi 邮件列表存档程序就使用了这项技术,为一个线索中的邮件给出它的提白。\n\n说到邮件的线索化,Mail::Thread 模块实现了 Jamie Zawinski 的邮件线索化算法,该算法先是被 Mozilla 所用,继而许多其他邮件客户端也开始使用这种技术。当然 Mariachi 也使用了这项技术,最近它还作了更新,使用 Email::Abstract 来处理各种你扔过去的邮件表达对象:\n//{{{\n my $threader = Mail::Thread->new(@mails);\n $threader->thread; # 计算线索\n for ($threader->rootset) { # 在一个线索内的原始邮件\n dump_thread($_);\n }\n//}}}\n!邮件过滤\n经典的 Perl 的邮件过滤工具莫不就是 Mail::Audit 了,我还在这里写过关于如何使用 Mail::Audit 模块的文章(http://www.perl.com/pub/a/2001/07/17/mailfiltering.html),以及如何与 Mail::SpamAssassin (http://www.perl.com/pub/a/2002/03/06/spam.html)模块相结合使用。\n\n我们已经提到过 Mail::ListDetector 模块好几次了。我把它和 Mail::Audit 结合在一起使用,帮助自己做了大量的自动邮件过滤工作。Mail::Audit::List 的插件使用 ListDetector 来查找信件中的邮件列表头信息,诸如 List-Id,X-Mailman-Version 等等类似的东西,这些头信息可以帮助判别该邮件是否来自于邮件列表。这意味着我有能力过滤所有来自邮件列表的信件到各自的文件夹中,就像这样:\n//{{{\n my $list = Mail::ListDetector->new($obj);\n if ($list) {\n my $name = $list->listname;\n $item->accept("mail/$name.-$date");\n }\n//}}}\n然而,Mail::Audit 本身还有很长一段路要走,所以如果你新架设的系统的话,我们鼓励您使用电子邮件项目的 Email::Filter 模块作为替代,它们的大部分操作界面是一致的,尽管功能并不完全相同。为了追求简洁和速度,它使用了新式的 Email::Simple 作为邮件表达对象模块。\n!邮件信息挖掘\n最后,我所做的比较高级的事情就是开发一个自动分类,组织,并索引邮件到数据库的应用框架,并尝试从中分析并提取有价值的信息。\n\n我的第一个完成这个预期目标的模块是 Mail::Miner ,它由三个主要部分组成。第一个部分获取一封邮件后,去除各种附件,并分别存储到数据库。第二部分纵览这封邮件并运行一系列的识别(Recogniser)模块,如此搜寻邮件地址,电话号码,一些关键字和短语等等,并把它们存储到另一个独立的数据库表中。第三部分为命令行工具,用来查询数据库中的邮件以及相关的信息。\n\n举个例子,如果我需要找 Tim O'Reilly 的邮政地址,我就会使用查询工具 mm ,从他发来的信中找出该地址:\n//{{{\n % mm --from "Tim O" --address \n Address found in message 1835 from "Tim O'Reilly" :\n Tim O'Reilly @ O'Reilly & Associates, Inc.\n 1005 Gravenstein Highway North, Sebastopol, CA 95472\n//}}}\n如果要获取完整的邮件,我可以说\n//{{{\n % mm --id 1835\n//}}}\n如果它原本包含一个附件,那么我们可能会看到类似下面的部分:\n//{{{\n [ text/xml attachment something.xml detached - use\n mm --detach 208\n to recover ]\n//}}}\n我粘贴中间的那一行 mm --detach 208 到 shell 中,然后很快的,something.xml 写到了磁盘上。\n\n现在 Mail::Miner 已经非常不错了,不过它把三种思想紧紧地捆绑在一个包中 -- 邮件的归档,邮件的数据挖掘以及查询数据库的命令行界面 -- 这使得很难单独开发或者扩展每块的功能。当然,它使用了老式的 Mail:: 名字空间。\n\n这引领我们走到这次邮件模块旅程的最后一站,最新发布的:Email::Store 模块。这是个基于 Class::DBI 的应用框架,用来存储邮件到数据库并以各种方式索引:\n//{{{\n use Email::Store 'dbi:SQLite:mail.db';\n Email::Store->setup;\n Email::Store::Mail->store($rfc2822);\n//}}}\n紧接着...\n//{{{\n my ($name) = Email::Store::Name->search( name => "Simon Cozens" )\n @mails_from_simon = $name->addressings( role => "From" )->mails;\n//}}}\n它可以用来构建类似 Mariachi 的邮件列表归档工具,或者类似 Mail::Miner 的数据挖掘。它仍然在初步的开发阶段,并在增强模块的扩展性方面使用了一些新的思想。\n\n在我们使用 Email::Store 写出第一个邮件归档和搜索工具的时候,我会再次给大家作详细介绍的。这也是为了 perl.org 的新的 Perl 邮件列表处理接口而准备做的工作。\n!小结\n我们已经看过了 CPAN 上的几个主要的邮件处理模块,当然还有更多。很明显的,我着实偏袒那些自己写的模块。特定的 Perl 电子邮件项目的模块则使用 Email::* 的名字空间。我们特别设计了这些简洁、高效的模块,而它们并不总是老式的 Mail::* 模块的优良替换方案,特别像 Mail::Box 之类。到此,我希望各位通过对本文的阅读,了解和认识更多的邮件处理工具模块,并在之后使用 Perl 来处理邮件时,胸中有丘壑。
默认的LWP::UserAgent对象像一个不支持cookies的浏览器一样工作。有不只一种的办法可以设定它的cookie_jar属性,从而让它支持cookies。“cookie jar” 是一个用来储存 HTTP cookie 的容器。你可以把它存到硬盘(像Netscape使用cookies.txt一样)或内存里。存到内存里的 cookies 会在程序完成后消失。\n\n内存式的 cookie 使用方法:\n//{{{\n $browser->cookie_jar({});\n//}}}\n也可以把 cookie 储存到硬盘的文件里:\n//{{{\n use HTTP::Cookies;\n $browser->cookie_jar( HTTP::Cookies->new(\n 'file' => '/some/where/cookies.lwp',\n # 储存 cookies 的地址\n 'autosave' => 1,\n # 当完成后自动储存到硬盘里\n )); \n//}}}\n文件里的 cookie 是以 LWP 自定的格式储存,如果你想在 netscape 里使用这个 cookie 文件,可以使用 HTTP::Cookies::Netscape class :\n//{{{\n use HTTP::Cookies;\n # yes, loads HTTP::Cookies::Netscape too\n\n $browser->cookie_jar( HTTP::Cookies::Netscape->new(\n 'file' => 'c:/Program Files/Netscape/Users/DIR-NAME-HERE/cookies.txt',\n # where to read cookies\n ));\n//}}}\n你也可以象上面一样使用 ‘autosave’ => 1 。 但 Netscape 的 cookie 有时会在写入硬盘之前就被丢掉,至少在写这篇文章的时候还是这样。
有时你希望(或者是必须)通过代理来连接某些站点或协议,就比如你的LWP程序是运行在某台处于防火墙之内的机器上。\n代理通常储存在环境变量 HTTP_PROXY 里。LWP 可以通过 user-agent object 里的 env_proxy 函数把环境变量里的代理地址装载。\n//{{{\n use LWP::UserAgent;\n my $browser = LWP::UserAgent->new;\n\n # And before you go making any requests:\n $browser->env_proxy;\n//}}}\n详细请参照 LWP::UserAgent 文档里的 proxy, env_proxy 和 no_proxy 方法.
LWP::UserAgent objects 有几个值得注意的属性 :\n* $browser->timeout(15): 设定缺省 request 的 timeout 时间.超过这个时间就放弃请求。\n* $browser->protocols_allowed( [ ‘http’, ‘gopher’] ): 这用来设定只接受 http 和 gopher 协议。连接其他协议时就返回 500 错误值,”Access to ftp URIs has been disabled” 的错误消息。\n* use LWP::ConnCache; $browser->conn_cache(LWP::ConnCache->new()): 这告诉 browser object 使用 HTTP/1.1 “keep-Alive” 特性,即重复使用先前的 socket 来加快请求速度。\n* $browser->agent( ‘SomeName/1.23 (more info here maybe)’ ): 设置 HTTP 请求的 User-Agent。LWP 缺省使用 “libwww-perl/versionnumber” 作为 User-Agent,比如 “libwww-perl/5.65”。你可以加上更多的信息:\n//{{{\n $browser->agent( ‘SomeName/3.14 (contact@robotplexus.int)’ );\n//}}} \n 或者可以伪装为\n//{{{\n$browser->agent( ‘Mozilla/4.0 (compatible; MSIE 5.12; Mac_PowerPC)’ );\n//}}} \n\n* push @{ $ua->requests_redirectable }, ‘POST’: 告诉 LWP 在 POST 请求发送后如果发生重新定向就自动跟随 ( 虽然 RFC 里不要求这么做 )\n\n详细请参见 LWP::UserAgent 文档.\n
如果想遵循 robots.txt 和避免在较短的时间发出太多的请求,你可以采用 LWP::RobotUA 而不是 LWP::UserAgent。\n\nLWP::RobotUA 用法与 LWP::UserAgent 一样:\n//{{{\n use LWP::RobotUA;\n my $browser = LWP::RobotUA->new(\n 'YourSuperBot/1.34', 'you@yoursite.com');\n # 机器人名字和 email 地址\n\n my $response = $browser->get($url);\n\nHTTP::RobotUA 多了几个特性:\n//}}}\n * 如果 $url 请求的服务器的 robots.txt 禁止了你对 $url的访问,那么 $browser 就不会发出对于这个地址的请求,而是返回 403 代码和一个错误信息 “Forbidden by robots.txt”。\n@@die "$url -- ", $response->status_line, "\snAborted" \n unless $response->is_success;@@\n 然后你会得到这样的错误信息:\n@@http://whatever.site.int/pith/x.html -- 403 Forbidden \n by robots.txt\n Aborted at whateverprogram.pl line 1234@@\n *如果 $browser 发现请求的地址是刚刚请求过的,就会暂停 (sleep) 来避免发送太多的请求。缺省暂停 1 分钟,但可以通过 $browser->delay( minutes ) 来设定。比如:\n@@$browser->delay( 7/60 );@@\n详细请参见 LWP::RobotUA文档.
编码转换\n//{{{\nperl -MEncode -pi -e '$_=encode_utf8(decode(gb2312=>$_))' ABS_Guide_cn.txt\n//}}}
使用 last 即可在loop内部直接终止循环 \n//{{{\n# Print all input lines mentioning fred, until the _ _END_ _ marker\n while (<STDIN>) {\n if (/_ _END_ _/) {\n # No more input on or after this marker line\n last;\n } elsif (/fred/) {\n print;\n }\n }\n ## last comes here ##\n//}}}
//{{{\nforeach $key (sort keys %hash) {\n $value = $hash{$key};\n print "$key => $value\sn";\n # Or, we could have avoided the extra $value variable:\n # print "$key => $hash{$key}\sn";\n}\n//}}}
eg,如果文件已存在,则终止程序,使用了-e 测试文件是否存在\n//{{{\n die "Oops! A file called '$filename' already exists.\sn"\n if -e $filename;\n//}}}\n\neg.如果的修改日期大于距今28天,则提示\n//{{{\n warn "Config file is looking pretty old!\sn"\n if -M CONFIG > 28;\n//}}}\n|File test|Meaning|\n|-r|File or directory is readable by this (effective) user or group|\n|-w|File or directory is writable by this (effective) user or group|\n|-x|File or directory is executable by this (effective) user or group|\n|-o|File or directory is owned by this (effective) user|\n|-R|File or directory is readable by this real user or group|\n|-W|File or directory is writable by this real user or group|\n|-X|File or directory is executable by this real user or group|\n|-O|File or directory is owned by this real user|\n|-e|File or directory name exists|\n|-z|File exists and has zero size (always false for directories)|\n|-s|File or directory exists and has nonzero size (the value is the size in bytes)|\n|-f|Entry is a plain file|\n|-d|Entry is a directory|\n|-l|Entry is a symbolic link|\n|-S|Entry is a socket|\n|-p|Entry is a named pipe (a "fifo")|\n|-b|Entry is a block-special file (like a mountable disk)|\n|-c|Entry is a character-special file (like an I/O device)|\n|-u|File or directory is setuid|\n|-g|File or directory is setgid|\n|-k|File or directory has the sticky bit set|\n|-t|The filehandle is a TTY (as reported by the isatty( ) system function; filenames can't be tested by this test)|\n|-T|File looks like a "text" file|\n|-B|File looks like a "binary" file|\n|-M|Modification age (measured in days)|\n|-A|Access age (measured in days)|\n|-C|Inode-modification age (measured in days)|
//{{{\n my $person = "betty";\n delete $books{$person}; # Revoke the library card for $person\n//}}}
request (请求) 常用的方法是 $response = $browser->get($url),但如果需要,你可以在 $url 后跟一个键值的列表来给你的 request 加上其他 HTTP headers。象这样 :\n//{{{\n $response = $browser->get( $url, $key1, $value1, $key2, $value2, ... );\n//}}}\n举个例子,如果你要对一个只允许 Netscape 浏览器连入的网站发出请求,那就需要发出类似 Netscape 的 header,如下:\n//{{{\n my @ns_headers = (\n 'User-Agent' => 'Mozilla/4.76 [en] (Win98; U)',\n 'Accept' => 'image/gif, image/x-xbitmap, image/jpeg, \n image/pjpeg, image/png, */*',\n 'Accept-Charset' => 'iso-8859-1,*,utf-8',\n 'Accept-Language' => 'en-US',\n );\n\n ...\n\n $response = $browser->get($url, @ns_headers);\n//}}}\n如果不打算重复使用这个 array,你可以把它写到 get 函数里\n//{{{\n $response = $browser->get($url,\n 'User-Agent' => 'Mozilla/4.76 [en] (Win98; U)',\n 'Accept' => 'image/gif, image/x-xbitmap, image/jpeg, \n image/pjpeg, image/png, */*',\n 'Accept-Charset' => 'iso-8859-1,*,utf-8',\n 'Accept-Language' => 'en-US',\n );\n//}}}\n如果只是打算修改 User-Agent,可以通过 LWP::UserAgent 的 agent 方法把缺省的agent ‘libwww-perl/5.65’(或者别的)改掉。\n//{{{\n $browser->agent('Mozilla/4.76 [en] (Win98; U)');\n//}}}
请求较大的文件时, 普通的请求方法 ( 例如 $response = $browser->get($url) ) 会给你带来内存问题. 因为 $response 储存着整个文件. 如果请求了一个 30MB 的文件, 那可能不是什么明智的做法.\n\n一个解决方法是把文件存到硬盘\n//{{{\n $response = $ua->get($url,\n ':content_file' => $filespec,\n );\n//}}}\n比如:\n//{{{\n $response = $ua->get('http://search.cpan.org/',\n ':content_file' => '/tmp/sco.html'\n );\n//}}}\n当使用 content_file 时, headers 还是在$response, 但$response->content 是空.\n\n值得注意的是LWP 5.66 之前的版本不支持content_file . 你应该使用 use LWP 5.66; 如果你的程序可能运行在低版本的 LWP 上, 你也可以使用下面的例子来保证兼容性, 这与 content_file 有同样效果.\n//{{{\n use HTTP::Request::Common;\n $response = $ua->request( GET($url), $filespec );\n//}}}
只要有安装 LWP 的HTTPs 的支持, 访问 HTTPs URLs 的方法和 HTTP 一样\n//{{{\n use LWP 5.64;\n my $url = 'https://www.paypal.com/'; # Yes, HTTPS!\n my $browser = LWP::UserAgent->new;\n my $response = $browser->get($url);\n die "Error at $url\sn ", $response->status_line, "\sn Aborting" \n unless $response->is_success;\n\n print "Whee, it worked! I got that ",\n $response->content_type, " document!\sn";\n//}}}\n如果没有 HTTPs 的支持, 如下错误信息会显示\n//{{{\n Error at https://www.paypal.com/\n 501 Protocol scheme 'https' is not supported\n Aborting at paypal.pl line 7. [or whatever program and line]\n//}}}\n如果你安装了 LWP 的 HTTPS 支持的话,你的请求应该是成功的,你可以像对待普通的 HTTP 请求一样处理 $response 对象。\n\n关于安装 HTTPS 支持的信息可以在libwww-perl 里的README.SSL 文件找到.
一些HTML表格不使用 POST 请求,而是使用 GET 请求来传输数据。例如,在 imdb.com 里检索电影名字 ‘Blade Runner’, 提交后在浏览器的网址栏里将显示 :\n//{{{\n http://us.imdb.com/Tsearch?title=Blade%20Runner&restrict=Movies+and+TV\n//}}}\n下面是使用 LWP 实现同样的结果 :\n//{{{\n use URI;\n my $url = URI->new( 'http://us.imdb.com/Tsearch' );\n # makes an object representing the URL\n\n $url->query_form( # And here the form data pairs:\n 'title' => 'Blade Runner',\n 'restrict' => 'Movies and TV',\n );\n\n my $response = $browser->get($url);\n//}}}\n第5章详细描述了 HTML 表格和表格数据,第6章到第9章描述了怎样从获得的HTML数据里提取出有用的信息。
//{{{\nwhile ( ($key, $value) = each %hash ) {\n print "$key => $value\sn";\n}\n//}}}
//{{{\nopendir(DIR, $dirname) or die "can't opendir $dirname: $!";\nwhile (defined($file = readdir(DIR))) {\n # do something with "$dirname/$file"\n}\nclosedir(DIR);\n//}}}
[[使用Perl处理电子邮件的方法的演化]]
开始一个空白的 TiddlyWiki, 修改下列 [[tiddlers]]:\n* SiteTitle & SiteSubtitle: 可以首先修改顶部的标题和副标题。\n* MainMenu: 左侧主菜单\n* DefaultTiddlers: TW打开后显示的默认页面\n你还可以修改你自己的签名: <<option txtUserName>>