如何在Ruby中替换带有重音的拉丁字符?

80

我有一个ActiveRecord模型,名为Foo,其中有一个name字段。 我希望用户能够按名称搜索,但我希望搜索时忽略大小写和任何重音符号。 因此,我还保存了一个canonical_name字段,以便进行搜索:

class Foo
  validates_presence_of :name

  before_validate :set_canonical_name

  private

  def set_canonical_name
    self.canonical_name ||= canonicalize(self.name) if self.name
  end

  def canonicalize(x)
    x.downcase.  # something here
  end
end

我需要填写“something here”来替换重音字符。有比这更好的方法吗?

x.downcase.gsub(/[àáâãäå]/,'a').gsub(/æ/,'ae').gsub(/ç/, 'c').gsub(/[èéêë]/,'e')....

另外,由于我没有使用 Ruby 1.9 版本,因此无法在代码中使用这些 Unicode 文字。实际的正则表达式看起来会更加丑陋。


2
即使在1.8版本中,你仍然可以使用“ruby -Ku”。 - Keltia
1
这个问题早已经被解决了,下面有许多很好的评论。现在重新阅读,我想澄清一件事:我的想法是创建一个只用ASCII字符就可以搜索的文本版本,而不是实际上强制转换数据。请注意有两个数据库属性:namecanonical_name。我并不赞成破坏实际数据,仅仅是创造一种在没有变音符号的情况下搜索所有语言用户常用的方式。 - James A. Rosen
1
实际上,这些都是错误的答案。你需要使用Unicode排序算法,并将比较强度设置为仅级别1。其他一切都会出问题。 - tchrist
8
所以你加入了这个讨论只是为了说“那些人是错的”,但除了最基本的回答之外,你什么也没有提供?请真正回答问题,这样我才能给你投反对票,因为你很讨厌。 - jcollum
@tchrist,“错”这个词可能取决于个人的需求。确实,做错事可能会回来困扰那些不知道后果的人(因此他们没有添加更好的要求)。但在被告知后果之前,他们不会注意到建议。 - Kelvin
@JamesA.Rosen,我相信您并没有将瑞典语/丹麦语转换为无意义的东西的想法。我会感到恼怒,因为一些“瑞典”程序员现在仍然以这篇文章作为实现/使搜索(对程序员来说更容易)的方式的参考,这已经过去了近5年。 - Jonke
15个回答

101

ActiveSupport::Inflector.transliterate(需要Rails 2.2.1+和Ruby 1.9或1.8.7)

示例:

>> ActiveSupport::Inflector.transliterate("àáâãäå").to_s => "aaaaaa"


在非 Rails 项目中,您必须 require 'active_support/inflector'(请参见下面的我的回答)。 - Dorian
1
你是真正的最有价值玩家。 - cesartalves

63

Rails已经内置了字符串规范化的功能,你只需要使用它将字符串规范化为KD形式,然后移除其他字符(例如重音标记)就可以了:

>> "àáâãäå".mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'').downcase.to_s
=> "aaaaaa"

4
我正试图在一个不属于 Rails 应用程序的另一个脚本中使用它。我以为它会在 activesupport 中,但是在要求后,我仍然得到了 normalizeNoMethodError。你知道我需要要求什么吗? - agentofuser
4
这段代码位于activesupport中,但您需要按照以下方式执行: ActiveSupport :: Multibyte :: Chars.new(“àáâãäå”)。mb_chars.normalize(:kd)。gsub(/ [^ \ x00- \ x7F] / n,' ')。downcase.to_s - unexist
7
这个方法很有效,但我必须像Christian一样使用 mb_charsfoo.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'').to_s.split - Sam Soffes
1
另一个提示:如果你遇到了"NoMethodError: undefined method `normalize'",你可能还需要显式地设置$KCODE = 'u'来强制字符串的默认编码为Unicode。 - lambshaanxy
51
至少在Rails3中,String#parameterize有效...因此,"öüâ".parameterize == "oua"。 - foz
显示剩余8条评论

44

更好的方法是使用I18n:

1.9.3-p392 :001 > require "i18n"
 => false
1.9.3-p392 :002 > I18n.transliterate("Olá Mundo!")
 => "Ola Mundo!"

1
在普通的 Ruby(非 Rails)中,我遇到了 LoadError: cannot load such file -- i18n rails library 的问题。顺便提一下,Rails 中的 ActiveSupport::Inflector.transliterate 方法实际上会在进行规范化以确保能够移除所有变音符号后,在底层调用 I18n 方法。 - rogerdpack
1
对于“无法加载文件 - i18n”,只需运行“sudo gem install i18n”即可。 - Camille Goudeseune
2
我遇到了一个错误,类似于“:en不是有效的区域设置(I18n :: InvalidLocale)”,直到我添加了“I18n.available_locales = [:en]”。这也将非拉丁字母的带音符号的非 ASCII 字符替换为带问号的字符,因此例如 ruby -ri18n -ne'I18n.available_locales=[:en];puts I18n.transliterate$_'<<<Дあ☆ 输出 ??? - nisetama

20

我尝试了很多方法,但它们没有达到以下一个或多个要求:

  • 保留空格
  • 保留 'ñ' 字符
  • 保留大小写(我知道这不是原问题的要求,但将字符串转换为小写并不难)

现在是这样的:

# coding: utf-8
string.tr(
  "ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž",
  "AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz"
)

- http://blog.slashpoundbang.com/post/12938588984/remove-all-accents-and-diacritics-from-string-in-ruby

您需要稍微修改一下字符列表以保留 'ñ' 字符,但这是一个简单的工作。


你能详细说明一下需要修改字符列表以尊重字符 ñ 的含义吗?在我看来,它已经在列表中并与 n 对齐了。 - user664833
我明白了。你能说一下为什么这个字符很特别吗?(我的意思是,为什么它被特别尊重?) - user664833
可能有很多原因,最常见的是它不是ASCII字符。 - fguillen
1
抱歉,我仍然不明白为什么您在要求的项目列表中单独挑选出ññ是“带波浪符的拉丁小写字母n”,它与您列表中的许多其他字符一样属于扩展ASCII集 - 请参见http://www.ascii-code.com/ - 而您列表中有许多字符在扩展ASCII集中,包括ĄĦ。因此,我仍然困惑为什么您挑选出了ñ - user664833
1
ActiveSupport::Inflector.transliterate 似乎满足您的要求,除了“保留ñ”之外,这种方式是纯 Ruby 的,非常好。不幸的是,使用 Unicode 您可以在基本上任何前面的字符上添加变音符号,所以这种方法将很难全面地满足所有情况 :| - rogerdpack
显示剩余3条评论

13

我的回答:使用String#parameterize方法:

"Le cœur de la crémiére".parameterize
=> "le-coeur-de-la-cremiere"

对于非Rails程序:

安装activesupport:gem install activesupport 然后:

require 'active_support/inflector'

"a&]'s--3\014\xC2àáâã3D".parameterize
# => "a-s-3-3d"

