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

12.06.2020

Задача: импортировать определенные данные из csv файлов с произвольной структурой столбцов и неопределенным разделителем. На php.

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

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

Прежде чем приступить к реализации импорта, необходимо распарсить этот самый csv файл (RFC 4180 en, RFC 4180 rus).

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

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

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

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

Использовать это решение можно следующим образом:

  • открыть файл на чтение
  • прочитать первую строку при помощи fgets
  • определить разделитель
  • вернуть курсор в начало файла
•••
php
if(!($handle = fopen($sPathFile, "r"))) return; $sHeader = fgets($handle); $sDelimiter = preg_replace('/((?:\"|\'.*?\"|\')|(?:\w+|\n|\r|\0|\s))/isU', '', $str)[0]; if(!in_array($sDelimiter, [",", ";", "|", "\t"])) return; fseek($handle, 0);

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

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

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

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

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

Не долго думая я решил замапить все это дело:

•••
php
$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 файле:

•••
php
// ключ - поле сущности, значение - номер стоблца в 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; // ищем позицию поля-синонима в CSV файле foreach($aCol as $value) { if( ($iNum = array_search($value, $aHeader) ) !== false) return $iNum; } return null; }

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

Минимальная проверка и сохранение

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

•••
php
// в 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 в дальнейшем при импорте используется для проверки наличия обязательных полей пока еще "сырой" сущности "клиент".

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

•••
php
while (($data = fgetcsv($handle, 1000, $sDel)) !== FALSE) { $aUser = []; foreach($aTheres as $key => $value) $aUser[$key] = $data[$value]; if(ImportEnoughData($aUser)) { //... } }