voilab / csv
使用 `fgetcsv` 解析文件、字符串、流或数组,提取列并提供每列方法来操作数据。
Requires
- php: >=7.1
- psr/http-message: ^1.0
Requires (Dev)
- guzzlehttp/guzzle: ^6.5
- phpunit/phpunit: ^7.5
- scrutinizer/ocular: ^1.5
- dev-develop
- 5.1.1
- 5.1.0
- 5.0.0
- 4.2.1
- 4.2.0
- 4.1.0
- 4.0.0
- 3.1.1
- 3.1.0
- 3.0.0
- 2.0.1
- 2.0.0
- 1.1.1
- 1.1.0
- 1.0.5
- 1.0.2
- 1.0.0
- 0.5.3
- 0.5.2
- 0.5.0
- 0.4.7
- 0.4.6
- 0.4.5
- 0.4.4
- 0.4.2
- 0.4.0
- 0.3.2
- 0.3.0
- 0.2.0
- dev-master
- dev-dependabot/composer/guzzlehttp/psr7-1.9.1
- dev-dependabot/composer/guzzlehttp/guzzle-6.5.8
- dev-feature/guess
- dev-feature/php5
This package is auto-updated.
Last update: 2024-09-16 12:27:18 UTC
README
此类使用 fgetcsv
解析文件或字符串,提取列并提供每列方法来操作数据。
它可以解析大型文件、HTTP流、任何类型的资源或字符串。
它具有基本的错误处理,因此可以收集CSV资源中的所有错误,然后对错误数组进行处理。
它是可扩展的,因此您可以解析自己的资源/流类型,如果您有非常特殊的需求。
目录
安装
通过Composer
在项目根目录下创建 composer.json 文件
{ "require": { "voilab/csv": "^5.0.0" } }
$ composer require voilab/csv
安装PHP5兼容版本
此PHP5版本无法解析流或可迭代对象。
{ "require": { "voilab/csv": "dev-feature/php5" } }
用法
可用方法
$parser = new \voilab\csv\Parser($defaultOptions = []); $result = $parser->fromString($str = "A;B\n1;test", $options = []); // or $result = $parser->fromFile($file = '/path/file.csv', $options = []); // or with a raw resource (fopen, fsockopen, php://memory, etc) $result = $parser->fromResource($resource, $options = []); // or with an array or an Iterator interface $result = $parser->fromIterable($array = [['A', 'B'], ['1', 'test']], $options = []); // or with a SPL file object $result = $parser->fromSplFile($object = new \SplFileObject('file.csv'), $options = []); // or with a PSR stream interface (ex. HTTP response message body) $response = $someHttpClient->request('GET', '/'); $result = $parser->fromStream($response->getBody(), $options = []); // or with a custom \voilab\csv\CsvInterface implementation $result => $parser->parse($myCsvInterface, $options = []);
简单示例
$parser = new \voilab\csv\Parser([ 'delimiter' => ';', 'columns' => [ 'A' => function (string $data) { return (int) $data; }, 'B' => function (string $data) { return ucfirst($data); } ] ]); $csv = <<<CSV A; B 4; hello 2; world CSV; $result = $parser->fromString($csv); foreach ($result as $row) { var_dump($row['A']); // int var_dump($row['B']); // string with first capital letter }
完整示例
$parser->fromFile('file.csv', [ // fgetcsv 'delimiter' => ',', 'enclosure' => '"', 'escape' => '\\', 'length' => 0, 'autoDetectLn' => null, // resources 'metadata' => [], 'close' => false, // PSR stream 'lineEnding' => "\n", // headers management 'headers' => true, 'strict' => false, 'required' => ['id', 'name'], // big files 'start' => 0, 'size' => 0, 'seek' => 0, 'chunkSize' => 0, // data pre-manipulation 'autotrim' => true, 'onBeforeColumnParse' => function (string $data) { return utf8_encode($data); }, 'guessDelimiter' => new \voilab\csv\GuesserDelimiter(), 'guessLineEnding' => new \voilab\csv\GuesserLineEnding(), 'guessEncoding' => new \voilab\csv\GuesserEncoding(), // data post-manipulation 'onRowParsed' => function (array $row) { $row['other_stuff'] = do_some_stuff($row); return $row; }, 'onChunkParsed' => function (array $rows) { // do whatever you want, return void }, 'onError' => function (\Exception $e, $index) { throw new \Exception($e->getMessage() . ": at line $index"); } // CSV columns definition 'columns' => [ 'A as id' => function (string $data) { return (int) $data; }, 'B as firstname' => function (string $data) { return ucfirst($data); }, 'C as name' => function (string $data) { if (!$data) { throw new \Exception("Name is mandatory and is missing"); } return ucfirst($data); }, // use of Optimizers (see at the end of this doc for more info) 'D as optimized' => new \voilab\csv\Optimizer( function (string $data) { return (int) $data; }, function (array $data) { return some_reduce_function($data); } ) ] ]);
文档
选项
这些是在构造函数级别或在调用 from*
方法时可以提供的选项。有关 fgetcsv
选项的详细信息,请参阅此处: https://php.ac.cn/fgetcsv 和 https://php.ac.cn/str_getcsv
列函数参数
在定义列的函数时,您可以访问这些参数
$parser->fromFile('file.csv', [ 'columns' => [ // minimal usage 'col1' => function (string $data) { return $data; } ] ]);
自动清理标题
请注意,标题会自动去空格和删除换行符。此外,所有空格后的空格也会被删除。这仅适用于标题。单元格内容不会被处理,除非 autotrim
设置为 true。
" a header " => "a header"
"a header" => "a header"
"a
header " => "a header"
如果您在代码中定义的列在CSV资源中不存在,并且不在
required
数组中,则$meta
参数将设置phantom
标志为true
。这是在解析过程中了解CSV资源中列是否存在的方法。
在解析列之前的功能参数
在解析任何CSV列数据之前,将调用一个标准方法,因此您可以以相同的方式处理每一行和每一列的数据。您可以使用此功能来管理编码,例如。
如果您想从这里返回其他类型的值,请注意列函数中的类型声明。
$parser->fromFile('file.csv', [ // minimal usage 'onBeforeColumnParse' => function (string $data) : string { return utf8_encode($data); } ]);
在解析行之后的功能参数
当一行完成时,您可以对该数据执行一些操作。
$parser->fromFile('file.csv', [ // minmal usage 'onRowParsed' => function (array $rowData) { return $rowData; } ]);
列别名
您可以为列定义别名以简化数据处理。只需写入as
来激活此功能,例如:CSV列名 as别名
。
别名必须不
包含as
字符串。但在CSV资源中,标题可以包含这样的字符串。
请注意,如果您在CSV资源标题中包含
as
,您必须
在列定义中对其别名。否则,解析器将找不到此列。
$str = <<<CSV A; B ; Just as I said 4; hello; hey 2; world; hi CSV; $parser = new \voilab\csv\Parser(); $result = $parser->fromString($str, [ 'delimiter' => ';', 'columns' => [ 'A as id' => function (string $data) { return (int) $data; }, 'B as content' => function (string $data) { return ucfirst($data); }, 'Just as I said as notes' => function (string $data) { return $data; } ] ]); print_r($result); /* prints: Array ( [0] => Array ( [id] => 4 [content] => Hello [notes] => hey ) [1] => Array ( [id] => 9 [content] => World [notes] => hi ) ) */
必需列
如果您已对列进行别名设置,并且它是必需列,则必须在required
选项中使用该别名。
$result = $parser->fromString($str, [ 'required' => ['id', 'content'], 'columns' => [ 'A as id' => function (string $data) { return (int) $data; }, 'B as content' => function (string $data) { return ucfirst($data); } ] ]);
无标题
如果您在CSV资源中没有标题,您需要这样定义解析器。
$str = <<<CSV 4; hello 2; world CSV; $result = $parser->fromString($str, [ 'columns' => [ '0 as id' => function (string $data) { return (int) $data; }, '1 as content' => function (string $data) { return ucfirst($data); } ] ]); print_r($result); /* prints: Array ( [0] => Array ( [id] => 4 [content] => Hello ) [1] => Array ( [id] => 9 [content] => World ) ) */
在定义列时打乱列顺序
您可以根据需要定义列的顺序。您不需要按CSV中出现的顺序提供它们。您只需确保您的键与CSV资源中的标题匹配即可。
请注意,列的执行顺序与您的代码对齐。在下面的示例中,函数
A()
在调用B()
之后执行,即使列A在CSV资源中先出现。
$str = <<<CSV A; B 4; hello 2; world CSV; $parser = new \voilab\csv\Parser(); $result = $parser->fromString($str, [ 'delimiter' => ';', 'columns' => [ 'B' => function (string $data) { // first call return ucfirst($data); }, 'A' => function (string $data) { // second call return (int) $data; } ] ]); print_r($result); /* prints: Array ( [0] => Array ( [B] => Hello [A] => 4 ) [1] => Array ( [B] => World [A] => 9 ) ) */
在大文件中查找
您可以使用查找机制来加速大文件的解析。
您可以指定起始索引。但这不是强制性的。它在错误管理中使用,以便知道哪一行有错误,或在其他方法调用中,其中提供了[$index]。
您负责保持[seek]和[start]同步。如果您不这样做,并且您有错误,则索引将不相关。
$str = <<<CSV A; B 4; hello 2; world ... CSV; $parser = new \voilab\csv\Parser(); $resource = new \voilab\csv\CsvString($str); $result = $parser->parse($resource, [ 'delimiter' => ';', 'size' => 2, 'columns' => [ 'B' => function (string $data) { return ucfirst($data); }, 'A' => function (string $data) { return (int) $data; } ] ]); $lastPos = $resource->tell(); $resource->close(); $resource2 = new \voilab\csv\CsvString($str); $nextResult = $parser->parse($resource2, [ 'delimiter' => ';', 'size' => 2, 'start' => 2, // yon **can** specify the start index. Not mandatory. 'seek' => $lastPos, 'columns' => [ 'B' => function (string $data) { return ucfirst($data); }, 'A' => function (string $data) { return (int) $data; } ] ]);
关闭资源
使用fromString()
和fromFile()
方法,资源将自动关闭。对于其他from*()
方法,您可以通过提供'close' => true
选项来关闭资源。
行结束问题
正如官方文档所述,如果您在识别行结束时有问题,您可以使用以下选项来激活自动检测。
$parser->parse($resource, [ 'autoDetectLn' => true ]);
请注意,在解析完成后,自动检测PHP ini参数不会被重置到初始值。
当解析流(如HTTP响应消息体)时,必须在数组选项中指定行结束。
错误管理
您可以使用onError
选项来收集所有错误,这样您就可以一次性向用户提供您在文件中找到的所有错误的消息。
您可以通过检查$meta
参数来停止行的处理过程。它有一个键type
,可以是row
或column
。如果是column
,您可以抛出错误,并且它将再次调用onError
,但类型为row
。其他列将跳过该行的此列。
如果您使用优化器,您也可以从那里调用异常。此时,键type
的值将是optimizer
。
$errors = []; $data = $parser->fromFile('file.csv', [ 'onError' => function (\Exception $e, $index, array $meta, array $options) use (&$errors) { $errors[] = "Line [$index]: " . $e->getMessage(); // do nothing more, so next columns and next lines can be parsed too. // meta types are the following: switch ($meta['type']) { case 'init': case 'column': case 'row': case 'reducer': case 'optimizer': case 'chunk': } }, 'columns' => [ 'email' => function (string $data) { // accept null email but validate it if there's one if ($data && !filter_var($data, FILTER_VALIDATE_EMAIL)) { throw new \Exception("The email [$data] is invalid"); } return $data ?: null; } ] ]); if (count($errors)) { // now print in some ways all the errors found print_r($errors); } else { // everything went well, put data in db on whatever }
初始化错误
一些错误在解析任何行之前抛出。您必须考虑这一点。
$data = $parser->fromFile('file.csv', [ 'onError' => function (\Exception $e, $index, array $meta) { if ($meta['type'] === 'init') { // called during initialization. var_dump($meta['key']); // for errors with specific key if ($e->getCode() === \voilab\csv\Exception::HEADERMISSING) { throw new \Exception(sprintf("La colonne [%s] est obligatoire", $meta['key'])); } } throw $e; } ]);
错误和国际化(i18n)
如果您想翻译错误消息,您可以使用带有meta['type'] === 'init'
的onError
函数来抛出翻译后的消息。
数据库操作,列优化
当解析大量数据时,如果某一列例如是用户ID,为每一行调用find($id)
方法是一个糟糕的想法。更好的方法是获取所有列的值,然后调用findByIds($ids)
。
内置的Optimizer
类允许您以这种方式定义列。它接受三个参数。第一个是用于从CSV解析值的函数。第二个是reduce函数。它接收列的所有数据,并必须返回一个索引数组。
例如,如果有两行值分别为a
和b
,reduce函数的索引结果将是Array ( a => something, b => something else )
。
第三个参数是当在reduce函数中找不到值时调用的函数。
解析函数
与列函数相同(见上文)
Reduce函数
返回一个索引数组。如果CSV列的值与reduce函数的结果不匹配,则不应返回缺失的值。例如,如果值是[10, 22],它们用于数据库查询以按ID查找用户,且用户ID 22不存在,则结果应该是
Array ( 10 => User(id=10) )
缺失函数
当在reduce结果中找不到值时,默认行为是将值设置为(例如,没有为该行定义reduce函数)。您可以通过定义缺失函数来覆盖此行为,并对此值执行所需的操作。
如果您已定义错误函数,并且在此处引发错误,则它将以
optimizer
类型(检查错误管理)调用。
示例
$str = <<<CSV A; B 4; updated John 2; updated Sybille CSV; $database = some_database_abstraction(); $data = $parser->fromString($str, [ 'delimiter' => ';', 'columns' => [ 'A as user' => new \voilab\csv\Optimizer( // column function, same as when there's no optimizer function (string $data) { return (int) $data; }, // reduce function that uses the set of datas from the 1st function function (array $data) use ($database) { $query = 'SELECT id, firstname FROM user WHERE id IN(?)'; $users = $database->query($query, array_unique($data)); return array_reduce($users, function ($acc, $user) { $acc[$user->id] = $user; return $acc; }, []); }, // absent function. data is [int] because the first function returns // an [int] function (int $data, int $index) { throw new \Exception("User with id $data at index $index doesn't exist!"); } ), 'B as firstname' => function (string $data) { return $data; } ] ]); print_r($result); /* prints: Array ( [0] => Array ( [user] => User ( id => 4, firstname => John ) [firstname] => updated John ) [1] => Array ( [user] => User ( id => 2, firstname => Sybille ) [firstname] => updated Sybille ) ) */
块
在某些情况下,优化器很有用,但有时您希望按块解析数据,对其进行操作,存储它,然后使用下一个块再次执行此操作。您可以通过块选项实现这一点。
$str = ''; // a hudge CSV string with tons of rows and two columns $parser->fromString($str, [ 'delimiter' => ';', 'chunkSize' => 500, 'onChunkParsed' => function (array $rows, int $chunkIndex, array $columns, array $options) { // count($rows) = 500 // do something with your parsed rows. This method will be called // as long as there are rows to parse. // This method returns void }, 'onError' => function (\Exception $e, $index, array $meta) { // if ($meta['type] === 'chunk') { do something } }, 'columns' => [ 'A as name' => function (string $data) { return (int) $data; }, 'B as firstname' => function (string $data) { return $data; } ] ]);
如果您使用优化器,则
$rows
将是优化后的结果集。
您不需要使用
fromString
(或类似)返回的数组,因为您在onChunkParsed
中执行的操作已经足够。
猜测器:自动检测行结束符、分隔符和编码
猜测CSV数据的结构(行结束符、分隔符或编码)是一个非常随机的任务,由于有这么多用例,无法全部规则。
此包仍然提供了一种猜测这些元素的方法,但如果它不符合您的需求,您可以轻松扩展或创建一个新类来管理您特定的用例。
如果您想以自己的方式实现猜测,请阅读每个猜测接口的代码库。
在某些CsvInterface实现中,猜测是没有用的。例如,可迭代对象被忽略,因为数据已经按单元格和行排列。在您使用猜测功能之前,请确保它对您有用。
猜测行结束符
解析器首先执行的操作是检测行结束符。提供的实现尝试在\n
、\r
和\r\n
中检测行结束符。
$str = 'A;B\r\n4;Hello\r\n;2;World'; $parser->fromString($str, [ 'guessLineEnding' => new \voilab\csv\GuesserLineEnding([ // maximum line length to read, which will be parsed // defaults to: see below 'length' => 1024 * 1024 ]) ]);
猜测分隔符
然后,解析器尝试检测分隔符。在提供的实现中,如果找不到分隔符或它太模糊,则抛出异常。
$str = 'A;B\n4;Hello\n;2;World'; $parser->fromString($str, [ 'guessDelimiter' => new \voilab\csv\GuesserDelimiter([ // delimiters to check. Defaults to: see below 'delimiters' => [',', ';', ':', "\t", '|', ' '], // number of lines to check. Defaults to: see below 'size' => 10, // throws an exception if result is amiguous. Defaults to: see below 'throwAmbiguous' => true, // score to reach for a delimiter. Defaults to: see below 'scoreLimit' => 50 ]) ]);
猜测编码
对于每个单元格,都会调用编码自动检测。提供的实现尝试找到当前单元格的编码,并将其编码为构造函数中给出的另一个编码。
它也用于标题行。如果您想在标题和数据之间使用不同的编码,您可以在您的encode
函数中检查$meta['type'] === 'init'
(检查代码库)。
此猜测器在
onBeforeColumnParse
之前调用。
$str = 'A;B\n4;Hellö\n;2;Wörld'; $parser->fromString($str, [ 'guessEncoding' => new \voilab\csv\GuesserEncoding([ // encoding in which data to retrieve. Defaults to: see below 'encodingTo' => 'utf-8', // encoding in file. If null, is auto-detected 'from' => null, // available encodings. If null, uses mb_list_encodings 'encodings' => null, // strict mode for mb_detect_encoding. Defaults to: see below 'strict' => false ]) ]);
已知问题
- 在PSR流中,标题和单元格内容不支持回车符。
- 猜测过程可能无法立即满足您的特定需求。在创建问题或PR之前,尝试扩展猜测类并制作您自己的特定适配。
测试
$ /vendor/bin/phpunit
安全
如果您发现任何与安全相关的问题,请使用问题跟踪器。
许可证
MIT许可(MIT)。有关更多信息,请参阅许可文件。