使用 CGI 创建多项选择测验系统
在过去的几个月中,我们研究了 CGI 程序员可以用来处理其程序的许多技术。本月,我们将研究一个多项选择测验系统,该系统结合了多种技术,创建了一个简单但有效的系统,用于为我们的用户创建测验。在这个简短的项目结束时,您不仅将对如何实现这种类型的交互有一个很好的了解,还将拥有一个可工作的四题测验。
在开始之前,我们需要确定一种文件格式,其中将包含我们测验的问题和答案。我们可以将所有问题和答案都放在程序本身内部,但是将它们移动到一个或多个外部文件将使我们能够在系统上的其他测验中重用该软件。鉴于这是一个简单的测验,我们假设每个测验的问题和答案都存储在一个文件中,该文件的名称与测验名称相同。因此,名为“presidents”的测验将存储在名为“presidents”的文件中,而名为“unix”的测验则存储在名为“unix”的文件中。
现在我们已经确定了文件名,我们需要确定文件内容的格式。让我们走简单的路线,将一个问题及其相关的可能答案放在文件中的每一行,每个答案之间用制表符分隔,并以字母“a”,“b”,“c”或“d”结尾,这对应于正确答案。
为了使文件可以包含注释和空格,我们将规定任何以井号(#)开头的行都被视为注释,将被忽略。任何仅由空格组成的行也是如此。允许注释和空格使我们有可能注释掉我们不再想使用的问题,而无需完全删除它们。
这是一个关于蔓越莓主题的示例测验,我们将它放在一个名为“cranberries”的文件中,这有点奇怪
# This is the quiz file about cranberries.
# Comment lines contain a hash mark (#) in the # first column, and are ignored, as are lines # containing only whitespace.
What color are cranberries? Red White\ Blue Dark green A What can you make with cranberries? Muffins\ Sauce Steak A and B D
请注意,此文件中的问题和答案可以包含空格字符,但不包含制表符字符。这通常不会对事情产生太大影响,但这需要考虑。另外,虽然每行可以根据需要尽可能长,但问题及其相关答案必须保留在单行文本中(即,必须以回车符结尾)。
我们的测验程序实际上由两个不同的程序协同工作组成。第一个程序 askquestion.pl 生成一个 HTML 表单,该表单向用户呈现一个问题和可能的答案列表。该表单将提交给另一个 CGI 程序 checkanswer.pl,该程序确定用户是否选择了正确答案。
由于这两个 CGI 程序都必须访问同一个测验文件,因此最好将此类功能集中在一个 Perl 5 对象中。这样的对象将必须读取文件并从可用问题列表中返回我们选择的问题。为了使事情更有趣,此对象应包含一个从文件中检索随机问题的方法,这使得测验对用户来说不太可预测。
我们在测验程序中使用的对象如清单 1所示。所有这些代码都意味着您可以在两个 CGI 程序的顶部附近放置一个
use QuizQuestions;
语句,以创建一个 Perl 对象,该对象读取“cranberries”测验的问题。为此,您可以使用以下语句
my $quiz = new QuizQuestions("cranberries");例如,您可以使用以下代码检索第五个问题
my @question = $quiz->getQuestion(5);或使用以下代码检索随机问题
my @question = $quiz->getRandomQuestion;如您所见,清单 1 中的 QuizQuestion 对象与 CGI 编程本身无关。即使我们正在创建一个不会在 Web 上使用的测验系统,此对象也将是一个很好的起点。通过使用对象来表示我们的数据,我们也使得在不修改访问数据的 CGI 程序的情况下更改我们正在使用的文件格式成为可能。如果我们愿意,我们可以将测验数据移动到 SQL 表中,并从 Perl 中的数据库客户端访问它。只要外部世界的接口保持不变,我们的 CGI 程序就不会在意。
现在我们已经为测验数据创建了一个相当简单的接口,让我们创建我们的两个程序中的第一个程序 askquestion.pl。此程序生成一个 HTML 表单,该表单不仅提出问题,还允许用户通过单击相应的单选按钮来选择答案。
程序的一个可能版本如清单 2所示,非常简单明了。它创建了一个 CGI 实例,一个帮助我们编写 CGI 程序的对象,以及一个 QuizQuestions 实例,我们上面创建的对象。在实例化这两个对象之后,我们然后生成一个简单的 HTML 表单,其中包含四个单选按钮,分别对应于每个可能的答案。然后,我们创建一个提交按钮和一个重置按钮,并完成 HTML 表单的创建。
但是,我们还创建一个隐藏字段,其中包含用户正在回答的问题的编号。此编号由 QuizQuestions 中的 getQuestion 和 getRandomQuestion 方法返回。如果您之前不理解为什么我们需要将这些值与问题和答案一起返回,那么现在可能更清楚了。HTTP 是一种无状态协议——对服务器的每个请求都独立于对其发出的任何其他请求。测验至少需要与 HTTP 服务器建立两个连接——一个连接用于获取问题并使用 askquestion.pl 生成表单,第二个连接用于提交用户的响应并检查答案 checkanswer.pl。
问题在于,checkanswer.pl 只有在知道用户被问到哪个问题时才能验证用户的答案是否正确。由于 checkanswer.pl 是通过对 HTTP 服务器的单独请求调用的,因此除非我们有某种方法可以从调用 askquestion.pl 传递该消息,否则它无法知道选择了哪个问题。
我们可以使用隐藏字段将正确答案传递给 checkanswer.pl,但这并不是一个好主意,因为隐藏字段仅对明显的视图隐藏。如果用户对查找正确答案感兴趣,他或她将能够查看页面的 HTML 源代码,这将很快显示答案。这样,用户只知道正在问哪个问题,而不知道哪个答案是正确的。
另请注意,测验的名称来自查询字符串,该查询字符串在 QUERY_STRING 环境变量中传递给我们。如上所述,这使我们可以将相同的测验程序用于多个程序。通过更改查询字符串中放置的值,您可以将这对程序变成许多不同的测验,每个测验都有自己的一组问题和答案。当我们在 <Form> 标记中设置 action 属性时,我们确保它不仅包含表单应提交到的程序的名称 checkanswer.pl,还包含测验的名称,该名称出现在查询字符串中。
正如我们前面看到的,由 askquestion.pl 生成的表单被提交给第二个 CGI 程序 checkanswer.pl。Checkanswer.pl 打开问题列表,通过检索隐藏在表单中的 questionNumber 表单元素的值来检索用户被问到的问题,并对照正确答案检查用户的答案。
如果用户正确回答了问题,程序将显示“祝贺”标题以及正确答案,并询问用户是否想要另一个问题。
如果用户回答错误,程序将显示正确答案,提供一些安慰,并询问用户是否要继续。
现在您可以看到为什么需要 getQuestion 和 getRandomQuestion 方法。仅使用 getQuestion,您可以检索问题,但不能从问题列表中随机选择。但是,如果您只有 getRandomQuestion,您将无法检索用户提出的问题,因此将无法对照正确答案检查用户的答案。
checkanswer.pl 的源代码在清单 3中。此实现的一个明显的缺陷是,如果站点管理员决定在用户收到问题和提交表单之间修改问题文件,则该问题可能会被标记为错误。这是因为程序期望问题顺序在问题被提出和问题被回答之间不会被修改。如果您在文件顶部插入一个新问题,这将使问题 1 变为问题 2,问题 2 变为问题 3,依此类推——这意味着 checkanswer.pl 会将用户的答案与不同问题的答案进行比较。
请注意,我们使用了 Perl 的 eval 函数来获取答案的实际文本。也许这仅仅是个人的一种固执,但我讨厌当我被告知我回答错误时,但没有人告诉我正确答案是什么。我们可以将答案存储在关联数组中,但我认为使用 eval 来获取变量的值会很有趣。在这种情况下,我们将字符串“$answer”和 $rightAnswer 的值连接起来,得到四个可能的字符串之一“$answerA”、“$answerB”、“$answerC”或“$answerD”。eval 被传递该字符串并返回字符串中命名的变量的值。
现在我们已经定义了 QuizQuestions、askquestion.pl 和 checkanswer.pl,剩下的就是创建一个 HTML 文件,作为测验的初始入口。
<HTML> <Head> <Title>Play our quiz!</Title> </Head> <Body> <H1>Play our quiz!</H1> <P>You can play our cranberry quiz by clicking <a href="/cgi-bin/askquestion.pl?cranberries"> here</a>.</P> </Body> </HTML>
请注意,指向初始问题的 URL 必须在其查询字符串中附加测验名称。除此之外,这是一个简单的 HTML 文档。
到目前为止,这个测验似乎运行良好,尽管您肯定可以添加一些功能——例如记分牌、在读取测验文件时更好的错误检查,或者确保用户不会看到相同问题两次的系统。
但比所有这些更重要的是,问题文件的格式对于程序员来说很容易理解,但想要添加、删除或修改问题的非程序员可能会觉得这种格式令人困惑。下个月,我们将致力于使此系统对作者更加友好,以便非程序员可以通过 HTML 表单修改问题文件中的条目。
Reuven M. Lerner 自 1993 年初以来一直在玩 Web,当时它看起来更像是一个有趣的玩具,而不是世界下一个伟大的媒介。他目前在以色列海法市的公寓里担任独立的互联网和 Web 顾问。在不从事 Web 工作或在非正式教育计划中做志愿者时,他喜欢阅读几乎所有主题的书籍,尤其是政治和哲学、烹饪、解决纵横字谜和远足。您可以通过 reuven@the-tech.mit.edu 或 reuven@netvision.net.il 与他联系。