CGI 编程
这一次,我们将研究人们希望他们的 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, ">>:$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 工作或非正式地为学龄儿童做志愿者时,他喜欢阅读(几乎任何主题,但尤其是计算机、政治和哲学——单独和一起)、烹饪、解决纵横字谜和远足。