【问题标题】:Why does opening a file in utf 8 mode change the behaviour of seek?为什么以 utf 8 模式打开文件会改变 seek 的行为?
【发布时间】:2024-05-03 04:10:04
【问题描述】:

这是一个没有特殊字符的简单文本文件,名为utf-8.txt,内容如下。

foo bar baz
one two three

新行遵循 unix 约定(一个字节),因此文件的整个大小为 26 = 11 + 1 + 13 + 1。(11 = foo bar baz, 13 = one two three

如果我使用以下 perl 脚本读取文件

use warnings;
use strict;

open (my $f, '<', 'utf8.txt');
<$f>;
seek($f, -4, 1);
my $xyz = <$f>;
print "$xyz<";

打印出来

baz
<

这是意料之中的,因为seek 命令返回四个字符,新行和三个属于baz

如果我现在将open 语句更改为

open (my $f, '<:encoding(UTF-8)', 'utf8.txt');

输出变为

 baz
<

也就是说,seek 命令返回五个字符(或者它返回四个字符但跳过新行)。

这是预期的行为吗?是否有标志或其他东西可以关闭此行为?

编辑

根据 Andrzej A. Filip 的建议,当我在 open 语句之后添加 print join("+",PerlIO::get_layers($f)),"\n"; 时,它会在“正常”打开的情况下打印:unix+crlf 和 @987654336 @案例:unix+crlf+encoding(utf-8-strict)+utf8.

【问题讨论】:

  • 在两个脚本中的 open 之后添加以下测试:print join("+",PerlIO::get_layers($f)),"\n";
  • 请看我修改后的问题
  • 与:crlf层有关。解决方法:open (my $f, '&lt;', 'utf8.txt'); binmode($f); binmode($f, ':encoding(UTF-8)');
  • (禁用 :crlf 层,但您表示不需要它。)
  • 在尝试重现某人的输出时,拥有正确的输入文件会有所帮助。 (重现问题我没有问题。)

标签: perl file utf-8 seek


【解决方案1】:

对于那些寻找 TL;DR 的人,seektell 以字节为单位。如果seek 使用tell 返回的值,它应该总是没问题的



Perl 的 seek 运算符的文档相当笨拙,但它有这个

寻找文件句柄、位置、时间

WHENCE 的值为 0 以将新位置(以字节为单位)设置为 POSITION ...

注意以字节为单位:即使文件句柄已设置为对字符进行操作(例如通过使用:encoding(utf8) 开放层),tell() 将返回字节偏移量,而不是字符偏移量(因为实现会导致 seek() 和 tell() 相当慢)。

虽然这暗示了问题,但并未明确说明

seektell 在文件中使用并返回字节偏移量不管任何其他 PerlIO 层。这意味着它们的工作条件与 sysread 类似,后者独立于 Perl 的流 IO,尽管 seektell 尊重 Perl 的缓冲,而 sysread 不这样做

不只是 :utf8:encoding 层会混淆您可能期望的单位:Windows :crlf 层也有效果,因为它在流输入之前和输出之后将 CR LF 对转换为 LF。这显然会导致每一行文本的差异,但据我所知,Perl 的文档中没有提到这一点。 Linux 和 OSX 是几乎所有其他 Perl 平台的咄咄逼人的丑陋姐妹

让我们看看你的代码。我已经在我的 Windows 10 和 Windows 7 系统上运行了这段代码(我保证它与你问题中的代码相同),甚至用 Windows 98 启动了一个虚拟机来尝试同样的事情

use warnings;
use strict;

open (my $f, '<', 'utf8.txt');
print join("+",PerlIO::get_layers($f)),"\n";
<$f>;
seek($f, -4, 1);
my $xyz = <$f>;
print "$xyz<";

全部输出这个

unix+crlf
az

这是我所期望的,而不是你所说的你得到的。这是核心,因为我们谈论的是单字节偏移

您的文件包含此内容

foo bar baz\r\none two three

第一次阅读需要我们从一开始就读到 13 个字符。 Perl 已经读取了 foo bar baz\r\n 并删除了 CR,将 foo bar baz\n 交给程序,它会丢弃它。很好

现在你seek($f, -4, 1)

那第三个参数1是SEEK_CUR,表示要移动当前读指针相对于当前位置

请不要使用幻数。 Perl 在这里几乎将底层的C file library 暴露给你,你需要对它负责。将 1 作为第三个参数传递是神秘且不负责任的。读过你代码的人都不会知道你写了什么

这样做

use Fcntl ':seek'

然后你可以像这样编写更易懂的代码。至少人们可以用谷歌搜索SEEK_CUR,而用1 尝试同样的方法比没有结果更糟糕

seek($f, -4, SEEK_CUR)

因为它让我们其他人有机会理解您的代码

所以你正在寻找 13 个字节,加上 -4 即 9。那是在 bazb 之后,所以我得到 az

这就是我在所有这些不同的 Windows 机器上运行的所有代码的结果。我不得不认为问题在于您的代码控制而不是 Perl,除了 CRLF 的问题

我希望这为您解释了一些异常情况,但请检查您的代码和结果。

【讨论】:

    最近更新 更多