使用Perl和Vim正确选择词典

最近我读到了James Summers的优秀文章《你很可能使用了错误的词典》,这启发我开始使用Webster的1913年版。按照文章中的说明,我能够将词典搜索集成到我的浏览器中,但我大部分时间都在终端工作,因此想要一个命令行解决方案。

我从archive.org获取了词典的文本版本,并开始编写一个Perl脚本来搜索它。

1913年文本版的每一项都以一行开头的首字母大写的术语开始,后面跟一个换行符,然后是条目的详细信息。Webster对“llama”的定义是典型的

LLAMA
Lla"ma, n. Etym: [Peruv.] (Zoöl.)

Defn: A South American ruminant (Auchenia llama), allied to the
camels, but much smaller and without a hump. It is supposed to be a
domesticated variety of the guanaco. It was formerly much used as a
beast of burden in the Andes.

在这种情况下,它告诉我们“llama”是名词,起源于秘鲁。“Zoöl.”缩写表示它是一个动物学术语。Wiktionary有一个方便的缩写列表

一个单术语可以包含大写字母、数字、空格、破折号和单引号。对于同一术语的不同拼写,每种拼写都出现在同一行上,由分号和空格分隔,如下所示

WOLVERENE; WOLVERINE

为了在词典中找到匹配的条目,我想要搜索匹配的术语,打印其内容,并在到达下一个术语时停止打印

