高级“新”标签

作者:Reuven M. Lerner

上个月,我们研究了网站管理员喜欢放在他们网站上的那些烦人的“新”标签。他们的意图是好的,向我们指出我们不太可能以前见过的文档。但在实践中,“新”标签是人为的,它告诉我们文档上次发布的时间,而不是它对我们来说是否真的是新的。

上个月我们探讨的技术——服务器端包含、CGI 程序和模板——很有趣,但效率低下且速度慢。本月,我们将研究如何通过使用 mod_perl(Apache 服务器的 Perl 模块)来提高性能。

什么是 mod_perl?

我们曾在之前的专栏中讨论过 mod_perl,但对于那些可能错过的读者来说,值得做一个快速介绍。Apache 服务器由模块构建而成,每个模块处理软件的不同功能部分。这种架构的优势之一是它允许网站管理员自定义他们的 Apache 副本,根据需要包含或排除模块。这也意味着程序员可以通过编写新模块来向 Apache 添加功能。

最受欢迎的模块之一是 mod_perl,它将 Perl 语言的副本放入 Apache 服务器内部。这在多个层面提供了功能,包括在 Perl 中设置配置指令(或有条件地,取决于某些 Perl 代码是否执行)的能力。更重要的是,它允许我们编写可以修改 Apache 行为的 Perl 模块。

当我提到“行为”时,我指的是用户看到的行为(显示文档和响应 HTTP 请求),以及幕后发生的行为,范围从身份验证的发生方式到日志记录的完成方式。

CGI 程序的每次调用都需要一个新进程以及启动时间。相比之下,mod_perl 将您的代码转换为 Apache 可执行文件的子例程。您的代码被加载和编译一次,然后保存以供将来调用。

当我们第一次思考 HTTP 请求提交到 Apache 时会发生什么时,它看起来相对简单。请求被 Apache 读取,传递到正确的模块或子例程,并在 HTTP 响应中返回到用户的浏览器。实际上,每个请求都必须经过许多(超过十几个)不同的“处理程序”,然后才能生成和发送响应。mod_perl 允许我们通过将 Perl 模块附加到任何或所有这些处理程序来修改和增强它们。最常修改的处理程序称为 PerlHandler。其他更具体的处理程序被赋予其他名称,例如 PerlTransHandler(用于 URL 到文件名的转换)和 PerlLogHandler(用于日志文件)。

本月,我们将研究一些 PerlHandler,它们将使创建真正有用的网站“新”标签成为可能。

简单版本

我们将定义的第一个 PerlHandler 非常简单:它在页面上的任何链接旁边放置一个“新”标签。这并不是一项特别困难的任务,也不是 mod_perl 的好用途。但是,它确实温和地引导我们编写 mod_perl 的 Perl 模块,并且它将构成我们未来编写的版本的基础。

我们的模块的开头与任何其他模块大致相同,声明自己的命名空间(在本例中为 Apache::TagNew),然后从 Apache::Constants 包中导入几个符号常量。该模块定义了一个名为“handler”的子例程。这是在 mod_perl 下定义处理程序的传统方式;也就是说,创建一个带有“handler”子例程的模块,然后告诉 Apache 将该模块用作特定目录的处理程序。

我们指示 Apache 在配置文件 httpd.conf 中调用我们的处理程序。例如,我的 httpd.conf 副本如下所示

PerlModule Apache::TagNew
<Directory /usr/local/apache/share/htdocs/tag>
SetHandler perl-script
PerlHandler Apache::TagNew
</Directory>

PerlModule 指令告诉 Apache 加载 Apache::TagNew 模块。<Directory> 部分告诉 Apache,我的 HTML 内容树的 /tag 子目录应特殊处理,使用 Apache::New 的 handler 方法而不是默认内容处理程序。一旦我们通过重启服务器(或通过向其发送 HUP 信号)激活我们的模块,/tag 目录中的任何文件都将由 Apache::TagNew 处理,而不是 Apache 的默认处理程序。

