Долой дубли!




Принцип обмена данными из 1С с сайтом (на MySQL) и выдачи (публикации) этих данных по запросу.
PHP-Скрипт автоматической загрузки данных из файла данных в формате CSV в базу данных сайта работающего на WordPress.

В продолжение моей темы: 1С:Альфа-Авто Автосалон Автосервис: обмен с сайтом.
С помощью данного скрипта можно загружать в автоматическом режиме, по расписанию, данные сервисных книжек (ремонтов авто) из 1С:Альфа-Авто Автосалон Автосервис.
Также можно загружать данные в ручном режиме: для этого делается скрытая страница, где размещается специальная кнопка.
Комментарии размещенные внутри скрипта разъяснят логику и порядок действия.
Комментарии с "/////    echo" использовались для отладки.
Дополнительно создана таблица для журналирования результатов загрузки данных.
Скрипт включает в себя защиту от SQL инъекций (думаю безопасность соблюдена в полной мере).
В кратце:
1. Пишется скрипт, который запускает этот.
2. Создается регламентное задание в WordPress, по которому запускается скрипт из п.1. 
3. Этот скрипт осуществляет проверку на существование файла обмена в папке.
4. Если данные не новые, загрузка не производится.
5. Если данные новые, очищается таблица сервисных книжек.
6. Загружаются новые данные.

Собственно сам скрипт:

<?php // Полная загрузка сервисных книжек, создан 2025-01-05 12:44:55

global $wpdb2;
global $failure;
global $file_hist;

/////  echo '<H2><b>Старт загрузки</b></H2><br>';

$failure=FALSE;
//подключаемся к базе
$wpdb2 = include_once 'connection.php'; ; // подключаемся к MySQL
// если не удалось подключиться, и нужно оборвать PHP с сообщением об этой ошибке
if (!empty($wpdb2->error))
{
/////   echo '<H2><b>Ошибка подключения к БД, завершение.</b></H2><br>';
$failure=TRUE;
wp_die( $wpdb2->error );
}

$m_size_file=0;
$m_mtime_file=0;
$m_comment='';
/////проверка существования файлов выгрузки из 1С
////файл выгрузки сервисных книжек
$file_hist = ABSPATH.'/_1c_alfa_exchange/AA_hist.csv';
if (!file_exists($file_hist))
{
/////   echo '<H2><b>Файл обмена с сервисными книжками не существует.</b></H2><br>';
$m_comment='Файл обмена с сервисными книжками не существует';
$failure=TRUE;
}

/////инициируем таблицу лога
/////если не существует файла то возврат и ничего не делаем
if ($failure){
///включает защиту от SQL инъекций и данные можно передавать как есть, например: $_GET['foo']
/////   echo '<H2><b>Попытка вставить запись в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>$m_comment));
wp_die();
/////    echo '<H2><b>Возврат в начало.</b></H2><br>';
return $failure;
}
/////проверка лога загрузки, что бы не загружать тоже самое
$masiv_data_file=stat($file_hist);   ////передаем в массив свойство файла
$m_size_file=$masiv_data_file[7];    ////получаем размер файла
$m_mtime_file=$masiv_data_file[9];   ////получаем дату модификации файла
////создаем запрос на получение последней удачной загрузки
////выбираем по штампу времени создания (редактирования) файла загрузки AA_hist.csv, $m_mtime_file

/////   echo '<H2><b>Размер файла: '.$m_size_file.'</b></H2><br>';
/////   echo '<H2><b>Штамп времени файла: '.$m_mtime_file.'</b></H2><br>';
/////   echo '<H2><b>Формирование запроса на выборку из лога</b></H2><br>';
////препарируем запрос
$text_zaprosa=$wpdb2->prepare("SELECT * FROM `vin_logs` WHERE `last_mtime_upload` = %s", $m_mtime_file);
$results=$wpdb2->get_results($text_zaprosa);

if ($results)
{   foreach ( $results as $r)
{
////если штамп времени и размер файла совпадают, возврат
if (($r->last_mtime_upload==$m_mtime_file) && ($r->last_size_upload==$m_size_file))
{////echo '<H2><b>Возврат в начало, т.к. найдена запись в логе.</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>'Загрузка отменена, новых данных нет, т.к. найдена запись в логе.'));
wp_die();
return $failure;
}
}
}
////если данные новые, пишем в лог запись о начале загрузки
/////echo '<H2><b>Попытка вставить запись о начале загрузки в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>0, 'last_size_upload'=>$m_size_file, 'comment'=>'Начало загрузки'));

