将CSV文件处理为带有列标题的数组作为键

31

我有一个CSV文件,第一行包含字段名称。 例如数据如下...

"Make","Model","Note"
"Chevy","1500","loaded"
"Chevy","2500",""
"Chevy","","loaded"

我需要将我的数据格式化为一组键-值对的数组,其中键名是列标题。我猜第一行的格式应该如下:

$array = [
    "Make" => "Chevy",
    "Model" => "1500",
    "Note" => "loaded"
];

...第二行...

$array = [
    "Make" => "Chevy",
    "Model" => "1500",
    "Note" => ""
];

...还有第三行...

$array = [
    "Make" => "Chevy",
    "Model" => "",
    "Note" => "loaded"
];

除了静态方法,我不确定如何做到这一点——问题在于与其相关数据的列可能会从一个文件变为另一个文件... 列重新排列、删除或添加。

非常感谢您的想法。


你可能是指第二个数组示例中的2500。 - user1899415
9个回答

69
$all_rows = array();
$header = fgetcsv($file);
while ($row = fgetcsv($file)) {
  $all_rows[] = array_combine($header, $row);
}
print_r($all_rows);

感谢您的快速回复。虽然这已经接近了,但我最终得到的数据在键中。返回的任何数组中都没有看到任何列标题。 - Bit Bucket
1
@BitBucket:如果你对 $all_rows 中的数据进行转储,你应该会看到一个包含子数组的数组,其中子数组的键是头部数据。 - user142162
请注意,在您首次创建$header时,需要通过它来运行,以确保为没有标题的任何列提供“unknown”等虚拟数据。$x或数组组合将具有不同的长度。 - Titan
如果您在使用此方法时遇到问题,请检查您的.csv文件字符编码。我的文件是UTF-8 BOM格式,因此我的第一个标题键实际上是"[0xEF]FirstHeader",但由于BOM是不可见的,所以我花了一段时间才找出来。 - niko

37

使用PHP的SplFileObject已经可以满足99.9%的需求,你可以通过继承来实现缺失的0.1%。在以下示例中,CSVFile是从SplFileObject继承而来的:

$csv = new CSVFile('../data/test.csv');

foreach ($csv as $line)
{
    var_dump($line);
}

以下是您提供的示例数据:

array(3) {
  ["Make"]=>  string(5) "Chevy"
  ["Model"]=> string(4) "1500"
  ["Note"]=>  string(6) "loaded"
}
array(3) {
  ["Make"]=>  string(5) "Chevy"
  ["Model"]=> string(4) "2500"
  ["Note"]=> string(0) ""
}
array(3) {
  ["Make"]=>  string(5) "Chevy"
  ["Model"]=> string(0) ""
  ["Note"]=>  string(6) "loaded"
}

CSVFile的定义如下:

class CSVFile extends SplFileObject
{
    private $keys;

    public function __construct($file)
    {
        parent::__construct($file);
        $this->setFlags(SplFileObject::READ_CSV);
    }

    public function rewind()
    {
        parent::rewind();
        $this->keys = parent::current();
        parent::next();
    }

    public function current()
    {
        return array_combine($this->keys, parent::current());
    }

    public function getKeys()
    {
        return $this->keys;
    }
}
如果你这样做,细节将被良好地封装起来。此外,在current()函数内处理错误(例如计数不匹配)更容易,因此使用数据的代码无需处理它。 编辑: 但是,所给示例在可重用性方面很短。与其从SplFileObject扩展,还不如聚合它。
class KeyedArrayIterator extends IteratorIterator
{
    private $keys;

    public function rewind()
    {
        parent::rewind();
        $this->keys = parent::current();
        parent::next();
    }

    public function current()
    {
        return array_combine($this->keys, parent::current());
    }

    public function getKeys()
    {
        return $this->keys;
    }
}

这段代码是完全相同的,但构造函数封装的细节被省略了。这种简化允许更广泛地使用该类型,例如(但不仅限于)使用所述SplFileObject

$file = new SplFileObject('../data/test.csv');
$file->setFlags($file::READ_CSV);

$csv = new KeyedArrayIterator($file);

foreach ($csv as $line) {
    var_dump($line);
}
如果这听起来过于冗长,它可以再次包装以使其外观更加美观。
class CSVFile extends KeyedArrayIterator
{
    /**
     * @param string $file
     */
    public function __construct($file)
    {
        parent::__construct(new SplFileObject($file));
        $this->setFlags(SplFileObject::READ_CSV);
    }
}

在可遍历迭代器TraversableIterator的标准修饰性的支持下,第一个CSVFile示例中的原始构造函数代码可以完全复制。

这个最后的补充还允许保持使用CSVFile迭代器的原始代码不变:

$csv = new CSVFile('../data/test.csv');

foreach ($csv as $line) {
    var_dump($line);
}

所以只需进行快速的重构以允许更多的代码复用。你将免费获得一个 KeyedArrayIterator


