从文件中删除UTF-8字符范围的脚本

3

我的问题是,我有一个包含UTF-8编码的数据文件,其中大部分是有效的并且必须保留,但有一些是随机的“垃圾”UTF-8编码,即在0xf0 - 0xff范围内。下面是一个坏数据的十六进制示例。

 f4 80 80  ab f4 80 80 b6 f4 80 80 
 a5 f4 80 80 a6 f4 80 80  83 f4 80 80 b6 f4 80 81  
 84 f4 80 81 98 f4 80 81  87 f4 80 81 8c f4

我试图编写一个perl脚本,用于搜索和替换首字节在0xf0-0xff范围内的字符。在这个网站上,该代码页被列为私有使用。
我已经尝试过的方法要么不起作用,要么只能删除多字节字符的第一个字节,例如perl -CSD -pi.orig -e 's/[\x{f4}-\x{ff}]/?/g',运行perl v5.12.5。
我并不是一位perl专家或者utf-8专家,如果可以在Linux环境下移植,我也可以尝试使用ruby/python/C++(98)/等等解决此问题。
这里有一段垃圾数据的链接:http://pastebin.com/LR0StPHu

现在我的问题是,我如何将演示数据导入到我的代码中?:D - simbabque
@simbabque 给您提供 Pastbin 链接:http://pastebin.com/LR0StPHu - Christopher Wirt
3个回答

5

好的,让我们不要混淆几个概念。

UTF-8字符的第一个字节为0xf0的长度为四个字节,这是你需要编码合法Unicode字符的最长长度。由于超过94%的Unicode范围需要第四个字节,0xf0不能映射到任何单个代码页,当然也不能映射到专用使用区域。

这些字符确实在基本多文种平面之外。但这与无效或专用使用不同;它只意味着它们的代码点大于U+FFFF(十进制值为65,535)。

如果您想排除BMP之外的所有字符,则应搜索与此正则表达式匹配的字符:

[\x{10000}-\x{10FFFF}]

这使用了Perl的\x{...}插值语法,通过十六进制编码来包含字符。如果你真正使用的是Perl,为了使用方便,你可能想把正则表达式放入一个变量中(使用引用正则表达式的结构qr(...),因为裸斜杠会立即在赋值时尝试将正则表达式与$_进行匹配):

my $not_bmp = qr([\x{10000}-\x{10FFFF}]);

但是,再次强调,删除与该正则表达式匹配的字符会消除超过94%的可能的Unicode字符,因此请确保您想要这样做。

如果您真的只想消除专用使用字符-其中一些在BMP内部-请特别排除这些范围。对于Perl、Python或任何其他UTF-8感知的语言,您不必担心字节;只需检查代码点即可。

维基百科所述,三个专用使用区域位于以下代码点范围内:

  • U+E000..U+F8FF
  • U+F0000..U+FFFFF
  • U+100000..U+10FFFF

因此,相应的Perl正则表达式如下:

my $pua = qr([\x{e000}-\x{f8ff}\x{f0000}-\x{fffff}\x{100000}-\x{10ffff}]);

许多其他编程语言也有类似的Unicode支持(匹配UTF-8字符,通过代码点包含字符串中的字符等)。例如,这里是Ruby的示例,主要区别在于使用\u{...}代替\x{...}进行插值:
not_bmp = %r([\u{10000}-\u{10FFFF}])
pua = %r([\u{e000}-\u{f8ff}\u{f0000}-\u{fffff}\u{100000}-\u{10ffff}])

Python \u 转义只能使用四位十六进制数字,但如果你使用的是 Python3 或者编译成 wide 模式的 Python2,你可以使用大写的 \U,它需要使用八位数字(不像 Perl 和 Ruby 那样支持变长的 {...}):

not_bmp = re.compile(u'[\U00010000-\U0010ffff]')
pua = re.compile(u'[\ue000-\uf8ff\U000f0000-\U000fffff\U00100000-\U0010ffff]')

我对 BMP 做了一些研究,它似乎是我们接收到的数据所关心的内容。任何在此之外的、不正确或其他无用的数据都不应该被存储。 - Christopher Wirt
为了提供一些背景,我们在将数据从文件发送到SQL Server时遇到了数据问题,因为SQL Server会对所有Unicode数据进行UTF-16转换,而我们特定的驱动程序在无法正确进行编码转换时会出现一些致命错误。这些数据从UTF-16 Windows机器-> UTF-8文件-> UTF-16 SQL Server-> UTf-16文件-> UTF-8文件-> UTF-16 SQL Server。真是一团糟。感谢您纠正我对UTF的错误理解,这正是我想要的。 - Christopher Wirt
2
听起来好像有些东西不支持UTF-16,而只支持UCS-2。无论如何,很高兴我能帮忙。 - Mark Reed

3

查找私有使用区域的十六进制范围是浪费时间。可以直接使用以下命令:

s/\p{Private_Use}//g

perluniprops是提供所有Unicode属性的pod文件。如果只想匹配基本多文种平面之上的私有使用区域,可以查询该文件(搜索“Private”)以找到如何匹配这些区域的方法。


3
你需要使用字符来工作,而不是字节。
如果你的数据在代码中,并且你使用use utf8指示Perl程序源码采用utf8编码。我们在这个例子中这样做,以便你可以复制/粘贴我的代码。
你可以使用\x{}转义序列在字符类[]中进行字符串替换。这些可以单独或者作为范围使用。
use utf8;

my $foo = "asfd ☃  Բարեւ ສະບາຍດີ";
$foo =~ s/[\x{10002b}\x{100036}]//g;
CORE::say $foo;

这将输出:
asfd ☃  Բարեւ ສະບາຍດີ

这里还有一个“在打印中有宽字符”的警告,但是我们忽略它,因为我的STDOUT没有正确打开。

我替换的两个字符\x{10002b}\x{100036}是你示例数据中的前两个字符。我IDE使用的字体显示了它没有任何字形的字符的序数,所以我很容易知道那些字符是什么。

my font shows character ordinals

这些字符来自补充专用区B。(维基百科

16 PUA-B U+100000..U+10FFFF 补充专用区B 65,536 65,534 未知

所以我们也可以做一个范围。

my $foo = "asfd ☃  Բարեւ ສະບາຍດີ";
$foo =~ s/[\x{100000}-\x{10ffff}]//g;
CORE::say $foo;

输出:

asfd ☃  Բարեւ ສະບາຍດີ

要获取所有的专用区域,您需要包括列在这里的三个范围。
/[\x{E000}-\x{F8FF}\x{F0_000}-\x{FF_FFD}\x{100_000}-\x{10f_fff}]//g;

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