在二进制文件和UTF16编码下进行grep操作

75
标准的 grep/pcregrep 工具可以方便地用于 ASCII 或 UTF8 数据的二进制文件 - 是否有一种简单的方法让它们也尝试使用 UTF16(最好同时进行,但单独进行也可以)?
我要获取的数据无论如何都是 ASCII 的(在库引用等中),只是有时会在任意两个字符之间出现 00,有时则没有被发现。
我没有看到任何语义上解决它的方法,但这些 00 应该可以解决问题,只是我不能轻松地在命令行上使用它们。

如果字符长度为两个字节,则不是ASCII。 - Matti Virkkunen
我的意思是字符的ASCII范围(U+0000到U+007F),而不是ASCII编码。 - taw
如果utf-16文件有BOM,grep就不应该报错。也许你应该预先注入BOM?{ printf "\xFF\xFE"; cat my-utf16-no-bom.txt; } | grep ... - Sandburg
10个回答

88
最简单的方法是将文本文件转换为utf-8格式,然后将其导入grep命令:
iconv -f utf-16 -t utf-8 file.txt | grep query

我试图做相反的事情(将我的查询转换为utf-16),但是似乎grep不喜欢那样做。我认为这可能与字节序有关,但我不确定。

似乎grep会将utf-16的查询转换为utf-8 / ascii。这是我尝试过的:

grep `echo -n query | iconv -f utf-8 -t utf-16 | sed 's/..//'` test.txt

如果test.txt是utf-16文件,这个方法就行不通了,但是如果test.txt是ascii文件,则可以使用。我只能得出grep正在将我的查询转换为ascii的结论。
编辑:这里有一个非常疯狂的方法,它有点起作用,但并没有给你太多有用的信息。
hexdump -e '/1 "%02x"' test.txt | grep -P `echo -n Test | iconv -f utf-8 -t utf-16 | sed 's/..//' | hexdump -e '/1 "%02x"'`

它是如何工作的?它将您的文件转换为十六进制(没有应用任何额外格式)。然后将其导入grep。 Grep使用由将查询回显(无换行符)到iconv构造的查询,iconv将其转换为utf-16。然后将其导入sed以删除BOM(utf-16文件的前两个字节用于确定字节顺序)。然后将其导入hexdump,使查询和输入相同。
不幸的是,如果有单个匹配,我认为这将最终打印出整个文件。此外,如果二进制文件中的utf-16存储在与您的机器不同的字节顺序中,则此方法将无法工作。
编辑2:解决了!
grep -P `echo -n "Test" | iconv -f utf-8 -t utf-16 | sed 's/..//' | hexdump -e '/1 "x%02x"' | sed 's/x/\\\\x/g'` test.txt

这将在文件test.txt中搜索以utf-16编码的字符串Test的十六进制版本。


5
由于iconv是一个二进制文件,其中有许多非UTF-16数据,并且iconv会在第一个错误时退出,所以它无法正常工作。 - taw
1
经过轻微修改,它似乎可以工作:pcregrep \echo -n "test" | iconv -f utf-8 -t utf-16le | hexdump -e '/1 "x%02x"' | sed 's/x/\\x/g'` <binary.file`。最重要的是,它不需要utf-16字符在2字节边界上 - 所有以前的方法都存在很大问题。甚至可以与“-i”一起使用。 - taw
太棒了!我发现我遇到的问题是反引号。由于某种原因,它们返回utf-8字符串并转义反斜杠。这就是为什么sed有四个''的原因。 - Niki Yoshiuchi
我需要为保存的Windows注册表文件执行此操作。 我发现上述命令非常好,但当我还需要知道文件名时,我在bash中创建了一个新方法来使用:通过在bash命令提示符处粘贴定义grepreg: grepreg() { find -name'*.reg'-exec echo {} ; -exec iconv -f utf-16 -t utf-8 {} ; | grep“$1 \ | \ .reg” }// 示例用法:grepreg SampleTextToSearchForInFiles - Andrew Stern
1
有些sed是支持Unicode的,那么它会在Unicode标记后剥离前两个字符而不是标记字符。请使用tail -c +3替换sed 's/..//'。 - Jason Pyeron
显示剩余2条评论

26

我发现下面的解决方案最适合我,来源于https://www.splitbits.com/2015/11/11/tip-grep-and-unicode/

Grep无法很好地处理Unicode,但可以通过一些方法解决。例如,要查找以下内容,

Some Search Term

在一个 UTF-16 文件中,使用正则表达式忽略每个字符的第一个字节。

S.o.m.e. .S.e.a.r.c.h. .T.e.r.m 

同时,告诉grep将该文件视为文本,使用“-a”,最终命令如下:

grep -a 'S.o.m.e. .S.e.a.r.c.h. .T.e.r.m' utf-16-file.txt

17

