PHP导出CSV文件及问题总结

第一部分:生成CSV文件,导出/下载文件

0、什么是CSV文件。
CSV(Comma-Separated Values),即逗号分隔值(有时也称为字符分隔值,因为分隔字符也可以不是逗号)。CSV文件是以CSV格式存储表格数据的纯文本文件。
注:Excel可以直接打开CSV文件, 可以另存为Excel文件扩展名xls(Excel 03之前的版本), xlsx(Excel 07之后的版本)。所以一般导出Excel数据的需求,直接导出CSV比较快捷。

1、PHP后台导出CSV文件,使用PHP官方自带函数fputcsv。

$lists = array (
    array('aaa', 'bbb', 'ccc'),
    array('123', '456', '789'),
    array('"xxx"', '"yyy"')
);

$fp = fopen('file.csv', 'w');

foreach ($lists as $list) {
    fputcsv($fp, $list);
}

fclose($fp);

# 导出的结果cat file.csv
aaa,bbb,ccc
123,456,789
"""xxx""","""yyy"""

以上是PHP官方fputcsv()的例子,引用这个例子主要是为了说明为啥生成的第三行每列是6个双引号。
首先我们需要明白CSV目前还没有严格的标准,但有一些基本规范,看完下面三条规范你就完全明白上面的显示方式了, 更多规范请参考CSV维基百科

    • 任何字段都可以被包裹(使用双引号字符)。
"1997","Ford","E350"
    • 包含换行符、双引号和/或逗号的字段应当被包裹。(否则,文件很可能不能被正确处理)。
1997,Ford,E350,"Super, luxurious truck"
    • 每个被嵌入的双引号字符必须被表示为两个双引号字符, 同时必须被双引号包裹。
1997,Ford,E350,"Super, ""luxurious"" truck"

2、PHP后台导出CSV文件,使用PHP官方自带函数file_put_contents。

$lists = array (
    array('aaa', 'bbb', 'ccc'),
    array('123', '456', '789'),
	array('"xxx"', '"yyy"', '"zzz"'),
);

$file_name = 'file.csv';

foreach ($lists as $list) {
    $row = implode(',', $list);
    file_put_contents($file_name, $row . PHP_EOL, FILE_APPEND | LOCK_EX);
}

3、PHP后台导出CSV文件,使用PHP官方自带函数fwrite/fputs。
fputs是fwrite的别名,在逻辑上还是有差异,一个是写入,一个是添加。

$lists = array (
    array('aaa', 'bbb', 'ccc'),
    array('123', '456', '789'),
	array('"xxx"', '"yyy"', '"zzz"'),
);

// 写入文件
$fp = fopen('file.csv', 'w');
foreach ($lists as $list) {
    $row = implode(',', $list);
    fwrite($fp, $row . PHP_EOL);
}
fclose($fp);

// 向文件添加内容
$fp = fopen('file.csv', 'a');
foreach ($lists as $list) {
    $row = implode(',', $list);
    fputs($fp, $row . PHP_EOL);
}
fclose($fp);

注意:
使用file_put_contents,每次都会在写入时先打开文件,写入结束后在关闭文件。然而使用fwrite,仅仅需要打开一次文件即可,因此使用fwrite效率更高。效率高的原因参考《PHP写日志fwrite和file_put_contents的区别

4、浏览器请求下载CSV文件

// 以下是一个API的主体内容
$file_name = 'uids' . '_' . date('Ymd') . '.csv';

header('Content-type: application/octet-stream');
header('Content-Transfer-Encoding: binary');
header("Content-Disposition: attachment; filename=" . $file_name);

$uids = []; // from db
echo 'uid' . "\r\n";
foreach ($uids as $uid) {
    echo $uid . "\r\n";

    ob_flush();
    flush();
}

测试验证,将以上代码存到download.php文件中,在该文件目录下启动PHP内置服务:

php -S localhost:8088

浏览器地址栏输入:localhost:8088/download.php将获取下载文件

第二部分: Excel打开CSV文件常见问题总结

一、导出的CSV文件,使用Excel打开,无法自动分列(没有按逗号分隔成多列)。

  • 解决方法一:使用Excel分列功能
a、选择一整列
b、在头部菜单中选择【数据】
c、【数据】菜单展开选择【分列】的功能
  • 解决方法二:Mac系统下,修改系统语言。

系统偏好设置-> 语言和地区->然后地区栏选“中国”之外的某个地方,如香港。

  • 解决方法三:如果之前使用fputcsv生成的数据,可以改为file_put_contents或fwrite试试。

测试发现fputcsv生成的CSV格式如果包含纯数字列,无法分列,file_put_contents或fwrite可以

例:

aaa,bbb,ccc
123,456,789
xxx,yyy,zzz

二、Windows下Excel打开CSV文件中文乱码。

