PHP: Каков эффективный способ анализа текстового файла, содержащего очень длинные строки? - PullRequest
8 голосов
/ 01 апреля 2010

Я работаю над парсером в php, который предназначен для извлечения записей MySQL из текстового файла. Конкретная строка может начинаться со строки, соответствующей таблице, в которую нужно вставить записи (строки), за которыми следуют сами записи. Записи разделяются обратной косой чертой, а поля (столбцы) разделяются запятыми. Для простоты, давайте предположим, что у нас есть таблица, представляющая людей в нашей базе данных, с полями: Имя, Фамилия и Занятие. Таким образом, одна строка файла может выглядеть следующим образом

[Люди] = "\ Хан, Соло, Контрабандист \ Люк, Скайуокер, Джедай ..."

Где эллипсы (...) могут быть дополнительными людьми. Одним простым подходом может быть использование fgets() для извлечения строки из файла и использование preg_match() для извлечения имени таблицы, записей и полей из этой строки.

Однако, давайте предположим, что у нас есть очень много персонажей из Звездных войн, которые нужно отслеживать. На самом деле, так много, что эта строка имеет длину 200 000 символов / байт. В таком случае использование вышеуказанного подхода для извлечения информации из базы данных кажется немного неэффективным. Сначала вы должны прочитать сотни тысяч символов в память, а затем прочитать обратно этих же символов, чтобы найти совпадения с регулярным выражением.

Существует ли способ, аналогичный методу Java String next(String pattern) класса Scanner, построенному с использованием файла, который позволяет сопоставлять шаблоны в строке при сканировании файла?

Идея состоит в том, что вам не нужно сканировать один и тот же текст дважды (чтобы прочитать его из файла в строку, а затем сопоставить с шаблонами) или избыточно сохранить текст в памяти (в обеих строках строки файла). и соответствующие шаблоны). Приведет ли это даже к значительному увеличению производительности? Трудно точно сказать, что делают PHP или Java за кулисами.

Вкл. fgetcsv()
Эта функция позволяет очень легко разбивать строки в файле на основе некоторого разделителя, и я уверен, что он проверяет символ разделитель за символом при сканировании файла. Однако проблема в том, что я ищу два разделителя, а fgetcsv() принимает только один. Например:

Я мог бы использовать ',' в качестве разделителя. При условии, что я изменил формат файла, чтобы у него также были запятые с обратной косой чертой, я мог прочитать всю строку в массив полей. Проблема в том, что мне нужно повторить по всем полям, чтобы определить, где начинаются и заканчиваются записи, и подготовить sql. Точно так же, если я использую '\' в качестве разделителя (здесь используется одна обратная косая черта, то мне нужно повторить по всем записям, чтобы извлечь поля и подготовить sql.

То, что я пытаюсь сделать, это проверить и запятые и обратную косую черту (и, возможно, другие вещи, такие как [имя таблицы]) одним махом на максимальную производительность. Если бы fgetcsv() позволил мне указать несколько разделителей (или регулярное выражение) или позволил мне изменить то, что он считает "концом строки" (с \ n или \ n \ r на просто \), тогда это сработало бы отлично, но это кажется невозможным.

Ответы [ 2 ]

3 голосов
/ 01 апреля 2010

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

while($c = fgetc($fp)) {
  if($c == ',') {
    $fields[] = implode(null,$accumulator);
    $accumulator = array();
  } else if($c == '\\') {
    save_fields_to_mysql($fields);
    $fields = array();
    $accumulator = array();
  } else
    $accumulator[] = $c;
}

Это, вероятно, сработает для вас, если вы уверены, что ваши поля никогда не содержат вашего поля или разделителей записей в качестве данных.

Если это возможно, вам нужно придумать escape-последовательность для представления литеральных значений вашего поля и разделителя записей (и, возможно, также вашей escape-последовательности). Давайте предположим, что это так, и примем знак% в качестве escape-символа:

define('ESCAPED',1);
define('NORMAL',0);

$readState = NORMAL;
while($c = fgetc($fp)) {
  if($readState == ESCAPED) {
    $accumulator[] = $c;
    $readState = NORMAL;
  } else if($c == '%') {
    $readState = ESCAPED;
  } else if($c == ',') {
    $fields[] = implode(null,$accumulator);
    $accumulator = array();
  } else if($c == '\\') {
    save_fields_to_mysql($fields);
    $fields = array();
    $accumulator = array();
  } else
    $accumulator[] = $c;
}

т. Е. Любое вхождение% устанавливает переменную состояния, которая указывает на следующем проходе цикла, какой бы символ мы ни читали, он будет восприниматься как литеральные данные, которые являются частью поля, а не символом.

Это должно поддерживать минимальное использование памяти.

[Обновить] Как насчет эффективности ввода / вывода?

Один комментатор правильно указал, что эта иллюстрация довольно интенсивно использует ввод-вывод, и, поскольку ввод-вывод, как правило, является наиболее дорогостоящей операцией с точки зрения времени, вполне возможно, что это не будет приемлемым решением.

На другом конце спектра у нас есть опция буферизации всего файла в памяти, которая включает в себя оригинальные решения с интенсивным использованием памяти, о которых Аскер упоминал, но хотел избежать. Счастливый носитель, вероятно, находится где-то посередине: мы можем использовать предел чтения, который вы можете передать в качестве второго аргумента fgets(), чтобы набрать несколько большое (но не до смешного большого) количество символов в одном вводе-выводе и затем обрабатывать этот буфер посимвольно вместо потока ввода-вывода, заполняя его, когда мы прожигаем буфер.

Это действительно делает процесс чтения немного более интенсивным, чем $c = fgetc($fp), потому что вы должны следить за тем, где вы находитесь в буфере и насколько заполнен буфер, а также где вы в файле. Вы можете сделать это с помощью ряда флагов и индексных переменных внутри цикла чтения, если хотите, но может быть удобнее иметь абстракцию примерно так:

class StrBufferedChrReader {

    private $_filename;
    private $_fp; 

    private $_bufferIdx;
    private $_bufferMax = 2048;
    private $_buffer;

    function __construct($filename=null,$bufferMax=null) {
        if($bufferMax) $this->_bufferMax = $bufferMax;
        if($filename) $this->open($filename);
    }

    function _refillBuffer() {
        if($this->_fp) {
            $this->_buffer = fgets($this->_fp,$this->_bufferMax + 1);
            $this->_bufferIdx = 0;
            return $this->_buffer;
        }
        return false;
    }

    function open($filename=null) {
        if($filename) $this->_filename = $filename;
        if($this->_fp = fopen($this->_filename)) 
            $this->_refillBuffer();
        return $this->_fp;
    }

    function getc() {
        if($this->_bufferIdx == $this->_bufferMax) 
            if(!$this->_refillBuffer())
                return false;
        return $this->_buffer[$this->_bufferIdx++];
    }

    function close() {
        $this->_buffer = null;
        $this->_bufferIdx = null;
        return fclose($this->_fp);
    }
}

Что вы можете использовать в любом из вышеприведенных циклов, например:

$r = new StrBufferedChrReader($filename,$bufferSize);
while($c = $r->getc()) {
    ...

Примерно так можно выделить много разных точек вдоль континуума между решением с интенсивным использованием памяти и решением с интенсивным вводом / выводом, изменив значение $ bufferSize. Больше $ bufferSize, больше использования памяти, меньше операций ввода-вывода. Меньший $ bufferSize, меньшее использование памяти, больше операций ввода-вывода.

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

0 голосов
/ 01 апреля 2010

Может быть, использовать функцию strtok ()?

$ string = "Привет, мир. Прекрасный день сегодня."; $ token = strtok ($ string, "");

while ($ token! = False) { echo "$ token
"; $ token = strtok (""); }

...