PHP — массив в CSV по столбцу

проблема: у меня есть ассоциативный массив, где все ключи представляют заголовки csv, а значения в каждом массиве $key => представляют элементы в этом столбце.

Исследования. Насколько мне известно, fputcsv любит работать построчно, но этот массив на основе столбцов усложняет задачу. Я не нашел никакой функции, которая выполняет это.

Пример:

Array(
    ['fruits'] => Array(
        [0] => 'apples',
        [1] => 'oranges',
        [2] => 'bananas'
    ),
    ['meats'] => Array(
        [0] => 'porkchop',
        [1] => 'chicken',
        [2] => 'salami',
        [3] => 'rabbit'
    ),
)

Должен стать:

fruits,meats
apples,porkchop
oranges,chicken
bananas,salami
,rabbit

Почему это сложно:

Вам нужно знать максимальное количество строк, чтобы сделать пустые места.


person amurrell    schedule 24.09.2014    source источник


Ответы (2)


Пришлось писать свою функцию. Подумал, может быть, это может помочь кому-то еще!

/*
 * The array is associative, where the keys are headers
 * and the values are the items in that column.
 * 
 * Because the array is by column, this function is probably costly.
 * Consider a different layout for your array and use a better function.
 * 
 * @param $array array The array to convert to csv.
 * @param $file string of the path to write the file.
 * @param $delimeter string a character to act as glue.
 * @param $enclosure string a character to wrap around text that contains the delimeter
 * @param $escape string a character to escape the enclosure character.
 * @return mixed int|boolean result of file_put_contents.
 */

function array_to_csv($array, $file, $delimeter = ',', $enclosure = '"', $escape = '\\'){
    $max_rows = get_max_array_values($array);
    $row_array = array();
    $content = '';
    foreach ($array as $header => $values) {
    $row_array[0][] = $header;
    $count = count($values);
    for ($c = 1; $c <= $count; $c++){
        $value = $values[$c - 1];
        $value = preg_replace('#"#', $escape.'"', $value);
        $put_value = (preg_match("#$delimeter#", $value)) ? $enclosure.$value.$enclosure : $value;
        $row_array[$c][] = $put_value;
    }
    // catch extra rows that need to be blank
    for (; $c <= $max_rows; $c++) {
        $row_array[$c][] = '';
    }
    }
    foreach ($row_array as $cur_row) {
    $content .= implode($delimeter,$cur_row)."\n";
    }
    return file_put_contents($file, $content);
}

И это:

/*
 * Get maximum number of values in the entire array.
 */
function get_max_array_values($array){
    $max_rows = 0;
    foreach ($array as $cur_array) {
    $cur_count = count($cur_array);
    $max_rows = ($max_rows < $cur_count) ? $cur_count : $max_rows;
    }
    return $max_rows;
}

Новый путь (с использованием класса)

Некоторое время спустя я написал для этого класс, который я предоставлю всем, кто ищет его сейчас:

class CSVService {

    protected $csvSyntax;

    public function __construct()
    {
        return $this;
    }

    public function renderCSV($contents, $filename = 'data.csv')
    {
        header('Content-type: text/csv');
        header('Content-Disposition: attachment; filename="' . $filename . '"');

        echo $contents;
    }

    public function CSVtoArray($filename = '', $delimiter = ',') {
        if (!file_exists($filename) || !is_readable($filename)) {
            return false;
        }

        $headers = null;
        $data = array();
        if (($handle = fopen($filename, 'r')) !== false) {
            while (($row = fgetcsv($handle, 0, $delimiter, '"')) !== false) {
                if (!$headers) {
                    $headers = $row;
                    array_walk($headers, 'trim');
                    $headers = array_unique($headers);
                } else {
                    for ($i = 0, $j = count($headers); $i < $j;  ++$i) {
                        $row[$i] = trim($row[$i]);
                        if (empty($row[$i]) && !isset($data[trim($headers[$i])])) {
                            $data[trim($headers[$i])] = array();
                        } else if (empty($row[$i])) {
                            continue;
                        } else {
                            $data[trim($headers[$i])][] = stripcslashes($row[$i]);
                        }
                    }
                }
            }
            fclose($handle);
        }
        return $data;
    }

    protected function getMaxArrayValues($array)
    {
        return array_reduce($array, function($carry, $item){
            return ($carry > $c = count($item)) ? $carry : $c;
        }, 0);
    }

    private function getCSVHeaders($array)
    {
        return array_reduce(
                array_keys($array),
                function($carry, $item) {
                    return $carry . $this->prepareCSVValue($item) . $this->csvSyntax->delimiter;
                }, '') . "\n";
    }

