Определение разделителя в 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)) { //... } }В итоге получился масштабируемый модуль импорта, который уже работает в реальном проекте :)