即使我没有从文件中读取有问题的数据,Perl 也会警告无效编码

Perl warns about invalid encoding even if I don't read the problematic data from the file

我正在尝试从文件的第一部分读取行,该文件包含以 cp1252 编码编码的文本 header,并在特定关键字后包含二进制数据。

问题

Perl 警告我从未阅读过的文件部分存在无效编码。我在两个文件中创建了一个示例来演示该问题。

linebug.pl的内容:

#!/usr/bin/perl
use 5.028;
use strict;
use warnings;
open( my $fh, "<:encoding(cp1252)", "testfile" );
while( <$fh> ) {
    print;
    last if /Last/;
}

testfile 的 Hexdump,其中 Wrong 后面的字节 0x81 是有意添加的,因为它不是有效的 cp1252 代码点:

46 69 72 73 74 0a         |First.|
4c 61 73 74 0a            |Last.|
42 75 66 66 65 72 0a      |Buffer.|
57 72 6f 6e 67 81 0a      |Wrong..|

第三行Buffer只是为了表明我没有读太远。这是我读到的最后一行和 "binary" 数据之间的有效行。

这是显示我只读过两行的输出,但 perl 仍然发出警告:

user@host$ perl linebug.pl
cp1252 "\x81" does not map to Unicode at ./linebug.pl line 6.
First
Last
user@host$

可以看出,我的程序读取并打印了前两行,然后退出。它不应该尝试读取和解释任何其他内容,但我仍然收到关于 \x81 未映射到 Unicode 的警告。

问题

我仍然想要读取初始行时的警告,以防文件损坏。

Perl 以 8 KiB 块的形式从文件中读取,因此一次读取的不止一行。数据在读取时立即解码(因为必须解码流以找到行尾),因此会注意到意外编码并发出警告。

处理此问题的一种方法:使用 non-buffered 读取,通过 sysread,一次读取较小的块。

计算读取的字符数,一旦您 运行 进入该位置,您可以备份并一次继续读取字符,再次计算它们,以便检测准确的位置。有关识别发出警告的位置的工作示例,请参阅 this post

为了能够在那里停下来,您可能希望从 $SIG{__WARN__} 处理程序中抛出 die,并将所有代码放入 eval。这将允许您在发出警告的地方停下来并收回控制权。

当您读到那个位置时,您可以 re-open 文件的编码适合文件的其余部分,然后搜索到那个位置并阅读其余部分。

我现在无法编写和测试所有内容,希望这会有所帮助。

文件没有行的概念;它们只是字节流。 Perl 必须从 OS 的文件中请求一定数量的字节,并找出该行结束的位置,以便 return 向程序添加一行。

Perl 可以从 OS 中一次请求一个字节,直到它有一个完整的行,但那将是非常低效的。进行系统调用涉及很多开销。因此,Perl 一次请求 8 KiB。

然后,在 Perl 可以确定行结束位置之前必须对原始数据进行解码,因为原始 0A 不一定表示行结束。

与为什么不一次从文件中读取一个字节类似,要求解码器只解码下一个字符是低效的。每次开始和停止解码时都会涉及开销。因此,Perl 在读取数据时对其读取的所有数据进行解码。

所以这意味着 Perl 对程序的读取和解码都比它 return 多。


解决方案是将文件视为二进制文件(因为如果编码按部分更改,它就不是真正的文本文件)并自己进行解码。

如果您正在处理像 cp1252 这样的 single-byte 编码,您可以继续使用 readline(又名 <$fh>)。但是,不是告诉 Perl 搜索换行代码点 (0A),而是需要将代码点的编码设置为 $/。碰巧,cp1252 也是 0A,因此无需更改。

use Encode qw( decode );

open( my $fh, "<:raw", $qfn )
   or die( "Can't open \"$qfn\": $!\n" );

while( <$fh> ) {
    $_ = decode( 'cp1252', $_ );      # :encoding(cp1252)
    s/\r\n\z/\n/ if $^O eq 'Win32';   # :crlf
    print;
    last if /Last/;
}

如果您没有使用 single-byte 编码,您可能必须改用 read。 (由于 UTF-8 的设计方式,您可以继续对 UTF-8 使用 readline。)使用 read 时,确切的解决方案取决于一些细节(与确定要读取多少和读取多少有关)解码)。