#!/usr/bin/perl
my $search_term = uc join ' ', @ARGV;
my $entry_pattern = qr/^[A-Z][A-Z0-9' ;-]*$/;
my $search_pattern = qr/^$search_term/;

open my $dict, '<:encoding(latin1)', 'webster-1913.txt' or die $!;

while (<$dict>) {
  next unless /$entry_pattern/ && /$search_term/;
  my $output = $_;
  while (1) {
   my $next_line = readline $dict;
   if ($next_line =~ /$entry_pattern/) {
     seek $dict, -length($next_line), 1;
     last;
   }
   $output .= $next_line;
  }
  print $output;
}

这个脚本从其命令行参数中读取一个搜索术语,将其转换为大写。然后它打开编码为Latin 1的词典,并扫描与模式:qr/^[A-Z][A-Z0-9' ;-]*$/匹配的行,这个模式试图只匹配标记条目开头的行(“WOLVERENE; WOLVERINE”)。然后它使用readline来提取词典定义,直到找到下一个条目,此时它将文件句柄指针回退一行,并打印匹配的文本。

Latin 1的一个不错的特性是每个字符都是单字节,这意味着我不必担心seek在字符上中断,因为length是按字符计算的,但seek使用字节。

运行脚本的方法如下

$ ./webster-search.pl tower

在我的笔记本电脑上,运行脚本大约需要一秒钟,考虑到词典大小为27MB,这个速度并不慢。

一个明显的改进是当脚本找到比搜索术语字母顺序更高的条目时退出。在“LLAMA”之后是“LLANDEILO GROUP”,我可以用cmp来比较。如果搜索术语在比较术语之前排序,cmp将返回1,如果它们匹配则返回0,否则返回-1

"LLAMA" cmp "LLANDEILO GROUP"; # -1

Webster的1913年版作为数据源的一个有趣特性是它永远不会改变,因此我可以利用这一点为每个字母的起始点构建一个静态索引。每个字母的部分以单独一行的大写字母开始。

#!/usr/bin/perl
open my $dict, '<:encoding(latin1)', 'webster-1913.txt' or die $!;

my @alphabet = 'A'..'Z';

while (<$dict>) {
  next unless /^$alphabet[0]$/;
  printf "%s => %d\n", shift @alphabet, tell $dict;
  last unless @alphabet;
}

当这个脚本遇到新的字母部分时,它会调用tell在文件句柄上以确定字节的定位,然后将其详细信息打印到标准输出。

$ ./build-index.pl
A => 601
B => 1796502
C => 3293436
D => 6039049
E => 7681559
...

奇怪的是,第一次运行时索引数据停止在“S”。这是因为archive.org上Webster的1913年词典的“T”条目缺失!我找到了在线的条目,并将其添加到我的副本中。

通过将此索引数据纳入我的脚本,我将跳转到搜索术语第一个字母的部分,并从那里开始搜索。

#!/usr/bin/perl
my $search_term = uc join ' ', @ARGV;
my $entry_pattern = qr/^[A-Z][A-Z0-9' ;-]*$/;
my $search_pattern = qr/^$search_term/;

my %index = (
  A => 601,
  B => 1796502,
  C => 3293436,
  D => 6039049,
  E => 7681559,
  F => 8833301,
  G => 10034091,
  H => 10926753,
  I => 11930292,
  J => 13148994,
  K => 13380269,
  L => 13586035,
  M => 14532408,
  N => 15916448,
  O => 16385339,
  P => 17042770,
  Q => 19439223,
  R => 19610041,
  S => 21015876,
  T => 24379537,
  U => 25941093,
  V => 26405366,
  W => 26925697,
  X => 27748359,
  Y => 27774096,
  Z => 27866401,
);

my $start = $index{ substr $search_term, 0, 1 };
open my $dict, '<:encoding(latin1)', 'webster-1913.txt' or die $!;
seek $dict, $start, 0;

my $found_match = undef;
while (<$dict>) {
  next unless $_ =~ $entry_pattern;

  if ($_ =~ $search_term) {
    my $output = $_;
    while (1) {
     my $next_line = readline $dict;
     if ($next_line =~ /$entry_pattern/) {
       seek $dict, -length($next_line), 1;
       last;
     }
     $output .= $next_line;
    }
    print $output;
    $found_match = 1;
  }
  last if $found_match && ($search_term cmp $_) == -1;
}

搜索“tower”时,该脚本在70毫秒内完成,比初始脚本提高了14倍。仅用2个简单的优化就不错了。我可以花时间用更具体的索引或优化的正则表达式进一步调整,但现在已经足够快了。

从Vim中进行搜索

使用vimscript插件将Perl脚本集成到Vim中相当简单。

" webster-search.vim
let s:parent_dir = expand('<sfile>:p:h')

function! WebsterSearch(term)
  let l:perl_script = 'webster-search.pl'
  let l:command =  s:parent_dir . '/' . l:perl_script . ' ' . a:term
  execute "let output = system('" . l:command . "')"
  vnew
  setlocal nobuflisted buftype=nofile bufhidden=wipe noswapfile
  call setline(1, split(output, "\n"))
endfunction
command! -nargs=1 WebsterSearch call WebsterSearch(<args>)

第一行获取插件文件的父目录,以避免硬编码Perl脚本的路径。接下来,它添加一个名为“WebsterSearch”的函数,该函数调用Perl脚本并使用搜索词打印输出到一个新的垂直窗口。最后一行调用command函数注册用户定义的函数,避免使用call调用。

要使用此插件,我在我的.vimrc中映射了一个快捷键。

nnoremap <leader>d :WebsterSearch(expand('<cWORD>'))<cr>

现在,当我的光标在想要查询单词的字典上时,我按“\d”,就可以在我的终端中直接看到Webster的条目!cWORD的一个缺点是它只会匹配光标下的第一个单词,但有些字典条目包含空格(“ad hominem”)。对于这些罕见的情况,我可以在视觉模式下突出显示单词,然后执行字典搜索。

vnoremap <leader>d :<c-u>WebsterSearch(@*)<cr>

这将在Vim处于视觉模式时映射相同的快捷键;<c-u>会自动清除Vim输入的范围,然后调用函数,并将寄存器变量@*(最后突出的文本)作为搜索词传递。

我已经将此代码上传到GitHub,并附上了Vim安装说明。

搜索原始字典文本的另一种方法是使用GCIDE(感谢frew),它基于Webster的1913年字典,并且有可机器读取的标记,便于解析。

标签

David Farrell

David是一位职业程序员,他经常在推特博客上关于代码和编程艺术发表评论。

浏览他们的文章

反馈

这篇文章有什么问题吗?请帮助我们通过在GitHub上打开问题或拉取请求。