Laravel 13 CSV 处理完全指南
CSV 是最常用的数据交换格式之一。本文将深入探讨 Laravel 13 中 CSV 处理的各种方法和最佳实践。
CSV 导出
基础 CSV 导出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <?php
namespace App\Services;
use Illuminate\Support\Facades\Storage;
class CsvExportService { public function export(array $data, string $filename, array $headers = null): string { $handle = fopen('php://temp', 'r+');
if ($headers) { fputcsv($handle, $headers); }
foreach ($data as $row) { fputcsv($handle, $row); }
rewind($handle); $content = stream_get_contents($handle); fclose($handle);
$path = "exports/{$filename}.csv"; Storage::put($path, $content);
return $path; }
public function download(array $data, string $filename, array $headers = null) { $callback = function () use ($data, $headers) { $handle = fopen('php://output', 'w');
if ($headers) { fputcsv($handle, $headers); }
foreach ($data as $row) { fputcsv($handle, $row); }
fclose($handle); };
return response()->stream($callback, 200, [ 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename="' . $filename . '.csv"', ]); } }
|
流式 CSV 导出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Services;
use Illuminate\Support\Collection;
class StreamingCsvExport { public function exportLargeDataset($query, string $filename, array $headers = null) { $callback = function () use ($query, $headers) { $handle = fopen('php://output', 'w');
if ($headers) { fputcsv($handle, $headers); }
$query->chunk(1000, function ($items) use ($handle) { foreach ($items as $item) { fputcsv($handle, $this->formatRow($item)); } });
fclose($handle); };
return response()->stream($callback, 200, [ 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename="' . $filename . '.csv"', 'Cache-Control' => 'no-cache', 'X-Accel-Buffering' => 'no', ]); }
protected function formatRow($item): array { return [ $item->id, $item->name, $item->email, $item->created_at->format('Y-m-d H:i:s'), ]; } }
|
CSV 导入
基础 CSV 导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| <?php
namespace App\Services;
use Illuminate\Support\Facades\Storage;
class CsvImportService { protected array $config = [ 'delimiter' => ',', 'enclosure' => '"', 'escape' => '\\', 'has_header' => true, ];
public function import(string $path, callable $callback, array $options = []): array { $config = array_merge($this->config, $options); $fullPath = Storage::path($path);
$handle = fopen($fullPath, 'r'); $results = [ 'total' => 0, 'success' => 0, 'failed' => 0, 'errors' => [], ];
if ($config['has_header']) { fgetcsv($handle, 0, $config['delimiter'], $config['enclosure'], $config['escape']); }
$lineNumber = $config['has_header'] ? 1 : 0;
while (($row = fgetcsv($handle, 0, $config['delimiter'], $config['enclosure'], $config['escape'])) !== false) { $lineNumber++; $results['total']++;
try { $callback($row, $lineNumber); $results['success']++; } catch (\Exception $e) { $results['failed']++; $results['errors'][] = [ 'line' => $lineNumber, 'error' => $e->getMessage(), 'data' => $row, ]; } }
fclose($handle);
return $results; } }
|
带验证的导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <?php
namespace App\Services;
use App\Models\User; use Illuminate\Support\Facades\Validator;
class UserCsvImport { protected CsvImportService $importService;
public function __construct(CsvImportService $importService) { $this->importService = $importService; }
public function import(string $path): array { return $this->importService->import($path, function ($row, $lineNumber) { $data = $this->mapRow($row);
$this->validate($data);
User::create($data); }); }
protected function mapRow(array $row): array { return [ 'name' => $row[0] ?? null, 'email' => $row[1] ?? null, 'password' => $row[2] ?? null, ]; }
protected function validate(array $data): void { $validator = Validator::make($data, [ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email', 'password' => 'required|string|min:8', ]);
if ($validator->fails()) { throw new \Exception($validator->errors()->first()); } } }
|
CSV 解析器
高级 CSV 解析器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| <?php
namespace App\Services;
class CsvParser { protected array $options;
public function __construct(array $options = []) { $this->options = array_merge([ 'delimiter' => ',', 'enclosure' => '"', 'escape' => '\\', 'has_header' => true, 'encoding' => 'UTF-8', ], $options); }
public function parse(string $content): array { $lines = $this->splitLines($content); $headers = null; $data = [];
foreach ($lines as $index => $line) { if (empty(trim($line))) { continue; }
$row = $this->parseLine($line);
if ($index === 0 && $this->options['has_header']) { $headers = $row; continue; }
if ($headers) { $data[] = array_combine($headers, $row); } else { $data[] = $row; } }
return $data; }
public function parseFile(string $path): array { $content = file_get_contents($path);
if ($this->options['encoding'] !== 'UTF-8') { $content = mb_convert_encoding($content, 'UTF-8', $this->options['encoding']); }
return $this->parse($content); }
protected function splitLines(string $content): array { return preg_split('/\r\n|\r|\n/', $content); }
protected function parseLine(string $line): array { $result = []; $field = ''; $inEnclosure = false; $len = strlen($line);
for ($i = 0; $i < $len; $i++) { $char = $line[$i];
if ($char === $this->options['enclosure']) { if ($inEnclosure && isset($line[$i + 1]) && $line[$i + 1] === $this->options['enclosure']) { $field .= $char; $i++; } else { $inEnclosure = !$inEnclosure; } } elseif ($char === $this->options['delimiter'] && !$inEnclosure) { $result[] = $field; $field = ''; } else { $field .= $char; } }
$result[] = $field;
return $result; } }
|
CSV 构建
CSV 构建器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <?php
namespace App\Services;
class CsvBuilder { protected array $headers = []; protected array $rows = []; protected string $delimiter = ','; protected string $enclosure = '"'; protected string $lineEnding = PHP_EOL;
public function setHeaders(array $headers): self { $this->headers = $headers; return $this; }
public function addRow(array $row): self { $this->rows[] = $row; return $this; }
public function addRows(array $rows): self { foreach ($rows as $row) { $this->addRow($row); } return $this; }
public function setDelimiter(string $delimiter): self { $this->delimiter = $delimiter; return $this; }
public function build(): string { $output = '';
if (!empty($this->headers)) { $output .= $this->buildRow($this->headers); }
foreach ($this->rows as $row) { $output .= $this->buildRow($row); }
return $output; }
protected function buildRow(array $row): string { $fields = array_map(function ($field) { return $this->encloseField($field); }, $row);
return implode($this->delimiter, $fields) . $this->lineEnding; }
protected function encloseField($field): string { $field = (string) $field;
$needsEnclosure = str_contains($field, $this->delimiter) || str_contains($field, $this->enclosure) || str_contains($field, "\n") || str_contains($field, "\r");
if ($needsEnclosure) { $field = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $field); return $this->enclosure . $field . $this->enclosure; }
return $field; }
public function download(string $filename) { return response($this->build(), 200, [ 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', ]); } }
|
CSV 验证
CSV 结构验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| <?php
namespace App\Services;
class CsvValidator { protected array $rules; protected array $errors = [];
public function __construct(array $rules) { $this->rules = $rules; }
public function validate(string $path): array { $handle = fopen($path, 'r'); $headers = fgetcsv($handle);
$this->validateHeaders($headers);
$lineNumber = 1; $validRows = []; $invalidRows = [];
while (($row = fgetcsv($handle)) !== false) { $lineNumber++;
$result = $this->validateRow($row, $headers, $lineNumber);
if ($result['valid']) { $validRows[] = $result['data']; } else { $invalidRows[] = $result; } }
fclose($handle);
return [ 'valid' => empty($this->errors), 'headers_valid' => empty($this->errors['headers'] ?? []), 'errors' => $this->errors, 'valid_rows' => $validRows, 'invalid_rows' => $invalidRows, 'total_rows' => $lineNumber - 1, ]; }
protected function validateHeaders(array $headers): void { $expected = $this->rules['headers'] ?? [];
if (count($expected) !== count($headers)) { $this->errors['headers'][] = '列数不匹配'; }
foreach ($expected as $index => $name) { if (!isset($headers[$index]) || $headers[$index] !== $name) { $this->errors['headers'][] = "第 " . ($index + 1) . " 列应该是 '{$name}'"; } } }
protected function validateRow(array $row, array $headers, int $lineNumber): array { $data = array_combine($headers, $row); $errors = [];
foreach ($this->rules['columns'] ?? [] as $column => $rules) { $value = $data[$column] ?? null;
foreach ($rules as $rule) { $error = $this->validateField($column, $value, $rule);
if ($error) { $errors[] = $error; } } }
return [ 'line' => $lineNumber, 'data' => $data, 'valid' => empty($errors), 'errors' => $errors, ]; }
protected function validateField(string $column, $value, string $rule): ?string { if ($rule === 'required' && empty($value)) { return "{$column} 不能为空"; }
if ($rule === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) { return "{$column} 不是有效的邮箱地址"; }
if ($rule === 'numeric' && !is_numeric($value)) { return "{$column} 必须是数字"; }
return null; } }
|
总结
Laravel 13 的 CSV 处理提供了:
- 灵活的 CSV 导出功能
- 流式大数据导出
- 带验证的导入处理
- 高级 CSV 解析器
- CSV 构建工具
- 结构验证支持
掌握 CSV 处理技巧可以高效处理数据交换需求。