handler 必须做的第一件事是检索 Apache 请求对象,传统上称为 $r。此对象是 mod_perl 中一切的关键,因为它允许我们检索有关 HTTP 请求、环境以及程序运行的服务器的信息。我们还使用 $r 将数据发送回用户的浏览器。

我们的方法预计返回我们从 Apache::Constants 导入的符号之一。返回 OK 表示我们成功处理了查询,数据已返回到用户的浏览器,并且 Apache 应继续执行其处理请求的下一个阶段。如果我们返回 DECLINED,Apache 会假定我们的模块未处理请求,并且它应该找到其他愿意执行此工作的处理程序。我们可以返回各种其他符号,包括 NOT_FOUND,它指示在我们的服务器上未找到该文件。

列表 1。

在 Apache::TagNew(参见列表 1)中,我们通常返回 OK。如果在打开文件时发生错误,我们返回 NOT_FOUND;如果文件没有“text/html”的 MIME 类型,我们返回 DECLINED。超链接将仅出现在 HTML 格式的文本文件中,因此我们可以节省大家一些时间和精力,让另一个处理程序来处理其他文件类型。

处理程序的其余部分通过读取文件的内容,然后将其替换为我们新的和改进的版本来工作。我们在每个 </a> 之后附加一个“新”标签,它在每个超链接之后出现。通过这种方式,每个超链接都被标记为新的。

在新文件上打印“新”

当然,这个项目的重点不是在所有链接旁边打印“新”,而是仅在新链接旁边打印。为了做到这一点,我们需要按顺序查看每个链接,并检查它是否在我们的系统上。如果它在,我们将检查相关文件上次更改的时间。如果该文件在最近一周内更改过,我们将将其标记为新的;否则,我们将保持原样。

列表 2。

为了做到这一点,我们将编写另一个子例程,它负责识别链接并在必要时添加适当的文本。也就是说,子例程将接受一个 URL 作为输入,并将输出相同的 URL 或附加了“新”标签的 URL。列表 2 是我们的 Apache::TagNew 新版本,它包含这样一个子例程,名为 label_url。label_url 子例程期望使用三个参数调用:$r(Apache 请求对象)、$url(问题中超链接的 URL)和 $text(位于超链接的 <a> 和 </a> 标签之间的文本)。

我们只有在文件位于我们的系统上时才能知道文件是否已更改。我没有解析 URL,而是采取了一种简单的方法,即检查问题中的 URL 是否以“http://”开头。如果是,那么我们假设 URL 指向不同系统上的文件,我们忽略它,并返回原始状态的 URL 和文本。

如果 URL 以任何其他字符开头,则假定它指向我们系统上的文件。我们使用 $r 来检索文档根目录的值,即存储所有 URL 的目录。无论您的 Web 文档位于 /usr/local/apache/share/htdocs、/etc/httpd/htdocs 还是甚至 /usr/local/bin 下,此模块都将工作。$r 从 httpd.conf 文件中检索信息,这也意味着如果您决定移动文档根目录,则无需更新模块。

然后,我们检查文件是否在最近七天内被修改过,使用 Perl 的 -M 运算符来获取上次修改时间。幸运的是,对于我们的目的,-M 以天而不是秒为单位返回其结果;因此,我们可以简单地将返回的结果与 7 进行比较,并在必要时添加标签。如果文件在最近七天内未被修改,则 $label 变量保持未定义状态,并在稍后变成空字符串。

我们的程序返回修改后的 URL,就像在以前版本的 Apache::TagNew 中一样。

我们可以使用 s///(Perl 的替换运算符)在文档中的每个超链接上评估此子例程。我们给 s/// 三个修饰符:g 全局执行操作,i 忽略大小写,e 用评估替换的结果替换初始文本

$contents =~
s|<a\s+href=['"]?(\S+?)['"]?\s*>([\s\S]+?)</a>
|label_url($r, $1, $2)|eigx;

上面的正则表达式很难理解,所以让我们更详细地检查它做了什么。我们使用“x”修饰符使 regexp 更具可读性,这允许我们在其中插入空格。我们查找打开的 <a> 和关闭的 </a> 标签,并从中提取 URL(分组在第一组括号内)和链接文本(分组在第二组括号内)。我们使用 Perl 的非贪婪运算符来确保我们只获得必要的文本。否则,诸如引号之类的内容可能会包含在我们的链接文本中。

然后我们调用子例程 label_url。我们向它传递三个参数:$r(Apache 请求对象)、$1(我们从第一组括号中抓取的 URL)和 $2(我们从第二组括号中抓取的链接文本)。label_url 返回的任何内容都会替换我们最初找到的文本。通过这种方式,我们可以选择性地将标签插入到文档的文本中。

跨会话存储信息

上述系统有几个优点,但它无法跟踪用户何时访问了特定链接。换句话说,它非常擅长跟踪特定 URL 的倒计时计时器,并在前七天将其标记为新的。但再一次,我们希望在文档对特定用户是新文档时生成“新”标签。如果我已经三个月没有访问过某个网站了呢?那么所有内容都可能是新的,并且所有内容上都会有“新”标签。相比之下,如果我两个小时前访问过该网站,则只有自上次访问以来发生更改的标签才会看起来不同。

跟踪此类信息将要求我们在 HTTP 请求之间保持状态,以便我们可以跟踪特定用户看到了哪些链接。不幸的是,HTTP 是一种无状态协议,这意味着我们无法保存此类信息。HTTP 请求和响应发生在真空中,既不为下一个事务存储信息,也不从先前的事务中检索信息。

HTTP 的无状态性质为希望创建有用应用程序的 Web 程序员和设计师带来了问题,并导致了许多巧妙的解决方案。也许最著名的解决方案是使用 HTTP Cookie,它允许 Web 服务器在用户的计算机上存储信息。每次用户向该服务器提交 HTTP 请求时,之前存储的所有 Cookie 都会随请求一起发送。

Cookie 可以通过多种方式存储信息。一种是将信息放入 Cookie 中,从而使服务器可以在请求中立即访问有关用户的更多详细信息。但是,如果您有太多数据,这很快就会变得繁琐。因此,通常在关系数据库中使用表来跟踪用户信息。如果我们为该表定义一个主键(即,保证唯一的列),我们可以在表中存储任意多的信息。

以这种方式访问表可能很麻烦,因为它涉及许多数据库存储和检索操作。幸运的是,我们可以使用 Apache::Session 模块来处理此类事情。Apache::Session 与 mod_perl 程序一起工作,以跨 HTTP 事务存储和检索信息。

我们可以使用 header_in 方法在我们的处理程序中检索 Cookie。请注意我们是如何使用原始 Cookie 的,这意味着我们必须使用 s/// 来检索感兴趣的值

my $id = $r->header_in('Cookie');
$id =~ s/SESSION_ID=(\w*)/$1/;

完成此操作后,我们可以使用 Apache::Session::DBI,该模块将会话连接到数据库表。我们使用 Perl 的 tie 例程,它在变量和模块之间创建连接,以提供无缝连接

tie %session, 'Apache::Session::DBI', $id,
      {
       DataSource => 'dbi:mysql:test',
       UserName   => 'username',
       Password   => 'password'
      };
您可能会从 DBI(Perl 数据库接口)中识别出上面代码片段中的三个属性。DBI 与许多不同的关系数据库一起工作,这要归功于它对特定数据库使用了数据库驱动程序。上面的示例使用了 MySQL 数据库,我将其用于我的许多数据库任务。此示例使用“test”数据库来存储我们的会话信息。虽然“test”是演示数据库的好地方,但您最好将生产数据库放在其他地方。

Apache::Session 无法为您在 MySQL 中创建表。在使用上述代码之前,您需要创建一个表,Apache::Session 可以在其中存储其会话信息。以下是来自 Apache::Session::DBI 手册页的推荐表定义

CREATE TABLE sessions (
     id char(16),
     length int(11),
     a_session text
    );
使用 Apache::Session

一旦我们的处理程序从 Cookie 中检索到用户的 ID 并与数据库建立了连接,我们就可以随时存储和检索会话信息。

我们可以将有关此用户的信息存储在 %session 中,这是我们将 Apache::Session 绑定的哈希。每次调用我们的处理程序时,我们都可以根据他或她的 ID 检索有关此用户的信息。例如,我们可以使用以下代码存储一个值

$session{"foo"} = "bar";

然后,我们可以在以后的会话中使用以下代码检索该值

my $stuff = $session{"foo"};
虽然我们的程序似乎正在 %session 中存储和检索值,但它实际上正在使用 DBI 从数据库中检索它们——这意味着,只要我们确保每个用户都有唯一的 ID,我们就可以将每个人的值分开。

既然我们拥有了跨会话扩展的哈希,我们如何存储有关我们访问过的 URL 以及访问时间的信息?最简单的方法是将 URL 用作 %session 的键,然后存储用户上次访问该站点的时间。例如,我们可以使用以下代码存储 URL

my $document_uri = $r->uri;
$session{$document_uri} = time;

我们希望在确定用户最近是否访问过特定链接时检索此信息。为了做到这一点,我们将修改 label_url,使其期望第四个参数,即对 %session 的引用。这样,label_url 将能够检索有关问题中 URL 的会话信息。我们在将 %session 传递给 label_url 之前,通过在 %session 前面加上反斜杠 (\%session) 来创建引用。然后,我们在 label_url 的开头按如下方式解引用 %session 的副本

my $session = shift;
my %session = %{$session};
Apache::TagNew 工作版本的完整代码,包括 label_url 子例程,在列表 3 中。

列表 3。

label_url 的其余代码基本相同,除了中间部分,我们在其中测试 URL 是否以斜杠 (/) 开头。我们必须确保从 %session 存储和检索相同的键;否则,我们将获得有关我们上次访问 URL 时间的错误读数。由于我们基于 $ruri 存储 URL,它始终以斜杠开头并且相对于我们服务器的根 URL 目录,因此我们应该以相同的方式检索 URL。

我们通过获取当前 URL 并删除最后一个斜杠后的所有内容来做到这一点

$current_directory =~ s|^(\S+/)[\w.]+$|$1|;

剩下的是当前目录,我们可以将 URL 预先添加到该目录

$url = $current_directory . $url;
现在我们可以检索有关该 URL 的会话信息,确信我们使用与之前用于存储的相同键集进行检索。我们检索有关我们上次查看文件的时间的会话信息,将其转换为相对于现在的天数
my $last_time = (time - $session{$url}) / 86400;
然后,我们检索此文件的修改时间戳,方法是将 $rdocument_root(通向网站上每个文件的完整路径名,通常对用户不可见)预先添加到该文件。我们可以轻松确定其修改日期
my $full_filename = $r->document_root . $url;
my $ctime = -M $full_filename;
最后,我们将 $ctime(自文件修改以来的天数)与 $last_time(自用户上次查看文件以来的天数)进行比较。如果前者小于后者,我们添加标签
if ($ctime < $last_time)
{
$label = "<font color=\"red\">New!</font>"";
}
此模块似乎在逐个用户地标记新文档方面做得很好。只要用户启用 Cookie,他们应该能够准确了解他们很长时间没有看过的文件。
结论

对于一个应该适应我们自身需求的媒介来说,Web 令人惊讶地原始——例如,在网站上标记“新”文档的方式方面。本月,我们已经了解了 mod_perl 如何使我们能够更加个性化我们的网站,从用户的角度而不是从网站管理员的角度向人们展示什么是真正新的。我希望您也注意到其中一些工具变得多么先进;通过 100 多行 Perl 代码,我们能够对我们的 Web 服务器进行重大更改,这对性能几乎没有影响,但为我们的用户带来了巨大的好处。

资源

Advanced “New” Labels
Reuven M. Lerner 是一位居住在以色列海法的互联网和 Web 顾问,自 1993 年初以来一直使用 Web。他的著作《Core Perl》将于今年晚些时候由 Prentice-Hall 出版。可以通过 reuven@lerner.co.il 联系 Reuven。ATF 主页,包括档案和讨论论坛,位于 http://www.lerner.co.il/atf/
加载 Disqus 评论