ログやCSVの1行をCString::Tokenizeで分割すると、たいていはそのまま使えます。困るのは、空欄の列があるときです。7列あるはずの文字列が4要素になり、3列目を読んでいるコードが4列目の値を受け取ります。
列の位置に意味があるデータなら、AfxExtractSubStringかFind + Midを使って分けましょう。
空欄を含む1行をTokenizeに通すと、要素数が合わない
分けるのは、この1行です。
2026-08-14,ERROR,,timeout,,retry,
カンマが6個で、末尾を含めて7列。空欄は3列目・5列目・7列目です。この行を3通りで分けて、取れた要素数を数えました。実測はVisual Studio 2026 / Debug / x64 / Unicode(MFC共有DLL)です。
| 分け方 | 取れた要素数 | 空欄の列 |
|---|---|---|
| CString::Tokenize | 4 | 結果から消える |
| AfxExtractSubString | 7 | 空文字列として残る |
| Find + Midの自前ループ | 7 | 空文字列として残る |
足りない3つが空欄の列です。Tokenizeのループはこう書きます。
// Tokenize で 1 行を分ける。連続した区切りはまとめて読み飛ばされる。空欄の列は結果に残らない。
static void SplitByTokenize(const CString& sLine, LPCTSTR lpszSeparators, CStringArray& arrFields)
{
arrFields.RemoveAll();
int iStart = 0;
CString sToken = sLine.Tokenize(lpszSeparators, iStart);
while (!sToken.IsEmpty())
{
arrFields.Add(sToken);
sToken = sLine.Tokenize(lpszSeparators, iStart);
}
}
取り出せたのは2026-08-14、ERROR、timeout、retryの4つ。3番目に取れるのはtimeoutで、本来の4列目の値です。

空欄が消える理由は、先頭の区切り文字の読み飛ばし
どこで消えたかは、iStartの動きで分かります。呼び出しごとに、次の検索開始位置がここへ書き戻されます。
iStart 0 -> [2026-08-14] iStart 11 iStart 11 -> [ERROR] iStart 17 ← 16 と 17 の連続したカンマのうち 1 つ分しか進んでいない iStart 17 -> [timeout] iStart 26 iStart 26 -> [retry] iStart 33 ← 文字列長と同じ。次の呼び出しは空文字列を返し iStart は -1
2回目の呼び出しはERRORを返して、iStartを17にします。17の位置は、16と並んだカンマの2つ目です。3回目はそれを読み飛ばしてtimeoutを切り出します。空欄の列はここで消えます。
公式リファレンスにも、呼び出しのたびに先頭の区切り文字を読み飛ばすこと、トークンが尽きると空文字列を返してiStartが -1になることが書かれています。空欄の列に対応する空のトークンは返りません。今回の実行では、エラーダイアログも例外も出ませんでした。分割した直後に件数を数えるまで、列がずれたことは表に出てきません。
区切りに渡しているのは文字列ではなく文字の集合
ずれる原因は、もうひとつあります。第1引数は「この文字列で区切る」という指定ではありません。「区切りとして扱う文字の集合」です。key, value;noteという1行に2通りの指定を通すと、こう分かれました。
入力: key, value;note
_T(",;") -> [key][ value][note] カンマとセミコロンのどちらでも切れる(空白は残る)
_T(", ") -> [key][value;note] カンマと空白のどちらでも切れる(セミコロンは残る)
_T(“, “) は「カンマと空白が並んだ区切り」という意味になりません。カンマでも空白でも切れます。値の途中に空白があると、そこでも分かれます。区切りが1種類なら、_T(“,”) のように1文字だけ渡すのが安全です。
空欄を保ったまま分ける:AfxExtractSubStringとFind + Midを使う
列の位置を保ちたいならAfxExtractSubStringです。インデックスで1列ずつ取り出し、区切りはTCHAR 1文字を渡します。
// AfxExtractSubString で 1 列ずつ取り出す。空欄の列でも TRUE が返る。列の位置はずれない。
static void SplitByAfxExtractSubString(const CString& sLine, TCHAR chSeparator, CStringArray& arrFields)
{
arrFields.RemoveAll();
CString sField;
for (int nIndex = 0; AfxExtractSubString(sField, sLine, nIndex, chSeparator); ++nIndex)
{
arrFields.Add(sField);
}
}
同じ行で0から順に呼ぶと、空欄の列でも末尾の空欄でもTRUEが返りました。このforは7回まわって止まり、8回目はFALSEでした。呼び出しのたびに、文字列の先頭から走査し直します。7列を取り出すのに8回、毎回先頭からです。
全列をまとめて取るなら、FindとMidで自分で回します。
// 区切り文字で分け、空欄はそのまま空文字列として残す
static void SplitKeepEmpty(const CString& sLine, TCHAR chSeparator, CStringArray& arrFields)
{
arrFields.RemoveAll();
int nPos = 0;
for (;;)
{
const int nNext = sLine.Find(chSeparator, nPos);
if (nNext < 0)
{
arrFields.Add(sLine.Mid(nPos)); // 最後の列。末尾が区切りなら空文字列が入る
break;
}
arrFields.Add(sLine.Mid(nPos, nNext - nPos));
nPos = nNext + 1;
}
}
Midは毎回コピーを返します。SplitKeepEmptyは列の数だけMidを呼びます。バッファを直接触って詰め替える作法はCStringのメモリ管理: GetBuffer / ReleaseBufferの作法にまとめています。
どちらの分け方でも、列を読む前に件数を確かめます。
// 列を読む前に件数を確かめる。合わない行は読まずに戻し、出荷ビルドにも残る形で記録する。
static BOOL LoadLogLine(const CString& sLine, CString& sDate, CString& sLevel, CString& sDetail)
{
const int nExpected = 7;
CStringArray arrFields;
SplitKeepEmpty(sLine, _T(','), arrFields);
if (arrFields.GetCount() != nExpected)
{
CString sWarning;
sWarning.Format(_T("列数が想定と違います: %d 列(想定 %d 列)\r\n"), (int)arrFields.GetCount(), nExpected);
::OutputDebugString(sWarning);
return FALSE;
}
sDate = arrFields.GetAt(0);
sLevel = arrFields.GetAt(1);
sDetail = arrFields.GetAt(3);
return TRUE;
}
記録にTRACEを使うと、Debugビルドにしか残りません。公式リファレンスは「In a release build, this macro compiles to nothing (no code is generated at all)」と書いています。出荷したビルドにも残したいなら、OutputDebugStringを直接呼びます。これはUNICODEの定義でOutputDebugStringWとOutputDebugStringAのどちらを呼ぶかを選ぶ別名で、実体はどちらもKernel32.dllの関数です。TRACEのように構成で丸ごと消えることはありません。受け取るのはデバッガか、OutputDebugString() で自作ログ出力のようなツールです。
実行して確かめました。7列そろった行はTRUEで、日付2026-08-14・種別ERROR・詳細timeoutを取り出せました。空欄が落ちた4列の行はFALSEです。
空白区切りの単語を拾うだけなら、Tokenizeの読み飛ばしがそのまま役に立ちます。避けるのは、列の位置に意味があるデータのときです。
まとめ
- Tokenizeは連続した区切り文字をまとめて読み飛ばします。空欄の列が消えて、以降の値が手前へずれます。エラーダイアログは出ません。
- 第1引数は区切り文字の集合です。_T(“, “) はカンマでも空白でも切れます。
- 列の位置を保つならAfxExtractSubString、全列をまとめて取るならFind + Mid。どちらも分割直後に件数を確かめます。
