CGI 编程

作者:Reuven M. Lerner

这一次,我们将研究人们希望他们的 CGI 程序执行的最常见的事情之一,即把数据保存到磁盘上的文件中。到本专栏结束时,我们将积累足够的工具来制作一个简单但功能齐全的留言簿程序,该程序将允许访问您网站的访客保存可以被其他人阅读的评论。

首先,让我们看一下一个简单的 HTML 表单,它将允许用户将数据发送到我们的 CGI 程序,我们将其称为 “entryform.pl”

<HTML>
<Head>
<Title>Data entry form</Title>
</Head>
<Body>
<H1>Data entry form</H1>
<Form action="/cgi-bin/entryform.pl"
method=POST>
<P>Name: <input type=text name="name"
value=""></P>
<P>E-mail address: <input type=text
name="email" value=""></P>
<P>Address: <input type=text
name="address" value=""></P>
<P>Country: <input type=text
name="country" value=""></P>
<P>Male <input type=radio name="sex"
value="male">
Female <input type=radio name="sex"
value="female"></P>
<input type=submit>
</Form>
</Body>
</HTML>

当然,HTML 表单本身不会做任何事情;它需要一个 CGI 程序来接受和处理其输入。下面是一个 Perl5 程序,如果命名为 “entryform.pl” 并放置在 Web 服务器上的主 “/cgi-bin” 目录中,则应该打印出来自上述表单的名称-值对

