- PR -

お手本になるようなソースコード

投稿者投稿内容
ぼのぼの
ぬし
会議室デビュー日: 2004/09/16
投稿数: 544
投稿日時: 2007-05-24 23:19
引用:

びしばしさんの書き込み (2007-05-24 14:54) より:
ご指摘ありがとうございます。そのOffice PIA/VSTOだと思います。
# .netの不勉強がばれてしまいました。

引用:

「お手本」としては、「車輪の再発明をせずに既存のものはどんどん活用する」ことが実用的と思い、上記のような発言をしました。


「車輪の再発明をせずに既存のものはどんどん活用する」という点は大賛成ですが、
.NETとOffice PIA(というかCOM全般)の相性があまり良くないのは結構有名な話で、
#ですよね…>じゃんぬねっとさん
参照カウンタを確実にデクリメントしないとExcelのプロセスが残るという、
考えようによっちゃお題のメインよりずっとめんどくさい問題がひっついてくるんですよ。
#まぁ使う側のプロセスと一緒に消えてくれるので、
#シングルスレッドのコンソールアプリならそんな問題にはならないかもですが、
#そこを考慮しないプログラムが「お手本」とはならないわけでして

VSTOの方はちょっとわからないんですが、そういう事情があるので、
Excelを活用するならVB6.0以前かExcel VBAがいいと思います。
ただし、それでも256列65536行以下のCSVファイルしか扱えないという問題が残ります。

で、「もちっと使える既存の車輪はいねがぁ」ってなると、
.NETの場合はじゃんぬねっとさんご提示のTextFieldParserクラスとかに行き当たるみたいです。
#私自身はこのクラスを使ったことがないので便利さを実感したわけではないんですが…
#にしても、なんでMicrosoft.VisualBasic名前空間なんだろう?

余談ですが、@ITのTextFieldParserクラスの解説ページの中で見つけたんですけども…
引用:

* CSVファイルの仕様については以前は明確なものがなかったが、現在ではRFC4180により標準化されている。


RFC4180、英語あまり得意じゃないし超斜め読みしかしてないんですが、2.6, 2.7あたり見る限り、
Excelがタブもダブルクォートで囲んじゃうのは標準仕様ではないみたいですね。
ただし、2.5を見ると改行や区切り文字やダブルクォートを含んでなくてもダブルクォートで両端を囲むのはありで、
つまりExcelが作るCSVは標準仕様に準拠していない、ということにはならないっぽいです。
Anonymous Coward
会議室デビュー日: 2007/05/09
投稿数: 8
投稿日時: 2007-05-24 23:39
私もチャレンジしてみました。
といっても、0から作ったわけではなく、

引用:

sawatさんの書き込み (2007-05-22 19:32) より:
コード:
function csv2tsv(csv) {
  var esc = [];
  return csv.replace(/"(""|[^"])+"/g, function(m0){
    esc.push(m0);
    return '""';
  }).replace(/,/g, "t").replace(/""/g, function() {
    return esc.shift();
  });
}



Javaには移植しづらくなったし、お手本という観点からみても微妙


このコードをC#に移植して、足りない部分を補完する形になりました。
キューに退避させておくことで、置換後の文字列が後続の置換処理に
ひっかかってしまうのを避ける、という考え方は大変勉強になりました。