你可以在搜索字符串中明确包含空值(00),尽管你将得到带有空值的结果,因此你可能想将输出重定向到文件中,以便你可以使用合理的编辑器查看它,或者通过sed管道替换空值。要在*.utf16.txt中搜索"bar":

grep -Pa "b\x00a\x00r" *.utf16.txt | sed 's/\x00//g'

"-P"告诉grep接受Perl正则表达式语法,允许 \x00 扩展为空值,而 "-a" 告诉它忽略Unicode看起来像二进制的事实。


1
好的技巧,我没有想到。这里的魔法是 grep-a 标志。假设您没有要搜索的大文件(在这种情况下可能会太慢),您可以通过仅指定“.”而不是“\x00”来使它更容易输入。 “.”将匹配任何内容,而不仅仅是空值。那可能并不总是你想要的,但大多数情况下都没问题。通常,清除空值的 sed 也是不必要的-它们不会在输出上打印任何内容。因此,对于您的示例,只需使用 grep -a b.a.r *.utf16.txt即可。 - Dan Pritts
我必须记住使用“-P”选项以允许“\xnn”。如果没有Perl,我可以使用“.”即任何单个字符的方式,就像@nirmal在下面回答的那样。 - northern-bradley

10

ripgrep

使用ripgrep实用工具来搜索UTF-16文件。

ripgrep支持搜索文本编码为UTF-8以外的文件,如UTF-16、latin-1、GBK、EUC-JP、Shift_JIS等。(提供了一些自动检测UTF-16的支持,其他文本编码必须在-E/--encoding flag中明确指定。)

例如语法:

rg sometext file

要导出所有行,请运行:rg -N . file

ripgrep非常快!谢谢。 - Matt Sephton

9

我经常在导出Windows注册表后使用这个工具,因为它的输出是Unicode格式。此工具运行在Cygwin环境下。

$ regedit /e registry.data.out
$ file registry.data.out
registry.data.out: Little-endian **UTF-16 Unicode text**, with CRLF line terminators

$ sed 's/\x00//g' registry.data.out | egrep "192\.168"
"Port"="192.168.1.5"
"IPSubnetAddress"="192.168.189.0"
"IPSubnetAddress"="192.168.102.0"
[HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Print\Monitors\Standard TCP/IP Port\Ports\192.168.1.5]
"HostName"="192.168.1.5"
"Port"="192.168.1.5"
"LocationInformation"="http://192.168.1.28:1215/"
"LocationInformation"="http://192.168.1.5:80/WebServices/Device"
"LocationInformation"="http://192.168.1.5:80/WebServices/Device"
"StandaloneDhcpAddress"="192.168.173.1"
"ScopeAddressBackup"="192.168.137.1"
"ScopeAddress"="192.168.137.1"
"DhcpIPAddress"="192.168.1.24"
"DhcpServer"="192.168.1.1"
"0.0.0.0,0.0.0.0,192.168.1.1,-1"=""
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\Standard TCP/IP Port\Ports\192.168.1.5]
"HostName"="192.168.1.5"
"Port"="192.168.1.5"
"LocationInformation"="http://192.168.1.28:1215/"
"LocationInformation"="http://192.168.1.5:80/WebServices/Device"
"LocationInformation"="http://192.168.1.5:80/WebServices/Device"
"StandaloneDhcpAddress"="192.168.173.1"
"ScopeAddressBackup"="192.168.137.1"
"ScopeAddress"="192.168.137.1"
"DhcpIPAddress"="192.168.1.24"
"DhcpServer"="192.168.1.1"
"0.0.0.0,0.0.0.0,192.168.1.1,-1"=""
"MRU0"="192.168.16.93"
[HKEY_USERS\S-1-5-21-2054485685-3446499333-1556621121-1001\Software\Microsoft\Terminal Server Client\Servers\192.168.16.93]
"A"="192.168.1.23"
"B"="192.168.1.28"
"C"="192.168.1.200:5800"
"192.168.254.190::5901/extra"=hex:02,00
"00"="192.168.254.190:5901"
"ImagePrinterPort"="192.168.1.5"

我猜这种方式有很小的误报几率,但在99.9%的情况下可能是想要的。它在MINGW64 Git Bash下也适用于我。 - mwfearnley
这可以合并为一个单独的sed命令:
sed -ne "s/\x00//g" -e "/192.168/p"
- Firstrock
那是唯一对我有效的方法。 - Ikem Krueger

7

ugrep (通用grep) 全面支持Unicode、UTF-8/16/32输入文件,检测无效的Unicode以确保正确结果,显示文本和二进制文件,并且速度快且免费:

ugrep搜索UTF-8/16/32输入和其他格式。选项--encoding允许搜索许多其他文件格式,例如ISO-8859-1到16、EBCDIC、代码页437、850、858、1250到1258、MacRoman和KOI8。