BOM的个人理解(可见维基百科,BOM中文BOM英文

BOM(byte order mark)是放在一段字节流开头的Unicode字符,字符编码内容是U+FEFF, 但他是不可见的,像是一串魔法数字,可以用了做一些标记。所以用来表示字节顺序或编码方式。在UTF-16和UTF-32中BOM是必须的,在字节流开头加上BOM用来表示编码和字节序。在UTF-8中BOM不是必须的,同时UTF-8是以一个字节为单位处理的,不存在字节序的问题(为什么UTF-8不存在字节序的问题),但是有时候需要在UTF-8编码的字节流开头加上标示编码方式的标记,即在BOM中加入编码EF BB BF即可(编码EF BB BF一般也就称为UTF-8 BOM头)。(FEFF不好理解,可参考:BOM In HTML)

乱码原因(参考: BOM头是什么)

Windows下的Excel打开CSV文件,会检查BOM,获取文件编码方式,如果没有获取到就按默认编码方式打开,当打开文件的编码方式和文件内容的编码方式不一致的时候,乱码大戏就上演了。而一般在Linux系统或Mac OS中生成的文件默认都是UTF-8编码,这也是乱码频发的原因。(Windows下的记事本打开就不会有问题,Excel估计是一个Bug, WPS打开测试一般正常,估计是微软修复了)。

乱码解决方法:
0、生成文件加上BOM解决乱码

// 说明,转换后Windows下打开不乱码,Mac下Excel打开乱码
function export_csv($data, $file_name)
{
	try {
		$fp = fopen($file_name, 'w');
		// 两种方式效果一样 fwrite($fp, $bom = "\xEF\xBB\xBF");
		fwrite($fp, $bom = (chr(0xEF) . chr(0xBB) . chr(0xBF)));
		foreach ($data as $list) {
			fputcsv($fp, $list);
		}
		fclose($fp);
	} catch (Exception $e) {
		var_dump($e);
		return false;
	}

        return true;
}

$lists = array (
    array('aaa', 'bbb', 'ccc'),
    array('123', '456', '789'),
	array('"xxx"', '"yyy"', '"zzz"'),
	array('春晚', '北京', '前程似锦')
);
export_csv($lists, 'log.csv');

Tip: 思考这种方式Mac下为啥还是乱码

1、转换编码方式(UTF-8转GBK)解决乱码:

// 说明,转换后Windows下,Mac下Excel打开都不会乱码
function export_csv($data, $file_name)
{
	foreach ($data as $field) {
		// 解决中文乱码
		$row = iconv('UTF-8', 'GBK', implode(',', $field));
		// $row = implode(',', $field);
		file_put_contents($file_name, $row . PHP_EOL, FILE_APPEND | LOCK_EX);
	}

	return true;
}

$lists = array (
    array('aaa', 'bbb', 'ccc'),
    array('123', '456', '789'),
	array('"xxx"', '"yyy"', '"zzz"'),
	array('春晚', '北京', '前程似锦')
);
export_csv($lists, 'log.csv');

提示:Mac下文件编码方式查看: vim 打开文件 :set fileencoding查看(file命令貌似不行):

2、Windows下手动转换
在Windows下用WPS打开乱码的CSV文件,另存为xls格式

3、Mac OS下手动转换
利用Mac自动的工具Automator生成一个服务,手动转换即可(亲测有效).
Mac Excel打开文件全是乱码,原因和解决办法是什么

Tip: 还有一种救急方法,在Mac上用Numbers打开CSV, 另存为Excel, 测试发现Mac和Windows下打开都没有乱码。
 

4、浏览器下载CSV文件stackoverflow完整例子《Export to CSV via PHP》(可能需要翻墙)

/**
 * 浏览器下载csv文件(加BOM)
 * @author salmonl
 * @date   2019-03-01
 */
class Tool_Csv
{
    /**
     * 导出UTF-8编码的csv文件(加BOM)
     * @param $array
     * @param array $filename
     * @param $filename
     */
    public static function export(&$array, $fieldsName = [], $filename = null) {
        !$filename && $filename = 'data_export';
        self::download_send_headers($filename . date('Y-m-d') . ".csv");
        echo self::array2csv($array, $filename);
    }

    /**
     * send header
     * @param $string
     */
    public static function download_send_headers($filename) {
        // disable caching
        $now = gmdate("D, d M Y H:i:s");
        header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate");
        header("Last-Modified: {$now} GMT");

        // force download
        header("Content-Type: application/force-download");
        header("Content-Type: application/octet-stream");
        header("Content-Type: application/download");

        // disposition / encoding on response body
        header("Content-Disposition: attachment;filename={$filename}");
        header("Content-Transfer-Encoding: binary");
    }

    /**
     * array to csv
     * @param $string
     */
    private static function array2csv(array &$array, $fields_name = []) {
        if (count($array) == 0) {
            return null;
        }
        ob_start();
        $df = fopen("php://output", 'w');
        fwrite($df, $bom = "\xEF\xBB\xBF");
        if ($fields_name) {
            fputcsv($df, $fields_name);
        } else {
            fputcsv($df, array_keys(reset($array)));
        }
        foreach ($array as $row) {
            fputcsv($df, $row);
        }
        fclose($df);
        return ob_get_clean();
    }
}

参考
linux(Mac)下查看文件编码及修改编码

发表评论

电子邮件地址不会被公开。 必填项已用*标注