Написать пост

Практика работы с файлами и базой данных в Laravel и ChatGPT

Аватарка пользователя Izenx

Рассказываем, как ChatGPT справилась с обработкой данных. Модель попросили взять данные из таблицы, перенести их в БД и экспортировать в csv.

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

Мне стало интересно, насколько оптимальный код напишет chatgpt 3.5 в рамках задачи: Есть большой (более 200 мб) xml файл (cities.xml)  со структурой: <cities><city><name>CityName</name></city></cities>.

Необходимо получить данные из файла и заполнить ими таблицу cities с полями name, created_at, updated_at.

Также нужно создать команду, которая получит все данные из таблицы cities, добавит каждой записи случайное число от 1 до 10 и выгрузит в csv файл в формате (Name, Created At, Random Number). Приложение написано на Laravel.

Условно, можно разделить задачу на этапы: 

  1. Получаем данные из xml, заполняем бд;
  2. Получаем данные из бд, заполняем csv.

Приступим к решению!

Получаем данные из xml, заполняем бд

Первым делом, составим запрос к chatgpt:

> Есть большой cities.xml файл со структурой: <cities><city><name>CityName</name></city></cities>. Создай команду, которая прочитает cities.xml и заполнит таблицу сities с полем name. На Laravel.

Ответ:

			Для создания сущности в Laravel, нужно выполнить команду:

php artisan make:model City -m

В файле миграции необходимо определить поля таблицы:

public function up()
{
    Schema::create('cities', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
}

Затем запустите миграцию:

php artisan migrate

Теперь можно создать команду для заполнения таблицы City из файла cities.xml. Создайте новый класс команды:

php artisan make:command FillCitiesTable

В методе handle этой команды необходимо прочитать файл cities.xml и заполнить таблицу City:

public function handle()
{
    $xml = simplexml_load_file('cities.xml');
    foreach ($xml->city as $city) {
        City::create([
            'name' => (string) $city->name,
        ]);
    }
}

Теперь можно запустить команду:

php artisan fill:cities

Готово! Таблица City заполнена данными из файла cities.xml
		

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

Но будет ли команда работать на практике? Нет, получаем ошибку по памяти. Проблема в том, что simplexml_load_file('cities.xml') создает объектную модель документа и полностью грузит её в память.

Так как файл большой, это сразу приводит к PHP Fatal error: Out of memory.

Корректность использования City::create([]) (в рамках этой задачи) вызывает сомнение и приводит к одной из вариации n+1 проблемы, так как на создание каждой сущности, будет создаваться запрос к бд.

Сообщим chatgpt об ошибке:

> Файл cities.xml очень большой, этот код не работает, возникает ошибка PHP Fatal error

Ответ:

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

Хороший ответ, надо бы запомнить.

Конкретизирую запрос:

> Оптимизируй код команды для работы с большими данными.

Ответ:

			Для оптимизации кода команды можно использовать чтение файла по частям (chunk), чтобы не загружать в память весь файл целиком. Также можно использовать транзакции для ускорения записи данных в базу данных. Исправленный код команды будет выглядеть следующим образом:

class FillCitiesTable extends Command
{
    protected $signature = 'fill:cities';

    protected $description = 'Fill cities table from XML file';

    public function handle()
    {
        $filePath = storage_path('app/cities.xml');
        $xmlReader = new \XMLReader();
        $xmlReader->open($filePath);

        $batchSize = 1000;
        $counter = 0;
        $cities = [];

        while ($xmlReader->read() && $xmlReader->name !== 'city');

        while ($xmlReader->name === 'city') {
            $node = new SimpleXMLElement($xmlReader->readOuterXML());
            $cities[] = ['name' => (string) $node->name];

            if (++$counter % $batchSize === 0) {
                City::insert($cities);
                $cities = [];
            }

            $xmlReader->next('city');
        }

        if (!empty($cities)) {
            City::insert($cities);
        }

        $xmlReader->close();

        $this->info('Cities table has been filled');
    }
}

В этом коде мы используем \XMLReader для чтения файла по частям, обрабатываем записи по батчам и используем метод insert() для массовой вставки данных в базу данных внутри транзакции. Это позволит ускорить процесс заполнения таблицы при большом файле.
		

В этот раз chatgpt написал оптимальный код. 

Он использует комбинацию XMLReader + SimpleXml для работы с xml. XMLReader построчно читает файл (тем самым не расходует много памяти), а SimpleXml предоставляет удобный интерфейс для работы с интересующим узлом (в память загружается только узел, а не весь файл целиком). 

При записи в бд, он использует массовую вставку City::insert() и Batch processing, где переменная $batchSize регулирует количество элементов для вставки за один запрос, тем самым уменьшая общее количество запросов к бд.

Получаем данные из бд, заполняем csv

На этом этапе контекст потерялся, поэтому запрос более развернутый:

> Есть таблица cities и модель City с полями name, created_at в Laravel. Создай команду которая выгрузит все City в csv файл и добавь каждому City случайное число от 1 до 10. Оптимизируй код для работы с большими данными

Ответ: 

			Для оптимизации работы с большими данными мы можем использовать метод chunk() модели City, который позволяет обрабатывать записи пакетами по несколько штук за раз. Также для работы с csv файлами мы можем использовать встроенный класс SplFileObject.

Вот пример команды:

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\City;
use SplFileObject;

class ExportCities extends Command
{
    protected $signature = 'export:cities';

    protected $description = 'Export cities to csv file';

    public function handle()
    {
        $file = new SplFileObject('cities.csv', 'w');

        $header = ['Name', 'Created At', 'Random Number'];
        $file->fputcsv($header);

        City::chunk(1000, function ($cities) use ($file) {
            foreach ($cities as $city) {
                $data = [
                    $city->name,
                    $city->created_at,
                    rand(1, 10)
                ];

                $file->fputcsv($data);
            }
        });

        $this->info('Cities exported successfully!');
    }
}

В этой команде мы создаем новый csv файл и записываем в него заголовок. Затем мы используем метод chunk() для обработки записей по 1000 штук за раз. Внутри цикла мы формируем массив данных для каждого города, добавляем случайное число и записываем в csv файл с помощью метода fputcsv().
		

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

Узнаем, почему этот код оптимален:

> Почему этот код оптимальный?

> Этот код является оптимальным, как указано выше.

Тогда действуем от противного:

> Почему это решение не является оптимальным?

Ответ: 

> Данное решение может быть не оптимальным в случае, если таблица City содержит очень большое количество записей. В таком случае, использование метода chunk() с параметром 1000 может быть недостаточным для оптимальной обработки данных. Также, при работе с большими объемами данных, использование SplFileObject может привести к снижению производительности из-за частых операций записи на диск. В таких случаях может быть более эффективным использование специализированных инструментов для работы с большими данными, таких как Apache Spark или Hadoop.

Действительно, параметр получаемых записей в chunk(), значимо влияет на производительность приложения. Например, это время работы и потребляемая память с параметром 100.000:

Time taken: 106.76 seconds

Memory used: 2.62 MB

А это с 300.000:

Time taken: 95.42 seconds

Memory used: 8.61 MB

Следовательно, необходимо экспериментально найти оптимальное значение параметра.

Решение этой задачи я выбирал из 12 предложенных вариантов кода.

7 из 12 были оптимальны, работоспособны и схожи по логике. Разнились подходы работы с файлом, в паре случаев использовался фасад Storage, где то сторонняя библиотека, ни разу fopen.  Метод chunk() встречался в 100% случаев. Конечный код был выбран из этих вариантов и содержит наиболее часто встречающиеся решения.

2 из 12 были рабочие, но не были оптимальны и загружали все данные за раз (City::all()).

Остальные не работали по разным причинам: вызывались несуществующие методы, была нарушена логика работы, переданы неправильные аргументы. Во всех случаях, chatgpt самостоятельно не нашёл ошибки. 

Итог

Chatgpt справился с поставленными задачами. Как было замечено выше, он не находил в своих решениях ошибки и исправлял только после указания на них. Я решил узнать об этом:

> Почему ты написал код не правильно?

> Как именно был написан код, я не могу сказать, так как его не видел. Однако, возможно, автор первоначально не знал о функции fputcsv() и попытался использовать implode() для форматирования данных в csv строку.

> То есть ты используешь не проверенные данные для ответов?

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

Да, chatgpt не пишет код, он его синтезирует. Он любит выдумывать и попытается убедить в этом Вас. Но это не мешает ему быть очень полезным инструментом. Если было полезно, ставьте классы. Всем добра!

Следите за новыми постами
Следите за новыми постами по любимым темам
738 открытий900 показов