Реализация импорта данных CSV, Excel, XML
Импорт данных обрабатывает загруженные файлы: валидирует структуру, трансформирует строки, пакетно сохраняет в БД. Для больших файлов — чтение по частям в фоне.
Laravel: импорт через Laravel Excel
// Импорт пользователей из CSV/Excel
class UsersImport implements ToModel, WithHeadingRow, WithValidation, SkipsOnError
{
use Importable, SkipsErrors;
private int $imported = 0;
private int $failed = 0;
public function model(array $row): ?User
{
$this->imported++;
return User::firstOrCreate(
['email' => $row['email']],
[
'name' => $row['name'],
'phone' => $row['phone'] ?? null,
'password' => bcrypt(Str::random(16)),
]
);
}
public function rules(): array
{
return [
'email' => 'required|email',
'name' => 'required|string|max:255',
'phone' => 'nullable|string|max:20',
];
}
public function customValidationMessages(): array
{
return [
'email.required' => 'Колонка email обязательна',
'email.email' => 'Некорректный формат email в строке :attribute',
];
}
public function onError(\Throwable $e): void
{
$this->failed++;
Log::warning('Import row failed', ['error' => $e->getMessage()]);
}
public function getStats(): array
{
return ['imported' => $this->imported, 'failed' => $this->failed];
}
}
// Controller
class ImportController extends Controller
{
public function store(Request $request): JsonResponse
{
$request->validate([
'file' => 'required|file|mimes:csv,xlsx,xls|max:10240',
]);
$import = new UsersImport();
Excel::import($import, $request->file('file'));
return response()->json([
'message' => 'Импорт завершён',
'stats' => $import->getStats(),
'errors' => $import->errors()->map(fn($e) => $e->getMessage()),
]);
}
}
Чанковый импорт для больших файлов
class LargeProductsImport implements ToModel, WithChunkReading, WithHeadingRow
{
public function chunkSize(): int
{
return 500;
}
public function model(array $row): Product
{
return new Product([
'sku' => $row['sku'],
'name' => $row['name'],
'price' => (float) str_replace(',', '.', $row['price']),
'stock' => (int) $row['stock'],
'category_id' => Category::getIdByName($row['category']),
]);
}
}
// Асинхронно в очереди
Excel::queueImport(new LargeProductsImport(), $request->file('file'));
Node.js: CSV парсинг
import { parse } from 'csv-parse';
import { createReadStream } from 'fs';
import { pipeline } from 'stream/promises';
interface UserRow {
email: string;
name: string;
phone?: string;
}
async function importUsersFromCsv(filePath: string): Promise<{ imported: number; failed: number }> {
let imported = 0;
let failed = 0;
const batch: UserRow[] = [];
const BATCH_SIZE = 100;
const parser = parse({
columns: true, // первая строка — заголовки
skip_empty_lines: true,
trim: true,
delimiter: [',', ';'], // автоопределение разделителя
bom: true, // убрать UTF-8 BOM
});
const processBatch = async () => {
if (batch.length === 0) return;
const rows = [...batch];
batch.length = 0;
try {
await db.user.createMany({
data: rows.map(row => ({
email: row.email.toLowerCase(),
name: row.name,
phone: row.phone || null,
})),
skipDuplicates: true,
});
imported += rows.length;
} catch (err) {
failed += rows.length;
console.error('Batch insert failed:', err);
}
};
for await (const record of createReadStream(filePath).pipe(parser)) {
if (!record.email || !record.name) {
failed++;
continue;
}
batch.push(record);
if (batch.length >= BATCH_SIZE) await processBatch();
}
await processBatch(); // последний неполный батч
return { imported, failed };
}
XML импорт (прайс-листы, B2B)
class XmlPriceImport
{
public function import(string $filePath): array
{
$xml = simplexml_load_file($filePath, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($xml === false) {
throw new \InvalidArgumentException('Некорректный XML файл');
}
$products = [];
foreach ($xml->offers->offer as $offer) {
$products[] = [
'sku' => (string) $offer['id'],
'name' => (string) $offer->name,
'price' => (float) $offer->price,
'url' => (string) $offer->url,
];
}
// Пакетное обновление
foreach (array_chunk($products, 200) as $chunk) {
Product::upsert($chunk, ['sku'], ['name', 'price', 'url']);
}
return ['total' => count($products)];
}
}
Валидация и отчёт об ошибках
Хороший импорт сообщает пользователю конкретно какие строки не прошли и почему:
// Возврат детального отчёта
return response()->json([
'total' => $total,
'imported' => $imported,
'failed' => $failed,
'errors' => [
['row' => 3, 'field' => 'email', 'message' => 'Некорректный email: not-an-email'],
['row' => 7, 'field' => 'price', 'message' => 'Цена должна быть числом'],
],
]);
Срок реализации
CSV/Excel импорт с валидацией для Laravel или Node.js: 2–3 дня. С чанковой обработкой, детальным отчётом об ошибках и XML поддержкой: 3–5 дней.







