Making Dictionaries with Perl

取自 PerlChina.org - wiki

跳转到: 导航, 搜索

Perl 与字典的生成


目录


早上起床时候,你最不可能想到的是“如果我有本字典就好了!”虽然地球上面有上千种需要学习语言,但是人们确无法开始学,因为他们没有必要的学习材料:没有 Mohawk-English 口袋字典,没有 Cherokee Poetry Reader ,没有 Everyday Otomi: Second Year 。直到最近几年人们才开始意识到这些语言不只是用来取乐的,而是不可补偿 ,也不能翻译的地方文化的一部分。而且它们正在像候鸟一样远离我们。

因为我正在学习 Perl ,又恰巧接触到很多语言学家(他们致力于提供材料来帮助学习这些濒危语种)。这些人的工作包括编写教材和其他的“语言材料”,自从80年代我们有了桌面排版技术以后,这是很好做到的。但是还有一个棘手的事情,就是字典。用字处理软件来编写一本字典是很疯狂的想法,好像在记事贴上写小说一样。因此他们开始用数据库,但是数据库还不是可以直接印成字典的东西。他们没法从这种格式:

  Headword: dagiisláng
  Citation: HSD
  Part of speech: verb
  English: wave a piece of cloth
  Example:  Dáayaangwaay hal dagiislánggan. | He was waving a flag.

转化成这种格式:

Image:Dagislang.gif

“好”,我曾经这么说而且一直坚持这么说:“这不困难,因为我是个程序员!用 CSV 或其他什么格式导出你的数据库内容,并且发个邮件给我,我就可以写个程序来输出一个漂亮的字处理文件格式的东西来满足你的要求。”

“就你一个人?你怎么能写成一个产生字处理器文档的软件?这肯定需要一年的时间,写出一百万行C++程序,花掉数千万美元!”

“没错,但是由于我用 Perl ,只要花几分钟写几十行程序就好了。” 因为一个按照通常格式排版的字典只是一个商业上面所说的报表,只不过更加华丽一些而已。在工作间里工作的人一直在做这个,现在我要给你看看他们是怎么做的。

[编辑] 读取输入

当然你要用到 Perl ,那并不难。绝大多数情况你只需要一个处理输入的模块和一个格式化输出模块就可以了。若输入或输出的格式非常简单,甚至可以不用那些。在这个例子里面,我要处理的输出格式非常简单,他被叫做 Shoebox Standard Format (标准鞋盒格式),他读起来像下面的段落:

  \hw dagiisláng
  \cit hsd
  \pos verb
  \engl wave a piece of cloth
  \ex Dáayaangwaay hal dagiislánggan. | He was waving a flag.

  \hw anáa
  \cit hsd; led-285
  \pos adverb
  \engl inside a house; at home
  
  \hw súut hlgitl'áa
  \cit hsd; led-149; led-411
  \engl speak harshly to someone; insult
  \ex 'Láa hal súut hlgitl'gán. | She said harsh words to her.
  
  \hw tlak'aláang
  \cit led-398
  \pos noun
  \engl the shelter of a tree

通常是“\字段名 字段值”的格式,每个记录项从一个 \hw 字段开始,所有记录和字段没有固定的顺序。(这些数据来源于一个名叫 Haida 的濒危语言,是我所居住的南阿拉斯加群岛的人说的语言)。

现在我们可以用 regexp 和一些 while() {...} 循环来读懂这些,但是已经有人写了一个模块来一次性读取整个文件并产生一个列表的列表的数据结构。粗略的看过模块的文档以后,我们可以写个简单的程序来读取这些词法,并且全部输出来确认我们已经正确的匹配上了。

  use Text::Shoebox::Lexicon;
  my $lex = Text::Shoebox::Lexicon->read_file( "haida.sf" );
  $lex->dump;