有关详细信息,请参见GitHub上的ugrep


4
我需要进行递归操作,以下是我想到的解决方案:
find -type f | while read l; do iconv -s -f utf-16le -t utf-8 "$l" | nl -s "$l: " | cut -c7- | grep 'somestring'; done

这真的很糟糕,而且非常慢;我相信有更好的方法,并希望有人能够改进它 - 但是我很匆忙:P

这些代码段的作用:

find -type f

给出一个相对于当前目录的带路径的递归文件名列表

while read l; do ... done

Bash循环; 对于文件路径列表中的每一行,将路径放入$l并在循环中执行操作。(为什么我使用shell循环而不是xargs,虽然后者速度更快:我需要在输出的每一行前缀当前文件的名称。如果我一次向iconv馈送多个文件,我想不到一种方法来实现这一点,而且由于我将逐个处理一个文件,因此shell循环更容易进行语法/转义。)
iconv -s -f utf-16le -t utf-8 "$l"

将名为$l的文件进行转换:假设输入文件是utf-16小端,并将其转换为utf-8。使用-s参数避免iconv报告任何转换错误(因为此目录结构中的某些文件不是utf-16格式)。该转换的输出结果将输出到标准输出流(stdout)。

nl -s "$l: " | cut -c7-

这是一个技巧:nl命令可以插入行号,不过它恰好有一个“使用任意字符串来分隔数字和行”的参数,所以我把文件名(后面加上冒号和空格)放在那里。然后我使用cut命令删除行号,只留下文件名前缀。(为什么我没有用sed呢:因为通过这种方式转义更容易。如果我使用了sed表达式,就必须担心文件名中的正则表达式字符,而我的情况中有很多这样的字符。nlsed要简单得多,它只会完全按照参数-s给定的文本进行操作,shell会帮我处理转义。)
因此,在这个管道的末端,我已经将一堆文件转换成了utf-8行,并以文件名为前缀,然后进行grep查找。如果有匹配项,我可以从前缀中知道它们在哪个文件中。
注意:
  • 这比grep -R要慢得多,因为我为每个文件都启动了新的iconvnlcutgrep进程。非常可怕。
  • 除uft-16le输入外的所有内容都将变成完全的垃圾,因此,如果有一个包含“somestring”的普通ASCII文件,这个命令就不会报告它——你需要做一个正常的grep -R和这个命令(如果你有多种unicode编码类型,比如一些大端和一些小端文件,则需要调整这个命令并为每种不同的编码再次运行它)。
  • 文件名中包含'somestring'的文件将显示在输出中,即使它们的内容没有匹配项。

1
我在 OS X 上不得不执行 find . -type f - Jake Brownson
我不得不在iconv命令中添加2>/dev/null以防止“iconv:incomplete character or shift sequence at end of buffer”的垃圾邮件。 - Arrrow

0
我将这个作为评论添加到上面被接受的答案中,但为了更容易阅读,我将其单独列出。这允许您在一堆文件中搜索文本,同时显示找到文本的文件名。所有这些文件都有一个.reg扩展名,因为我正在搜索导出的Windows注册表文件。只需将.reg替换为任何文件扩展名即可。
// Define grepreg in bash by pasting at bash command prompt
grepreg ()
{
    find -name '*.reg' -exec echo {} \; -exec iconv -f utf-16 -t utf-8 {} \; | grep "$1\|\.reg"
}

// Sample usage
grepreg SampleTextToSearch

0

这个 sed 语句超出了我的理解范围。我有一个简单、远非完美的 TCL 脚本,我认为它在我一个测试点上做得不错:

#!/usr/bin/tclsh

set insearch [lindex $argv 0]

set search ""

for {set i 0} {$i<[string length $insearch]-1} {incr i} {
    set search "${search}[string range $insearch $i $i]."
}
set search "${search}[string range $insearch $i $i]"

for {set i 1} {$i<$argc} {incr i} {
    set file [lindex $argv $i]
    set status 0
    if {! [catch {exec grep -a $search $file} results options]} {
        puts "$file: $results"
    }
}

0
您可以使用以下 Ruby 的一行代码:
ruby -e "puts File.open('file.txt', mode:'rb:BOM|UTF-16LE').readlines.grep(Regexp.new 'PATTERN'.encode(Encoding::UTF_16LE))"

为了简单起见,可以定义为如下的 shell 函数:

grep-utf16() { ruby -e "puts File.open('$2', mode:'rb:BOM|UTF-16LE').readlines.grep(Regexp.new '$1'.encode(Encoding::UTF_16LE))"; }

然后它可以像grep一样使用:

grep-utf16 PATTERN file.txt

来源: 如何在Ruby中使用readlines.grep处理UTF-16文件?


虽然这个方法可以运行,但在一个有450,000行的UTF16LE文本文件上,它比ugrep慢得多。 - Matt Sephton

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