Самописный импорт (получение данных из *.xls).

Аватар пользователя semasping semasping 2 декабря 2012 в 19:38

Это первая часть из темы "3. Migrate, Feeds и свой вариант импорта и обновления данных."

Мы напишем свой собственный модуль для импорта данных из excel файла. Давайте представим что нам нужно импортировать список планшетов с некоторыми характеристиками на наш сайт.
Есть Excel файл с колонками :

|Производитель|    Модель           | Описание               | Внутренняя память                 | Изображение
--------------|----------------|------------------------|-----------------------------------|-------------
|Таксономия   | Текстовое поле | Большое текстовое поле      | Таксономия с множественным вводом | Изображение
--------------|----------------|------------------------|-----------------------------------|-------------
|Объединенное тестовое поле    | Уже готовое поле Body  |
|являющееся заголовком ноды    |                

Создадим тип материала Планшет, с данными полями. Тип полей указан во второй строке.
Получаем :
Файл который мы будем импортировать: Файл для импорта
Теперь приступим к написанию модуля. Модуль в процессе написания я буду выкладывать на GitHub. Чтобы можно было взять состояние модуля на любом этапе. (Пока не могу понять как сделать ссылку на скачивание кода после определённого коммита). https://github.com/semasping/Drupal_own_import_from_file

Написание модуля разобью на несколько этапов.
I. Для начала. Загрузка файла. Получения массива. Импорт в Drupal.
II. Модифицируем Импорт для работы через Batch, и добавим возможность загружать архив с изображениями
III. Модифицируем модуль так чтоб протащить различные настройки из формы в Batch обработчик.

Итак Первый этап.
Разместим вход в наш модуль на сайте через меню «Содержимое», так как работа будет происходить по импорту материалов.

1. Создадим страницу (форму) загрузки файла.

  function own_import_from_file_menu() {
    
$items['admin/content/own_import'] = array(
      
'title' => 'Import from file',
      
'page callback' => 'drupal_get_form',
      
'access arguments' => array('own_import_from_file'),
      
'file' => '',
    );
    return 
$items;
  }

  function 

own_import_from_file_permission() {
    return array(
      
'own_import_from_file' => array(
        
'title' => t('own import from file'),
        
'description' => t('own import from file')
      ),
    );
  }

  function 

own_import_from_file_form()
  {
    
$form = array(
      
'#attributes' => array('enctype' => "multipart/form-data"),
    );

    

$form['import_file'] = array(
      
'#type' => 'file',
      
'#title' => 'Файл для импорта',
    );
    
    
$form['actions']['submit'] = array(
      
'#type' => 'submit',
      
'#value' => t('Import')
    );
    return 
$form;
  }
?>

Давайте зайдем на сайт и посмотрим на нашу страницу. Перед этим естественно надо включить наш модуль. А если он уже включен то обязательно очистим кэш сайта, так как мы внесли изменения в систему меню своей функцией "own_import_from_file_menu()".
А вот и наша страница:

2. Далее нам необходимо написать функцию проверки отправленного файла. Приступим.

function own_import_from_file_form_form_validate($form, &$form_state) {
  
$path_parts pathinfo($_FILES['files']['name']['import_file']);
  if (!isset(
$path_parts["extension"])) {
    
form_set_error('import_file't('File not choose'));
  }

  if (isset(

$path_parts["extension"]) && ($path_parts["extension"] != 'xlsx') && ($path_parts["extension"] != 'xls')) {
    
form_set_error('import_file't('Wrong type of file! Choose Excel file. You choose: ext', array('ext'=>$path_parts["extension"])));
  }
}
?>

В данной функции мы обрабатываем расширение файла. А так же выводим ошибку если файл не выбран. (прим. Автора: В английских словах и фразах могут быть ошибки. Но я думаю что лучше с ошибками, но через функцию перевода t(), чтобы сразу привыкать к правильному подходу.)

3. Переходим к этапу обработки файла.
Полученный файл нам необходимо сохранить. Так же в этой функции мы можем обработать различные параметры которые мы можем запросить в форме. (К дополнительным параметрам мы перейдем позже.)

function own_import_from_file_form_submit($form, &$form_state) {
  
$dir 'public://import_files/';
  
file_prepare_directory($dirFILE_CREATE_DIRECTORY); // проверка на наличие директории.
  
$file $dir $_FILES['files']['name']['import_file'];
  if (
$result move_uploaded_file($_FILES['files']['tmp_name']['import_file'], $file)) {
    
own_import_from_file_work($file); // передаем наш файл на дальнейшую обработку.
  
}else{
    
form_set_error('import_file't('Error with saving. File not saved. Try another file or contact the administrator.'));
    return 
'';
  }
}
?>

