Как нормализовать CSV-контент в PHP?

0

Проблема:

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

Может ли кто-нибудь предложить лучшее решение?

Почему бы не использовать fputcsv/fgetcsv?

Потому как:

  • он требует по крайней мере PHP 5.1.0 (который иногда недоступен)
  • он может читать только файлы, но не из строки. хотя иногда вход не является файлом (например, если вы получаете CSV из электронной почты)
  • размещение содержимого во временном файле может быть недоступно из-за политик безопасности.

Почему/какая нормализация?

Нормализуйте таким образом, чтобы ограждение охватывало каждое поле. Потому что приложение может быть необязательным и разным для каждой строки и для каждого поля. Это может произойти, если вы выполняете нечистые/неполные спецификации и/или используете CSV-контент из разных источников/программ/разработчиков.

Пример вызова функции:

$csvContent = "'a a',\"b\",c,1, 2 ,3 \n a a,'bb',cc, 1, 2, 3 ";
echo "BEFORE:\n$csvContent\n";
normaliseCSV($csvContent);
echo "AFTER:\n$csvContent\n";

Вывод:

BEFORE:
'a a',"b",c,1, 2 ,3 
a a,'bb',cc, 1, 2, 3 
AFTER:
"a a","b","c","1","2","3"
"a a","bb","cc","1","2","3"
  • 0
    Tmpfile не доступен по умолчанию, если вы специально не отключили его с ограничениями безопасности?
Теги:
csv
parsing

4 ответа

1

Чтобы конкретно рассмотреть вашу озабоченность по поводу f*csv работающего только с файлами:

  1. Поскольку PHP 5.3 есть str_getcsv.
  2. По крайней мере PHP> = 5.1 (и я очень надеюсь, что самое старое, с чем вам придется иметь дело в эти дни), вы можете использовать обтекатели потоков:

    $buffer = fopen('php://memory', 'r+');
    fwrite($buffer, $string);
    rewind($buffer);
    
    fgetcsv($buffer) ..
    

    Или, очевидно, наоборот, если вы хотите использовать fputcsv.

  • 0
    deceze хороший ответ, но для тех PHP <5.1 я думаю, что мой ответ ниже будет хорошим подходом ;-) жду ваших комментариев
0

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

Я разделил логику в трех основных сценариях:

A: CHAR is a separator
B: CHAR is a Fuc$€/& quotation
C: CHAR is a Value

Получение для этого класса оружия (включая журнал для него) для нашего арсенала:

<?php

    Class CSVParser 
    {
        #basic requirements
        public $input;
        public $separator;

        public $currentQuote;
        public $insideQuote;
        public $result;
        public $field;

        public $quotation = array();
        public $parsedArray = array();

        # for logging purposes only 
        public $logging = TRUE;
        public $log = array();

        function __construct($input, $separator, $quotation=array())
        {
            $this->separator = $separator;
            $this->input     = $input;
            $this->quotation = $quotation;
        }
        /**
        * The main idea is to go through the string to parse char by char to analize
        * when a complete field is detected it´ll be quoted according and added to an array 
        */
        public function parse()
        {
            for($i = 0; $i < strlen($this->input); $i++){
                $this->processStream($i);               
                }
            foreach($this->parsedArray as $value)
            {
                if(!is_null($value))
                    $this->result .= '"'.addslashes($value).'",';
            }
            return rtrim($this->result, ',');
        }


        private function processStream($i)
        {
            #A case (its a separator)
            if($this->input[$i]===$this->separator){
                $this->log("A", $this->input[$i]);
                if($this->insideQuote){
                    $this->field .= $this->input[$i];
                }else
                {
                    $this->saveField($this->field);
                    $this->field = NULL;
                }
            }

            #B case (its a f"·%$% quote)
            if(in_array($this->input[$i], $this->quotation)){
                $this->log("B", $this->input[$i]);
                if(!$this->insideQuote){
                    $this->insideQuote  = TRUE;
                    $this->currentQuote = $this->input[$i];
                }
                else{
                    if($this->currentQuote===$this->input[$i]){
                        $this->insideQuote = FALSE;
                        $this->currentQuote ='';
                        $this->saveField($this->field);
                        $this->field = NULL;        
                    }else{
                        $this->field .= $this->input[$i];
                    }

                }
            }

            #C case (its a value :-) )
            if(!in_array($this->input[$i], array_merge(array($this->separator), $this->quotation))){
                $this->log("C", $this->input[$i]);
                $this->field .= $this->input[$i];
            }


        }

        private function saveField($field)
        {
            $this->parsedArray[] = $field;
        }

        private function log($type, $value)
        {
            if($this->logging){
                $this->log[] = "CASE ".$type." WITH ".$value." AS VALUE";
            }
        }
    }

и пример того, как его использовать, будет:

$original = 'a,"ab",\'ab\'';
$test = new CSVParser($original, ',', array('"', "'"));

    echo "<PRE>ORIGINAL: ".$original."</PRE>";
    echo "<PRE>PARSED: ".$test->parse()."</PRE>";
    echo "<pre>";
    print_r($test->log);
    echo "</pre>";

и вот результаты:

ORIGINAL: a,"ab",'ab'
PARSED: "a","ab","ab"
Array
(
    [0] => CASE C WITH a AS VALUE
    [1] => CASE A WITH , AS VALUE
    [2] => CASE B WITH " AS VALUE
    [3] => CASE C WITH a AS VALUE
    [4] => CASE C WITH b AS VALUE
    [5] => CASE B WITH " AS VALUE
    [6] => CASE A WITH , AS VALUE
    [7] => CASE B WITH ' AS VALUE
    [8] => CASE C WITH a AS VALUE
    [9] => CASE C WITH b AS VALUE
    [10] => CASE B WITH ' AS VALUE
)

У меня могут быть ошибки с тех пор, как я выделил только 25 минут, поэтому любые комментарии будут оценены отредактированными.

  • 0
    Ваша версия сбрасывает пустые значения. codepad.org/MjsMH51h
  • 0
    я предполагаю, что это только потому, что я поместил в массив цитат параметры "и", без двойных кавычек ...
0

Хотя я согласен с @deceze, что в наши дни вы можете ожидать почти 5.1, я уверен, что есть некоторые внутренние серверы компаний, которые не хотят обновляться.

Я изменил ваш метод, чтобы иметь возможность использовать разделители полей и строк между двойными кавычками или в вашем случае значение $encloser.

<?php

/*
In regards to the specs on http://tools.ietf.org/html/rfc4180 I use the following rules:

- "Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes." 
- "If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote." 

Exception:
Even though the specs says use double quotes, I 'm using your $encloser variable

*/

echo normaliseCSV('a,b,\'c\',"d,e","f","g""h""i","""j"""' . "\n" . "\"k\nl\nm\"");


function normaliseCSV($csv,$lineseperator = "\n", $fieldseperator = ',', $encloser = '"')
{
    //We need 4 temporary replacement values
    //line seperator, fieldseperator, double qoutes, triple qoutes
    $keys = array();
    while (count($keys)<3) {
        $tmp = "##".md5(rand().rand().microtime())."##";
        if (strpos($csv, $tmp)===false) {
            $keys[] = $tmp;
        }
    }

    //first we exchange "" (double $encloser) and """ to make sure its not exploded
    $csv = str_replace($encloser.$encloser.$encloser, $keys[0], $csv);
    $csv = str_replace($encloser.$encloser, $keys[0], $csv);

    //Explode on $encloser
    //Every odd index is within quotes
    //Exchange line and field seperators for something not used.
    $content = explode($encloser,$csv);
    $len = count($content);
    if ($len>1) {
        for ($x=1;$x<$len;$x=$x+2) {
            $content[$x] = str_replace($lineseperator,$keys[1], $content[$x]);
            $content[$x] = str_replace($fieldseperator,$keys[2], $content[$x]);
        }
    }
    $csv = implode('',$content);


    $csvArray = explode ($lineseperator,$csv);
    foreach ($csvArray as &$line)
    {
            $lineArray = explode ($fieldseperator,$line);
            foreach ($lineArray as &$field)
            {
                    $val = trim($field,"\0\t\n\x0B\r '");
                    //put back the exchanged values
                    $val = str_replace($keys[0],$encloser.$encloser,$val);
                    $val = str_replace($keys[1],$lineseperator,$val);
                    $val = str_replace($keys[2],$fieldseperator,$val);

                    $val = $encloser.$val.$encloser;


                    $field = $val;

            }
            $line = implode ($fieldseperator,$lineArray);
    }
    $csv = implode ($lineseperator,$csvArray);


    return $csv;

}
?>

Результатом будет:

"a","b","c","d,e","f","g""h""i","""j"""
"k
l
m"

Пример Codepad

  • 0
    только что проверил это. Это решение не работает для кавычек пустых значений. преобразует "" в "" "". В то время как обычные пустые значения, такие как ,,, правильно преобразуются в "", .... Пустые строки действительны как CSV afaik.
  • 0
    @KenyakornKetsombut и Hugo Delsing, пожалуйста, проверьте мой ответ ниже, я думаю, что получил его ...
0

Это возможное решение. Но он не учитывает случай, когда разделитель (,) может быть включен во входящие строки.

function normaliseCSV(&$csv,$lineseperator = "\n", $fieldseperator = ',', $encloser = '"')
{
    $csvArray = explode ($lineseperator,$csv);
    foreach ($csvArray as &$line)
    {
        $lineArray = explode ($fieldseperator,$line);
        foreach ($lineArray as &$field)
        {
            $field = $encloser.trim($field,"\0\t\n\x0B\r \"'").$encloser;
        }
        $line = implode ($fieldseperator,$lineArray);
    }
    $csv = implode ($lineseperator,$csvArray);
}

Это простая цепочка explodeexplodetrimimplodeimplode.

  • 0
    так вы говорите, что разделитель полей может быть частью содержимого поля без инкапсуляции в кодировщиках? ....
  • 0
    @DiegoCoderPlus Нет. Это сделало бы CSV неправильным и недетерминированным. Эта функция не обрабатывает входящие запятые, даже если они правильно заключены.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню