Многобайтовый фред в PHP

У меня есть файл, который слишком велик для размещения в памяти, из которого мне нужно удалить определенные символы (если быть точным, управляющие символы). Моя текущая функция выглядит так:

$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, вероятно, мог бы использовать некоторую логику вроде этой, но я не настолько разбираюсь в юникоде, как умею.


person Peter Kruithof    schedule 16.10.2014    source источник
comment
Я не знаю, насколько это оптимально, но вы можете использовать fgetc () для чтения numChars. Таким образом, вы будете разбивать по символам, а не по байтам.   -  person Chad    schedule 16.10.2014
comment
Если в этом файле есть строки, размер которых не помещается в памяти - это ваша основная проблема. Перейдите по строке и напишите первый сценарий, который разбивает большие строки на что-то, что действительно подходит без потери внутренней целостности.   -  person Tymoteusz Paul    schedule 17.10.2014
comment
@cwscribner fgetc является двоичным, а не многобайтовым. Он все равно будет разбиваться на многобайтовые символы.   -  person Peter Kruithof    schedule 17.10.2014
comment
@Puciek Я не согласен с тем, что это основная проблема: PHP отлично поддерживает буферизованное чтение, но не так, как это. Это было бы решение, но не то, которое я бы предпочел, поскольку я не хочу делать предположений о содержимом файла (например, разделение на определенные символы и т. Д.)   -  person Peter Kruithof    schedule 17.10.2014


Ответы (4)


Пока не могу комментировать. Но можно было бы читать данные кусками, как вы сказали, и использовать unpack ('C *', $ chunk), оттуда вы можете перебирать массив байтов и находить совпадение для вашего символа в зависимости от последовательности байтов в байтовый массив. Если вы найдете совпадение в этом массиве, замените или удалите эти байты и упакуйте () строку обратно.

P.S. : не забудьте перечитать последние несколько байтов в следующем фрагменте (чтобы у вас не было никаких проблем согласованности с последней замененной строкой).
Я не знаю, соответствует ли мой пример распаковки вашим предпочтениям, но вы можете прочитать больше здесь: распаковать документ

Вот еще один указатель того, как работает кодировка utf-8, если вы используете utf-8: кодировка utf-8

person Geo    schedule 16.10.2014
comment
Это интересно, и я думаю, что это сработает. Хотя я не уверен, зачем мне нужно перечитывать байты, если я не касаюсь исходной строки / файла? - person Peter Kruithof; 17.10.2014
comment
@PeterKruithof Да, вам не придется повторно считывать предыдущий фрагмент размером 4 байта, если вы интерпретируете биты, как в спецификации utf-8 (или в любой другой кодировке, которую вы используете). Если в последних байтах фрагмента чего-то не хватает, что помогает вам построить символ, просто продолжите синтаксический анализ в следующем фрагменте. Я говорил перечитать последние байты, чтобы во всей строке файла была непрерывность. - person Geo; 17.10.2014

В конце концов, мое решение было довольно простым. Проблема заключалась в использовании 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), он решает мою проблему.

person Peter Kruithof    schedule 17.10.2014
comment
В этом случае вам, возможно, следует подумать о бедных людях, которые приходят сюда в поисках ответа на действительно выполнение многобайтовых операций чтения и изменения заголовка вопроса или что-то в этом роде. ;) - person scy; 07.11.2014
comment
Что ж, бедные люди могут попробовать возможное решение, которое я опубликовал, и посмотреть, работает ли оно. Они могут даже опубликовать ответ, если это так! ;) - person Peter Kruithof; 08.11.2014

За последние несколько дней я потратил немало часов на поиск многобайтовой версии PHP fread(), fgetc(), file_get_contents() и т. Д.

К сожалению, я не думаю, что он существует, особенно для очень больших файлов. Итак, я написал свое (хорошо это или плохо):

Jstewmc \ Chunker \ File :: getChunk ()

Надеюсь, это не ужасно; это помогает кому-то, кроме меня; и я не выгляжу как самовозвеличивающий придурок, ТАК, ха-ха.

person Jack Clayton    schedule 03.07.2015

Не проверено. Слишком много, чтобы поместиться в комментарии, но это суть того, что я имел в виду.

$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);
person Chad    schedule 16.10.2014