这会打印如下的信息:

 Lexicon Text::Shoebox::Lexicon=HASH(0x15550f0) contains 4 entries:
 
 Entry Text::Shoebox::Entry=ARRAY(0x1559104) contains:
   hw = "dagiisláng"
   cit = "hsd"
   pos = "verb"
   engl = "wave a piece of cloth"
   ex = "Dáayaangwaay hal dagiislánggan. | He was waving a flag."
 
 Entry Text::Shoebox::Entry=ARRAY(0x1559194) contains:
   hw = "anáa"
   cit = "hsd; led-285"
   pos = "adverb"
   engl = "inside a house; at home"
 
 Entry Text::Shoebox::Entry=ARRAY(0x155920c) contains:
   hw = "súut hlgitl'áa"
   cit = "hsd; led-149; led-411"
   engl = "speak harshly to someone; insult"
   ex = "'Láa hal súut hlgitl'gán. | She said harsh words to her."
 
 Entry Text::Shoebox::Entry=ARRAY(0x1559284) contains:
   hw = "tlak'aláang"
   cit = "led-398"
   pos = "noun"
   engl = "the shelter of a tree"

再仔细看看文档就发现 $lexicon->entries 会返回一个记录的对象列表,并且 $entry->as_list 会返回一个记录的内容的列表,这是 (key1, value1, key2, value2) 格式的。正是这种格式非常适合导入到一个 Perl 哈希表里面。于是就有了:

  foreach my $entry ($lex->entries) {
    my %e = $entry->as_list;
  }

这干的很棒。但如果我们有类似下面的记录:

  \hw súut hlgitl'áa
  \cit hsd; led-149; led-411
  \engl speak harshly to someone
  \engl insult
  \ex 'Láa hal súut hlgitl'gán. | She said harsh words to her.<、pre>

这种情况下有了两个 engl 字段,调用 $entry->as_list 会得到这个:
<pre>
 (
  'hw'   => "súut hlgitl'áa",
  'cit'  => "hsd; led-149; led-411",
  'engl' => "speak harshly to someone",
  'engl' => "insult",
  'ex'   => "'Láa hal súut hlgitl'gán. | She said harsh words to her.",
 )

只要我们把它导入到 %e 哈希表,就会得到这个:

 (
  'hw'   => "súut hlgitl'áa",
  'cit'  => "hsd; led-149; led-411",
  'engl' => "insult",
  'ex'   => "'Láa hal súut hlgitl'gán. | She said harsh words to her.",
 )

当然 Perl 的哈希表必须有唯一的键值。如果你需要处理一个包含这样记录的词,在 Text::Shoebox::Entry 类里面有很多的方法可用。但是对于一个简单的词来说,在每个字段只有一个值的情况下,使用哈希表比较简单。你甚至可以调用 $entry->assert_keys_unique 来看看是不是字段唯一。这个断言只对记录里面存在重复字段名的情况才会中断程序并打印一个帮助发现错误的信息。

但是对我们的数据来说,键都是唯一的,使用哈希表就可以了:

  foreach my $entry ($lex->entries) {
    my %e = $entry->as_list;
  }

我们可以在循环中对 $e 的内容进行如下的处理 :要么就在那里产生输出,要么把它放到 Perl 变量里面等待别的子程序来输出。

[编辑] 产生输出

因为已经有了输入代码作为基础,现在我们可以考虑如何输出了。一旦有了答案,我们就可以知道如何编写代码来产生正确的输出。

作为一种通用的输出格式 HTML 可以被考虑,基本上来说每个程序员都可以很好的处理它,而且每个人都可以用浏览器或字处理器来打印 HTML 。但是尽管过了很多年,还是有很多基本的问题困扰着 HTML :作为一个标记语言,还是没有可靠的办法来控制页面布局相关的东西(如页眉,页数和灵活的分栏等等)。更加要命的是,所见即所得的 HTML 编辑器都不怎么样。这些问题都使得在输出中使用 HTML 几乎不可能,因为那样的话几乎没法对产生的结果再做任何的手工修改。