你能否想到一种方法来处理无标题CSV文件呢,仅供参考? - Gga
1
这很简单:省略rewind函数,在构造函数中传递键。如果需要更多的灵活性,我将一些代码放在了一个 gist 中,并提供了示例,但仍然处于相当初步阶段:https://gist.github.com/4153380 - hakre
非常好!要使用不同的分隔符(例如冒号;),请在CSVFile构造函数中添加$this->setCsvControl(';'); - sMyles
1
我知道这是一个相当陈旧的帖子,但最近在一个项目中我很成功地使用了它,所以谢谢你。我遇到的一个问题是无法使用SKIP_EMPTY标志。有没有办法添加它呢? - Adam Christianson
@AdamChristianson:你试过这样写吗$...->setFlags(SplFileObject::READ_CSV | SKIP_EMPTY);?还是说已经试过但无效? - hakre
显示剩余3条评论

6
$csv_data = array_map('str_getcsv', file('Book.csv'));// reads the csv file in php array
$csv_header = $csv_data[0];//creates a copy of csv header array
unset($csv_data[0]);//removes the header from $csv_data since no longer needed
foreach($csv_data as $row){
    $row = array_combine($csv_header, $row);// adds header to each row as key
    var_dump($row);//do something here with each row
}

$csv_header = $csv_data[0]; unset($csv_data[0]); 等同于 $csv_header = array_shift($csv_data); 并且在 [0] 不存在时不会出现问题,正如 Azam 所示。 - mickmackusa

3
function processCsv($absolutePath)
{
    $csv = array_map('str_getcsv', file($absolutePath));
    $headers = $csv[0];
    unset($csv[0]);
    $rowsWithKeys = [];
    foreach ($csv as $row) {
        $newRow = [];
        foreach ($headers as $k => $key) {
            $newRow[$key] = $row[$k];
        }
        $rowsWithKeys[] = $newRow;
    }
    return $rowsWithKeys;
}

1

我假设你已经解决了这个问题,但是我想提供一种解决方法,可能不是最好/最优雅的解决方案,但它能解决问题:

$row = 1;
$array = array();
$marray = array();
$handle = fopen('file.csv', 'r');
if ($handle !== FALSE) {
    while (($data = fgetcsv($handle, 0, ',')) !== FALSE) {
        if ($row === 1) {
            $num = count($data);
            for ($i = 0; $i < $num; $i++) {
                array_push($array, $data[$i]);
            }
        }
        else {
            $c = 0;
            foreach ($array as $key) {
                $marray[$row - 1][$key] = $data[$c];
                $c++;
            }
        }
        $row++;
    }
    echo '<pre>';
    print_r($marray);
    echo '</pre>';
}

1

试试这个

$csv = array_map("str_getcsv", file('file.csv', FILE_SKIP_EMPTY_LINES));    
$header = array_shift($csv); // get header from array

foreach ($csv as $key => $value) {    
    $csv[$key] = array_combine($header, $value);
    var_dump($csv[$key]['Model']);
}

var_dump($csv);

0
在Tim Cooper的回答中,不要使用


$all_rows = array();
$header = null;
while ($row = fgetcsv($file)) {
    if ($header === null) {
        $header = $row;
        continue;
    }
    $all_rows[] = array_combine($header, $row);
}

我会以更优雅和高效的方式编写代码:

$rows = null;
$header = fgetcsv($file);
while ($row = fgetcsv($file)) {
    $rows[] = array_combine($header, $row);
}

0

array_combine() 函数只有在标题列与数据列匹配时才能正常工作,否则会抛出错误。


1
这应该是对现有答案的评论,而不是一个答案本身? - Steffen Mächtel
感谢您的帮助,但我认为这可能有助于解决方案的聚焦;在这个主题上已经很紧张了 - 有这么多答案。我只是提到它是为了避免混淆。许多自称为PHP“专家”的人已经发布了使用array_combine函数的解决方案,但未能注意到其中的缺陷。我的解决方案是编辑CSV文件标题以匹配数据,然后在使用array_combine之前存储输出... - Pellumb
@Pellumb:除非错误是有意的,否则该备注(在我看来,这很好地符合评论的条件,最好放在问题下面,这样更容易看到)是有效的。如果你运行大多数(如果不是全部)示例(甚至那些不使用array_combine()的示例,尤其是那些没有使用array_combine()的示例),会导致CSV文件/缓冲/流中在实际标题列之前的注释不能正确处理。另一个常见问题是在末尾存在终止符行(或多个终止符行)(array_combine()也会用错误消息突出显示)。 - hakre

0

尝试使用这段代码:

$query = "SELECT * FROM datashep_AMS.COMPLETE_APPLICATIONS";
$export= mysql_query($query);
$first = true;
$temp = $export[0];
//echo "<pre>"; print_r($first); exit;

header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=file.csv');
header('Pragma: no-cache');
header("Expires: 0");

$outstream = fopen("php://output", "w");



foreach($export as $result)
{
    if($first){
        $titles = array();
        foreach($temp as $key=>$val){
            $titles[] = $key;
        }
        //print_r ($titles);exit;
        fputcsv($outstream, $titles);
    }
    $first = false;
    fputcsv($outstream, $result);
}

fclose($outstream);

谢谢


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接