客户端 Web 脚本
Linux 有许多 Web 浏览器和 FTP 客户端,它们功能丰富,能够满足所有用户,从命令行狂热者到 3D 多屏桌面爱好者。然而,它们都有一个共同的缺陷:您必须在键盘前才能驱动它们。当然,像 wget 这样的优秀工具可以在您睡觉时镜像整个站点,但您仍然必须首先找到正确的 URL,并且完成后,您仍然必须通读下载的每一位内容。
对于小型静态站点,这没什么大不了的,但是如果每天您都想下载一个被赋予随机 URL 的页面怎么办?或者,如果您不想阅读 100K 的内容,只是为了滚动浏览几个标题怎么办?
这就是客户端 Web 脚本的用武之地,即所有允许您只花时间查看您感兴趣的网页(或网页部分)的技术,并且仅在您的计算机为您找到它们之后。通过这样的脚本,您可以只阅读与您所在地区相关的交通或天气信息,只从网页上下载某些图片,或自动找到您需要的单个链接。
除了节省时间外,客户端 Web 脚本还可以让您了解一些重要问题,并教会您一些自律。首先,不加选择地执行此处解释的内容在某些情况下可能被视为侵犯版权,或者可能消耗大量带宽,以至于导致您的互联网帐户关闭或更糟。另一方面,只要网页保持非专有语言(HTML/XML),以非专有 ASCII 编写,这种自由浏览才是可能的。
最后,许多优秀的网站只有在发送足够的横幅广告时才能生存下来并保持免费,因此所有这些真的应该适度应用。
像往常一样,在从头开始做某事之前,应该检查已经完成的工作并重用它,对吧?在 Freshmeat.net 上快速搜索“新闻滚动条”会返回 18 个项目,从 Kticker 到 K.R.S.S 再到 GKrellM Newsticker。
这些都是非常有效的工具,但它们只获取新闻,因此在不同情况下不经修改就无法工作。此外,它们几乎都是图形工具,而不是您可以作为 cron 条目运行的工具,也许可以将输出管道传输到其他程序。
在这个领域,为了仅仅解决您自己的问题,几乎必须为自己编写一些东西。这也是为什么我们在这里不提供任何完整的解决方案,而是讨论一般方法论的原因。
利用本文的唯一先决条件是了解足够的 Perl,以便将一些正则表达式组合在一起,以及以下 Perl 模块:LWP::UserAgent、LWP::Simple、HTML::Parse、HTML::Element、URI::URL 和 Image::Grab。您可以从 CPAN (www.cpan.org) 获取这些模块。请记住,即使您没有系统的 root 密码(通常在您的办公室计算机上),您仍然可以将它们安装在您选择的目录中,如 Perl 文档和相关的 README 文件中所述。
本文中的所有内容都已在 Red Hat Linux 7.2 下测试过,但在更改代码中存在的所有绝对路径后,应该可以在每个支持 Perl 和使用的几个外部应用程序的 UNIX 系统上工作。
下面描述的所有任务,以及一般的 Web 客户端脚本,都要求您可以下载并存储在内部以供进一步分析某个初始网页的全部内容、其最后修改日期、它包含的所有 URL 列表或上述任何组合。所有这些信息都可以在每个 Web 客户端脚本的开头用几行代码收集,如清单 1 所示。
代码以几乎是强制性的“use strict”指令开始,然后加载所有必需的 Perl 模块。完成之后,我们继续通过 get() 方法将网页的全部内容保存在 $HTML_FILE 变量中。通过接下来的指令,我们将 HTTP 标头的每一行保存在 @HEADER 数组的一个元素中。最后,我们定义一个数组 (@ALL_URLS),并使用 for() 循环,我们提取并将其内部保存原始网页中包含的所有链接,并在必要时使它们成为绝对链接(使用 abs() 方法)。在循环结束时,@ALL_URLS 数组将包含在初始文档中找到的所有 URL。
有关此代码中使用的 Perl 方法的完整描述以及更多内容,可以在 Web 客户端编程 一书中找到(请参阅资源)。
收集完所有这些材料后,我们可以开始使用它了。如果您只想将网页的内容保存在磁盘上以供稍后阅读,您必须在原始脚本中添加一个 print 指令
print $HTML_FILE;
然后从 shell 提示符运行它
./webscript.pl http://www.fsf.org > fsf.html这将允许您将整个页面保存在本地文件 fsf.html 中。但是请记住,如果这就是您想要的全部,那么 wget 是更好的选择(请参阅资源,“无需浏览器即可下载”)。
如果所有绝对 URL 都已在 @ALL_URLS 数组中,我们可以使用以下 for() 循环下载所有图像
foreach my $GRAPHIC_URL (grep /(gif|jpg|png)$/, @ALL_URLS) { $GRAPHIC_URL =~ m/([^\/]+)$/; my $BASENAME = $1; print STDERR "SAVING $GRAPHIC_URL in $BASENAME....\n"; my $IMG = get ($GRAPHIC_URL); open (IMG_FILE, "> $BASENAME") || die "Failed opening $BASENAME\n"; print IMG_FILE $IMG; close IMG; }
该循环对文档中所有以 .gif、.jpg 或 .png 扩展名结尾的 URL 进行操作(从原始数组中使用 grep 指令提取)。首先,正则表达式找到实际的文件名,定义为 URL 中从最右边的斜杠符号到结尾的所有内容;这应该推广到处理那些托管在系统上的 URL,这些系统如此扭曲,以至于目录分隔符都是向后的。
匹配的结果加载到 $BASENAME 变量中,图像本身使用已知的 get() 方法保存在 $IMG 中。之后,我们打开一个具有正确名称的文件,并将整个内容打印到其中。
当然,很多时候您不会对所有图像都感兴趣(尤其因为其中许多通常是广告横幅、网站徽标或其他无趣的东西)。在这种情况下,简单地查看 HTML 源代码将帮助您弄清楚是什么将您需要的图像与其余图像区分开来。例如,您可能会发现有趣的图片有一个随机名称,但始终是列表中的第三个。如果是这种情况,请按如下方式修改之前的循环
my $IMG_COUNT = 0; my $WANTED_IMG = 3; foreach my $GRAPHIC_URL (grep /(gif|jpg|png)$/, @ALL_URLS) { $IMG_COUNT++; next unless ($IMG_COUNT == $WANTED_IMG); # rest of loop as before..... last if ($IMG_COUNT == $WANTED_IMG); } print "FILE NOT FOUND TODAY\n" if ($IMG_COUNT != $WANTED_IMG);
循环中的第一条指令递增图像计数器;第二条指令跳转到下一次迭代,直到我们到达第三张图片。“last”指令避免了不必要的迭代,循环之后的指令通知脚本无法执行复制,因为它在源代码中找到的图片少于 $WANTED_IMG 张。
如果图像名称不是完全随机的,那就更容易了,因为您可以直接在开头的 grep 指令中对其进行过滤
foreach my $GRAPHIC_URL (grep /(^daily(\d+).jpg)$/, @ALL_URLS) {
这将仅循环处理名称以“daily”字符串开头,后跟任意数量的数字 (\d+) 和 .jpg 扩展名的文件。
这两种技术可以随意组合,并且可以实现更复杂的功能。如果您知道图片名称等于页面标题加上以 YYYYMMDD 格式表示的当前日期,请首先提取标题
$HTML_FILE =~ m/<TITLE>([^<]+)<\/TITLE>/; my $TITLE = $1;
然后计算日期
my ($sec, $min, $hour, $day, $month, $year, @dummy) = localtime(time); $month++; # months start at 0 $year += 1900; # Y2K-compliant, of course ;-))) $TODAY = $year.$month.$day;最后,以此进行过滤
foreach my $GRAPHIC_URL (grep /(^$TITLE$TODAY.jpg)$/, <@>ALL_URLS) {
现在开始变得真正有趣了。自定义您的脚本以仅获取网页文本的特定部分通常比此处描述的任何其他操作需要更多的时间和精力,因为它几乎必须在每个页面上从头开始完成,并且如果页面结构发生变化则需要重复。如果您有较慢的互联网连接,甚至速度很快但又不想减慢 MP3 下载或网络游戏的速度,您将迅速恢复花在准备脚本上的时间。如果您(像我一样)仍然按分钟付费,您还将节省相当多的钱。
您必须打开并研究原始网页的 HTML 源代码,以弄清楚哪个 Perl 正则表达式过滤掉所有且仅过滤掉您需要的文本。Perl LWP 库已经提供了从 HTML 代码中提取所有文本的方法。如果您只想要整个内容的纯 ASCII 版本,请使用它们。
您可能会想让 LWP 库从源代码中提取所有文本,然后在上面进行操作,即使您只想从网页中提取一些行。但是,我发现这种方法在实际情况下更难管理。当然,ASCII 格式使文本对人类来说立即具有可读性,但它也抛弃了所有 HTML 标记,这些标记对于告诉脚本您想要保存哪些部分非常有用。这个错误开始的最简单的例子是,如果您想保存或显示所有且仅显示新闻标题,并且它们在源代码中用 <H1></H1> 标签标记。这些标记在 Perl 正则表达式中很容易使用,但是一旦它们消失了,脚本就更难识别标题了。
为了在一个真实的网页上演示该方法,让我们尝试在我们的终端内部打印来自 FSF 页面 www.fsf.org/press/press.html 的所有新闻稿标题。将我们的脚本指向此 URL 会将其所有内容保存在 $HTML_FILE 变量中。现在,让我们对其应用以下正则表达式序列(我建议您也使用浏览器查看该页面及其源代码,以了解所有正在发生的事情)
$HTML_FILE =~ s/.*>Press Releases<//gsmi; $HTML_FILE =~ s/.*<DL>//gsmi; $HTML_FILE =~ s/<\/DL>.*$//gsmi; $HTML_FILE =~ s/<dt>([^<]*)<\/dt>/-> $1: /gi; $HTML_FILE =~ s/<dd><a href=[^>]*>([^<]*)<\/a>/ $1 /gsmi; $HTML_FILE =~ s/\.\s+\([^\)]*\.\)<\/dd>/<DD>/gsmi; $HTML_FILE =~ s/\s+/ /gsmi; $HTML_FILE =~ s/<DD>/\n/gsmi;
前三行剪掉了实际新闻稿列表之前和之后的所有内容。第四行找到日期并从中剥离 HTML 标签。正则表达式编号五和六对新闻稿主题执行相同的操作。最后两个消除冗余的空格并在需要的地方放置新行。截至 2001 年 12 月 14 日,shell 提示符下的输出如下所示(标题已被我手动剪切以获得更好的格式)
-> 3 December 2001: Stallman Receives Prestigious... -> 22 October 2001: FSF Announces Version 21 of the... -> 12 October 2001: Free Software Foundation Announces... -> 24 September 2001: Richard Stallman and Eben Moglen... -> 18 September 2001: FSF and FSMLabs come to agreement...上面的正则表达式集并不完整;首先,它不管理带有更新部分的新闻。还应该使其尽可能独立于 HTML 标签内部的额外空格或某些字体颜色或大小的变化。此正则表达式剥离了所有字体标记
$HTML_FILES =~ s/<font face="Verdana" size="3"> ([^<]+)<\/font>/$1/g;这执行相同的任务,但适用于任何字体类型和(正数)字体大小
$HTML_FILES =~ s/<font face="[^"]+" size="\d+"> ([^<]+)<\/font>/$1/g;但是,此处显示的示例仍然足够详细,可以显示原理,并且再次为任何给定页面编写自定义集的一次性工作确实可以节省大量时间。
一旦您设法提取了您想要的文本并将其格式化为您的口味,就没有理由将自己限制为手动使用脚本,或者仅仅在控制台上使用它。如果您想做其他事情,并且只有当出现关于 Stallman 的新标题时才让计算机通知您,则只需要三个步骤。
首先,将脚本放在您的 cron 条目中(man cron 将告诉您关于此的一切)。之后,将以下检查添加到您的 Perl 脚本
if ($HTML_FILE =~ m/Stallman/) { # INFORM ME!!! }
只有当剩余文本确实包含 Stallman 字符串(或您想了解的任何其他内容,当然)时,这才会执行您想要的操作。
接下来,用类似这样的东西填充块
open (XMSG, "|/usr/bin/X11/xmessage -title \"NEWS!\" -file -") || die; print XMSG $HTML_FILE; close XMSG;
这将打开一个到 xmessage 程序的 UNIX 管道,该程序弹出一个窗口,窗口标题由相应的开关给出,并包含 -file 选项后面的文件文本。在我们的例子中,“-”告诉 xmessage 从标准输入获取文本。就目前而言,Perl 脚本将等待退出,以便您关闭 xmessage 窗口。这可能是您想要的,也可能不是。在 cron 脚本的情况下,最好让它在后台在临时文件上启动 xmessage 并退出,像这样
open (XMSG, "> /tmp/gee") || die; print XMSG $HTML_FILE; close XMSG; exec "/usr/bin/X11/xmessage -title \"NEWS!\" -file /tmp/gee&";
如果您只想在上次访问后或最近两小时内内容发生更改时才处理页面,则需要 Last-Modified HTTP 标头。它已经可用,以自 1970 年 1 月 1 日以来的秒数表示,在我们的 @HEADER 数组的第三个元素中。因此,如果您只想对最近两小时内修改的页面执行某些操作,请开始计算那一刻的时间(始终以“自...以来的秒数”为单位)
$NOW = time; $TWO_HOURS_AGO = $NOW - (3600*2);
然后将该时间与网页的修改日期进行比较
if ($HEADER[2] > $TWO_HOURS_AGO) { # do whatever is needed }
这是开头所述的 DIY 规则的罕见例外之一:下载 WMHeadlines(请参阅资源),安装它,然后配置和修改以适合您的口味。开箱即用,它可以从 120 多个站点获取标题,并将它们放在 Blackbox、WindowMaker、Enlightenment 和 GNOME 的根菜单中,这样您就可以在您点击的动态菜单声音上启动浏览器。
可以从提示符或任何脚本向 Netscape 发出多个命令。这些命令将导致 Netscape 在尚未运行时启动,或者在当前窗口甚至新窗口中加载请求的 URL。但是,运行的命令会根据 Netscape 是否已在运行而改变。查看 WMHeadlines 发行版中的 nslaunch.pl 脚本,了解如何检查 Netscape 是否已在运行。
您还可以驱动 Netscape 从脚本执行其他操作:打印页面,就像手动驱动 Netscape 一样,首先让它加载页面
exec($NETSCAPE, '-noraise', '-remote', "openURL($URL,new-window)");
然后将其另存为 PostScript
exec($NETSCAPE, '-noraise', '-remote', "saveAs(/tmp/netscape.ps, PostScript)");最后,打印它
exec("mpage -PYOURPRINTER -1 /tmp/netscape.ps");或者,甚至将其添加到书签
exec($NETSCAPE, '-noraise', '-remote', "addBookmark($SOME_URL, $ITS_TITLE)");KDE Web 浏览器 Konqueror 可以通过以这种方式调用它来简单地启动
system("/usr/bin/konqueror $URL");Konqueror 可以通过脚本驱动执行许多与 Web 无关的任务,例如复制文件、启动应用程序和挂载设备。键入 kfmclient --commands 以了解更多详细信息。
Galeon 可以以几乎相同的方式启动
system("/usr/bin/galeon $URL");
如 Galeon 用户指南(请参阅资源)中所述,您还可以决定 Galeon(如果已经在运行)是否应在新标签页中打开 URL
system("/usr/bin/galeon -n $URL");在新窗口中
system("/usr/bin/galeon -w $URL");或临时将 $URL 添加到书签
system("/usr/bin/galeon -t $URL");
相反的方法,即从浏览器启动通用镜像或图像获取脚本,在 Konqueror(甚至 KMail)的正常浏览期间是可能的。如果您右键单击链接并选择“打开方式..”选项,它将允许您输入要使用的脚本的路径,并在下次将其添加到选项中。这意味着您可以按照此处给出的说明准备镜像或 fetch_images 脚本,并通过单击几下在后台启动它,处理您希望的任何 URL。
@ALL_URLS 数组中包含的 URL 列表也可以用于启动镜像或(并行)FTP 会话。这可以完全在 Perl 中完成,使用许多可用的 FTP 和镜像模块,或者只是收集要镜像或通过 FTP 获取的 URL,并将实际工作留给 wget 或 curl,如 A. J. Chung 的文章“无需浏览器即可下载”(请参阅资源)中所述。
如果您的首选 Web 门户每天选择一个不同的酷站点,并且您希望您的 PC 为您镜像它,只需像对图像一样获取 URL,然后在您的脚本中说
exec "wget -m -L -t 5 $COMPLETE_URL";
Chung 的文章中解释的所有并行 FTP 和镜像命令都可以通过这种方式从 Perl 脚本启动,并将由此脚本找到的 URL 作为参数。
我们中的许多人都有不止一个喜欢的站点,并且希望将它们都放在同一个窗口中。对此的一般解决方案是以这种方式提取每个页面的完整 HTML 正文
$HTML_FILE = s/^.*<body[^>]*>//i; # strips everything # before $HTML_FILE = s/<\/body[^>]*>.*$//i; # strips everything # after
然后打印出一个 HTML 表格,每个原始页面都在每个框中
print<<END_TABLE; ....All HTML <HEAD> and <BODY> stuff here <TABLE> <TR><TD>$HTML_FILE_1</TD></TR> <TR><TD>$HTML_FILE_2</TD></TR> ......... </TABLE></BODY></HTML> END_TABLE将脚本输出保存在 $HOME/.myportal.html 中,将该文件设置为浏览器中的起始页,尽情享受吧!完整的脚本可能需要进行一些调整,以清理不同的 CSS、字体等等,但您现在知道该怎么做了,对吧?
我们仅仅触及了客户端 Web 脚本的表面。还有更多复杂的任务是可能的,例如处理 cookie 和受密码保护的站点、自动表单提交、使用您可以想到的所有条件进行 Web 搜索、扫描整个网站并以直方图形式显示十个最常指向的 URL 以及 Web 邮件检查。
您只需要一些耐心、Perl 实践和对相关模块的良好了解即可成功。祝您浏览愉快!