PHP中的多字节安全fread函数

4

我有一个文件太大无法放入内存,需要从中剥离掉某些字符(确切来说是控制字符)。我的当前函数如下:

$old = fopen($file, 'r');
$new = fopen($tmpFile, 'w');

while (!feof($old)) {
    fwrite($new, preg_replace('/[^\P{Cc}\t\r\n]/u', '', fgets($old)));
}

rename($tmpFile, $file);

这在大多数情况下都可以正常工作。但是可能会出现一个问题,即fgets读取整行。我处理的一些文件是巨大的单行文本,这仍然会导致内存问题。

可以使用fread来解决这个问题,块大小为8192。但是,我喂给preg_replace的文本可能会被截断多字节字符。

我一直在思考如何在保留多字节字符的同时进行fread,但我还没有找到一个好的解决方案。任何帮助都将是很棒的。

可能的解决方案

虽然我已经用另一种方式解决了这个问题,但我仍然对我的最初问题很感兴趣:如何做一个mb-safe的fread?我认为这样的函数可能有效:

  1. 使用fread读取一块字节
  2. 检查最后一个字节,检查它是否是多字节序列的一部分。如果不是,则停在这里。
  3. 继续读取字节,直到最后一个字节不是多字节序列或结束当前序列。

第2步可能需要使用类似于这样的逻辑,但我对unicode并不那么熟悉。


我不知道这是否是最优的方法,但你可以使用fgetc()函数来读取numChars。这样你就可以按字符而不是按字节进行分块了。 - Chad
如果这个文件的行数超出了内存容量 - 这就是你主要的问题。检查每一行,并编写第一个脚本,将大行分割成适合内存的小块,同时保持内部完整性。 - Tymoteusz Paul
@cwscribner fgetc是二进制安全的,但不是多字节安全的。它仍然会在多字节字符上出现问题。 - Peter Kruithof
@Puciek 我不同意这是主要问题:PHP完全可以进行缓冲读取,只是不能像这样以mb方式进行。这可能是一种解决方案,但不是我喜欢的解决方案,因为我不想对文件内容做出任何假设(例如在某些字符上进行拆分等)。 - Peter Kruithof
4个回答

1

我的解决方案最终相当简单。问题是使用 preg_replace 处理可能截断多字节字符,导致了失败的块。

由于我只需要剥离控制字符,这些字符在 ASCII 范围内因此是单字节的,我可以很容易地使用 str_replace,这样其他字节就不会受到影响。

我的工作解决方案现在看起来像这样:

$old = fopen($file, 'r');
$new = fopen($tmpFile, 'w');

// list control characters, but leave out \t\r\n
$chars = array_map('chr', range(0, 31));
$chars[] = chr(127);
unset($chars[9], $chars[10], $chars[13]);

while (!feof($old)) {
    fwrite($new, str_replace($chars, '', fread($old, 8192)));
}

虽然它并没有回答我的原始问题(即如何进行一个mb-safe fread),但它解决了我的问题。


在这种情况下,您可能应该考虑那些通过谷歌搜索答案来真正执行多字节fread操作的可怜人,并更改问题的标题或类似内容。 ;) - scy
好的,穷人可以尝试我发布的可能解决方案,看看是否有效。如果有效,他们甚至可以发布一个答案!;) - Peter Kruithof

1
我在过去几天里花了相当多的时间寻找 PHP 的 fread()fgetc()file_get_contents() 等多字节安全版本,但很不幸,我认为这样的版本并不存在,特别是对于非常大的文件。所以我自己写了一个(好或坏都有可能):

Jstewmc\Chunker\File::getChunk()

希望它不会太糟糕,能够帮助除了我之外的其他人,并且我不会在 SO 上看起来像个自吹自擂的笨蛋。

1
我还不能发表评论。但是一个选项是像你说的那样分块读取数据并使用unpack('C *',$ chunk),从那里您可以迭代字节数组并根据字节数组中的字节序列找到匹配字符。如果在该数组中找到匹配项,请替换或删除这些字节,并将字符串重新打包。

P.S.:记得在下一个块中重新阅读最后几个字节(这样你就不会有任何与最终替换字符串不一致的问题)。 我不知道我的解包示例是否符合您的喜好,但您可以在此处阅读更多信息:解包文档 这是另一个指针,说明UTF-8编码的工作原理,以防您正在使用UTF-8:UTF-8编码


这很有趣,我认为它会起作用。虽然我不确定为什么需要重新读取字节,因为我没有触及原始字符串/文件? - Peter Kruithof
@PeterKruithof 是的,如果您按照utf-8规范(或任何其他编码)解释位,则无需重新阅读过去的块4个字节。如果在块的最后几个字节中缺少有助于构建字符的内容,请继续在下一个块中进行解析。我是说要重新阅读最后几个字节,以便整个文件字符串具有连续性。 - Geo

0

未经测试。 无法在评论中完全表达,但这就是我想要表达的要点。

$old = fopen($file, 'r');
$new = fopen($tmpFile, 'w');

while (!feof($old)) {
    // Your search subject
    $subject = '';

    // Get $numChars
    for($x = 0, $numChars = 100; $x < $numChars; $x++){
        $subject .= fgetc($old);
    }

    // Replace and write to $new
    fwrite($new, preg_replace('/[^\P{Cc}\t\r\n]/u', '', $subject));

    // Clean out the characters
    $subject = '';
}

rename($tmpFile, $file);

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