Обработка большого CSV файла с помощью Batch API

Ключевым фактором в предлагаемом подходе является использование функций fseek и ftell. В конце каждого прохода мы будем сохранять в $context номер байта на котором мы остановились, полученный с помощью ftell, а в начале будем перематывать указатель на этот байт с помощью функции fseek.

Рассмотрим пример на основе простого модуля Large CSV import (large_csv_import):

Файл large_csv_import.info:

name = Large CSV import
core = 7.x
package = Custom	

Файл large_csv_import.module:

// чтобы не усложнять код жестко задаем путь к файлу
define('LARGE_CSV_FILE_PATH', 'sites/all/files/large.csv');
 
/**
 * Implements hook_menu().
 */
function large_csv_import_menu() {
  // создаем страницу с формой для запуска обработки
  $items['import-large-csv'] = array(
    'title' => 'Import large CSV',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('large_csv_import_form'),
    'access callback' => TRUE,
    'type' => MENU_NORMAL_ITEM,
    'menu_name' => 'main-menu',
  );
  return $items;
}
 
/**
 * Large CSV import form.
 */
function large_csv_import_form() {
  $form = array();
 
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Start import'),
  );
 
  return $form;
}
 
/**
 * Large CSV import start.
 */
function large_csv_import_form_submit($form, &$form_state) {
  $batch = array(
    'operations' => array(
      array('large_csv_import_test_operation', array()),
      ),
    'finished' => 'batch_example_finished',
    'title' => t('Processing large CSV'),
    'init_message' => t('Large CSV Batch is starting.'),
    'progress_message' => t('Processed @current out of @total.'),
    'error_message' => t('Large CSV Batch has encountered an error.'),
  );
  batch_set($batch);
}
 
/**
 * Large CSV test operation.
 */
function large_csv_import_test_operation(&$context) {
  if (!isset($context['sandbox']['progress'])) {
    // при инициализации получаем количество строк в файле
    $file = LARGE_CSV_FILE_PATH;
    $linecount = 0;
    $handle = fopen($file, "r");
    while(!feof($handle)){
      $line = fgets($handle);
      $linecount++;
    }
    // так как это первый проход перематываем указатель обратно в 0
    fseek($handle, 0);
 
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['position'] = 0;
    $context['sandbox']['max'] = $linecount;
  }
 
  // будем обрабатывать по 100 записей за проход
  $limit = 100;
 
  if (!isset($handle)) {
    $file = LARGE_CSV_FILE_PATH;
    $handle = fopen($file, "r");
    // перематываем указатель на тоже место
    // на котором завершился прошлый проход
    fseek($handle, $context['sandbox']['position']);
  }
 
  $count = 0;
  while ($data = fgetcsv($handle, 1000, ",")) {
    // создаем нод на основе данных из CSV файла
    $node = new stdClass();
    $node->type = 'article';
    $node->title = $data[0];
    $node->body[LANGUAGE_NONE][0]['value'] = $data[1];
    node_save($node);
 
    $context['sandbox']['progress']++;
    $count++;
    if ($count >= $limit) {
      break;
    }
  }
  // в конце прохода записыаем текущее положение указателя
  $context['sandbox']['position'] = ftell($handle);
 
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

Преимущество этого подхода в том что мы не загружаем весь CSV файл в $context а читаем его частями.

Поделись с друзьями: