HOWTO · C++
C++ で CSV ファイルを読む
この記事では、C++ で CSV ファイルを読み取る方法を示します
このページの内容
C++ で単純なカンマ区切りファイルを読むには、std::getline で1行ずつ読み取り、2回目の std::getline の呼び出しでその行をフィールドに分割します。この方法が使えるのは、物理行ごとに1レコードがあり、カンマがフィールドの区切りにだけ使われている場合です。以下の例では小さな入力ファイルを作成するため、別のデータファイルを用意せずにコンパイルして実行できます。
std::getline と std::istringstream で CSV ファイルを読み込む
プログラムはまず grades.csv に2つのレコードを書き込み、その後、読み取り用にファイルを開きます。外側の std::getline は物理行を std::string に読み込み、std::istringstream はその行をストリームに変換します。さらに内側の std::getline がカンマを区切り文字として各フィールドを取り出します。各レコードは std::vector<std::string> になり、レコード全体も別の vector に格納されます。これなら元の順序を保てます。行が単純に順番に並んでいる場合、整数をキーとする map を使う必要もありません。
この自己完結型プログラムを実行します。
このセクションのコードと表示されるラベルや出力は英語のままです。
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
std::string trimEnds(const std::string& value) {
const auto first = std::find_if_not(value.begin(), value.end(),
[](unsigned char ch) { return std::isspace(ch); });
const auto last = std::find_if_not(value.rbegin(), value.rend(),
[](unsigned char ch) { return std::isspace(ch); }).base();
if (first >= last) {
return "";
}
return std::string(first, last);
}
int main() {
const std::string path = "grades.csv";
{
std::ofstream sample(path);
if (!sample) {
std::cerr << "Could not create input file\n";
return 1;
}
sample << "Name, Score\nAda Lovelace, 98 \nGrace Hopper, 95\n";
}
std::ifstream input(path);
if (!input) {
std::cerr << "Could not open input file\n";
return 1;
}
std::vector<std::vector<std::string>> rows;
std::string record;
while (std::getline(input, record)) {
std::istringstream line(record);
std::vector<std::string> fields;
std::string field;
while (std::getline(line, field, ',')) {
fields.push_back(trimEnds(field));
}
rows.push_back(fields);
}
for (const auto& row : rows) {
std::cout << row[0] << " | " << row[1] << '\n';
}
return 0;
}
成功時の実行結果は以下のとおりです。出力に表示される Name と Score は、コード内の英語ラベルのままです。
Name | Score
Ada Lovelace | 98
Grace Hopper | 95
ファイルの作成またはオープンに失敗すると、どちらの場合も std::cerr にエラーを書き込み、ステータス 1 を返します。正常に処理が終わるとステータス 0 を返します。自分のプログラムでは、読み取りを始める前に入力ストリームが正常に開けたか確認してください。プログラムの外部から渡されたファイルを扱う場合は、ファイルが存在すると決めつけず、パスを示して、アプリケーションの要件に応じてオープン失敗を処理します。
CSV フィールド内の空白を扱う
空白をどう扱うかはデータ処理上の選択であり、分割時に自動で決まるものではありません。trimEnds は、取り出した各フィールドの先頭と末尾にある空白だけを削除します。Ada Lovelace のような値の途中にある空白は保持します。std::isspace を呼び出す前に unsigned char に変換することで、負の値になり得る通常の char を文字分類関数に渡すのを避けています。
カンマの後の空白を書式上のものとして扱うファイルもあれば、フィールドの一部として扱うファイルもあります。保持すべきデータの仕様に合う場合にだけトリミングしてください。すべての空白文字を削除すると、名前や住所の単語まで連結されます。また、両端をトリミングすると、先頭や末尾の内容が変わります。元データで空白に意味がある場合は、読み取ったまま保持するか、ASCII の空白だけを削除するなど、より限定的なルールを実装してください。
この分割ループでは、空フィールドも生じます。他の内容の間にカンマが連続していると、その間に空文字列ができます。一方、行末のカンマがあっても、この std::getline のループは最後の空フィールドを追加しません。入力形式で必要なら、末尾の区切り文字を明示的に処理してください。想定する列に応じて、空の値を保持するか拒否します。ヘッダーよりフィールド数が少ない行は、行の要素にアクセスする前に明示的に処理する必要があります。上の短い表示ループは、どちらの列も存在することを前提としています。
区切り文字による分割の限界を理解する
この方法は、すべての物理行をカンマごとに分割します。CSV の引用符の規則は解釈しません。たとえば、"Paris, France" のような値は、CSV リーダーなら1つのフィールドとして扱うべきですが、この方法では2つに分かれます。引用符付きフィールド内のエスケープされた引用符も復元されず、引用符付きフィールド内の改行はレコードの終わりと見なされます。また、レコードごとにフィールド数が異なることもあるため、各行のカンマ数が同じだとは限りません。
この方法は、1物理行が1レコードで、引用符で囲まれた区切り文字がないことが保証された、単純で管理された入力に使ってください。一般的な CSV ファイルには、受け取る形式の引用符とエスケープの規則に対応した CSV パーサーを使います。解析後も、アプリケーションが必要とするフィールド数と内容を検証してください。ここで示した標準ストリーム操作でできるのは、ファイルや行の読み取りです。それだけでは CSV 形式の文法全体を処理できません。