voilab/csv

使用 `fgetcsv` 解析文件、字符串、流或数组,提取列并提供每列方法来操作数据。

安装: 82

依赖项: 0

建议者: 0

安全: 0

星标: 0

关注者: 4

分支: 0

开放问题: 2

类型:application

5.1.1 2023-10-16 10:29 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/fgetcsvhttps://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,可以是rowcolumn。如果是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函数。它接收列的所有数据,并必须返回一个索引数组。

例如,如果有两行值分别为ab,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)。有关更多信息,请参阅许可文件。