0    #!/usr/local/bin/perl5
1    # We want to use the CGI module
2    use CGI;
3    # Create a new CGI object
4    my $query = new CGI;
5    # Print an a appropriate MIME header
6    print $query->header("text/html");
7    # Print a title for the page
8    print $query->start_html(-title=>"Form
     contents");
9    # Print all of the name-value pairs
10   print $query->dump();
11   # Finish up the HTML
12   print $query->end_html;

以下是每行代码作用的快速概述

第 0 行告诉 Unix 系统在哪里找到 Perl 解释器。如果您的 Perl 副本被称为其他名称,则需要修改此行。

如果没有在第 2 行显式导入 CGI 模块,Perl 将不知道如何创建和使用 CGI 对象。(尝试使用来自您尚未导入的模块的代码肯定会使 Perl 感到困惑并生成错误消息。)然后我们将 $query 声明为 CGI 的实例(第 4 行)。

然后我们告诉用户的浏览器我们的响应将是 HTML 格式的文本,我们通过使用 MIME 标头来做到这一点。缺少 MIME 标头是 500 错误的最常见原因;每当您的 CGI 程序之一产生这些错误时,请确保您没有在标头之前尝试打印 HTML!请注意,第 6 行等同于说

print "Content-type: text/html\n\n";

这也告诉浏览器期望 HTML 格式的文本数据。但总的来说,我更喜欢使用 CGI 对象,以便于阅读。

第 8 行创建开始文档所需的基本 HTML,包括为其提供标题“表单内容”。

第 10 行使用 CGI 对象的内置工具,以易于阅读的格式“转储”HTML 表单的内容。这使我们能够查看分配给 HTML 表单的每个元素的值,这在调试有问题的程序时非常宝贵。但是现在,我们只是使用 CGI “dump” 方法来开始并确认程序有效。

将数据保存到文件

现在我们已经证明我们的 HTML 表单正在向我们的 CGI 程序发送数据,并且我们的程序可以将输出发送回用户的 Web 浏览器,让我们看看我们可以用这些数据做什么。首先,让我们尝试将表单中的数据保存到磁盘上的文件中。(这是客户要求我实现的最常见的任务之一,通常是因为他们想收集有关其访问者的数据。)

#!/usr/local/bin/perl5
# We want to use the CGI module
use CGI;
# Set the filename to which we want the elements
# saved
my $filename = "/tmp/formcontents";
# Set the character that will separate fields in
# the file
my $separation_character = "\t";
# Create a new CGI object
my $query = new CGI;
# ----------------------------------------------
# Open the file for appending
open (FILE, ">>$filename") ||
        die "Cannot open \"$filename\"!\n";
# Grab the elements of the HTML form
@names = $query->param;
# Iterate through each element from the form,
# writing each element to $filename. Separate
# elements with $separation_character defined
# above.
foreach $index (0 .. $#names)
{
        # Get the input from the appropriate
        # HTML form element
        $input = $query->param($names[$index]);
        # Remove any instances of
        # $separation_character
        $input =~ s/$separation_character//g;
        # Now add the input to the file
        print FILE $input;
        # Don't print the separation character
        # after the final element

print FILE $separation_character if
                ($index < $#names);
}
# Print a newline after this user's entry
print FILE "\n";
# Close the file
close (FILE);
# -----------------------------------------------
# Now thank the user for submitting his
# information
# Print an a appropriate MIME header
print $query->header("text/html");
# Print a title for the page
print $query->start_html(-title=>"Thank you");
# Print all of the name-value pairs
print "<P>Thank you for submitting the ";
print "form.</P>\n";
print "<P>Your information has been ";
print "saved to disk.</P7gt;\n";
# Finish up the HTML
print $query->end_html;

上面的程序与之前的程序几乎相同,只是我们添加了一个部分,该部分获取每个 HTML 表单元素并将它们保存到文件中。结果文件中的每一行都对应于一次 HTML 表单的“提交”按钮的按下。

上面的程序使用 TAB 字符分隔字段,但我们也可以轻松地使用逗号、波浪号或字母 “a”。但是请记住,最终有人会想要使用这些数据——无论是通过将其导入数据库还是通过使用 Perl 或另一种编程语言将其拆分。为了确保用户不会弄乱我们的数据库格式,我们使用 Perl 的 substitution(s) 运算符删除了用户输入中分隔符的所有实例。有点苛刻,但有效!

上述程序的最大问题之一是它依赖于 HTML 表单元素始终以相同的顺序出现。也就是说,如果您的 HTML 表单上有元素 X、Y 和 Z,它们会按照它们在表单中出现的顺序放置在 @names 中吗?按字母顺序?随机顺序?老实说,没有任何方法可以确定,因为 CGI 规范对此保持沉默。因此,一种可能是,一个用户的表单将以 (X, Y, Z) 的顺序提交,而另一个用户的表单将以 (Y, Z, X) 的顺序提交——这可能会导致我们的数据文件出现问题,其中字段由其位置标识。

一个简单的修复方法是维护一个我们期望从 HTML 表单接收的字段列表。这需要在程序和表单之间进行更多的协调,但考虑到同一个人经常同时处理两者,这是一个小问题。

首先,我们在程序顶部附近定义一个列表 @fields。此列表包含我们期望接收的所有字段的名称,以及我们期望接收它们的顺序

my @fields = ("name",
                     "email",
                     "address",
                     "country",
                     "sex");

接下来,我们更改 “foreach” 循环(它将字段元素放置在输出文件中),使其迭代 @fields 的元素,而不是 @names

foreach $index (0 .. $#fields)
{
  # Get the input from the appropriate HTML form
  # element
  $input = $query->param($fields[$index]);
  # Remove any instances of $separation_character

  $input =~ s/$separation_character//g;
  # Now add the input to the file
  print FILE $input;
  # Don't print the separation character after the
  # final element
  print FILE $separation_character if
        ($index < $#fields);
}
必填字段

如果我们想确保用户填写了某些字段怎么办?当我们收集有关网站访问者的数据,并希望确保我们收到他们的姓名、地址和其他重要数据时,这尤其重要。一种简单的方法是创建一个列表 @required_fields,其中列出了必填字段

my @required_fields = ("name",
                              "email",
                              "address");

如果您只是想要一条通用消息,指示一个或多个必填字段未填写,则可以在程序文件的底部添加以下子例程

sub missing_field
{
  # Print an a appropriate MIME header
  print $query->header("text/html");
  # Print a title for the page
  print $query->start_html(-title=>
  "Missing field(s)");
  # Tell the user what the error is
  print "<P>At least one required ";
  print "field is missing.</P>\n";
  # Finish up the HTML
  print $query->end_html;

}

然后,我们可以将以下代码插入程序本身,就在我们打开文件之前——因为如果我们只是要再次关闭文件,则没有任何理由打开文件

foreach $field (@required_fields)
{
  # Make sure that the field contains more than
  # just whitespace
  &missing_field if
  ($query->param($field) !~m/\w/);
  exit;
}

上面的代码确实可以解决问题,但会给出通用的错误消息。告诉用户 哪个 字段包含错误不是更好吗?我们可以通过修改 missing_field 使其接受一个参数来做到这一点,如下所示

sub missing_field
{
  # Get our local variables
  my (@missing_fields) = @_;
  # Print an a appropriate MIME header
  print $query->header("text/html");
  # Print a title for the page
  print $query->start_html
  (-title=>"Missing field(s)");
  print "<P>You are missing the following ";
  print "required fields:</P>\n";
  print "<ul>\n";
  # Iterate through the missing fields, printing
  # them foreach $field (@missing_fields)
  {
    print "<li> $field\n";
  }
  print "</ul>\n";

  # Finish up the HTML
  print $query->end_html;
  exit;
}

然后我们修改检查必填字段的循环

foreach $field (@required_fields)
{
  # Add the name of each missing field
  push (@missing_fields, $field) if
   ($query->param($field) !~ m/\w/);
}
# If @missing_fields contains any elements, then
# invoke the error routine
&missing_field(@missing_fields)
  if @missing_fields;

如果我们想要更高级的功能,我们可以为每个必填字段提供英文名称,这样用户就不必忍受我们在 HTML 表单中使用的名称。我们可以通过使用关联数组来做到这一点

$FULLNAME{"name"} = "your full name";
$FULLNAME{"email"} = "your e-mail address";
$FULLNAME{"address"} = "your mailing address";

然后我们修改 &missing_fields 中的 foreach 循环,使其打印缺失字段的全名,而不是与其在 HTML 表单上关联的名称

# Iterate through the missing fields, printing
# them foreach $field (@missing_fields)
{
  print "<li> $FULLNAME{$field}\n";
}
print "</ul>\n";
有风格地终止

还记得我们在原始程序中放入的 die 语句吗?好吧,想想如果程序的这一部分真的被调用会发生什么——die 会产生一个错误消息,这是一件好事。但是该错误消息将在 HTML 标头之前发送到我们的 Web 浏览器,从而给我们带来可怕的“服务器错误”消息,表明我们的脚本出了问题(但没有说明是什么)。

更有用的是将错误消息打印到屏幕的例程。例如,我们可以添加以下子例程

sub error_opening_file
{
    my ($filename) = @_;
    # Print an a appropriate MIME header
    print $query->header("text/html");
    # Print a title for the page
    print $query->start_html(-title=>"Error
    opening file");
    # Print the error
    print "Could not open the file
    \"$filename\".</P>\n";
    # Finish up the HTML
    print $query->end_html;
    exit;
}

现在,我们可以将 “open” 语句重写如下

open (FILE, ">&gt:$filename") ||
  &error_opening_file($filename);

您可能不想告诉您的用户您的程序无法打开特定文件——不仅您的用户不关心,而且您也不需要告诉他们您正在使用哪些文件。更友好的 error_opening_file 版本可以告诉用户服务器遇到一些问题,或者正在进行维护,或者给出类似的不会向全世界广播灾难的消息。

整合在一起

程序的最终版本,具有 (a) 必填字段,(b) 这些字段的完整英文描述,以及 (c) 当我们无法打开文件时更好的错误消息,如下所示

#!/usr/local/bin/perl5
# We want to use the CGI module
use CGI;
# Set the filename to which we want the elements
# saved
my $filename = "/tmp/formcontents";
# Set the character that will separate fields in
# the file
my $separation_character = "\t";
# In what order do we want to print fields?
my @fields = ("name",
              "email",
              "address",
              "country",
              "sex");
# Which fields are required?
my @required_fields = ("name",
                       "email",
                       "address");
# What is the full name for each required field?
$FULLNAME{"name"} = "your full name";
$FULLNAME{"email"} = "your e-mail address";
$FULLNAME{"address"} = "your mailing address";
# Create a new CGI object
my $query = new CGI;
# ---------------------------------------------
# Make sure that all required fields have arrived
foreach $field (@required_fields)
{
    # Add the name of each missing field
    push (@missing_fields, $field)
    if ($query->param($field) !~ m/\w/);
}
# If any fields are missing, invoke the error
# routine
&missing_field(@missing_fields)
  if @missing_fields;
# ---------------------------------------------
# Open the file for appending
  open (FILE, "7gt;>$filename") ||
  &error_opening_file($filename);
# Grab the elements of the HTML form
@names = $query->param;
# Iterate through each element from the form,
# writing each element to $filename. Separate
# elements with $separation_character defined
# above.
foreach $index (0 .. $#fields)
{
    # Get the input from the appropriate HTML
    # form element
    $input = $query->param($fields[$index]);
    # Remove any instances of
    # $separation_character
    $input =~ s/$separation_character//g;
    # Now add the input to the file
    print FILE $input;
    # Don't print the separation character after
    # the final element
    print FILE $separation_character if
    ($index < $#fields);
}
# Print a newline after this user's entry
print FILE "\n";
# Close the file
close (FILE);
# ---------------------------------------------
# Now thank the user for submitting their
# information
# Print an a appropriate MIME header
print $query->header("text/html");
# Print a title for the page
print $query->start_html(-title=>"Thank you");
# Print all of the name-value pairs
print "<P>Thank you for submitting ";
print "the form.</P>\n";
print "<P>Your information has been ";
print "saved to disk.</P>\n";
# Finish up the HTML
print $query->end_html;
# ---------------------------------------------
# Subroutines
sub missing_field
{
    # Get our local variables
    my (@missing_fields) = @_;
    # Print an a appropriate MIME header
    print $query->header("text/html");
    # Print a title for the page
    print $query->start_html(-title=>
    "Missing field(s)");
    print "<P>You are missing the following
    required fields:</P>\0";
    print "<ul>\n";
    # Iterate through the missing fields,
    # printing them
    foreach $field (@missing_fields)
    {
        print "<li> $FULLNAME{$field}\n";
    }

    print "</ul>\n";

    print "</ul>\n";

    # Finish up the HTML
    print $query->end_html;

    exit;
}
sub error_opening_file
    {
        my ($filename) = @_;
        # Print an a appropriate MIME header
        print $query->header("text/html");
        # Print a title for the page
        print $query->start_html(-title=>
        "Error opening file");
        # Print the error
        print "Could not open the file
        \"$filename\".</P>\n";
        # Finish up the HTML
        print $query->end_html;
        exit;
    }
创建留言簿

Web 上最常见的 CGI 应用程序之一是“留言簿”,它允许网站访问者注册,留下他们的姓名、电子邮件地址和简短的注释。我们可以使用上述程序中看到的基本框架轻松构建这样的程序。“留言簿”程序与我们迄今为止看到的程序之间的唯一区别在于,留言簿必须以 HTML 格式格式化,以便用户能够在浏览器中阅读它。

这是一个非常简单的留言簿程序,它与我们之前看到的程序几乎相同

<HTML>
<Head>
<Title>Guestbook entry</Title>
</Head>
<Body>
<H1>Guestbook entry</H1>
<Form action="/cgi-bin/guestbook.pl"
    method=POST>
<P>Name: <input type=text name="name"
    value=""></P>
<P>E-mail address: <input type=text name="email"
value=""></P>
<input type=submit>
</Form>
</Body>
</HTML>

以下程序与上面的程序相同,只是它将数据保存到 “guestbook.html” 并以 HTML 格式格式化数据。

#!/usr/local/bin/perl5
# We want to use the CGI module
use CGI;
# Set the filename to which we want the elements
# saved
my $filename =
"/home/reuven/Consulting/guestbook.html";
# Set the character that will separate fields in
# the file
my $separation_character = "</P><P>";
# In what order do we want to print fields?
my @fields = ("name", "email");
# Which fields are required?
my @required_fields = ("name", "email");
# What is the full name for each required
# field?
$FULLNAME{"name"} = "your full name";
$FULLNAME{"email"} = "your e-mail address";
# Create a new CGI object
my $query = new CGI;
# ---------------------------------------------
# Make sure that all required fields have arrived
foreach $field (@required_fields)
{
  # Add the name of each missing field
  push (@missing_fields, $field) if
  ($query->param($field) !~ m/\w/);
}
# If any fields are missing, invoke the error
# routine
&missing_field(@missing_fields) if
  @missing_fields;
# ----------------------------------------------
# Open the file for appending
open (FILE, ">>$filename") ||
  &error_opening_file($filename);
# Grab the elements of the HTML form
@names = $query->param;
# Iterate through each element from the form,
# writing each element to $filename. Separate
# elements with $separation_character defined
# above.
foreach $index (0 .. $#fields)
{
  # Get the input from the appropriate HTML form
  # element
  $input = $query->param($fields[$index]);
  # Remove any instances of $separation_character
  $input =~ s/$separation_character//g;
  # Now add the input to the file
  print FILE $input;
  # Don't print the separation character after the
  # final element
  print FILE $separation_character if
  ($index < $#fields);
}
# Print a newline after this user's entry
print FILE "<BR><HR><P>\n\n";
# Close the file
close (FILE);
# -------------------------------------------
# Now thank the user for submitting his
# information
# Print an a appropriate MIME header
print $query->header("text/html");
# Print a title for the page
print $query->start_html(-title=>"Thank you");
# Print all of the name-value pairs
print "<P>Thank you for submitting ";
print "the form.</P>\n";
print "<P>Your information has been ";
print "saved to disk.</P>\n";
# Finish up the HTML
print $query->end_html;
# --------------------------------------------
# Subroutines
sub missing_field
{
  # Get our local variables
  my (@missing_fields) = @_;
  # Print an a appropriate MIME header
  print $query->header("text/html");
  # Print a title for the page
  print $query->start_html(-title=>"
  Missing field(s)");
  print "<P>You are missing the ";
  print "following required fields:</P>\n";
  print "<ul>\n";
  # Iterate through the missing fields, printing
  # them
  foreach $field (@missing_fields)
    {
        print "<li> $FULLNAME{$field}\n";
        }
  print "</ul>\n";
  print "</ul>\n";
  # Finish up the HTML
  print $query->end_html;
  exit;
}
sub error_opening_file
{
  my ($filename) = @_;
  # Print an a appropriate MIME header
  print $query->header("text/html");
  # Print a title for the page
  print $query->start_html(-title=>
  "Error opening file");
  # Print the error
  print "Could not open the ";
  print "file \"$filename\".</P>\n";
  # Finish up the HTML
  print $query->end_html;
  exit;
}

上面的程序将从 HTML 表单获取输入并将数据保存在 HTML 格式的文件中。如果该文件可以从 Web 服务器访问,您的用户应该能够查看其他人在留言簿中的条目。

Reuven M. Lerner (reuven@the-tech.mit.edu) (reuven@netvision.net.il) 自 1993 年初以来一直在玩 Web,当时它看起来更像是一个有趣的玩具,而不是世界的下一个伟大媒介。他目前在以色列海法的公寓里担任独立的互联网和 Web 顾问。在不从事 Web 工作或非正式地为学龄儿童做志愿者时,他喜欢阅读(几乎任何主题,但尤其是计算机、政治和哲学——单独和一起)、烹饪、解决纵横字谜和远足。

加载 Disqus 评论