4. Обработка файла.
Мы будем импортировать excel файл, соответственно нам понадобится библиотека для работы с excel файлами. Для этого мы возьмем библиотеку PHPExcel. Чтобы проинформировать пользователя о необходимости данной библиотеки при установке модуля создадим файл «own_import_from_file.install» и добавим следующую информацию:

function own_import_from_file_install() {
  
drupal_set_message(t("In order to use own_import_from_file, you must first download the entire library at !link and put it under sites/all/libraries/PHPExcel/ (so you should have sites/all/libraries/PHPExcel/PHPExcel.php)", array('!link' => 'phpexcel.net')), 'warning');
}
?>

Теперь при включении нашего модуля мы увидим такой скрин:

Скачаем библиотеку с сайта http://www.phpexcel.net/ и положим ее в sites/all/libraries/PHPExcel/ (так чтобы получился следующий путь: sites/all/libraries/PHPExcel/PHPExcel.php).

Пишем функцию обработки нашего файла. В функции own_import_from_file_work($file); нам нужно получить массив из файла и передать его в функцию для импорта.

function own_import_from_file_work($file) {
  
drupal_set_message('Тут мы будем работать с файлом.');
  
$arr_from_file own_import_from_file_get_array_from_file($file);
  
dpm($arr_from_file);
  return 
'';
}
?>

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

function own_import_from_file_get_array_from_file($file) {
  
$lib_dir libraries_get_path('phpexcel');

  require_once(

$lib_dir '/PHPExcel/IOFactory.php');
  
$objPHPExcel PHPExcel_IOFactory::load(drupal_realpath($file));
  
$objPHPExcel->setActiveSheetIndex(0);
  
$aSheet $objPHPExcel->getActiveSheet();
  
// обрабатываем каждую строку
  
foreach ($aSheet->getRowIterator() as $rkey => $row) {
    
$cellIterator $row->getCellIterator();
    
// в зависмости от номера строки создаем массив заголовков.
    
switch ($rkey) {
      case 
1:         // Нормально название колонки
        
break;
      case 
2:         // Название колонки для Drupal
        
foreach ($cellIterator as $ckey => $cell) {
          
$kkey[$ckey] = $cell->getCalculatedValue();
        }
        break;

      default:
        

// обрабатываем каждую ячейку строки
        
foreach ($cellIterator as $ckey => $cell) {
          
$res[$rkey][$kkey[$ckey]] = trim($cell->getCalculatedValue());
        }
    }
  }
  return 
$res;
}
?>

Давайте посмотрим на получившуюся структуру массива.

Из структуры мы видим одну ошибку связанную с форматом ячеек в Excel. При использовании «запятой» как разделителя в поле «Внутренняя память» мы получаем дробное число. Исправить это можно заменив разделитель значений «,» на «;». Хотя думаю что можно покопаться в библиотеке PHPExcel и найти решение там. Разделителем может быть любой символ, который вам удобен. Главное что на текущем этапе нам нужно чтобы данные из файла попадали в массив без изменений.

И так массив у нас есть. Теперь нам нужно узнать структуру «ноды» для импорта. Идем на api.drupal.org смотреть в каком виде принимает данные функция node_save($node);
В комментариях находим:

$node = new stdClass();
  
$node->type 'article';
  
node_object_prepare($node);
  
  
$node->title    'Title';
  
$node->language LANGUAGE_NONE;
?>

Соответственно у нас $node->type = ‘planshet’;
Но этого нам не достаточно. Нам необходимо знать, в каком виде нужно сохранять информацию в полях нашей ноды. Давайте добавим тестовую запись «Test title». И при наличии модуля devel мы можем посмотреть на вкладке «Разработка» структуру нашей ноды.

Тут куча информации но нам нужна только часть.
Значение заголовка:

Значение большого текстового поля:

Значение простого текстового поля:

Значение поля - ссылка не термин:

Значение поля с изображение, (аналогично с любым файлом):

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

Приступим к написанию функции сохранения полученного массива в drupal.