コード:
class Program
{
    public static void Main(String[] args)
    {
        //引数チェック
        if (args.Length != 1)
        {
            Console.WriteLine("引数にCSVファイル名を指定してください。");
            return;
        }

        //ファイルの存在チェック
        string csvName = args[0];
        if (!File.Exists(csvName))
        {
            Console.WriteLine("ファイルが存在しません。");
            return;
        }

        //出力ファイル名の作成
        string tsvName = Path.GetDirectoryName(csvName);
        if (tsvName != "") tsvName += Path.DirectorySeparatorChar;
        tsvName += Path.GetFileNameWithoutExtension(csvName) + ".tsv";

        //CSVからTSVへの変換
        try
        {
            Encoding sjis = Encoding.GetEncoding("Shift-JIS");
            using (StreamReader reader = new StreamReader(csvName, sjis))
            {
                using (StreamWriter writer = new StreamWriter(tsvName, false, sjis))
                {
                    writer.Write(Csv2Tsv(reader.ReadToEnd()));
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    //退避用
    private static Queue<string> esc = new Queue<string>();

    //CSVからTSVへの変換
    private static string Csv2Tsv(string csv)
    {
        //クォートされたデータとタブを含むデータを退避
        MatchEvaluator eval = new MatchEvaluator(EnqueueReplace);
        string pattern = "(\"(\"\"|[^\"])+\"|([^\",\r\n])*\t([^\",\r\n])*)";
        csv = Regex.Replace(csv, pattern, eval);
        //区切り文字の置換
        csv = csv.Replace(",", "\t");
        //退避したデータの復元
        eval = new MatchEvaluator(DequeueReplace);
        csv = Regex.Replace(csv, "\"\"", eval);

        return csv;
    }

    //退避
    private static string EnqueueReplace(Match m)
    {
        esc.Enqueue(m.Value);
        return "\"\"";
    }

    //退避したデータの復元
    private static string DequeueReplace(Match m)
    {
        string data = esc.Dequeue();
        //特殊文字がタブのみの場合は両端をクォート
        if (data[0] != '\"')
        {
            return "\"" + data + "\"";
        }
        //カンマしか含んでない場合はクォートを外す
        if (Regex.Match(data, "^\"([^\"\r\n\t])+\"$").Success)
        {
            return data.Substring(1, data.Length - 2);
        }
        return data;
    }
}


問題点などありましたら、遠慮なく突っ込んでください。
マーサ
ベテラン
会議室デビュー日: 2004/11/26
投稿数: 87
投稿日時: 2007-05-25 12:49
引用:

ぼのぼのさんの書き込み (2007-05-24 22:18) より:

あのロジックだとデータ内の改行が全てCRLFに置き換わっちゃいますよね。
お題ではデータ内の改行コードについては言及されていないですが、
理想を言うならデータ内は元と変わらない方がいいのかな、と



あ〜〜〜!?
ダメダメですな
sawat
大ベテラン
会議室デビュー日: 2006/08/02
投稿数: 112
投稿日時: 2007-05-25 12:52
引用:

Anonymous Cowardさんの書き込み (2007-05-24 23:39) より:

問題点などありましたら、遠慮なく突っ込んでください。



問題のうちに入るか分かりませんが、以下のような微妙な入力のときにExcelと結果が異なります。

abc,"",def ⇒ abcTAB""TABdef となるが、abcTABTABdef の方が正解?
ab"c,d"ef ⇒ ab"c,d"ef(変化無し) だが、 Excelにあわせるなら、 ab"cTABd"ef の方が正解?

環境がないので実際に試したわけではありませんが、こうなると思います。
まぁ、僕の書いたコードが引き継がれた問題ですが

引用:

キューに退避させておくことで、置換後の文字列が後続の置換処理に
ひっかかってしまうのを避ける、という考え方は大変勉強になりました。


この方法はいろいろ応用が利くのですが、全置換を3回(以上)行なうのでパフォーマンス面に問題があります。そこそこ大きなテキストを置換する必要がある場合など特にご注意を

Anonymous Coward
会議室デビュー日: 2007/05/09
投稿数: 8
投稿日時: 2007-05-26 00:22
引用:

sawatさんの書き込み (2007-05-25 12:52) より:
問題のうちに入るか分かりませんが、以下のような微妙な入力のときにExcelと結果が異なります。

abc,"",def ⇒ abcTAB""TABdef となるが、abcTABTABdef の方が正解?
ab"c,d"ef ⇒ ab"c,d"ef(変化無し) だが、 Excelにあわせるなら、 ab"cTABd"ef の方が正解?

環境がないので実際に試したわけではありませんが、こうなると思います。
まぁ、僕の書いたコードが引き継がれた問題ですが


突っ込みありがとうございます。実際に試してみました。
abc,"",def ⇒ System.InvalidOperationException: Queue が空です。
ab"c,d"ef ⇒ abc,def

1つめの方は、元のコードが退避データを""に置換し、復元の際に""に対して置換する方法をとっていますが、
これは「入力ファイルがお題中に書かれた仕様を満たしていること」を前提にしているものと解釈し、
そのまま使わせて頂きましたので、「こういう不正データがきたらこうなるだろうな」という、
ある意味予想通りの結果でした。

しかし、
引用:

ぼのぼのさんの書き込み (2007-05-24 23:19) より:
ただし、2.5を見ると改行や区切り文字やダブルクォートを含んでなくてもダブルクォートで両端を囲むのはありで、


これによれば1つめの入力形式はお題中に書かれた仕様は満たしていなくても、
標準仕様は満たしていることになりますので、対処すべきかもしれません。

2つめの方は予想外の結果ではありましたが、完全にCSVとして不正な形式ですので、
こちらは問題のうちに入れなくて良いのではないでしょうか。

引用:

引用:

キューに退避させておくことで、置換後の文字列が後続の置換処理に
ひっかかってしまうのを避ける、という考え方は大変勉強になりました。


この方法はいろいろ応用が利くのですが、全置換を3回(以上)行なうのでパフォーマンス面に問題があります。そこそこ大きなテキストを置換する必要がある場合など特にご注意を


私のコードは、ファイルの全内容をReadToEnd()でstringに読み込んでますので、
CSVファイルが巨大だとパフォーマンス面だけでなく、メモリにも優しくなさそうですね(苦笑)。
確かに、大きなテキストの処理には向いていないと思います。
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2007-05-26 01:13
自分がライブラリとして作っているのがあるので、コードのリンクを紹介します。

http://herringroe.googlecode.com/svn/trunk/herringroe/src/commons/main/java/jp/herringroe/commons/util/csv/

パース方法としては、オートマトンを使っています。
文脈によってトークン(引用符、改行、カンマ)の意味が変わるものは、
状態遷移を行った方が楽だと思います。
1つのループの中で様々な状態をフラグでもつよりも、
よっぽど分かりやすいかなと思います。

CSVからTSVの変換はやっていないですが、データモデルを作成できるので、
それをタブ区切りでシリアライズするだけです。

ぼのぼの
ぬし
会議室デビュー日: 2004/09/16
投稿数: 544
投稿日時: 2007-05-29 00:19
ReadLine()で一行ずつ読み込んで処理する方法だと、データ内改行がCRLF以外だった場合に見分けがつかない。
ReadToEnd()で全て読み込んで処理する方法だと、入力ファイルが巨大だった場合メモリに優しくない。

てことで、1文字ずつ読み込んで処理する方法で作ってみましたが、なんつーか、ベッタベタというか、車輪の再発明的な香りがぶんぶん漂うコードになってしまいました orz

敢えてポイントを挙げるとすれば、カレントキャラがクォータだったら、何も考えずにフラグを反転させてるところが、マーサさんの「クォータは必ず偶数」理論に近いかもしれません。

言語はC#です。

コード:

public class SeparatedDataConverter {
    /// <summary>入力の区切り文字</summary>
    private char iSep;
    /// <summary>出力の区切り文字</summary>
    private char oSep;
    /// <summary>クォータ</summary>
    private char quoter;
    /// <summary>データの両端をクォートする条件</summary>
    private Regex quoteMatcher;
    /// <summary>データの両端のクォートを外す条件</summary>
    private Regex unquoteMatcher;

    /// <summary>コンストラクタ</summary>
    public SeparatedDataConverter() {
        Init(',', '\t', '\"');
    }

    /// <summary>コンストラクタ</summary>
    public SeparatedDataConverter(char iSep, char oSep, char quoter) {
        Init(iSep, oSep, quoter);
    }

    /// <summary>初期化</summary>
    private void Init(char iSep, char oSep, char quoter) {
        this.iSep = iSep;
        this.oSep = oSep;
        this.quoter = quoter;
        this.quoteMatcher = new Regex(
            string.Format("^(?=.*{0})[^{1}\r\n]+$", oSep, quoter), RegexOptions.Compiled);
        this.unquoteMatcher = new Regex(
            string.Format("^{0}[^{0}{1}\r\n]+{0}$", quoter, oSep), RegexOptions.Compiled);
    }

    /// <summary>変換実行</summary>
    public void Convert(TextReader reader, TextWriter writer) {
        bool isInnerQuote = false;
        StringBuilder buf = new StringBuilder();
        while (reader.Peek() != -1) {
            char c = (char)reader.Read();
            if (c == quoter) {
                //クォータだったら、クォート内フラグを反転させる
                isInnerQuote = !isInnerQuote;
                buf.Append(c);
            } else if (!isInnerQuote && c == iSep) {
                //入力の区切り文字が現れたらバッファを書き込む
                WriteBuffer(writer, buf.ToString());
                writer.Write(oSep);
                buf.Length = 0;
            } else if (!isInnerQuote && (c == '\r' || c == '\n')) {
                //行区切りが現れたらバッファを書き込む
                WriteBuffer(writer, buf.ToString());
                writer.Write(c);
                buf.Length = 0;
            } else {
                buf.Append(c);
            }
        }
        //残りのバッファを書き込む
        WriteBuffer(writer, buf.ToString());
    }

    /// <summary>バッファを書き込む</summary>
    private void WriteBuffer(TextWriter writer, string data) {
        if (data.Length == 0) return;
        if (quoteMatcher.Match(data).Success) {
            //データの両端をクォートする条件に合致した場合
            writer.Write("{0}{1}{0}", quoter, data);
        } else if (unquoteMatcher.Match(data).Success) {
            //データの両端のクォートを外す条件に合致した場合
            writer.Write(data.Substring(1, data.Length - 2));
        } else {
            writer.Write(data);
        }
    }
}

スキルアップ/キャリアアップ(JOB@IT)