因为这些问题,我基本上决定要用 RTF 来作为输出格式。技术上来说 RTF 是一个微软的数据格式,但是它却避开了很多老掉牙的技术纷争。几乎所有的字处理器都支持它,微软的 Word 可以很好的编辑和打印这种格式(它总得有点擅长的东西)。最后 Perl 也很好的通过 CPAN 的 RTF::Writer 和 RTF::Document 支持 RTF 格式的输出,所以你可以不必完全理解这个语言就开始使用它。我会使用 RTF::Writer ,因为我更熟悉它。(这也有可能是因为 O'Reilly 有本名叫 RTF Pocket Guide 的书,这本书的作者是一个英俊潇洒的人,也很谦虚,不愿轻易让大家知道他就是我。)

在 RTF::Writer 的文档里浏览一番,我们就会知道你需要有个文件句柄来输出一个 RTF 文件。然后你就可以用 print 或者 paragraph 方法来输出数据:

  use RTF::Writer;
  my $rtf = RTF::Writer->new_to_file( "sample.rtf" );
  $rtf->prolog();  # sets up sane defaults

  $rtf->paragraph( "Hello world!" );
  $rtf->close;

That writes an RTF document consisting of just a sane header and then basically the text, "Hello world!":

这会产生一个包含标题和 "Hello world!" 文字的 RTF 文件。

  {\rtf1\ansi\deff0{\fonttbl
  {\f0 \froman Times New Roman;}}
  {\colortbl;\red255\green0\blue0;\red0\green0\blue255;}
  {\pard
  Hello world!
  \par}
  }

关于 RTF::Writer 的文档介绍了一系列必须的排版和词法用的转义符:

  \b      粗体
  \i       斜体
  \f2    切换到文章的第二字体
  \fs40  切换到20点大小字体,40是半个点的大小

从设计上来说 RTF::Writer 有个对输入的普通文本的转义过程,然后才输出到 RTF 文件中。若你确实想要在文档中显示一个反斜杠后面跟着一个 b 的话,你需要用字符串的引用来调用 RTF::Writer 接口。如下:

  $rtf->paragraph( \'\i', "Hello world!" );

你还可以用一个数组引用(也就是用方括号包起的[代码,文字])来包起字符串来限制这种效果的范围:

  $rtf->paragraph(
    "And ",
    [ \'\i', "Hello world!" ],
    " is what I say."
  );

这会产生一个文档,内容是: And Hello world! is what I say.

这就是我们在下面简单的例子代码里面所需要知道的关于 RTF 的全部东西:

  use RTF::Writer;
  my $rtf = RTF::Writer->new_to_file( "lex.rtf" );
  $rtf->prolog();  # sets up sane defaults

  $rtf->paragraph(
    [ \'\b',    "tlak'aláang: " ],
    [ \'\b\i',  "n." ],
    " the shelter of a tree"
  );
  $rtf->paragraph(
    [ \'\b',    "anáa: " ],
    [ \'\b\i',  "adv." ],
    " inside a house; at home"
  );

  $rtf->close;

这就几乎产生了你会在一本理想的字典里面会看到的格式:

Image:Two entries.gif

当然,我们还想对空格和字体做些调整,但是那可以留到以后细细地做代码改进。我们现在所懂得的知识已经足够我们用了,下面就是代码的轮廓:

  foreach my $entry (...) {
    ...
    $rtf->paragraph(
      [ \'\b',    $headword, ": " ],
      [ \'\b\i',  $part_of_speech ],
      " ", $english,
      ...and something to drop the example sentences, if any...
    );
  }

实际上,我们可以用这个来拼接到前面的读入代码上,来产生一个不怎么漂亮却能工作的原型:

  use strict;
  use Text::Shoebox::Lexicon;
  my $lex = Text::Shoebox::Lexicon->read_file( "haida.sf" );

  use RTF::Writer;
  my $rtf = RTF::Writer->new_to_file( "lex.rtf" );
  $rtf->prolog();  # sets up sane defaults

  foreach my $entry ($lex->entries) {
    my %e = $entry->as_list;
    $rtf->paragraph(
      [ \'\b',    $e{'hw'}  || "?hw?", ": " ],
      [ \'\b\i',  $e{'pos'} || "?pos?" ],
      " ", $e{'engl'} || "?english?"
    );
  }
  $rtf->close;

这会产生下面的结果:

Image:Four entries.gif

当然现在记录还不是按字母顺序排列的,我们也没有用 "n." 来替换 "noun" ,还有例句也还没有加进去。但我们只写了20行不到的 Perl 代码,就有个能够用的字典转换器,就可以猜到下面的事情也不会太难了。

[编辑] 排序和重复词

下面我们就动手来输出按字母顺序的记录,只要简单的改写一点内容就好:

  my %headword2entry;
  foreach my $entry ($lex->entries) {
    my %e = $entry->as_list;
    $headword2entry{ $e{'hw'} } = \%e;
  }
  
  foreach my $headword (sort keys %headword2entry) {
    my %e = %{ $headword2entry{$headword} };
    ...and print it here...
  }

那就干的很好,但是若有一个语言学家往我们的数据库里面加入下面3条记录:

  \hw gíi
  \pos auxiliary verb
  \engl already; always; often
  
  \hw gu
  \pos postposition
  \engl there
  
  \hw gíi
  \pos verb
  \engl swim away [of fish]

我们运行程序时就会产生错误的结果:

Image:Six entries sorted1.gif

首先,第二个 "gíi" (表示鱼在水里游的动词)被存储到 $headword2entry{'gíi'} 里面,这就覆盖了第一个 "gíi" 记录(这个意思是早已,总是和经常)。其次, "gíi" 居然排在 "gu" 后面!

第一个问题可以通过修改数据结构来解决,把下面的代码:

  $headword2entry{ 'gíi' } = ...one_entry...;

改成使用新的数据结构的方式,如下:

  $headword2entries{ 'gíi' } =
    [ ...one_entry... , ...another_entry..., ...maybe_even_another... ];<pre>

尽管在绝大多数时候列表里面只有一个记录。

这很容易结合到我们的程序里面,尽管反引用的语法有点笨重:
<pre>
  my %headword2entries;
  foreach my $entry ($lex->entries) {
    my %e = $entry->as_list;
    push @{ $headword2entries{ $e{'hw'} } },  \%e;
  }
  
  foreach my $headword (sort keys %headword2entries) {
    foreach my $entry ( @{ $headword2entries{$headword} } ) {
      ...code to print the entry...
    }
  }

这就正确了:两个 "gíi" 都在结果里面显示了。

现在要如何来解决 sort keys %headword2entries 正确排序的问题?通常 Perl 总是使用 ASCII 的顺序来排序,这样 "i" 不是紧紧的排在 "u" 后面,而是在所有混音字母以后。我们可以加上 "use locale;" 这一行,来让 Perl 排序得更加聪明点,这样若我们的本地化参数设成法语或德语的时候 "i" 就会在 "u" 的前面。但是有时候你会遇到有的语言使用 "dh" 作为一个联合字母,排在 "d" 之后。这时候你的运气就没那么好了,因为我知道本地化设置在这件事上无能为力,而且大多数系统也不让你去定义自己的本地化设置。

这时候 CPAN 再一次站出来帮助我们。Sort::ArbBiLex 模块让你用回调函数的方式设置排序的顺序。我们从模块的文档中抽出这个例子来测试:

  use Sort::ArbBiLex (
    'custom_sort' =>    # that's the function name to define
    "
     a A à à á á a ? ? ? ? ? ? ? ? ?
     b B
     c C ? ?
     d D e D
     e E è è é é ê ê ? ?
     f F
     g G
     h H
     i I ì ì í í ? ? ? ?
     j J
     k K
     l L
     m M
     n N ? ?
     o O ò ò ó ó ? ? ? ? ? ? ? ?
     p P
     q Q
     r R
     s S ?
     t T t T
     u U ù ù ú ú ? ? ü ü
     v V
     w W
     x X
     y Y y Y ?
     z Z
    "
  );

若我们需要让 "dh" 成为一个 "d" 和 "e" 之间的新字母,只要简单的在上面的代码中加上一行就好了:

     ...
     d D e D
     dh Dh
     e E è è é é ê ê ? ?
     ...

即使上面的排序不对,我们可以重新排序来解决。例如有些 Haida 部落的字母使用一个 x 的变音的字母来表达喉音,这个字母不在 Latin-1 里面,为 Haida 造字的人用了一个特殊的字来代替 Latin-1 里面的 t 来表达这个 x 的变音。若要让这个字母排在 x 后面,我们需要重新写上面的排序表:

     ...
     t T
     u U ù ù ú ú ? ? ü ü
     v V
     w W
     x X
        t T
     y Y y Y ?
     z Z

一旦我们写好了大段的 use Sort::ArbBiLex (...); 语句,就可以把我们的 "sort keys" 里面的 "sort" 换成 "custom_sort" ,如下所示:

  foreach my $headword (custom_sort keys %headword2entries) {
    foreach my $entry ( @{ $headword2entries{$headword} } ) {
      ...code to print the entry...
    }
  }

有了这个改进,我们的记录就排序正常了:

[编辑] 反序索引

完成了一个字典以后每个人都需要的是反过来做一个逆向字典,这样在语义学上面是完整的。你编成一个 Haida-to-English 字典以后自然会有人说 “老兄,最好还能有个 English-to-Haida 字典!”

在以前人们开始用数据库来编辑字典之前,“翻转字典”的工作是人工的。现在我们可以在数据库的基础上来把 "gu" = "there" 的主记录翻转过来变成 "there" = "gu" 。

翻转过程只是要用到 %english2native 这个哈希表和下面的记录:

  $english2native{'there'} = "gu";

但是可能还有很多单词也表达 "there" 这个意思,例如 "gyaasdáan" ,所以我们要用数组来表达,就和我们在处理 %headword2entries 的时候一样:

   $english2native{'there'} = [ "gu", "gyaasdáan" ];

我们可以在原先的检查输入词义的代码里面加一行来顺便完成对 @{$english2native{each_english_bit}} 加入数据的工作:
<pre> 
  foreach my $entry ($lex->entries) {
    my %e = $entry->as_list;
    push @{ $headword2entries{ $e{'hw'} } },  \%e;
    foreach my $engl ( reversables( $e{'engl'} ) ) {
      push @{ $english2native{ $engl } }, $e{'hw'}
    }
  }

然后我们可以在主字典后面接着输出 %english2native 的内容:

 
  $rtf->paragraph( "\n\nEnglish to Haida Index\n" );

  foreach my $engl ( custom_sort keys %english2native) {
    my $n = join "; ", custom_sort @{ $english2native{ $engl } };
    $rtf->paragraph( "$engl: $n" );
  }

我们所缺的就是一个函数 reversables() ,要能输入 "already; always; often" 字符串,输出 ("already," "always," "often") 列表,输入 "the shelter of a tree“ ,输出一个单项列表 ("shelter of a tree") 。若我们把单词 "the" 也包含进去,就会有一个非常巨大的记录列在 "the" 的后面。

这是第一次尝试的结果:

 
  sub reversables {
    my $in = shift || return();
    my @english;
    foreach my $term ( split /\s*;\s*/, $in ) {
      $term =~ s/^(a|an|the|to)\s+//i;
       # Take the "to" off of "to swim away [of fish]",
       # and the "the" off of "the shelter of a tree"
      push @english, $term;
    }
    return @english;
  }

但是,考虑到这个单词 anáa: "inside a house; at home" ,我们的 reversables() 函数会返回这样的一个列表 ("inside a house", "at home") 。这看上去不错,但是如果仔细的考察英语字典,就会发现最好是能把他们放在 "home" 和 "house" 下面比较合适。

现在我们可以采取 4 种办法来优化翻转过程:

  1. 不管这个,把所有的工作留给最后的草稿人工校对步奏。
    这个方法不好,因为按照我的经验,在词义输入阶段对于翻转算法的马虎会导致最终的版本质量很难提高。
  2. 不要用自动翻转,而是为每个记录设计一个必须的字段来标明需要挂靠到的英文单词。
    例如,若我们把这个字段命名为 "ehw" (英文抬头词),那么 "at home; inside a house" 就得加长成为"\ehw home, at; house, inside a" 。但是这个必选字段对类似 "gu," 这样的简单记录来说很麻烦,你可能是倾向于这么写:
     
    
    \hw gu
    \engl there
    \ehw there
  3. 让 "ehw" 成为一个可选字段,若此字段不存在时候,就使用智能翻转方法。
    这样对于 "\hw gu \engl there" 类型的记录而言,翻转算法自然知道如何归类,好像那里有个不可见的 "\ehw there." 一样。而且还进一步会聪明到把 "wave a piece of cloth" 归类到 "wave" 和 "cloth" 而不是 "a," 的下面或者 "of." 的下面。这个非常聪明的算法需要用户能够完全了解到什么时候用自动的计算,什么时候应该用 "\ehw" 来覆盖自动计算。问题在于有时候人们会忘记这其中的技巧,要么总是在记录里加上 "\ehw" ,要么完全不用 "\ehw" ,更惨的是有时候会时不时的在两种情况之间摇摆。这就是为什么聪明的主意有时候会失败,这驱使我们去找到最后一个方法:
  4. 使用可选的 "ehw" 字段,若没有这个字段时候,使用一个傻瓜型的翻转算法。
    说到“傻瓜”,我的意思是两个规则的最佳折衷,若更加复杂的话,人们就会忘记算法的含义或者不会记得使用一个显式的 "\ehw" 字段。
    所以我们虽然可以在 reversables() 算法里面加入更多的内容,但是我们应该聪明的抵制这种趋势。我们要满足于目前的 s/^(a|an|the|to)\s+//i 的规则,和一个额外的对 "\ehw" 的支持。下面我们简单的修改对reversables() 的调用:
 
foreach my $engl ( reversables( $e{'engl'} ) ) {
push @{ $english2native{ $engl } }, $e{'hw'}
}
改成这样
  
    my @reversed = $e{'ehw'} ? split( m/\s*;\s*/, $e{'ehw'} )
                             : reversables( $e{'engl'} );
    foreach my $engl ( @reversed ) {
      push @{ $english2native{ $engl } }, $e{'hw'}
    }

有了这些以后, "\ehw home, at; house, inside a" 这个 "anáa" 记录就可以刚好通过测试。我们的程序也就可以正确的运行,完成对 Englist - Haida 字典的英文归类。

Image:Reversed index.gif

[编辑] 可选项和例句

还有两个可选项我们还没有用到:引用字段(如 "\cit hsd; led-149; led-411" )和例句字段(如 "\ex 'Láa hal súut hlgitl'gán. | She said harsh words to her." )。引用字段一般来说只对编辑有用,这会记录在哪段文字里面使用了这个单词。而且一般来说也只有编辑才能熟练使用这个缩写,例如明白 "led-149" 是 Haida 语 Leer-Edenso 字典的149页的意思。

完整的来说,这个程序应该分别为编辑和用户(不包括引用)输出。这可以通过在程序开始的时候设定一个 $For_Editor 变量来实现。

 my $For_Editors = 0; # set to 1 to add citations

之后的程序里我们有:

 
  foreach my $headword ( custom_sort keys %headword2entries) {
    foreach my $entry ( @{ $headword2entries{$headword} } ) {
      print_entry( $entry );
    }
  }

  sub print_entry {
    my %e = %{$_[0]};
    $rtf->paragraph(
      [ \'\b',    $e{'hw'}  || "?hw?", ": " ],
      [ \'\b\i',  $e{'pos'} || "?pos?" ],
      " ", $e{'engl'} || "?english?", ".", 
      $For_Editors && $e{'cit'} ? " [$e{'cit'}]" : (),
    );
  }

这里充满了符合的 $For_Editors && $e{'cit'} 行实际上是在说 “如果这是编辑的输出,并且有引用,先打印一个空格和方括号,然后在输出结尾加一个方括号。否则不用做任何事情”。

("\ex 'Láa hal súut hlgitl'gán. | She said harsh words to her".) 可能是个很普通的字典例子。如果例句不存在的话,我们当然并不想去尝试把 $e{'ex'} 也格式化了。所以我们可以使用类似上面的程序 $value ? "...$value..." : () 来判断。但我们必须先把 "|" 删掉。程序是这样的:
    my($ex, $ex_eng);
    ($ex, $ex_eng) = split m/\|/, $e{'ex'} if $e{'ex'};
    $rtf->paragraph(
      ...
      $ex_eng ? (" $ex = $ex_eng") : (),
    );

使用以上的程序,例子句子的结果就变成:

Image:Suut.gif

[编辑] 美化格式

基本功能都齐全了,我们可以尝试美化一下格式了。简单的黑体,斜体已经存在了,接下来的就是使用不同的字体。我们可以在 headword 上使用 Bookman 字体,其他的内容使用 Times -- 除了例句,我们可以对 Haida 使用 Bookman,英文使用 Arial。

大致的看了一下 RTF 口袋小册子,里面没有如何改变字体的功能 -- 只可以给字体定义一个号码 -- 即,文档的第二个字体。其实我们也可以利用这个声明来给 $rtf->prolog() 方法加上 'fonts' = [ ...font names...], 的变量。RTF::Writer 文档里提到:在你使用一个字体号码时请确保你已经给这个字体定义此号码了。我们的 prolog 那行程序就变为:

  $rtf->prolog( 'fonts' => [ "Times New Roman", "Bookman", "Arial" ] );

然后我们就可以使用 \f0 来定义使用 Times New Roman 的字体(这也是缺省的字体 )\f1 使用 Bookman, \f2 使用 Arial 字体。

假如我们想让所有字体大小都是 10,除了 Arial 字体我们为了让他不会被人注意想定义为 9 号。sans-serif 的字体一般是这么使用的。解决的方法是使用 \fs20 和 \fs18 -- "fs" 是 字体大小的意思,紧跟着的数字是字体实际大小。

使用以上的更改,我们的 print_entry 函数成为:

  sub print_entry {
    my %e = %{$_[0]};
    my($ex, $ex_eng);
    ($ex, $ex_eng) = split m/\|/, $e{'ex'} if $e{'ex'};
    $rtf->paragraph(  \'\fs20',  # Start out in ten-point
      [ \'\f1\b', $e{'hw'}  || "?hw?", ": " ],
      [ \'\b\i',  $e{'pos'} || "?pos?" ],
      " ", $e{'engl'} || "?english?", ".", 
      $For_Editors && $e{'cit'} ? " [$e{'cit'}]" : (),
      $ex_eng ? (" ", \'\f1', $ex, \'\f2\fs18', $ex_eng) : (),
    );
  }


虽然看上去密密麻麻都是代码,但是确实值得写这么多的东西。结果看起来像这个样子:

Image:All pretty2.gif

如果要更进一步加入漂亮的排版,最好仔细看看 RTF Pocket Guide 里面每个技巧。例如有时候我们可能会对 (\fi-300) 代表的段首对齐感兴趣,或者 (\col2) 代表的双列排版,还有 ({\header \pard\ql\plain p.\chpgn \par}) 代表的页数。

现在为了节省打印成本,你得在字的大小和打印份数之间有个平衡点。有个办法能够帮你把尽可能多的东西挤到很小的地方,这就是对最常用的文字使用缩写(或者叫做标签)。这样我们可以把 "noun" 缩写成 "n." , "verb" 写成 "v." 等等。每次只节省很小的空间,但是总体的效果非常惊人。而且这么做也非常直观,你至少尝试一下看看结果。我们只要把 print_entry() 里面的一行,从这样:

[ \'\b\i',  $e{'pos'} || "?pos?" ],

改为:

[ \'\b\i',  $Abbrev{$e{'pos'}||''} || $e{'pos'} || "?pos?" ],<、pre>

当然我们还要提前把缩写定义:
<pre>
  my %Abbrev = (
   'auxiliary verb' => 'aux.',
   qw(noun n. verb v. adverb adv.),
  );

这就足够了,下面就是产生的效果:

Image:All pretty3.gif

目前还是有可能打印出 "?pos?" ,这表明这个单词还没有读音字段,我们不能把它进一步缩写成 "pp." ,因为这有可能使人们认为是不及物动词。但是最常用的单词缩写已经节省了足够的空间。

[编辑] 其他格式

我们一直专注于做一本传统的字典,但是同样的数据库和 Perl 脚本,换个排版的代码(新的页面布局和一个双面打印机/复印机)就可以做成一盒速记卡片。或者如果你在数据库里面加上话题(如 "plant", "color", "body part", "food" )字段,就很容易按照话题来对记录排序,也就可以做成一本话题字典。教语言的老师会发现这个字典在为课堂练习做准备的时候非常有用。

[编辑] 余下的时间

好像 A. N. Whitehead 那句名言所说的:“文明通过增加那些我们每天做但不去思考的行为前进。思考就好象是站场上的骑兵,数量是严格限制的,优良的战马是必须的,总在决定性的时候才排上用场”。我发现这句话在对待濒危语种的时候非常管用,用传统的办法写任何一本字典都需要很多“人年”来完成,这包括语言学家和社会上的人。对于绝大多数北美的本地语言来说,最熟悉的人的年纪都在65岁以上,没有充足的资源留给我们了。

Whitehead 虽然不知道但他的名言却很适用:节省时间和精力不但能够推动文化进步,也保证了文化的延续。

这样 Perl 帮助我们把数据库、打印机、字处理器连接起来,编写一部字典不再需要数月时间,而是几分钟。这节省了语言学家和长者宝贵的时间。使他们把决定性的时间花在语言的教学上面。我们需要节约每分钟来挽救这些作为文化根基的语言。包括他们的诗歌,箴言,笑话,礼节,农业和种植术语。所有这些并不能通过翻译成英文完全幸存下来。

情况很迫切,所以我们很欣赏 Perl 。

  use strict;
  use warnings;

  my $For_Editors = 0; # set to 1 to add citations

  use RTF::Writer;
  use Text::Shoebox::Lexicon;
  my $lex = Text::Shoebox::Lexicon->read_file( "haida.sf" );

  my $rtf = RTF::Writer->new_to_file( "lex.rtf" );
  $rtf->prolog( 'fonts' => [ "Times New Roman", "Bookman", "Arial" ] );

  use Sort::ArbBiLex (
    'custom_sort' =>
    "
     a A à à á á a ? ? ? ? ? ? ? ? ?
     b B
     c C ? ?
     d D e D
     e E è è é é ê ê ? ?
     f F
     g G
     h H
     i I ì ì í í ? ? ? ?
     j J
     k K
     l L
     m M
     n N ? ?
     o O ò ò ó ó ? ? ? ? ? ? ? ?
     p P
     q Q
     r R
     s S ?
     t T t T
     u U ù ù ú ú ? ? ü ü
     v V
     w W
     x X
     y Y y Y ?
     z Z
    "
  );
  my %headword2entries;
  my %english2native;

  my %Abbrev = (
   'auxiliary verb' => 'aux.',
   qw(noun n. verb v. adverb adv.),
  );

  foreach my $entry ($lex->entries) {
    my(%e) = $entry->as_list;
    push @{ $headword2entries{ $e{'hw'} } },  \%e;
    my @reversed = $e{'ehw'} ? split( m/\s*;\s*/, $e{'ehw'} )
                             : reversables( $e{'engl'} );
    foreach my $engl ( @reversed ) {
      push @{ $english2native{ $engl } }, $e{'hw'}
    }
  }

  $rtf->paragraph( "Haida to English Dictionary\n\n" );

  foreach my $headword ( custom_sort keys %headword2entries) {
    foreach my $entry ( @{ $headword2entries{$headword} } ) {
      print_entry( $entry );
    }
  }

  $rtf->paragraph( "\n\nEnglish to Haida Index\n" );

  foreach my $engl ( custom_sort keys %english2native) {
    my $native = join "; ", custom_sort @{ $english2native{ $engl } };
    $rtf->paragraph( "$engl: $native" );
  }

  $rtf->close;
  exit;

  sub reversables {
    my $in = shift || return;
    my @english;
    foreach my $term ( grep $_, split /\s*;\s*/, $in ) {
      $term =~ s/^(a|an|the|to)\s+//;
      push @english, $term;
    }
    return @english;
  }

  sub print_entry {
    my %e = %{$_[0]};
    my($ex, $ex_eng);
    ($ex, $ex_eng) = split m/\|/, $e{'ex'} if $e{'ex'};
    $rtf->paragraph(  \'\fs20',  # Start out in ten-point
      [ \'\f1\b', $e{'hw'}  || "?hw?", ": " ],
      [ \'\b\i',  $Abbrev{$e{'pos'}||''} || $e{'pos'} || "?pos?" ],
      " ", $e{'engl'} || "?english?", ".", 
      $For_Editors && $e{'cit'} ? " [$e{'cit'}]" : (),
      $ex_eng ? (" ", \'\f1', $ex, \'\f2\fs18', $ex_eng) : (),
    );
  }

Perl.com Compilation Copyright ? 1998-2004O'Reilly Media, Inc.

个人工具