如何将 Ruby 的 readlines.grep 用于 UTF-16 文件?

How to use Ruby's readlines.grep for UTF-16 files?

给定以下两个由以下命令创建的文件:

$ printf "foo\nbar\nbaz\n" | iconv -t UTF-8 > utf-8.txt
$ printf "foo\nbar\nbaz\n" | iconv -t UTF-16 > utf-16.txt
$ file utf-8.txt utf-16.txt
utf-8.txt:  ASCII text
utf-16.txt: Little-endian UTF-16 Unicode text

我想在 UTF-16 格式的文件中找到匹配的模式,就像在 UTF-8 中使用 Ruby 一样。

这是 UTF-8 文件的工作示例:

$ ruby -e 'puts File.open("utf-8.txt").readlines.grep(/foo/)'
foo

但是,它不适用于 UTF-16LE 格式的文件:

$ ruby -e 'puts File.open("utf-16.txt").readlines.grep(/foo/)'
Traceback (most recent call last):
    3: from -e:1:in `<main>'
    2: from -e:1:in `grep'
    1: from -e:1:in `each'
-e:1:in `===': invalid byte sequence in US-ASCII (ArgumentError)

我尝试通过以下方式转换基于 this post 的文件:

$ ruby -e 'puts File.open("utf-16.txt", "r").read.force_encoding("ISO-8859-1").encode("utf-8", replace: nil)' 
ÿþfoo
bar
baz

但它在foo之前打印了一些无效字符(ÿþ),其次我不知道如何在转换后使用grep方法(它报告为未定义的方法).

如何对 UTF-16 文件使用 readlines.grep() 方法? 或者其他一些简单的方法,我的目标是打印具有特定正则表达式模式的行.


理想情况下在一行中,因此该命令可用于 CI 测试。

这是一些真实世界的场景:

ruby -e 'if File.readlines("utf-16.log").grep(/[1-9] error/) {exit 1}; end'

但由于日志文件的 UTF-16 格式,该命令不起作用。

简答:

你差不多搞定了,只需要说出你要替换哪些字符(我猜是无效的和未定义的):

$ ruby -e 'puts File.open("utf-16.txt", "r").read.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")'
foo
bar
baz

另外我认为你不需要force_encoding

如果您想在打开时忽略 BOM 转换并使用 readlines,您可以使用:

 ruby -e 'puts File.open("utf-16.txt", mode: "rb:BOM|UTF-16LE:UTF-8").readlines.grep(/foo/)'

更多详情:

你这样做时得到无效字符的原因:

$ruby -e 'puts File.open("utf-16.txt", "r").read.force_encoding("ISO-8859-1").encode("utf-8", replace: nil)'
ÿþfoo
bar
baz

是在每个Unicode文件的开头都有字节顺序标记,显示字节顺序和编码形式。在您的情况下,它是 FE FF(意思是 Little-endian UTF-16),它们是无效的 UTF-8 字符。

您可以通过在没有 force_encoding 的情况下调用 encode 来验证:

$ruby -e 'puts File.open("utf-16.txt", "r").read.encode("utf-8")'
��foo
bar
baz

黑框中的问号用于替换未知、无法识别或无法表示的字符。

您可以在 BOM 上查看更多 here

虽然 Viktor 的回答在技术上是正确的,但将整个文件从 UTF-16LE 重新编码为 UTF-8 是不必要的,并且可能会影响性能。您真正需要的只是以相同的编码构建正则表达式:

puts File.open(
  "utf-16.txt", mode: "rb:BOM|UTF-16LE"
).readlines.grep(
  Regexp.new "foo".encode(Encoding::UTF_16LE)
)
#⇒ foo