【发布时间】:2011-07-20 16:41:51
【问题描述】:
我正在尝试使用list of hundreds of common misspellings 在搜索重复项之前清理一些输入。
这是一个时间紧迫的过程,所以我希望有一种比拥有数百个正则表达式(或一个具有一百个分支)更快的方法。
有没有一种在 Ruby 中执行数百个文本替换的有效方法?
【问题讨论】:
-
仅供参考,我运行了一些代码,它们的列表中有 4980 个单词。 :-)
我正在尝试使用list of hundreds of common misspellings 在搜索重复项之前清理一些输入。
这是一个时间紧迫的过程,所以我希望有一种比拥有数百个正则表达式(或一个具有一百个分支)更快的方法。
有没有一种在 Ruby 中执行数百个文本替换的有效方法?
【问题讨论】:
如果您的输入数据是单独的单词,另一种方法是构建{error => correction} 的哈希表。
哈希表查找是快速,所以如果你可以将你的输入数据转换成这种格式,它几乎肯定会足够快。
【讨论】:
我很高兴地说我刚刚找到了“RegexpTrie”,它是代码的可用替代品,并且需要 Perl 的 Regexp::Assemble。
安装它,试一试:
require 'regexp_trie'
foo = %w(miss misses missouri mississippi)
RegexpTrie.union(foo)
# => /miss(?:(?:es|ouri|issippi))?/
RegexpTrie.union(foo, option: Regexp::IGNORECASE)
# => /miss(?:(?:es|ouri|issippi))?/i
这是输出的比较。数组中的第一个注释输出来自 Regexp::Assemble,尾随输出来自 RegexpTrie:
require 'regexp_trie'
[
'how now brown cow', # /(?:[chn]ow|brown)/
'the rain in spain stays mainly on the plain', # /(?:(?:(?:(?:pl|r)a)?i|o)n|s(?:pain|tays)|mainly|the)/
'jackdaws love my giant sphinx of quartz', # /(?:jackdaws|quartz|sphinx|giant|love|my|of)/
'fu foo bar foobar', # /(?:f(?:oo(?:bar)?|u)|bar)/
'ms miss misses missouri mississippi' # /m(?:iss(?:(?:issipp|our)i|es)?|s)/
].each do |s|
puts "%-43s # /%s/" % [s, RegexpTrie.union(s.split).source]
end
# >> how now brown cow # /(?:how|now|brown|cow)/
# >> the rain in spain stays mainly on the plain # /(?:the|rain|in|s(?:pain|tays)|mainly|on|plain)/
# >> jackdaws love my giant sphinx of quartz # /(?:jackdaws|love|my|giant|sphinx|of|quartz)/
# >> fu foo bar foobar # /(?:f(?:oo(?:bar)?|u)|bar)/
# >> ms miss misses missouri mississippi # /m(?:iss(?:(?:es|ouri|issippi))?|s)/
关于如何使用维基百科链接和拼写错误的单词:
require 'nokogiri'
require 'open-uri'
require 'regexp_trie'
URL = 'https://en.wikipedia.org/wiki/Wikipedia:Lists_of_common_misspellings/For_machines'
doc = Nokogiri::HTML(open(URL))
corrections = doc.at('div#mw-content-text pre').text.lines[1..-1].map { |s|
a, b = s.chomp.split('->', 2)
[a, b.split(/,\s+/) ]
}.to_h
# {"abandonned"=>["abandoned"],
# "aberation"=>["aberration"],
# "abilityes"=>["abilities"],
# "abilties"=>["abilities"],
# "abilty"=>["ability"],
# "abondon"=>["abandon"],
# "abbout"=>["about"],
# "abotu"=>["about"],
# "abouta"=>["about a"],
# ...
# }
misspelled_words_regex = /\b(?:#{RegexpTrie.union(corrections.keys, option: Regexp::IGNORECASE).source})\b/i
# => /\b(?:(?:a(?:b(?:andonned|eration|il(?:ityes|t(?:ies|y))|o(?:ndon(?:(?:ed|ing|s))?|tu|ut(?:it|the|a)...
此时您可以使用gsub(misspelled_words_regex, corrections),但是,corrections 中的值包含一些数组,因为可以使用多个单词或短语来替换拼写错误的单词。您必须做一些事情来确定要使用哪些选项。
Ruby 缺少一个在 Perl 中发现的非常有用的模块,称为 Regexp::Assemble。 Python 有 hachoir-regex 似乎做同样的事情。
Regexp::Assemble 基于单词列表和简单表达式创建一个非常有效的正则表达式。这真的很了不起……还是……恶魔?
查看模块的示例;它的基本形式使用起来非常简单:
use Regexp::Assemble;
my $ra = Regexp::Assemble->new;
$ra->add( 'ab+c' );
$ra->add( 'ab+-' );
$ra->add( 'a\w\d+' );
$ra->add( 'a\d+' );
print $ra->re; # prints a(?:\w?\d+|b+[-c])
注意它是如何组合模式的。它对常规单词也是如此,只是它会更有效,因为常见的字符串会被组合起来:
use Regexp::Assemble;
my $lorem = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
my $ra = Regexp::Assemble->new('flags' => 'i');
$lorem =~ s/[^a-zA-Z ]+//g;
$ra->add(split(' ', lc($lorem)));
print $ra->anchor_word(1)->as_string, "\n";
哪些输出:
\b(?:a(?:dipisicing|liqua|met)|(?:consectetu|tempo)r|do(?:lor(?:emagna)?)?|e(?:(?:li)?t|iusmod)|i(?:ncididunt|psum)|l(?:abore|orem)|s(?:ed|it)|ut)\b
此代码忽略大小写并尊重单词边界。
我建议编写一个小的 Perl 应用程序,它可以获取单词列表并使用该模块输出正则表达式模式的字符串化版本。您应该能够将该模式导入 Ruby。这会让你很快找到拼写错误的单词。您甚至可以让它将模式输出到 YAML 文件,然后将该文件加载到您的 Ruby 代码中。定期解析拼写错误的单词页面,通过 Perl 代码运行输出,您的 Ruby 代码将具有更新模式。
您可以将该模式用于一大段文本,以查看是否存在拼写错误的单词。如果是这样,那么您将文本分解为句子或单词并再次检查正则表达式。不要立即对单词进行测试,因为大多数单词都会拼写正确。这几乎就像对您的文本进行二进制搜索 - 测试整个内容,如果有命中然后分成较小的块以缩小搜索范围,直到您找到单个拼写错误。如何分解块取决于传入文本的数量。正则表达式模式可以测试整个文本块并返回 nil 或索引值,除了单个单词以相同的方式,因此您在处理大块文本时获得了很大的速度。
然后,如果您知道自己有一个拼写错误的单词,您可以进行哈希查找以找到正确的拼写。这将是一个很大的哈希,但筛选出好拼写和坏拼写的任务是最耗时的。查找速度会非常快。
下面是一些示例代码:
get_words.rb
#!/usr/bin/env ruby
require 'open-uri'
require 'nokogiri'
require 'yaml'
words = {}
['0-9', *('A'..'Z').to_a].each do |l|
begin
print "Reading #{l}... "
html = open("http://en.wikipedia.org/wiki/Wikipedia:Lists_of_common_misspellings/#{l}").read
puts 'ok'
rescue Exception => e
puts "got \"#{e}\""
next
end
doc = Nokogiri::HTML(html)
doc.search('div#bodyContent > ul > li').each do |n|
n.content =~ /^(\w+) \s+ \(([^)]+)/x
words[$1] = $2
end
end
File.open('wordlist.yaml', 'w') do |wordfile|
wordfile.puts words.to_yaml
end
regex_assemble.pl
#!/usr/bin/env perl
use Regexp::Assemble;
use YAML;
use warnings;
use strict;
my $ra = Regexp::Assemble->new('flags' => 'i');
my %words = %{YAML::LoadFile('wordlist.yaml')};
$ra->add(map{ lc($_) } keys(%words));
print $ra->chomp(1)->anchor_word(1)->as_string, "\n";
运行第一个,然后运行第二个将其输出通过管道传输到文件以捕获发出的正则表达式。
还有更多单词示例和生成的输出:
'how now brown cow' => /\b(?:[chn]ow|brown)\b/
'the rain in spain stays mainly on the plain' => /\b(?:(?:(?:(?:pl|r)a)?i|o)n|s(?:pain|tays)|mainly|the)\b/
'jackdaws love my giant sphinx of quartz' => /\b(?:jackdaws|quartz|sphinx|giant|love|my|of)\b/
'fu foo bar foobar' => /\b(?:f(?:oo(?:bar)?|u)|bar)\b/
'ms miss misses missouri mississippi' => /\bm(?:iss(?:(?:issipp|our)i|es)?|s)\b/
Ruby 的Regexp.union 远不及Regexp::Assemble 的复杂程度。捕获拼错单词列表后,有4225个单词,由41817个字符组成。在针对该列表运行 Perl 的 Regexp::Assemble 后,生成了 30,954 个字符的正则表达式。我会说这很有效。
【讨论】:
换一种方式试试。与其更正拼写错误并检查结果中的重复项,不如将所有内容放到一个类似的格式(如 Metaphone 或 Soundex),然后检查该格式的重复项。
现在,我不知道哪种方式可能更快 - 一方面,您有数百个正则表达式,每个正则表达式几乎都无法立即匹配并返回。另一方面,您有 30 多个潜在的正则表达式替换,其中一个或两个将绝对匹配每个单词。
现在,metaphone 非常快 - 算法真的不多 - 所以我只能建议您尝试一下,并衡量其中一个是否足够快以供您使用。
【讨论】: