Импорт из произвольного CSV файла на PHP

Категория: Решение задач | Скилл: php | Дата: 12.06.2020
Задача: импортировать определенные данные из csv файлов с произвольной структурой столбцов и неопределенным разделителем.

Определение разделителя в CSV файле

Прежде чем приступить к реализации импорта, необходимо распарсить этот самый csv файл (RFC 4180 en, RFC 4180 rus). Несмотря на название csv (Comma-Separated Values - значения, разделённые запятыми) на практике в csv файлах значения могут разделяться символами , ; | \t что свойственно для dsv (delimiter-separated values - значения разделённые разделителем) файлов.

В php есть функция fgetcsv для чтения строки из файла и разбора csv данных в индексировнный массив. Однако, эта функция должна явно получить символ разделителя, чтобы корректно отработать. Штатных средств для определния разделителя мне не удалось найти, а интернеты предлагали слишком громоздкие решения, поэтому сделал свое маленькое решение на регулярках:

$aDelimiter = preg_replace('/((?:\"|\'.*?\"|\')|(?:\w+|\n|\r|\0|\s))/isU', '', $str)[0];

Суть в том, чтобы заменить значения столбцов (и не важно заключены ли они в кавычки, одинарные или двойные) на пустые строки, то есть удалить значение столбцов (пробелы и иные лишние символы), оставив только одни разделители, после чего можно взять нулевой символ, который и будет разделителем. Удаление \n \r \0 здесь будет лишним, но эта регулярка писалась в расчете на ручной парсинг csv данных, так что пусть останется в этом коде :)

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

if(!($handle = fopen($sPathFile, "r")))
  return;

$sHeader = fgets($handle);
$aDelimiter = preg_replace('/((?:\"|\'.*?\"|\')|(?:\w+|\n|\r|\0|\s))/isU', '', $str)[0];
if(!in_array($aDelimiter, [",", ";", "|", "\t"]))
  return;
  
fseek($handle, 0);

Теперь можно построчно читать файл функцией fgetcsv .

Сопоставление данных в CSV файле с данными сущности

Конкретно в моей задаче необходимо было импортировать базу данных клиентов в сервис uppleseen.com, где у сущности "клиент" есть несколько важных полей (для импорта), а некоторые из них являются обязательными (для существования самой сущности).

CSV файл для импорта надо было экспортировать из mail.ru почты (контакты) и из интернет-магазина на advantshop (покупатели). Естественно в обоих случаях заголовочные столбцы разные.

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

$aMatching = [
	"lastname"    => ["last Name"],
	"firstname"   => ["first Name", "name"],
	"patronymic"  => ["middle Name"],
	"email"       => ["e-mail address", "e-mail"],
	"phone"       => ["mobile phone"],
	"date_birth"  => ["birthDay"],
	"segment"     => ["categories"]
];

Здесь в коде ключи массива aMatching это поля сущности "клиент", а значения ключей это линейный массив с возможными именами заголовочных столбцов в импортируемом csv файле (обязательно в нижнем регистре, потому что дальше будет регистронезависимое сравнение). Возможные имена брал из импортируемых csv файлов.

Имея массив имен заголовочных столбцов можно сопоставить каждое импортируемое поле сущности "клиент" с номером столбца в csv файле:

//ключ - поле сущности, значение - номер стоблца в csv файле
$aTheres = [];
	
foreach($aMatching as $key => $value)
{
	if( ($iNum = ImportGetNumCol($key, $aHeader) ) !== null)
		$aTheres[$key] = $iNum;
}

//...
	
function ImportGetNumCol($sColumn, $aHeader)
{
	//предположим что $aMatching в этой области видимости

	$aCol = [];
	if(array_key_exists($sColumn, $aMatching))
		$aCol = $aMatching[$sColumn];
	
	$aCol[] = $sColumn;

	foreach($aCol as $value)
	{
		if( ($iNum = array_search($value, $aHeader) ) !== false)
			return $iNum;
	}

	return null;
}

Внимательный читатель наверняка заметил, что функция ImportGetNumCol проверяет не только по значению ключа, но и по самому ключу ($aCol[] = $sColumn;), так как возможно на вход поступил ранее экспортированный файл из целевого хранилища (где имена заголовочных столбцов это имена полей сущности).

Теперь когда есть массив сопоставления, необходимо проверить есть ли в csv файле столбцы с обязательной информацией (в моем случае обязательными были email или номер телефона):

//в aUser передается aTheres из предыдущего куска
function ImportEnoughData($aUser)
{
	return (
		array_key_exists("email", $aUser) && strlen($aUser["email"]) > 0 || 
		array_key_exists("phone", $aUser) && strlen($aUser["phone"]) > 0
	);
}

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

Дальше все зависит от конкретной реализации работы с сущностью "клиент", но подготовка сырых данных для вставки в базу данных выглядит так:

while ( ($data = fgetcsv($handle, 1000, $sDel) ) !== FALSE)
{
	$aUser = [];

	foreach($aTheres as $key => $value)
		$aUser[$key] = $data[$value];
		
	if(ImportEnoughData($aUser))
	{
	  //...
	}
}

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

Проекты
SkyXEngine, PHP-API, S4G
Категории
В разработке :)
Популярное
В разработке :)