CSV файл на PHP — создание и чтение

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

Ниже приведены простые PHP функции для создания и парсинга csv файлов. Никаких библиотек - для CSV это лишнее!

Формат CSV

Чтобы понимать суть вещей, нужно разобраться в спецификации CSV файлов, как устроен формат. Давайте коротко...

CSV (Comma-Separated Values — значения, разделённые запятыми) — текстовый формат, предназначенный для представления табличных данных.

  • Каждая строка файла — это одна строка таблицы.

  • Разделителем значений колонок является символ: , (запятая). Для русского языка используется ; (точка с запятой), потому что в русском запятая используется в дробных числах.

  • Значения, содержащие зарезервированные символы: " , ; \r\n или \n или \r (двойная кавычка, запятая, точка с запятой, новая строка) обрамляются двойными кавычками ".

  • Если в значении встречаются двойные кавычки ", то они должны выглядеть как двое кавычек подряд "".

  • Строка файла может разделяться символами: \r\n или \n.

Это все что нужно знать, чтобы работать с CSV!

Пример для рус. языка:

1965;Пиксель;E240 – формальдегид (опасный консервант)!;"красный, зелёный, битый";3000,00
1965;Мышка;"А правильней использовать ""Ёлочки""";;4900,00
"Н/д";Кнопка;Сочетания клавиш;"MUST USE! Ctrl, Alt, Shift";4799,00

Пример для англ. языка:

1997,Ford,E350,"ac, abs, moon",3000.00
1999,Chevy,"Venture «Extended Edition»","",4900.00
1996,Jeep,Grand Cherokee,"MUST SELL! air, moon roof, loaded",4799.00

Wiki-справка

Большинство программ под CSV понимают более общий формат DSV (delimiter-separated values — значения разделённые разделителем), допускающий использование иных символов в качестве разделителя. В частности, в русской и других локалях запятая по умолчанию зарезервирована под десятичный разделитель. Поэтому как разделитель используется точка с запятой или табуляция (формат TSV).

Сегодня под CSV понимают набор значений, разделенных какими угодно разделителями, в какой угодно кодировке с какими угодно окончаниями строк. Это значительно затрудняет перенос данных из одних программ в другие, несмотря на всю простоту формата.

Создание CSV файла в PHP

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

Важным моментом, является кодировка файла. Для корректного отображения кириллицы следует использовать кодировку cp1251 (windows-1251).

Разделитель колонок

Для русского языка символом-разделителем является ; (точка с запятой). Для англ. , (запятая).

Строки содержащие спец символы: " , ; \r\n или \n или \r должны быть в двойных кавычках "строка".

Двойные кавычки внутри строки, нужно «очистить» поставив перед кавычкой еще одну такую же кавычку: строка "csv" превратиться в "строка ""csv""". Обрамление в кавычки нужно, чтобы можно было внутри значений колонок использовать разделители ;, , и не переживать что что-то сломается при чтении файла.

Разделитель строк

Для разделения строк в csv файлах можно использовать \r\n (возврат каретки и перенос строки, CR LF). В этом случае, если нужен перенос строки внутри значения колонки, то там используется просто \n.

Также, для разделения строки, может использоваться просто \n (перенос строки, LF). В этом случае, перенос строки внутри значения колонки должен обозначаться как \r (возврат каретки CR).

Функция для создания CSV файла
/**
 * Создает CSV файл из переданных в массиве данных.
 *
 * @param array  $create_data   Массив данных из которых нужно созать CSV файл.
 * @param string $file          Путь до файла 'path/to/test.csv'. Если не указать, то просто вернет результат.
 * @param string $col_delimiter Разделитель колонок. Default: `;`.
 * @param string $row_delimiter Разделитель рядов. Default: `\r\n`.
 *
 * @return false|string CSV строку или false, если не удалось создать файл.
 *
 * @version 2
 */
function kama_create_csv_file( $create_data, $file = null, $col_delimiter = ';', $row_delimiter = "\r\n" ){

	if( ! is_array( $create_data ) ){
		return false;
	}

	if( $file && ! is_dir( dirname( $file ) ) ){
		return false;
	}

	// строка, которая будет записана в csv файл
	$CSV_str = '';

	// перебираем все данные
	foreach( $create_data as $row ){
		$cols = array();

		foreach( $row as $col_val ){
			// строки должны быть в кавычках ""
			// кавычки " внутри строк нужно предварить такой же кавычкой "
			if( $col_val && preg_match('/[",;\r\n]/', $col_val) ){
				// поправим перенос строки
				if( $row_delimiter === "\r\n" ){
					$col_val = str_replace( [ "\r\n", "\r" ], [ '\n', '' ], $col_val );
				}
				elseif( $row_delimiter === "\n" ){
					$col_val = str_replace( [ "\n", "\r\r" ], '\r', $col_val );
				}

				$col_val = str_replace( '"', '""', $col_val ); // предваряем "
				$col_val = '"'. $col_val .'"'; // обрамляем в "
			}

			$cols[] = $col_val; // добавляем колонку в данные
		}

		$CSV_str .= implode( $col_delimiter, $cols ) . $row_delimiter; // добавляем строку в данные
	}

	$CSV_str = rtrim( $CSV_str, $row_delimiter );

	// задаем кодировку windows-1251 для строки
	if( $file ){
		$CSV_str = iconv( "UTF-8", "cp1251",  $CSV_str );

		// создаем csv файл и записываем в него строку
		$done = file_put_contents( $file, $CSV_str );

		return $done ? $CSV_str : false;
	}

	return $CSV_str;

}

Теперь, чтобы сгенерировать CSV файл нужно использовать эту функцию так:

$create_data = array(
	array(
		'Заголовок 1',
		'Заголовок 2',
		'Заголовок 3',
	),
	array(
		'строка 2 "столбец 1"',
		'4799,01',
		'строка 2 "столбец 3"',
	),
	array(
		'"Ёлочки"',
		4900.01,
		'красный, зелёный',
	)
);

echo kama_create_csv_file( $create_data, THEME_PATH .'csv_file.csv' );

/* Получим

Заголовок 1;Заголовок 2;Заголовок 3
"строка 2 ""столбец 1""";"4799,00";"строка 2 ""столбец 3"""
"""Ёлочки""";4900.01;"красный, зелёный"

*/

Чтение CSV файла в PHP

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

Есть еще функция fgetcsv(), но она оказалась капризной и не всегда работает как нужно (может перепутать переносы строк)...

Вариант на базе функции str_getcsv():

/**
 * Читает CSV файл и возвращает данные в виде массива.
 *
 * @param string $file_path      Путь до csv файла.
 * @param array  $file_encodings
 * @param string $col_delimiter  Разделитель колонки (по умолчанию автоопределине)
 * @param string $row_delimiter  Разделитель строки (по умолчанию автоопределине)
 *
 * @version 6
 */
function kama_parse_csv_file( $file_path, $file_encodings = ['cp1251','UTF-8'], $col_delimiter = '', $row_delimiter = '' ){

	if( ! file_exists( $file_path ) ){
		return false;
	}

	$cont = trim( file_get_contents( $file_path ) );

	$encoded_cont = mb_convert_encoding( $cont, 'UTF-8', mb_detect_encoding( $cont, $file_encodings ) );

	unset( $cont );

	// определим разделитель
	if( ! $row_delimiter ){
		$row_delimiter = "\r\n";
		if( false === strpos($encoded_cont, "\r\n") )
			$row_delimiter = "\n";
	}

	$lines = explode( $row_delimiter, trim($encoded_cont) );
	$lines = array_filter( $lines );
	$lines = array_map( 'trim', $lines );

	// авто-определим разделитель из двух возможных: ';' или ','.
	// для расчета берем не больше 30 строк
	if( ! $col_delimiter ){
		$lines10 = array_slice( $lines, 0, 30 );

		// если в строке нет одного из разделителей, то значит другой точно он...
		foreach( $lines10 as $line ){
			if( ! strpos( $line, ',') ) $col_delimiter = ';';
			if( ! strpos( $line, ';') ) $col_delimiter = ',';

			if( $col_delimiter ) break;
		}

		// если первый способ не дал результатов, то погружаемся в задачу и считаем кол разделителей в каждой строке.
		// где больше одинаковых количеств найденного разделителя, тот и разделитель...
		if( ! $col_delimiter ){
			$delim_counts = array( ';'=>array(), ','=>array() );
			foreach( $lines10 as $line ){
				$delim_counts[','][] = substr_count( $line, ',' );
				$delim_counts[';'][] = substr_count( $line, ';' );
			}

			$delim_counts = array_map( 'array_filter', $delim_counts ); // уберем нули

			// кол-во одинаковых значений массива - это потенциальный разделитель
			$delim_counts = array_map( 'array_count_values', $delim_counts );

			$delim_counts = array_map( 'max', $delim_counts ); // берем только макс. значения вхождений

			if( $delim_counts[';'] === $delim_counts[','] )
				return array('Не удалось определить разделитель колонок.');

			$col_delimiter = array_search( max($delim_counts), $delim_counts );
		}

	}

	$data = [];
	foreach( $lines as $key => $line ){
		$data[] = str_getcsv( $line, $col_delimiter ); // linedata
		unset( $lines[$key] );
	}

	return $data;
}

Использование:

$data = kama_parse_csv_file( '/path/to/file.csv' );
print_r( $data );

Конвертация .lsx, .xlsx файла в .csv

Чтобы перевести Excel файл в CSV, нужно открыть его в Excel и сохранить в формате .csv:

Если такую конвертацию нужно сделать программно, смотрите в сторону онлайн конвертеров с API или готовых библиотек.

-

Сталкивались с такой задачей и знаете более универсальный способ? Прошу поделиться в комментариях.

Я столкнулся и потратил несколько часов перебирая всякий не универсальный «мусор» из гугла - то одно не работает, то другое, то библиотеку нужно ставить «хитро-мудрую»... Так и появилась эта заметка...

Как отдать сгенерированный CSV файл для загрузки

Для этого нужно установить заголовки:

<?php

header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="file.csv"' );

$create_data = [
	[
		'Заголовок 1',
		'Заголовок 2',
		'Заголовок 3',
	],
];

echo kama_create_csv_file( $create_data );