Perl - 匹配偶数位置并移除不可打印字符

3

我有一个来自数据库表转储的hex2string,类似于

"41424320202020200A200B000C"

我希望做的是匹配偶数位置并检测控制字符,这些字符在打印时可能会破坏字符串。例如删除ASCII空字符\x00、\n、\r、\f和\x80到\xFF等。
我尝试删除ASCII空字符,如:
perl -e ' $x="41424320202020200A200B000C"; $x=~s/00//g; print "$x\n" '

但结果是不正确的,因为它删除了空格\x20的尾部十六进制值中的0和换行符\x0A的前导0,即20 0A变为2A
414243202020202A2B0C

我想要的是。
414243202020202020
4个回答

3
say unpack("H*", pack("H*", "41424320202020200A200B000C") =~ s/[^\t[:print:]]//arg);

或者

my $hex = "41424320202020200A200B000C";
my $bytes = pack("H*", $hex);
$bytes =~ s/[^\t[:print:]]//ag;
$hex = unpack("H*", $bytes);
say $hex;

或者

my $hex = "41424320202020200A200B000C";
my $bytes = pack("H*", $hex);
$bytes =~ s/[^\t\x20-\x7E]//g;
$hex = unpack("H*", $bytes);
say $hex;

使用/a/r 的解决方案需要 Perl 5.14+。


以上内容始于以下字符串:

 41424320202020200A200B000C

使用pack将其转换为以下内容:
 ABC␠␠␠␠␠␊␠␋␀␌

该替换操作会移除所有非ASCII和非可打印字符(制表符除外),最终保留以下内容:

 ABC␠␠␠␠␠␠

使用 unpack 将其转换为以下内容:

 414243202020202020

这个解决方案不仅比以前的解决方案更短,而且速度更快,因为它分配的变量要少得多,并且只启动了一次正则表达式匹配。


你能确认它会保留制表符和空格吗?我不想去掉它们。 - stack0114106
只需将其添加到要保留的字符列表中即可。(我已调整答案) - ikegami
请问一下 /a 修饰符的含义,这个解决方案在我的 Cygwin 上有效,但不确定在我的企业版 Linux 上是否可行。 - stack0114106
只要您拥有Perl 5.14或更高版本(用于/r/a),它就可以正常工作。 /a会导致[:print:]仅匹配ASCII字符(00..7F)。 - ikegami
添加了不需要5.14的替代方案。它们将至少在5.6之前起作用(虽然在Perl 5.10之前你需要用print替换say)。 - ikegami

2
检测可能在打印时破坏字符串的控制字符。例如,删除ASCII空值\x00、\n、\r、\f和\x80至\xFF等。

在Hakon的答案基础上进行改进(它只剥离了空字节,而没有剥离其他所有字节):

#!/usr/bin/perl
use warnings;
use strict;
use feature qw/say/;
my $x="41424320202020200A200B000C";
say $x;
say grep { chr(hex($_)) =~ /[[:print:]\t]/ && hex($_) < 128 } unpack("(A2)*", $x);

提供给你

41424320202020200A200B000C
414243202020202020

字符类[:print:]在字符集中匹配所有可打印字符,包括空格(但不包括换行和回车等控制字符),我还添加了制表符。然后它还检查字节是否在ASCII范围内(因为在许多语言环境中,更高的字符仍然可打印)。


非常好的答案。我正在构建一个带有交替匹配的正则表达式,而您已经简化了它。 - stack0114106
只有在使用/l时才会查询活动语言环境,因此最后一句话没有意义。 - ikegami

2

直接使用字符的十六进制形式是可能的,但更加复杂。我不建议使用这种方法。本答案旨在说明为什么没有提出此解决方案。


您希望排除以下所有字符:

  • ASCII可打印字符(2016到7E16
  • TAB(0916

这意味着您希望排除以下字符:

  • 0016到0816
  • 0A16到1F16
  • 7F16到FF16

如果我们按首位数字分组,则得到:

  • 0016到0816,0A16到0F16
  • 1016到1F16
  • 7F16
  • 8016到FF16

因此,我们可以使用以下内容:

$hex =~ s/\G(?:..)*?\K(?:0[0-8A-Fa-f]|7F|[189A-Fa-f].)//sg;     # 5.10+

$hex =~ s/\G((?:..)*?)(?:0[0-8A-Fa-f]|7F|[189A-Fa-f].)/$1/sg;   # Slower

perlre。匹配从上次匹配结束的位置(如果是第一次匹配,则从字符串开头开始)。您可以想象每个模式都以隐式的\G(?s:.)*?\K为前缀。通过使用\G(?s:..)*?\K,我们在查找匹配时每次前进两个位置而不是一个。 - ikegami
在这种情况下,它会失败第一次出现的时候,对吧?.. 004142?.. 但它成功了!! - stack0114106
就像我上面提到的那样,\G在第一次尝试时匹配字符串的开头(因为pos=undef=0)。 - ikegami
太好了!- 我看到你提到的字符串“2009”的区别了...所以对于我的情况,\G会更快,因为它每次都会跳过2个字符,是吗? - stack0114106
比什么更快?比首先从十六进制转换的其他解决方案更快吗?是的,但我不会使用这个解决方案。我不应该告诉你这一点;仅仅通过看它就应该清楚它有多么复杂。复杂的解决方案很难阅读和维护。这使它们容易出错并引起各种问题。(实际上,在此答案的初始版本中有三个错误。)最快的解决方案很少是最好的,而最简单的解决方案通常是最好的。(当然,我会选择一个快速且简单的解决方案而不是一个慢速且简单的解决方案。) - ikegami
显示剩余6条评论

1
您可以尝试使用unpack将字符串拆分成两个字节的子字符串:
my $x="41424320202020200A200B000C";
say $x;
say join '', grep { $_ !~ /00/} unpack "(A2)*", $x;

输出:

41424320202020200A200B000C
41424320202020200A200B0C

这里的“13”是什么意思? - stack0114106
它是字符串长度除以二。 - Håkon Hægland
我移除了固定的数字13。请查看我的更新答案。 - Håkon Hægland

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