使用.parameterize与ActiveSupport :: Inflector.transliterate之间存在巨大差异。输入:“不要陷入这个编程陷阱”.parameterize输出:“don-t-fall-into-this-programming-trap”ActiveSupport :: Inflector.transliterate输出:“?不要陷入这个编程陷阱”这是一个非常大的差异。 - fuzzygroup
@fuzzygroup 使用Markdown的代码格式(例如\method`)有助于评论部分的阅读。回答你的问题,"Le cœur de la crémiére".parameterize`是用于URL的最佳UTF-8到ASCII转换,它非常好用和方便。 - Dorian

8

将字符串分解并从中删除非间距标记

irb -ractive_support/all
> "àáâãäå".mb_chars.normalize(:kd).gsub(/\p{Mn}/, '')
aaaaaa

如果在.rb文件中使用,您可能也需要这个。

# coding: utf-8

这里的normalize(:kd)部分会尽可能地拆分出变音符号(例如:"带波浪线的n"单个字符被拆分为一个n,后面跟着一个组合变音符号波浪线),而gsub部分则会删除所有变音符号。

请参考此处unexist的答案,其本质上使用了与1.8.x兼容的正则表达式来实现此功能。 - rogerdpack
3
这个应该更高一些。其他解决方案完全剥夺了其他字符集(例如:I18n.transliterate('日本語') #=> "???"),并且 '日本語'.parameterize #=> ""。这个答案最符合我的需求,即能够在标题/作者上近似匹配各种数据集。'日本語 àáâãäå'.unicode_normalize(:nfkd).gsub(/\p{Mn}/, '') #=> "日本語 aaaaaa" - Bo Jeanes

7
我认为你可能不太想走这条路。如果你正在为具有这些字母的市场开发,你的用户可能会认为你是一种...小人物。因为对于用户来说,“å”与“a”根本没有任何意义上的相似之处。走另外一条路,了解一下以非ASCII方式进行搜索的相关知识。这只是有人发明Unicode和排序规则的其中一种情况。
一个非常晚的附言:

http://www.w3.org/International/wiki/Case_folding http://www.w3.org/TR/charmod-norm/#sec-WhyNormalization

除此之外,我不知道为什么连接到排序的链接会跳转到 MSDN 页面,但我将其保留在那里。它应该是 http://www.unicode.org/reports/tr10/

在斯洛伐克语中,例如á、ä非常接近a。所有带重音的字符都与没有重音的字符非常相似。许多人在即时通讯等场合根本不使用它们。 - Vojto
@Vojto:在大多数北欧语言中,带重音的字符与不带重音的版本相差甚远。实际上,它们代表着非常不同的声音符号。例如德语单词öl(http://en.bab.la/dictionary/german-english/oel)或瑞典语单词ål(鳗鱼)和al(一棵树)。 - Jonke
很酷,我只是想指出,并不是所有欧洲语言都是这样的。我提到了斯洛伐克语,但捷克语、波兰语、克罗地亚语和几乎所有斯拉夫语言也是如此。而且搜索引擎等支持无重音字符搜索非常重要——因为在大多数情况下,人们只是懒得打重音符号。 - Vojto
你需要使用基于UCA的比较方式,仅在1级别上,并且如果你想让比较结果符合该地区人们的期望(如果与标准排序不同),则需要使用UCA定制工具来适应当前环境。 - tchrist
@tchrist 我(天真地)认为那正是我建议的。 - Jonke
显示剩余2条评论

4

假设您使用Rails。

"anything".parameterize.underscore.humanize.downcase

根据您的要求,这可能是我会做的... 我认为它很整洁、简单,并且将在未来版本的Rails和Ruby中保持最新。
更新:dgilperez指出parameterize有一个分隔符参数,因此"anything".parameterize(" ")(已弃用)或"anything".parameterize(separator: " ") 更短更干净。

3
“anything.parameterize(" ")”会不会更短? - dgilperez
哦,谢谢,我不知道 parameterize 接受参数。 - Sudhir Jonathan
.parameterize(" ")虽然更短,但它会将所有字符转换为小写。我发现无法在表达式中添加preserve_case参数。I18n.transliterate是最高效和最有效的选择。 - Jerome

3
关键是在您的数据库中使用两列:canonical_textoriginal_text。使用original_text进行显示,使用canonical_text进行搜索。这样,如果用户搜索“Visual Cafe”,她将看到“Visual Café”结果。如果她真的想要另一个名为“Visual Cafe”的项目,则可以单独保存。
要在Ruby 1.8源文件中获取canonical_text字符,可以执行以下操作:
register_replacement([0x008A].pack('U'), 'S')

也许这只是一个小问题,但是“canonical_text”这个名称会让我有点困惑,因为我们正在进行的是有损处理。我更希望使用类似于“compatible_text”或“decomposed_text”的名称(尽管我也可以看到对这些名称同样存在争议)。也许只需要使用“search_text”? - Christian - Reinstate Monica C
这里的register_replacement是什么? - rogerdpack

3

相关答案:https://dev59.com/rXVC5IYBdhLWcg3wdw65 - CesarB
所有这些涉及规范化形式的答案都是错误的。你需要进行UCA一级比较,可能需要进行区域设置定制。 - tchrist
请参见这里的程先生的答案。 - rogerdpack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接