动态图形和个性化
上个月,我们讨论了 CGI 程序创建动态生成的图形输出的不同方式。也就是说,我们编写了几个程序,其中程序将其输出描述为“graphics/gif”而不是“text/html”。
本月,我们将研究更多可以动态创建图形的工具。但是,这次图形将有一个额外的变化,即它们将反映个人用户的股票投资组合,而不是全局数据集值。
与任何重要的软件项目一样,我们的第一步必须是创建一个简要的规范。在这个特定的项目中,我们将有两个主要的程序。在第一个程序中,用户将能够创建和编辑个人资料,描述他或她拥有的证券。第二个程序将获取用户个人资料中的信息,并使用它来创建一个个性化的图形股票投资组合。
这个项目汇集了我们在 ATF 前几期中讨论过的许多工具。尽管如此,回顾一下它们似乎是个好主意,因为我们将要调用这么多工具。
MySQL:MySQL 是一个小型、廉价的关系数据库,适用于 Linux 和许多其他操作系统。(有关在哪里获取它的信息,请参阅“资源”。)除了价格低廉外,MySQL 还非常快速高效,这使其在许多网站上很受欢迎。作为一个关系数据库,MySQL 强制我们将信息存储在一个或多个表中,其中每一行都指的是一个单独的记录。与大多数关系数据库一样,我们使用 SQL(一种数据库查询语言)与 MySQL 通信。我们不能用 SQL 编写程序;相反,我们必须将我们的查询嵌入到用完整的编程语言编写的程序中。在我们的例子中,该语言是 Perl。
DBI:虽然 SQL 可能是与数据库通信的标准查询语言,但用于与这些数据库对话的软件和库差异很大。要与 Oracle 服务器对话,您需要 Oracle 库;要与 MySQL 服务器对话,您需要 MySQL 库,等等。因此,Perl 数据库世界长期以来一直处于分裂状态,为各个数据库提供特殊版本。
但是,现在有一种更好的方法:DBI 模块标准化了关系数据库的 API,这意味着程序员从一个数据库服务器迁移到另一个数据库服务器,只需要学习 SQL 实现的不同细微之处。以前,他们还必须学习一个单独的 Perl API,这令人沮丧。这是通过将数据库代码分成两个部分来完成的,一部分是通用的 (DBI),另一部分是特定于每个服务器的 (DBD)。为了使用 DBI,您需要安装通用的 DBI 库,然后安装一个或多个适用于您使用的产品的 DBD。
然而,DBI 和 Web 之间存在一个问题,这与数据库服务器的设计方式有关。一般来说,它们期望客户端程序打开一个连接,执行许多查询,然后断开连接。因此,打开连接非常缓慢且效率低下。当 CGI 程序是数据库客户端时,它必须为每个 HTTP 事务打开一个新的数据库连接。有关此问题的一个解决方案,请参阅下面的 mod_perl 部分,Apache::DBI。
Apache 的 mod_perl:Web 服务器传统上通过调用外部程序(使用 CGI 标准)来提供自定义和动态输出。HTTP 服务器会将信息传递给 CGI 程序,然后 CGI 程序应将其输出发送到用户的浏览器。此输出通常以 HTML 格式的文本形式出现,但正如我们上个月看到的那样,也可以生成图形。
但是,CGI 非常慢;每次调用 CGI 程序都需要创建一个新进程。如果您使用的是 Perl,则每次调用都需要将程序编译成 Perl 的内部格式,然后执行。
另一种方法是使用 mod_perl,它是免费的 Apache HTTP 服务器的一个模块,它在服务器内部嵌入了一个功能齐全的 Perl 版本。这有几个结果,其中之一是我们现在可以创建自定义输出,而无需依赖外部程序。
当使用 mod_perl 时,您可以利用一个名为 Apache::DBI 的模块。此模块假装与 DBI 的工作方式相同,但实际上跨调用缓存数据库句柄 ($dbh)。因此,即使您的程序认为它正在打开一个新的数据库连接,它实际上是在重用来自先前调用的数据库句柄。
GIFgraph:GIFgraph Perl 模块集允许我们从 CGI 程序或 mod_perl 模块中动态创建图表和图形。上个月我们探索了 GIFgraph 的基本用法。顾名思义,GIFgraph 以 GIF 格式生成输出。上个月,我们看到了如何将 GIF 直接返回到用户的浏览器。本月,我们将改为将生成的图形保存到单独的文件中,我们将为这些文件创建超链接。
Apache::Session:HTTP(Web 基于的协议)被设计为轻量级和简单。作为这种考虑的一部分,它也被设计为“无状态”的,这意味着每个事务都是独立的。然而,这产生了一个问题,因为您通常希望跟踪哪个用户是哪个用户。例如,在这个应用程序中,我们希望确保我们正在跟踪正确的用户的投资组合。没有状态,我们根本无法跟踪投资组合,更不用说多个用户的投资组合了。正如我们将在下面看到的那样,Apache::Session 允许我们通过使用数据库和 HTTP Cookie 来存储一条或多条信息,并使用唯一的标识符来解决这个问题。
借助以上五种技术,我们可以创建一个相当令人印象深刻的股票投资组合跟踪器,允许用户定义他们拥有的证券,并以图形格式查看他们当前的持仓。正如本月介绍的那样,该应用程序确实有点粗糙,但它应该向您展示编写这样一个应用程序是多么容易,以及上述工具在创建一个应用程序时是多么灵活。
在我们开始处理应用程序本身之前,我们需要创建它们将使用的底层数据库表。我们将需要两个不同的表:一个用于保存不同日期的单个股票值,另一个用于存储用户个性化信息。
第一个表名为 StockValues,有三列:一个符号,最多可以包含六个字符;一个值,范围从 0 到 999999.999;和一个日期。我们可以使用以下 SQL 创建这样的表,最常见的方法是使用 MySQL 附带的交互式 mysql 客户端程序
CREATE TABLE StockValues ( symbol CHAR(6) NOT NULL, value NUMERIC(6,3) NOT NULL, date DATE NOT NULL );
上面表中的每一行都指的是单只股票在一天中的价值。通过像这样存储信息,我们可以轻松地创建股票在任意时间段内的图表。为了简洁起见,我们的应用程序将始终显示股票的所有可用值。上面的表格还为我们提供了许多附加应用程序的可能性,例如查找股票在给定时间段内的高值和低值。
StockValues 将如何填充值?大多数商业网站都从商业服务检索股票信息,使用后台进程将信息放入数据库表中。我的预算比普通商业网站更有限,所以我决定将一些任意值插入 StockValues。为了做到这一点,我使用了交互式 mysql 客户端程序,并输入了几个这种类型的查询
INSERT INTO StockValues (symbol, value, date) VALUES ("ZZZZ", 100, "1999-07-14");
我们将创建的第二个表是用于 Apache::Session::DBI,它是 Apache::Session 的一个版本,允许我们将有关特定用户的信息存储在数据库表中。表的名称和格式由 Apache::Session API 确定
CREATE TABLE sessions ( id char(16), length int(11), a_session text );一旦我们创建了这个表,我们就可以忽略 Apache::Session 将其信息存储在数据库中的事实。就我们而言,我们在代码的开头执行一个魔法咒语,它检索当前会话值。我们通过读取 HTTP Cookie 来检索用户的会话 ID
my $id = $r->header_in('Cookie'); $id =~ s|SESSION_ID=(\w*)|$1|;然后,一旦我们将 $id 分配为用户的会话 ID 值,我们就使用 Apache::Session::DBI 模块将 %session 哈希绑定到“sessions”表
my %session; tie %session, 'Apache::Session::DBI', $id, { DataSource => 'dbi:mysql:test:localhost:3306', UserName => '', Password => '' };从那时起,存储在先前会话的 %session 中的任何名称、值对都将可用。同样,我们可以赋值
$session{key} = "value";并确保在我们的下一次调用中,尽管 HTTP 是无状态的,我们仍然可以检索相同的值。因此,Apache::Session 使我们有可能存储关于用户的任意数量和类型的信息。
我们将为每个用户存储三个会话变量。电子邮件地址和姓名将存储为标量,用户的当前持仓将存储为哈希引用。%portfolio 哈希的键将是股票代码,而特定证券中拥有的股票数量将存储为值。
当我们想要将 %portfolio 存储为会话的一部分时,我们将其转换为引用并将其与键“portfolio”一起存储在 %session 中
$session{portfolio} = \%portfolio;
引用是一个特殊标记的标量,它允许我们将其存储在哈希中。我们稍后使用以下看起来很复杂的代码检索它
my %portfolio = defined $session{portfolio} ? %{$session{portfolio}} : ();上面使用了 Perl 的三元运算符 ?: 作为“if-then”的快捷方式。它的意思是,如果 $session{portfolio} 已定义,则将其解引用为其原始哈希值并将其分配给 %portfolio。如果未定义,则将空哈希 () 分配给 %portfolio。在此代码行执行后,%portfolio 将包含用户当前的投资组合。通过使用 Apache::Session,我们可以跨 HTTP 事务维护状态的错觉,并将许多用户的投资组合存储在我们的数据库中。
现在我们将编写两个将使用此信息的应用程序。这两个应用程序的代码可以在 ftp://ftp.linuxjournal.com/pub/lj/listings/issue66/3629.tgz 的存档文件中找到。第一个是 StockProfile.pm,一个用于 mod_perl 的 Perl 模块,它将允许用户创建和编辑他们的投资组合和个人信息。
由于我们的程序将作为 mod_perl 的一部分运行,因此我们需要记住几件事。首先也是最重要的是,我们必须创建一个新的 Perl 模块和包,其中包含一个名为“handler”的子例程。我们将配置 Apache 以在 HTTP 服务器请求特定 URL 时调用此“handler”子例程。因为我们的子例程将是 Apache 的一部分,而不是在单独的进程中调用,并且因为 mod_perl 编译并缓存我们编写的代码,所以我们的例程的运行速度将比 CGI 程序快得多。
我们还必须记住遵守 mod_perl 编程约定,其中最重要的是尽可能多地使用词法(“临时”或“my”)变量。全局变量会跨 mod_perl 的调用持续存在,这可能会导致内存泄漏和奇怪的错误。我们确保在变量之前使用“my”,并在程序顶部使用 use strict 编译指示。
我们的模块 Apache::StockProfile.pm(请参阅存档文件中的列表 1)有三个阶段:首先,它初始化所有变量和信息,从 StockValues 中抓取当前证券列表,并初始化用户的个人资料。然后,如果模块使用 POST 方法调用,它会根据需要设置或修改用户的个人资料信息。最后,它生成一个 HTML 表单,可用于进一步修改个人资料。
与所有 mod_perl 模块一样,我们在“handler”中做的第一件事是检索 Apache 请求对象,传统上称为 $r。此对象的方法允许我们检索和设置与 HTTP 事务相关的所有内容。例如,我们可以使用 $r->header_out 设置传出标头,使用 $r->content_type 设置“Content-Type”标头,并使用 $r->send_http_header 发送最终标头。
但是,对于有经验的 CGI 程序员来说,某些事情更容易完成——至少使用 CGI.pm,CGI 编程的标准模块。我们可以通过使用和创建 CGI::Apache 的实例来获得该 API 的一个版本。创建的对象使我们能够使用 CGI.pm 中的熟悉界面访问 HTML 表单元素和调试工具(包括非常宝贵的 dump 方法)。并非所有内容都以相同的方式工作,但对于几乎所有目的来说都足够好。
我们在此程序中对 CGI::Apache 的主要用途是检索通过 POST 方法提交的 HTML 表单元素。StockProfile.pm 既创建表单又处理其提交,乍一看这可能看起来很奇怪,但它构成了一种紧凑且易于维护的代码类型。
我们使用简单的 SELECT 语句从数据库中检索当前符号列表。但是,如果我们只是简单地说“SELECT symbol FROM StockValues”,我们将为每个符号的值获取一行,或者如果我们每天添加一个新值,则每周大约五个值。为了检索不同的值,我们在 SELECT 查询中添加限定符 DISTINCT。我们还要求符号按字母顺序排列,以便它们以合理的顺序排列
my $sql = "SELECT DISTINCT symbol FROM StockValues"; $sql .= "ORDER BY symbol ";
我们根据用户的会话信息(如我们之前讨论的那样)设置 $name、$email 和 %portfolio 的值。然后,我们使用此信息填写 HTML 表单,该表单允许用户修改他或她的个人资料。我更喜欢使用表格来处理此类表单,以确保列对齐,但这仅仅是一个美学问题;重要的是每个元素都必须有其自己唯一的名称,并且它们将与我们程序顶部的 POST 处理代码期望使用的名称相同。
我们的第二个 mod_perl 处理程序是 StockReport.pm(请参阅列表 2 中的存档)。此模块使用用户输入的投资组合信息,并基于它创建一个或多个图形。
如果用户定义了投资组合,那么我们遍历其中的每个符号。然后,我们 SELECT StockValues 中具有该符号的所有行,并按日期顺序检索它们
my $sql = "SELECT value,date FROM StockValues "; $sql .= "WHERE symbol = \"$symbol\" "; $sql .= "ORDER BY date ";
现在我们遍历每个返回的行,将值添加到 @values 数组,并将日期添加到 @dates 数组。我们还计算了用户当天持仓的价值(将股票数量乘以股价),并将其放入 @holdings 数组。
然后,我们通过创建一个 @data 数组来绘制我们的数据集,其中元素是对 @dates、@values 和 @holdings 的引用。@dates 将用作我们图表中的 X 轴,而 @values 和 @holdings 也将被绘制。由于 @holdings 的每个元素都注定是其在 @values 中的对应元素的倍数,因此我们告诉 GIFgraph 使用两个 Y 轴——一个用于值,一个用于持仓。
我们使用 GIFgraph 的 plot_to_gif 方法创建图形本身,该方法采用一组数据点(@data 数组,StockReport.pm),以 GIF 格式创建图形,然后将其保存到磁盘。我们在变量中设置文件名,以便我们可以保存文件并在 IMG 标签中引用它。请记住,文件必须在 Web 文档树中,才能供用户的 Web 浏览器使用!
将此类文件放在 /tmp(Linux 系统的标准临时目录)中可能很诱人,但这样一来,外部浏览器将无法访问这些图形。此目录必须可由 Web 服务器写入,这通常意味着使其对更多人开放,而不是您的其余 Web 层次结构。如果您的系统上是这种情况,请确保只有此目录可由其他人写入,这样您就不会冒着入侵者查看或损坏您网站的敏感文件的风险。
以这种方式创建文件效果很好,但有一个主要缺陷:它有可能用大量旧图形填充您的文件系统。可以使用多种方法来克服这个问题,但也许最简单的方法是使用 cron 来识别和删除任何早于特定时间的文件。根据您的网站有多繁忙,您可能希望每十分钟、每小时或每月运行一次这样的 cron 作业。这完全取决于您收到的访问者数量和您的磁盘大小。最好更频繁地运行这样的删除程序,以避免可能填满您的磁盘的拒绝服务攻击。
虽然我没有在此版本的 StockReport 中实现它,但您可能会看到允许用户选择图形中的日期范围是多么容易。使用 HTML 表单,您可以允许用户选择开始和结束日期;然后可以将这些表单元素的值插入到 SQL 查询中,以便 SELECT 仅介于指定日期之间的那些行。
一旦我们编写了 StockProfile.pm 和 StockReport.pm 并将其安装到我们的 Perl 模块层次结构中,我们必须以某种方式告诉 Apache 何时使用它们。我们可以通过多种方式做到这一点,但我更喜欢创建特殊的 URL 来调用这些模块。也就是说,每次有人从我们的服务器请求 URL“/stock-profile”时,他们都应该获得个人资料编辑器。同样,当有人从我们的服务器请求“/stock-report”时,他们应该看到他们当前股票的报告。
为了实现这一点,我们必须首先通过将以下两行添加到 Apache 配置文件 httpd.conf 中来加载每个模块
PerlModule Apache::StockProfile PerlModule Apache::StockReport
完成此操作后,我们可以创建新的 URL,这些 URL 不一定与服务器文件系统上的文件相对应。为此,我们在 httpd.conf 中使用 <Location> 部分。我们指示相关 URL 应由 Perl 模块处理(“SetHandler perl-script”),然后告诉 Apache 要用于该特定 URL 的特定模块
<Location /stock-profile> SetHandler perl-script PerlHandler Apache::StockProfile </Location> <Location /stock-report> SetHandler perl-script PerlHandler Apache::StockReport </Location>您需要重新启动 Apache 才能使这些新 URL 生效。如果其中一个 Perl 模块中存在错误,或者 mod_perl 在模块路径 @INC 中找不到其中一个模块,则 Apache 的重新启动将失败。这确保了当您的模块在 mod_perl 下运行时,您不会遇到任何编译时错误。与此同时,它要求您在将模块包含在实时站点上之前对其进行广泛测试,因为在大型网站上关闭服务器可能会令人尴尬或在经济上难以承受。
上个月,我们看到了如何使用 GIFgraph 包创建简单的动态股票图表。本月,我们看到了这种动态创建的图形如何适应更大的应用程序,允许用户查看有关其股票投资组合的信息。关系数据库、mod_perl 和 GIFgraph 的结合使得在不到 400 行代码中创建这样一个简单的应用程序成为可能。毫无疑问,您可以想到许多其他应用程序,在这些应用程序中,动态创建的图形将非常有用——让您的想象力尽情驰骋!