function own_import_from_file_save_nodes($arr_from_file) {
  foreach (
$arr_from_file as $row_key => $row_value) {
    
$node = new stdClass();
    
$node->type 'planshet';
    
node_object_prepare($node);

    

$node->title $row_value['field_vendor_pad'] . ' ' $row_value['field_model_pad'];
    
$node->language LANGUAGE_NONE;

    

$node->body[$node->language] = _own_import_from_file_get_value($row_value['body'], 'bigtext');
    
$node->field_model_pad[$node->language] = _own_import_from_file_get_value($row_value['field_model_pad'], 'text');
    
$node->field_vendor_pad[$node->language] = _own_import_from_file_get_tid($row_value['field_vendor_pad'], 'vendor');
    
$node->field_hdd_pad[$node->language] = _own_import_from_file_get_tid($row_value['field_hdd_pad'], 'hdd');

    

node_save($node);

    

drupal_set_message(t('Imported !title with nid: nid', array(
      
'!title' => l($node->title'node/' $node->nid),
      
'nid=> $node->nid
    
)));
  }
}
?>

Эта функция формирует необходимый нам объект для передачи в node_save();
Получение значений для различных полей мы вынесли в отдельные функции.
Соответственно эти функции:

function _own_import_from_file_get_value($value$type) {
  switch (
$type) {
    case 
'bigtext':
      
$format 'filtered_html';
      
$ret_value[] = array('value' => check_markup($value$format), 'format' => $format);
      break;
    case 
'text':
      
$ret_value[] = array('value' => $value);
      break;
    default:
      
$ret_value[] = $value;
  }
  return 
$ret_value;
}

function 

_own_import_from_file_get_tid($value$vocab_name) {
  
$arr_val_tax explode(';'$value);
  foreach (
$arr_val_tax as $val_tax) {
    
$term taxonomy_get_term_by_name($val_tax$vocab_name);
    if (!empty(
$term)) {
      
$arr_tid[] = reset($term)->tid;
    }
    else {
      
$vocabulary taxonomy_vocabulary_machine_name_load($vocab_name);
      
$term = new stdClass();
      
$term->name $val_tax;
      
$term->vid $vocabulary->vid;
      
taxonomy_term_save($term);
      
$arr_tid[] = $term->tid;
    }
  }
  return 
$arr_tid;
}
?>

На данном этапе модуль закончен.
В нем много чего не учтено. Например:
- отсутствует проверка на дублирование ноды при импорте.
- модуль не умеет обновлять существующие ноды
- не подходит для большого количества импортируемой информации (думаю не больше 100 строк в Excel).
Но общее направление уже понятно.

Итоговый код можно взять на GitHub https://github.com/semasping/Drupal_own_import_from_file (скорее всего будут вносится некоторые правки.)
Либо в приложении: http://www.drupal.ru/files/own_import_from_file.zip

Пытался сюда приложить архив с модулем в формате tar.gz - получил ошибку:

Комментарии

Аватар пользователя andypost@drupal.org andypost@drupal.org 2 декабря 2012 в 20:48

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

Аватар пользователя alexsaab alexsaab 2 декабря 2012 в 22:53

Я тоже пытался долгое время экспортировать товар из feeds - результат куча неправильных данных: корявые термины таксономии, лишние и ненужны файлы с картинками типа image1_23.jpg (где 23 порядковый номер файла а кроме того в директории files лежат еще 22 файла с порядковыми номерами 1-22 и это все одинаковые картинки), отсутствие описания у изображение (теги alt и title) и т.д. И починить это оказалось невозможным.
В итоге: написали кастомный модуль по закачке материалов на сайт работает стабильно и правильно - но не быстро Smile Так как используются стандартные функции Drupal node_save и node_load - а это тормоза, пока не оптимизируем ничего, объем экспорта небольшой просто. Модуль migrate не пользовал, но о нем хорошо отзываются.

Удачи вам.

С ув., Алексей

Аватар пользователя semasping semasping 3 декабря 2012 в 10:08

"<a href="mailto:andypost@drupal.org">andypost@drupal.org</a>" wrote:
рекомендую обязательно начать с описания отличий feeds & migrate - разные прикладные возможности.
оба модуля предлагают свои механизмы для обработки получаемых значений и работы со связанными сохраняемыми сущностями

К сожалению ни с одним из этих модулей не работал.
А опыт самописного импорта есть, поэтому с него и начал. После этой статьи, возможно разберусь с модулями и опишу импорт этой же таблицы через данные модули.
Но так как это моя первая статья - то написание идет очень медленно.

"alexsaab" wrote:
Удачи вам.
Спасибо.