////очищаем таблицу
$clear_tbl_zap=$wpdb2->prepare("TRUNCATE TABLE %s", 'vin_history');
$clear_tbl_zap_repl=str_replace("'","`",$clear_tbl_zap);
$results=$wpdb2->query($clear_tbl_zap_repl);
/////   echo '<H2><b>Очистка таблицы сервисных книжек</b></H2><br>';
if (empty($results))
{
/////   echo '<H2><b>Ошибка очистки таблицы книжек, завершение.</b></H2><br>';
//// если очистка не удалась, возврат
$failure=TRUE;
wp_die();
return $failure;
}

////загружаем данные
$table='vin_history';         // Имя таблицы для импорта
//$file_hist Имя CSV файла, откуда берется информация     // (путь от корня web-сервера)
$delim=';';          // Разделитель полей в CSV файле
$enclosed='"';      // Кавычки для содержимого полей
$escaped='\

35 Comments

  1. CheBurator

    неэстетично…

    имхается проще и красивше рекурсивную процедуру заюзать…

    Reply
  2. poppy

    Как-то уж очень мудрено…

    Самый простой способ избавиться от дублей — использовать процедуры. Ведь они именно для этого придуманы?

    Reply
  3. int3

    Если есть желание сэкономить число сравнений, почему бы не построить индекс? Например так:

    Код
    СтрокаКолонок = "Ключ_1,Ключ_2,...,Ключ_N";   
    ТЗ.Сортировать(СтрокаКолонок);
    ТЗиндекс = СоздатьОбъект("ТаблицаЗначений");
    ТЗ.Выгрузить(ТЗиндекс,,,СтрокаКолонок);
    ИндексКолонка = ТЗиндекс.КоличествоКолонок()+1;
    ТЗиндекс.КоличествоКолонок(ИндексКолонка);
    ТЗиндекс.Заполнить(1,,,ИндексКолонка);
    ТЗиндекс.Свернуть(СтрокаКолонок,ИндексКолонка);
    ТЗ.ВыбратьСтроки();
    РазмерБлока = 0;
    НомерБлока = 0;
    Пока ТЗ.ПолучитьСтроку() = 1 Цикл
        Если РазмерБлока=0 Тогда
          НомерБлока = НомерБлока + 1;
          РазмерБлока = ТЗиндекс.ПолучитьЗначение(НомерБлока,ИндексКолонка);
            // БЛОК 1 Создать новый документ
            // БЛОК 2 Заполнить заголовок документа
        КонецЕсли;
        // БЛОК 5 Заполнение строки спецификации документа
       РазмерБлока = РазмерБлока - 1;
        Если РазмерБлока=0 Тогда
            // БЛОК 3 Заполнить подвал документа
            // БЛОК 4 Записать документ
        КонецЕсли;
    КонецЦикла;
    

    Показать полностью

    всяко пошустрее будет

    Reply
  4. tarasenkov

    А не проще написать так:

    Код
    Если (ТЗ.НомерСтроки    = 1) ИЛИ
    (ТЗ.Ключ_1<>ПредЗначение_Ключ_1) ИЛИ
    (ТЗ.Ключ_2<>ПредЗначение_Ключ_2) ИЛИ
    ...
    (ТЗ.Ключ_N<>ПредЗначение_Ключ_N) Тогда
    
    Если (ТЗ.НомерСтроки    = 1) Тогда
            // БЛОК 1 Создать новый документ !дублируем код!
            // БЛОК 2 Заполнить заголовок документа !дублируем код!
    Иначе
            // сменились ключи         
            // БЛОК 3 Заполнить подвал документа
            // БЛОК 4 Записать / вывести документ
    КонецЕсли;
    
    КонецЕсли;

    Показать полностью

    При этом никакого дублирования кода нет,

    и удобочитаемость остается.

    Reply
  5. tarasenkov

    И почему нельзя редактировать комментарии?

    Я имел ввиду такой код:

    Если (ТЗ.НомерСтроки = 1) ИЛИ

    (ТЗ.Ключ_1<>ПредЗначение_Ключ_1) ИЛИ

    (ТЗ.Ключ_2<>ПредЗначение_Ключ_2) ИЛИ



    (ТЗ.Ключ_N<>ПредЗначение_Ключ_N) Тогда

    // БЛОК 1 Создать новый документ

    // БЛОК 2 Заполнить заголовок документа

    Если (ТЗ.НомерСтроки > 1) Тогда

    // сменились ключи

    // БЛОК 3 Заполнить подвал документа

    // БЛОК 4 Записать / вывести документ

    КонецЕсли;

    КонецЕсли;

    Reply
  6. tarasenkov

    * и предварительного просмотра нет — тоже плохо 🙁

    Итак.

    Код
    Если (ТЗ.НомерСтроки = 1) ИЛИ
    (ТЗ.Ключ_1<>ПредЗначение_Ключ_1) ИЛИ
    (ТЗ.Ключ_2<>ПредЗначение_Ключ_2) ИЛИ
    ...
    (ТЗ.Ключ_N<>ПредЗначение_Ключ_N) Тогда
       Если (ТЗ.НомерСтроки > 1) Тогда
          // сменились ключи
          // БЛОК 3 Заполнить подвал документа
          // БЛОК 4 Записать / вывести документ
       КонецЕсли;
       // БЛОК 1 Создать новый документ
       // БЛОК 2 Заполнить заголовок документа
    КонецЕсли;

    Показать полностью

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

    Хотя тоже можно избежать добавив в таблицу фиктивную последнюю строку.

    Reply
  7. begemot

    Как оригинальный вариант решения имеет право на существование…

    Reply
  8. Shaman100M

    (1) (2) Рекурсивная, скорее всего, на сравнение и заполнение строк ТЧ. Иногда может получиться громоздко, т.к. нужно хранить / передавать в процедуру(ы) ключи и все, что там необходимо будет сделать. Я попытался реализовать одной вспомогательной универсальной процедурой с четко определенными параметрами.

    (3) +1 хороший индекс! Единственное, добавил бы после свертки ТЗИндекс

    Код
     ТЗИндекс.Сортировать(СтрокаКолонок); // Сортировка после Свернуть() может сбиться. 

    Показать полностью

    Reply
  9. Shaman100M

    (4) (5) (6) от объединения условий отказался, — можно запутаться в условиях. Блоки 3 и 4 всегда должны идти после блока 5 (для текущего документа), их дублирования условиями не избежать. Если добавлять фиктивную строку, — необходимо проверить/задать уникальные ключи для нее.

    Reply
  10. orefkov

    Я бы это сделал так:

    Код
    ПредЗначение_Ключ_1   = ПолучитьПустоеЗначение(99);
    ПредЗначение_Ключ_2   = ПолучитьПустоеЗначение(99);
    ...
    ПредЗначение_Ключ_N   = ПолучитьПустоеЗначение(99);
    
    ТЗ.Сортировать("Ключ_1,Ключ_2,...,Ключ_N");
    ТЗ.НоваяСтрока();
    ТЗ.ВыбратьСтроки();
    Пока ТЗ.ПолучитьСтроку() = 1 Цикл
        Если (ТЗ.Ключ_1<>ПредЗначение_Ключ_1) ИЛИ
          (ТЗ.Ключ_2<>ПредЗначение_Ключ_2) ИЛИ
          ...
          (ТЗ.Ключ_N<>ПредЗначение_Ключ_N) Тогда
          Если ТЗ.НомерСтроки > 1 Тогда
               // БЛОК 3 Заполнить подвал документа
               // БЛОК 4 Записать / вывести документ
          КонецЕсли;
          Если ТЗ.НомерСтроки = ТЗ.КоличествоСтрок() Тогда
             Прервать;
          КонецЕсли;
            // БЛОК 1 Создать новый документ
            // БЛОК 2 Заполнить заголовок документа
        КонецЕсли;
        // БЛОК 5 Заполнение строки спецификации документа
          
        ПредЗначение_Ключ_1   = ТЗ.Ключ_1;
        ПредЗначение_Ключ_2   = ТЗ.Ключ_2;
        ...
        ПредЗначение_Ключ_N   = ТЗ.Ключ_N;
    КонецЦикла;
    
    

    Показать полностью

    Reply
  11. Shaman100M

    (10) а если ТЗ.ПолучитьЗначение(1,»Ключ_1″) = ПолучитьПустоеЗначение() ?

    Reply
  12. Shaman100M

    Замерил время выполнения, 10т записей

    традиционный, (6) (10) 100%

    3 ключа, значения от 1 до 100:

    (3) от +40% до +50%

    мой: от +250 до +300%

    3 ключа, значения от 1 до 10:

    (3) +400%

    мой: +180%

    1 ключ, значения от 1 до 10, от 1 до 100

    (3) от + 20% до +40%

    мой: +130%

    Ст’оит ли красота времени?

    Reply
  13. Shaman100M

    Хотя мож и стоит. Сравнивается с голым циклом с парой-тройкой условий и присваиваний.

    Reply
  14. int3

    (8) Согласен, никто не гарантирует порядка после свертки, правда ни разу не сталкивался с такой ситуацией :-/ (Хотя в реальных алгоритмах дополнительную сортировку включаю в код — надежность дороже 😉 )

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

    ну или иначе — при варировании ключевых условий, тут уже выйдет на первое место гибкость :-/

    Reply
  15. int3

    > 3 ключа, значения от 1 до 10:

    > (3) +400%

    > мой: +180%

    Кстати, очень интересный результат. На что ушло время?

    Reply
  16. Shaman100M

    (15) Замерял не в отладчике, а с пом. _GetPerformanceCounter() . Отладчик показывает +100% Максимум времени ушло на Свернуть()

    Reply
  17. Shaman100M

    (14) при десятке ключей на сотне т: минус 40%

    Reply
  18. int3

    (16) хм… Такая картина — несовпадение результатов разных методов замера наверное объясняется «особенностями» работы платформы, в частности уборщика мусора.

    (17) т.е. сфера применения у него найдется, и не только в плане «красивости» кода? буду рад, если пригодится 🙂

    когда-то на проклабе обнаружил конкурс на самый быстрый метод удаления строк из ТЗ (правда он к тому времени уже закончился), заинтересовался и написал свой вариант с использованием такого индексирования — занятный вариант получился, но не без «особенностей» 🙂

    Reply
  19. orefkov

    (11)

    Ну, сделай

    ключ1 = «bcad7cf3-2fd3-4286-a654-139b37840a79»

    Reply
  20. Shaman100M

    (18) да, полезный конкурс, столько особенностей при оптимизации обнаружилось, вплоть до наличия запятой в НоваяКолонка() 🙂

    (19) )))) Ну тогда какое-нить другое красивое слово.

    Reply
  21. Shaman100M

    (18) если не против, добавлю в статью с ссылкой.

    Reply
  22. int3

    (21) Добро

    Reply
  23. poppy

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

    Код
    Функция ПроверитьСтрокуТаблицы(ТЗ, НомерСтроки, СписокКлючей)
       
       Если НомСтр < 1 Тогда
           Возврат 1;
       КонецЕсли;
       
       Если НомСтр >= ТЗ.КоличествоСтрок() Тогда
           Возврат 1;
       КонецЕсли;
       
       Для ии = 1 По СписокКлючей.РазмерСписка() Цикл
          Колонка = СписокКлючей.ПолучитьЗначение(СписокКлючей.РазмерСписка() - ии + 1);
          Если не (ТЗ.ПолучитьЗначение(НомерСтроки, Колонка) = ТЗ.ПолучитьЗначение(НомерСтроки + 1, Колонка)) Тогда
              Возврат 1;
          КонецЕсли;
       КонецЦикла;
       
       Возврат 0;
       
    КонецФункции
    
    
       СтрокаКолонок = "Ключ_1,Ключ_2,...,Ключ_N";
       
       СписокКлючей = СоздатьОбъект("СписокЗначений");
       СписокКлючей.ИзСтрокиСРазделителями("""" + СтрЗаменить(СтрокаКолонок, ",", """,""") + """");
    
          ТЗ.Сортировать(СтрокаКолонок);
          
       Флаг = 1;
    
       ТЗ.ВыбратьСтроки();
       Пока ТЗ.ПолучитьСтроку() = 1 Цикл
          
          Если Флаг = 1 Тогда
             // БЛОК 1 Создать новый документ
             // БЛОК 2 Заполнить заголовок документа
          КонецЕсли;
          
          // БЛОК 5 Заполнение строки спецификации документа
          
          Флаг = ПроверитьСтрокуТаблицы(ТЗ, ТЗ.НомерСтроки, СписокКлючей);
       
          Если Флаг = 1 Тогда
             // БЛОК 3 Заполнить подвал документа
             // БЛОК 4 Записать / вывести документ
          КонецЕсли;
          
       КонецЦикла;
    

    Показать полностью

    Очевидно, что представленный код работает медленнее, чем при индексировании. Но ведь речь идет о простоте и понятности алгоритма и кода? Не так ли?

    Reply
  24. Shaman100M

    Так. Включаем в статью?

    Reply
  25. poppy

    (24)

    Я не против.

    Можно еще описать решение (6). Считаю, что оно вполне справляется с поставленной в статье задаче.

    Остальные решения стоит позиционировать не только как избавление от дублей, но как универсальные решения, которые не зависят от списка ключевых полей.

    З.Ы. Попробуй длинный комментарий к процедуре ИзмененияКлючейТЗ() сделать в несколько строк, а то он у меня на экран не помещается — неудобно читать статью. 🙁

    Reply
  26. Shaman100M

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

    По комментарию — разве он у тебя не переносится на след. строки? Хотя, при копировании в конфигуратор лучше, чтобы строки были короткие.

    Reply
  27. poppy

    (26)

    > По комментарию — разве он у тебя не переносится на след. строки?

    Есть ощущение, что строки внутри конструкции ( code ) ( /code) не переносятся… 🙁 В IE7

    Но это претензия скорее к саппорту, чем к тебе. Сделай опиcание ИмяКолонкиНач и ИмяКолонкиКон в несколько строк, возможно, будет лучше…

    З.Ы.

    Решение описанное в (10) не стоит внимания, потому-что неправильное.

    Reply
  28. Shaman100M

    (27) Чем оно неправильное? Решения (6) и (10) — одно и тоже, просто (10) дописано до конца, а в (6) код + идея.

    Reply
  29. poppy

    (28)

    Да, действительно, решение (10) правильное. Я невнимательно его прочтала. Приношу извинения перед автором за напрасный поклеп. 😉

    Reply
  30. maloi_a

    (23)

    Внесу свою копейку.

    В Функции ПроверитьСтрокуТаблицы(ТЗ, СписокКлючей)

    надо добавить объявление локальных переменных НомСтр и М1.

    Перем НомСтр и М1;

    Иначе может поменять глобальные переменные с такими именами.

    Reply
  31. poppy

    (30) Правильное замечание.

    В (23) вкралась ошибка. В условиях вместо НомСтр должно быть написано НомерСтроки.

    Получается, что локальными переменными нужно объявлять: Перем ии, Колонка.

    Reply
  32. Shaman100M

    (30) нашел, таки, но про колонку забыл. 😉

    Правило про переменные, — это классика. Стараюсь его выполнять. Ну, вряд ли кто-то додумается сделать глобальные переменные с такими короткими и простыми именами, но вероятность есть. Обычно добавляется префикс «гл».

    (31) (23) в статье скорректировано, ошибка с НомСтр изначально была устранена. Кстати, зачем передавался НомерСтроки ?

    Reply
  33. poppy

    (32)

    > Кстати, зачем передавался НомерСтроки ?

    Наверно, для бОльшей универсальности. Ведь можно написать и так:

    Код
    ТЗ.ВыбратьСтроки();
    Пока ТЗ.ПолучитьСтроку() = 1 Цикл
       Если ПроверитьСтрокуТаблицы(ТЗ, ТЗ.НомерСтроки - 1, СписокКлючей) = 1 Тогда
          // БЛОК 1 Создать новый документ
          // БЛОК 2 Заполнить заголовок документа
       КонецЕсли;
       
       // БЛОК 5 Заполнение строки спецификации документа
       
       Если ПроверитьСтрокуТаблицы(ТЗ, ТЗ.НомерСтроки, СписокКлючей) = 1 Тогда
          // БЛОК 3 Заполнить подвал документа
          // БЛОК 4 Записать / вывести документ
       КонецЕсли;
    КонецЦикла;
    

    Показать полностью

    Reply
  34. Shaman100M

    (33) Да, действительно.

    Reply
  35. Shaman100M

    еще метод

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *