【问题标题】:Perl warns about invalid encoding even if I don't read the problematic data from the file即使我没有从文件中读取有问题的数据,Perl 也会警告无效编码
【发布时间】:2019-06-06 23:24:37
【问题描述】:

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

问题

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 只是为了表明我没有读得太远。这是我读到的最后一行和“二进制”数据之间的有效行。

这是显示我只读过两行的输出,但 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 file-io character-encoding


【解决方案1】:

文件没有行的概念;它们只是字节流。 Perl 必须向操作系统请求文件中的一些字节,并找出行结束的位置,以便将一行返回给程序。

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

然后,必须先解码原始数据,然后 Perl 才能确定行的结束位置,因为原始的 0A 不一定表示行的结束。

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

这意味着 Perl 读取和解码的次数多于返回给程序的次数。


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

如果您正在处理像 cp1252 这样的单字节编码,您可以继续使用readline(又名&lt;$fh&gt;)。但是,您需要将 $/ 设置为代码点的编码,而不是告诉 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/;
}

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

【讨论】:

    【解决方案2】:

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

    解决这个问题的一种方法:使用非缓冲读取,通过sysread,一次读取更小的块。

    计算读取的字符数,一旦遇到该位置,您可以一次备份并继续读取字符,再次计算它们,以便检测确切的位置。请参阅this post 了解识别发出警告的位置的工作示例。

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

    当您一直阅读到该位置时,您可以使用适合文件其余部分的编码重新打开该文件,然后查找该位置并阅读其余部分。

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

    【讨论】:

    • Re "使用非缓冲读取,通过 sysread,一次读取较小的块。",这都是错误的。解决方案不是切换到无缓冲;解决方案是切换到自己进行解码。您可以继续使用缓冲 I/O(read 甚至 readline aka &lt;&gt;),但要解码您应该解码的内容。块大小无关紧要。
    • @ikegami 您的意思是阅读(带有:raw 或静音警告)然后通过它并(编码::)解码(安静地)?好的,我已经看到了。仍然需要做一些工作来检测事情的变化等等。但是为什么这“全错了”?感谢您的编辑。
    • Re "您的意思是阅读(带有 :raw 或静默警告)然后通过它并 (Encode::)decode(安静地)?",是的(除了用于静音警告部分)。这不是你的意思吗? (毕竟:encoding不能和sysread一起使用)
    • Re "但是为什么这“全错了”?",读取是否缓冲无关紧要。切换到无缓冲读取没有帮助。它正在切换到自己进行解码。
    • @ikegami 对于无缓冲读取,它不会(尝试)比我要求它读取的内容进一步解码,因此通过较小的读取,我可以更好地定位编码更改的位置。 (但是,现在我看到这种情况发生在“在特定关键字之后”,我不知何故错过了,并认为代码需要发现该位置)。当然,我确实喜欢手动解码的解决方案。
    猜你喜欢
    • 2012-05-25
    • 1970-01-01
    • 2019-12-02
    • 1970-01-01
    • 2018-06-05
    • 2014-12-06
    • 1970-01-01
    • 2016-06-29
    • 2018-01-13
    相关资源
    最近更新 更多