C++11正则表达式中UTF-8字符的范围

30

这个问题是关于C++11正则表达式是否适用于UTF-8字符串的扩展。

#include <regex>  
if (std::regex_match ("中", std::regex("中") ))  // "\u4e2d" also works
  std::cout << "matched\n";

该程序在Mac Mountain Lion上使用clang++编译,选项如下:

clang++ -std=c++0x -stdlib=libc++

以上的代码是有效的。这是一个用于匹配任何日语汉字或中文字符的标准范围正则表达式:"[一-龠々〆ヵヶ]"。它适用于JavaScript和Ruby,但我似乎无法在C++11中使用类似版本的 [\u4E00-\u9fa0] 进行范围匹配。以下代码不匹配字符串。

if (std::regex_match ("中", std::regex("[一-龠々〆ヵヶ]")))
  std::cout << "range matched\n";

更改语言环境也没有帮助。有什么想法吗?

编辑

所以我发现,如果在结尾添加+,所有范围都有效。在这种情况下是[一-龠々〆ヵヶ]+,但如果添加{1} [一-龠々〆ヵヶ]{1}则不起作用。此外,它似乎超出了边界。它不能匹配拉丁字符,但它会匹配\u306f)和\u3041)。它们均位于\u4E00以下。

nhahtdh还建议使用regex_search,它也可以在不添加+的情况下工作,但它仍然遇到与上述相同的问题,即将值拉出其范围。还稍微调整了语言环境。Mark Ransom表示它将UTF-8字符串视为一组哑字节,我认为这可能就是它正在做的事情。

进一步推测UTF-8有些混乱,[a-z]{1}[a-z]+匹配a,但只有[一-龠々〆ヵヶ]+匹配任何字符,而不是[一-龠々〆ヵヶ]{1}


1
你的本地语言环境是什么?如果你使用默认设置,它可能会把UTF-8字符串视为一系列愚蠢的字节,并将多字节序列拆分成片段。 - Mark Ransom
1
我怀疑可能是这样的,我尝试设置全局本地化 std::locale::global(std::locale("ja_JP.UTF-8")); 和使用相同结果的 imbue (std::locale("ja_JP.UTF-8"));,就像我在编辑部分展示的那样。还尝试了 ja_JP、ja_JP.eucJP 和 ja_JP.SJIS。 - MCH
2
std::string 是一个 字节 字符串。为什么多字节字符可以工作?如果您想使用 Unicode 字符,请使用诸如 ogonek 的库。 - Konrad Rudolph
2
@Qtax 对的,但我们在这里做的不仅是存储,我们还在操作(或至少分析)字符串。而std::regex只是分析底层代码单元,如果那些恰好是字节,那么它就处理字节。只要我们想要将某个东西视为一个单位的东西不超过一个字节,那就没问题了。 “到处使用UTF8”建议很好,但仅适用于字符串的透明存储(大多数情况下足够),当您从一个点检索字符串并将其传递给另一个点而不对其进行任何其他操作时。 - Konrad Rudolph
1
@KonradRudolph 可以说 Ogonek 也是由一个受虐狂设计的 - 我们心爱的机器人。你想要表达的观点可能是设计会更少地体现出“施虐者”的特点 :) - sehe
显示剩余6条评论
1个回答

37

使用UTF-8编码,字符串"[一-龠々〆ヵヶ]"等同于这个字符串:"[\xe4\xb8\x80-\xe9\xbe\xa0\xe3\x80\x85\xe3\x80\x86\xe3\x83\xb5\xe3\x83\xb6]"。但是这并不是你要找的字符类。

你要找的字符类应包括:

  • 范围在U+4E00..U+9FA0之间的任何字符;或者
  • 任何这些字符:々、〆、ヵ、ヶ。

而你指定的字符类是这样的:

  • 任何这些“字符”:\xe4、\xb8;或者
  • 范围在\x80..\xe9之间的任何“字符”;或者
  • 任何这些“字符”:\xbe、\xa0、\xe3、\x80、\x85、\xe3(再次)、\x80(再次)、\x86、\xe3(再次)、\x83、\xb5、\xe3(再次)、\x83(再次)、\xb6。

很混乱吧?你看到问题了吗?

它将不匹配“拉丁”字符(我想你的意思是a-z之类的字符),因为在UTF-8中,这些字符都使用单字节低于0x80,而这些字符不在那个混乱的字符类中。

它也不会匹配"中",因为"中"有三个“字符”,而你的正则表达式只匹配那个奇怪的长列表中的一个“字符”。尝试assert(std::regex_match("中", std::regex("..."))),你就能看到。

如果你添加一个+,它会起作用,因为"中"在你那个奇怪的长列表中有三个“字符”,现在你的正则表达式匹配了一个或多个。

如果你添加{1},它不匹配,因为我们又回到了将三个“字符”与一个进行匹配的状态。
顺便说一下,"中""中"匹配,因为我们按照相同顺序匹配相同的三个“字符”。
正则表达式使用+实际上会匹配一些不需要的内容,因为它不关心顺序。任何可以由UTF-8字节列表中的字符制作的字符都将匹配。它将匹配"\ xe3 \ x81 \ x81"(ぁ U + 3041),甚至会匹配无效的UTF-8输入,如"\ xe3 \ xe3 \ xe3 \ xe3"
更大的问题是您正在使用不支持Unicode最低级别1的正则表达式库。它混淆字节,您珍贵的小型正则表达式几乎无能为力。
更大的问题是,您正在使用硬编码的字符集来指定“任何日文汉字或中文字符”。为什么不使用Unicode脚本属性呢? R"(\p{Script=Han})" 哦对了,这在C ++ 11正则表达式中行不通。有那么一刻,我几乎忘记了它们与Unicode相比要糟得多。
那么你应该怎么做呢?
您可以将输入解码为std::u32string并在整个匹配中使用char32_t。这不会给您带来这种混乱,但是当您需要“一组具有共同属性的字符”时,仍然需要硬编码范围和异常情况。
我建议您忘记C ++ 11正则表达式并使用某些正则表达式库,该库具有最低级别1的Unicode支持,例如ICU中的库。

谢谢Martinho,这是一篇非常有启发性的文章。让我更好地理解了UTF-8和正则表达式。无论如何,在这个项目中我放弃了正则表达式,因为我只需要知道一个字形是否属于特定范围,然后将其标记到该范围,所以硬编码是一个快速简便的解决方案。我认为正则表达式会是一个简单而优雅的解决方案,但我发现在C++11中并非如此。 - MCH
9
是的,我想抓取 ICU 仅仅为了一个小小的匹配可能有些过头了。如果你想要使用 Unicode 和正则表达式,Perl 几乎是唯一认真对待它们的语言。这是一个悲哀的事实,但这就是我们现在所拥有的。个人认为 <regex> 是标准库中的一些垃圾。现在已经是2013年了,假装 Unicode 不存在是牵强附会的,只会助长那种处理 Unicode 太痛苦而无法关注的想法(提示:如果你的锤子没有锤头,你将很难钉钉子)。 - R. Martinho Fernandes
请改用 wregex,并使用 http://utfcpp.sourceforge.net/,或在字符串前加上 'L' 前缀。 - user484068
1
我认为Go最终会认真对待UTF-8,参见http://golang.org/pkg/regexp。 - oblitum
@chico 不错的建议。我还没有在Go中尝试使用re,也许我应该尝试一下 :) - R. Martinho Fernandes
@R.MartinhoFernandes,Python 3的Unicode正则表达式支持有哪些缺陷? - Bob Kline

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