PHP fputcsvで出力するとダブルクォーテーションで括られる

PHP のfputcsvは配列データをCSVファイルへ出力する際に自動でエスケープ処理もしてくれるので便利ですが、エンコーディングを UTF-8 から Shift-JIS へ変換して出力すると、文字列が予期せずダブルクォーテーションで括られる場合があります。

<?php
function convertEnc($arg) {
    if (is_array($arg)) {
        return array_map('convertEnc', $arg);
    } else {
        return mb_convert_encoding($arg, "SJIS-win", "UTF-8");
    }
}

$array = array(
              "あ"
             ," "
             ,"\r\n"
             ,"ソ"
             ,"十"
);
$fp = fopen("php://stdout", "w");

// UTF-8 のまま出力
fputcsv($fp, $array);

// Shift-JIS に変換して出力
//fputcsv($fp, convertEnc($array));

fclose($fp);
?>

// UTF-8 出力結果
UTF-8 のままで出力すると改行コードだけがダブルクォーテーションで囲まれます。

あ," ","
",ソ,十

// Shift-JIS 出力結果
Shift-JIS へ出力すると「ソ」、「十」もダブルクォーテーションで囲まれてしまいます。

あ," ","
","ソ","十"

「ソ」、「十」は Shift-JIS では、れぞれ「0x8F5C」、「0x835C」で 0x5C はバックスラッシュ、バックスラッシュはエスケープ文字扱いなのでダブルクォーテーションで囲まれるようです。

参考:
https://github.com/php/php-src/blob/master/ext/standard/file.c

		/* enclose a field that contains a delimiter, an enclosure character, or a newline */
		if (FPUTCSV_FLD_CHK(delimiter) ||
			FPUTCSV_FLD_CHK(enclosure) ||
			FPUTCSV_FLD_CHK(escape_char) ||
			FPUTCSV_FLD_CHK('\n') ||
			FPUTCSV_FLD_CHK('\r') ||
			FPUTCSV_FLD_CHK('\t') ||
			FPUTCSV_FLD_CHK(' ')
		) {

PHP fputcsvの改行コードをCRLFで出力

PHP のfputcsv関数で出力した CSV ファイルの各行の改行コードは LF なんですね。
記憶の片隅に残しておかないと、改行コードに CRLF を期待しているシステムで読み込めませんなんてことになってしまいます。 😉

改行に CRLF を出力する方法は、いろいろあると思いますが、php_user_filter を使った方法がスマートに思えます。
フィルタをつくって、stream_filter_register で登録、開いたストリームに stream_filter_append で追加すると、読み込みまたは書き込み時にフィルタが適用されます。

// filter class that applies CRLF line endings
class crlf_filter extends php_user_filter
{
    function filter($in, $out, &$consumed, $closing)
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            // make sure the line endings aren't already CRLF
            $bucket->data = preg_replace("/(?data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

// フィルタの登録
stream_filter_register('crlf', 'crlf_filter');

// 出力ファイルのオープン
$fp = fopen($foldername."/".$filename, 'w');

// 出力ファイルへフィルタをアタァッチ
stream_filter_append($fp, 'crlf');

while(繰り返す) {
    $output = array();
... 省略 ...
    // 出力
    fputcsv($fp, $output);
}

// 出力ファイルのクローズ
fclose($fp);

参考:
stream_filter_register
http://stackoverflow.com/questions/12722894/how-can-i-change-the-line-endings-used-by-fputcsv

PHP implode で配列データをダブルクォートで囲む

配列の各要素を implode 関数をつかってカンマ区切りにして文字列として出力するのですが、さらに各要素をダブルクォーテーションで囲む方法です。

<?php
$arr = array('aaa','bbb','ccc','DDD','EEE');
$str = implode('","', $arr);
// --> aaa","bbb","ccc","DDD","EEE
$str = '"'.$str.'"';
// --> "aaa","bbb","ccc","DDD","EEE"
echo $str;
?>

C#でCSVファイルをDataTableへ読み込む

C# で CSVファイルを読み込んでデータテーブル(DataTable)へ格納するサンプルを作成しました。VisualStudio 2010 で動作確認しています。

using System;
using System.IO;
using System.Data;

namespace CSVTest
{
    class CSVImporter
    {
        static void Main(string[] args)
        {
            CSVImporter csvimporter = new CSVImporter();

            if (args.Length > 0 && !File.Exists(args[0]))
            {
                return;
            }

            // CSVファイルの読み込み
            DataTable dt = csvimporter.ImportCSVToDataTable(args[0], ',', false);

            // 読み込んだCSVを確認
            DataRow[] rows = dt.Select();

            for (int i = 0; i < rows.Length; i++)
            {
                string s = string.Empty;
                for (int j = 0; j < dt.Columns.Count; j++)
                {
                    s += rows[i][j];
                    if (j 
0) { string[] fields = csvRows[0].Split(delim); // DataTable 列の作成 for (int i = 0; i < fields.GetLength(0); i++) { if (is1stRowHeader) { // 先頭行をヘッダーとして使用 dt.Columns.Add(fields[i], typeof(string)); } else { dt.Columns.Add(); } } // CSVデータをDataTableへ読み込む int firstDataRow = is1stRowHeader ? 1 : 0; for (int i = firstDataRow; i < csvRows.GetLength(0); i++) { fields = csvRows[i].Split(delim); DataRow row = dt.NewRow(); row.ItemArray = fields; dt.Rows.Add(row); } } return dt; } } }

Visual C++/MFCライブラリ CSVカンマ区切りの処理

Visual C++ と MFC ライブラリは長い間使っているけど、他の言語のライブラリにはカンマ区切りの文字列を一発で配列に収めて返してくれる split 関数なるものが普通にあります。あらためて MFC CString にはそれがないことを認識しました。むかし、そのような処理をやったことがあったので、そのコードを抜粋しました。

以下、コード抜粋

    CString str = "111,222,333,444,555,,aaa,bbb,ccc,ddd,eee,fff";
    CArray<CString,CString> arr;

    // カンマ区切りを配列に格納
    arr.RemoveAll();
        
    int iStart = 0;
    int iPos = -1;

    CString restoken;
    iPos = str.Find(',', iStart);

    while(iPos > -1)
    {
        restoken = str.Mid(iStart, iPos - iStart);
        arr.Add(restoken);
        printf("Resulting token: %sn", restoken);

        iStart = iPos + 1;
        iPos = str.Find(',', iStart);

        if (iPos == -1)
        {
            restoken = str.Mid(iStart);
            arr.Add(restoken);
            printf("Resulting token: %sn", restoken);
        }
    }

出力結果

Resulting token: 111
Resulting token: 222
Resulting token: 333
Resulting token: 444
Resulting token: 555
Resulting token:
Resulting token: aaa
Resulting token: bbb
Resulting token: ccc
Resulting token: ddd
Resulting token: eee
Resulting token: fff

CSVファイルによっては、各列データををダブルクォーテーションで囲む場合もあるので、それは対応していません。;-)

また、MSDN CStringT::Tokenize のサンプル トークンの区切り文字をカンマにすると良さそうです。
※ 但し、カンマとカンマに文字が何もない場合(赤字の部分)は、その列は無視して次の列をトークンとして読み込むようです。CSV ファイルの処理で、空き列も処理する必要がある場合は要注意です。

以下、MSDN サンプルを一部変更して

    CAtlString str( "111,222,333,444,555,,aaa,bbb,ccc,ddd,eee,fff" );
    CAtlString resToken;
    int curPos= 0;

    resToken= str.Tokenize(",",curPos);
    while (resToken != "")
    {
        printf("Resulting token: %sn", resToken);
        resToken= str.Tokenize(",",curPos);
    }

出力結果

Resulting token: 111
Resulting token: 222
Resulting token: 333
Resulting token: 444
Resulting token: 555
Resulting token: aaa
Resulting token: bbb
Resulting token: ccc
Resulting token: ddd
Resulting token: eee
Resulting token: fff