    private function prepareCSVValue($value, $delimiter = ',', $enclosure = '"', $escape = '\\')
    {
        $valueEscaped = preg_replace('#"#', $escape . '"', $value);
        return (preg_match("#$delimiter#", $valueEscaped)) ?
                $enclosure . $valueEscaped . $enclosure : $valueEscaped;
    }

    private function setUpCSVSyntax($delimiter, $enclosure, $escape)
    {
        $this->csvSyntax = (object) [
            'delimiter' => $delimiter,
            'enclosure' => $enclosure,
            'escape'    => $escape,
        ];
    }

    private function getCSVRows($array)
    {
        $n = $this->getMaxArrayValues($array);
        $even = array_values(
            array_map(function($columnArray) use ($n) {
                for ($i = count($columnArray); $i <= $n; $i++) {
                    $columnArray[] = '';
                }
                return $columnArray;
            }, $array)
        );

        $rowString = '';

        for ($row = 0; $row < $n; $row++) {
            for ($col = 0; $col < count($even); $col++) {
                $value = $even[$col][$row];
                $rowString .=
                        $this->prepareCSVValue($value) .
                        $this->csvSyntax->delimiter;
            }
            $rowString .= "\n";
        }

        return $rowString;
    }

    public function arrayToCSV($array, $delimiter = ',', $enclosure = '"', $escape = '\\', $headers = true) {
        $this->setUpCSVSyntax($delimiter, $enclosure, $escape);

        $headersString = ($headers) ? $this->getCSVHeaders($array) : '';

        $rowsString = $this->getCSVRows($array);


        return $headersString . $rowsString;
    }

}
person amurrell    schedule 24.09.2014
comment
Работайте идеально. Примите ваш ответ, хотя. - person Adam Sinclair; 25.09.2014
comment
Я жду, прежде чем принять свой собственный ответ, потому что у кого-то может быть лучше: D - person amurrell; 25.09.2014
comment
Оба терпят неудачу на preg_match. Предупреждение: preg_match() ожидает, что параметр 2 будет строкой, задан массив... Результат: col1, col2, col3\nArray,Array,Array - person danielh; 16.07.2018
comment
Я раньше не сталкивался с этой проблемой... почему второй параметр является массивом? это должна быть строка? Какова структура ваших данных? - person amurrell; 18.07.2018

$data = array(
    'fruits' => array(
        'apples',
        'oranges',
        'bananas'
    ),
    'meats' => array(
        'porkchop',
        'chicken',
        'salami',
        'rabbit'
    ),
);

$combined = array(array('fruits', 'meats'));

for($i = 0; $i < max(count($data['fruits']), count($data['meats'])); $i++)
{   
    $row = array(); 

    $row[] = isset($data['fruits'][$i]) ? $data['fruits'][$i] : '';
    $row[] = isset($data['meats'][$i])  ? $data['meats'][$i]  : '';

    $combined[] = $row;
}

ob_start();

$fp = fopen('php://output', 'w');

foreach($combined as $row)
  fputcsv($fp, $row);

fclose($fp);

$data = ob_get_clean();

var_dump($data);

Преобразование в csv и разбор массива можно выполнить в одном цикле. Или вы имели в виду, что столбцов может быть больше? Этот код можно легко изменить для общего типа массива. Таким образом, для любого количества столбцов

$data = array(
    'fruits' => array(
        'apples',
        'oranges',
        'bananas'
    ),
    'meats' => array(
        'porkchop',
        'chicken',
        'salami',
        'rabbit'
    ),
);
$heads = array_keys($data);
$maxs = array();
foreach($heads as $head)
  $maxs[] = count($data[$head]);
ob_start();
$fp = fopen('php://output', 'w');
fputcsv($fp, $heads);
for($i = 0; $i < max($maxs); $i++)
{   
    $row = array(); 
    foreach($heads as $head)
       $row[] = isset($data[$head][$i]) ? $data[$head][$i] : '';
    fputcsv($fp, $row);   
}
fclose($fp);
$data = ob_get_clean();
var_dump($data);
person Cheery    schedule 24.09.2014
comment
Теперь вопрос в том, какое из этих решений менее затратно/более эффективно, особенно с огромными массивами (много столбцов). - person amurrell; 25.09.2014
comment
@amurrell мой код должен без проблем работать с огромными массивами, промежуточные данные нигде не хранятся, никаких регулярных выражений. одна вещь, которую можно улучшить, - это переместить max() из цикла в отдельную переменную. он просто читает строку за строкой, заменяя отсутствующие элементы пустой строкой и преобразуя каждую строку в строку csv, ничего больше. буферизация вывода предназначена только для сохранения строки csv в переменной, вы можете сохранить ее непосредственно в файл без нее. - person Cheery; 25.09.2014