在Perl中如何反转包含合并字符的字符串?

13
我有一个字符串 "re\x{0301}sume\x{0301}"(显示为 résumé),我想要将它反转成 "e\x{0301}muse\x{0301}r"(émusér)。我无法使用Perl的reverse函数来实现,因为它会把像"\x{0301}"这样的组合字符当做独立的字符处理,导致最后结果变成了"\x{0301}emus\x{0301}er"( ́emuśer)。那么怎样才能反转该字符串,同时还保持原有组合字符的正确顺序呢?
5个回答

12

你可以使用\X特殊转义字符(匹配非组合字符和所有后续的组合字符)及split将一个单词拆成字母列表(之间包含空字符串),反转这个字母列表,然后再join起来:

#!/usr/bin/perl

use strict;
use warnings;

my $original = "re\x{0301}sume\x{0301}";
my $wrong    = reverse $original;
my $right    = join '', reverse split /(\X)/, $original;
print "original: $original\n",
      "wrong:    $wrong\n",
      "right:    $right\n";

1
对于那些感到困惑的人(就像我一开始一样)为什么在图形之间有空字符串,原因是 split 被反转了:它使用所需数据作为分隔符。空字符串是两个图形之间的内容。只有在结果中包含分隔符时,您才会将图形与“真实”结果混合在一起——一堆空字符串。另一种避免这种情况的替代方法(而且稍微快一些)是使用 m//g 来捕获字形:join '', reverse $original =~ /(\X)/g - Michael Carman
2
为了澄清Michael的评论,当您在正则表达式中使用内存括号并将其提供给split时,您会触发“分隔符保留模式”。您会得到分割部分之间的内容。但是您不需要这样做。模式(?=\X)可以在没有额外位的情况下完成相同的操作。对于小字符串来说,空字符串并不是很重要。 - brian d foy
你指出“保留分隔符模式”是正确的,谢谢,这非常有帮助。然而,(?=\X)并不相同。为了证明,考虑以下两个示例:split /(a)/, "abc" 不等同于 split /(?=a)/, "abc"以及 split /(b+c)/, "abbcd" 不等同于 split /(?=b+c)/, "abbcd" - Flimm
确实,它们并不等价,但我并没有使用那些。我只是在谈论我正在使用的特定事物。 - brian d foy

8
最好的答案是使用Unicode::GCString正如Sinan所指出的那样

我稍微修改了Chas的示例:

  • 设置标准输出的编码,以避免“打印宽字符”警告;
  • split中使用正向先行断言(而不是保留分隔符模式)(显然在5.10之后不起作用,因此我将其删除)

基本上是相同的东西,只是进行了一些微调。

use strict;
use warnings;

binmode STDOUT, ":utf8";

my $original = "re\x{0301}sume\x{0301}";
my $wrong    = reverse $original;
my $right    = join '', reverse split /(\X)/, $original;

print <<HERE;
original: [$original]
   wrong: [$wrong]
   right: [$right]
HERE

哇。我喜欢Perl,但是那个分割表达式真的很神奇。我的第一个想法是“蛮力”:编写一个函数来执行split的操作--返回一个字符串列表,其中每个条目表示一个逻辑字符。无论您如何获取该列表(称其为@x),都可以显然地遵循join('',reverse(@x))部分。 - Roboprog
2
神奇?为什么?它只是一个没有副作用的正则表达式,它只会做你看到的那样。如果你认为这是魔法,那么你还没有见过 Perl 的真正黑魔法。你可能会称其为聪明(虽然我不会这么说),但它并不神奇。这可能只是你从未使用过的东西。 - brian d foy
我尝试使用Perl v5.12.4运行此示例,但它没有起作用。改用/(\X)/则可以。 出于好奇,这个答案在以前的Perl版本中是否有效,还是我们只是忽略了显而易见的问题? - Flimm
看起来它在5.10下工作,但在5.12或5.14下不工作。我认为这一定是一个新的错误。 - brian d foy
@briandfoy 我现在太懒了,你是否已经报告了这个错误? - Chas. Owens

2
您可以使用Unicode::GCString:

Unicode::GCString将Unicode字符串视为由Unicode标准附录#29 [UAX#29]定义的扩展字形集群序列。

#!/usr/bin/env perl

use utf8;
use strict;
use warnings;
use feature 'say';
use open qw(:std :utf8);

use Unicode::GCString;

my $x = "re\x{0301}sume\x{0301}";
my $y = Unicode::GCString->new($x);
my $wrong = reverse $x;
my $correct = join '', reverse @{ $y->as_arrayref };

say "$x -> $wrong";
say "$y -> $correct";

输出:
``` 简历 -> ́emuśer 简历 -> émusér ```

1

Perl6::Str->reverse也可以工作。

对于字符串résumé,您还可以使用Unicode::Normalize核心模块在reverse之前将字符串更改为完全组合形式(NFCNFKC);但是,这不是一般解决方案,因为某些基字符和修饰符的组合没有预先组合的Unicode代码点。


0

其他答案中有一些元素不太好用。这里提供一个在 Perl 5.12 和 5.14 上测试过的工作示例。如果未指定 binmode,则会导致输出生成错误消息。在 split 中使用正向先行断言(而不保留分隔符模式)将导致在我的 Macbook 上输出不正确。

#!/usr/bin/perl

use strict;
use warnings;
use feature 'unicode_strings';

binmode STDOUT, ":utf8";

my $original = "re\x{0301}sume\x{0301}";
my $wrong    = reverse $original;
my $right    = join '', reverse split /(\X)/, $original;
print "original: $original\n",
      "wrong:    $wrong\n",
      "right:    $right\n";

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