C++からPythonのcsvモジュールを呼び出して、CSVファイルを読み込む方法を説明します。後半では、C++のみの方法も説明します。
開発環境 | Visual Studio 2022 |
---|---|
言語 | C++ Python |
動作確認 | Windows 11 Windows 10 Windows 7 |
Ubuntu (g++) FreeBSD (clang++) |
※Python 3.11にて確認しました。(Windows 7のみ、Python 3.8.10)
CSVファイルは、フィールドをカンマで区切ったテキストファイルですが、歴史が古いのでローカルな実装がいろいろあります。後付けの公式仕様はRFC4180(https://www.ietf.org/rfc/rfc4180.txt)ですが、実際の開発の現場では「Excelで読み込み可能な形式」が事実上の標準と言えます。
Pythonには、標準にてExcelに準拠したCSVファイル読み書き機能があります。それをC++から呼び出すことで、簡単にCSVファイルを読み込むことが可能です。私のPC(第9世代Core i7、Windows 11)では、日本郵便が公開している約12万行*15列の郵便番号データ(KEN_ALL.CSV)を読み込むのにかかる時間は、約0.4秒です。
これだけ速いと、C++でロジックを組む気が無くなるし、Pythonが流行っているのも理解できます。
Pythonのインストールと、開発環境の設定
Windows
https://www.python.org/downloads/windows/からインストーラーをダウンロードして、実行します。ダウンロードの際、32bit版と64bit版の選択は、C++から呼び出す場合のソリューションに合わせます。インストーラーの最初の画面にて「Customize installation」を選択して、途中の「Advanced Options」画面では、
- Add Python to environment variables
- Download debugging symbols
- Download debug binaries
の3項目を選択します。その他の項目は任意です。
他のPCに実行環境を作成する場合は、上記リンクからembeddable packageをダウンロードして、展開後、パスを通します。
Visual Studio 2022
プロジェクトのプロパティに以下の設定を追加します。インストール先のフォルダーがデフォルトと異なる場合は、そちらに合わせてください。(3.11.Xの例)
[C/C++]-[全般]-[追加のインクルードディレクトリ] |
---|
C:\Users\ユーザー名\AppData\Local\Programs\Python\Python311\include; |
[リンカー]-[全般]-[追加のライブラリディレクトリ] |
C:\Users\ユーザー名\AppData\Local\Programs\Python\Python311\libs; |
Ubuntu
# インストール (3.11.Xの例)
> sudo apt install python3.11
> sudo apt install python3.11-dev
# 確認
> python3.11 -V
> ls /usr/include/python3.11/Python.h
# コンパイル例
> g++ -Wall -I/usr/include/python3.11 test.cpp -lpython3.11 -o test
Ubuntuに最初から入っているPythonは、OSが使用するものなので、絶対に削除しないでください。
FreeBSD
# インストール (3.11.Xの例)
> pkg install python311
# 確認
> python3.11 -V
> ls /usr/local/include/python3.11/Python.h
# コンパイル例
> clang++ -Wall -I/usr/local/include/python3.11 test.cpp -L/usr/local/lib -lpython3.11 -o test
サンプルソース
Python
import csv
def csvread(f, e):
data = []
with open(f, encoding = e, newline = '') as csvfile:
reader = csv.reader(csvfile)
for row in reader:
data.append(row)
return data
上記のソースファイルをcsv_read.pyとします。ファイル名、関数名は、呼び出し側と合っているなら任意です。
C++
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string>
#include <vector>
// 読み込んだフィールドを格納するvector
// その際の文字コードは、CSVファイルの文字コードとは関係無い
// UTF-8
std::vector<std::string> row;
std::vector<std::vector<std::string>> data;
// UTF-16(Windows)、UTF-32(UNIX系OS)
//std::vector<std::wstring> row;
//std::vector<std::vector<std::wstring>> data;
PyObject *pArgs{};
PyObject *pValue{};
PyObject *pRow{};
PyObject *pField{};
Py_Initialize();
// カレントフォルダーをPythonのパスに追加
PyRun_SimpleString(
"import os\n"
"import sys\n"
"sys.path.append(os.getcwd())"
);
// モジュール名(Pythonソースファイルから、拡張子を除外)
PyObject *pName = PyUnicode_DecodeFSDefault("csv_read");
// モジュールをimport
PyObject *pModule = PyImport_Import(pName);
Py_DECREF(pName);
if (!pModule)
{
// モジュール指定に誤り
PyErr_Print();
Py_FinalizeEx();
return 1;
}
// Python関数
PyObject *pFunc = PyObject_GetAttrString(pModule, "csvread");
if (pFunc && PyCallable_Check(pFunc))
{
// 引数の設定
pArgs = PyTuple_New(2);
// CSVファイル名
// 日本語が含まれる場合、UTF-8にて指定する
pValue = PyUnicode_FromString(u8"ファイルのパス");
PyTuple_SetItem(pArgs, 0, pValue);
// CSVファイルの文字コード指定
// (改行コードは、CR+LF,LF,CRのいずれにも対応している)
// Shift_JIS → cp932
// UTF-8 → utf_8_sig
// UTF-16 BOM有 → utf_16
// UTF-16 BOM無 ビッグエンディアン → utf_16_be
// UTF-16 BOM無 リトルエンディアン → utf_16_le
// 16を32に変更すると、UTF-32についても同様
pValue = PyUnicode_FromString("cp932");
PyTuple_SetItem(pArgs, 1, pValue);
// Python関数の呼び出し
pValue = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
// pValueに、CSVファイルをパースした結果が入っている
if (pValue)
{
// 行数
Py_ssize_t rc = PyList_Size(pValue);
for (Py_ssize_t i = 0; i < rc; i++)
{
// 行の読み取り
pRow = PyList_GetItem(pValue, i);
// 列数
Py_ssize_t cc = PyList_Size(pRow);
for (Py_ssize_t j = 0; j < cc; j++)
{
// フィールドの読み取り
pField = PyList_GetItem(pRow, j);
// 文字コードを変換して、vectorに格納
// UTF-8
row.push_back(PyUnicode_AsUTF8(pField));
// UTF-16(Windows)、UTF-32(UNIX系OS)
//row.push_back(PyUnicode_AsWideCharString(pField, nullptr));
}
data.push_back(row);
row.clear();
}
Py_DECREF(pValue);
}
else
{
// CSVファイル名、文字コード指定に誤り
Py_DECREF(pFunc);
Py_DECREF(pModule);
PyErr_Print();
Py_FinalizeEx();
return 1;
}
}
else
{
// 関数指定に誤り
Py_XDECREF(pFunc);
Py_DECREF(pModule);
if (PyErr_Occurred())
{
PyErr_Print();
}
Py_FinalizeEx();
return 1;
}
Py_DECREF(pFunc);
Py_DECREF(pModule);
if (Py_FinalizeEx() < 0)
{
return 1;
}
// フィールドは、data[i][j]にて取り出す。iは行、jは列のインデックス(ゼロ開始)
C++のソースが非常に長いですが、よく読むと、C++からPythonを呼び出す際の定型文であることが理解できると思います。つまり、一部を変更するだけで、Pythonのいろいろな機能を呼び出すことが可能です。
CSVファイルの文字コード指定
上記ソースのコメントに書いた文字コード以外は、https://docs.python.org/3/library/codecs.html#standard-encodingsを参照してください。
Shift_JISに「cp932」を指定する理由
- Shift_JISファイルが作成される可能性が高いのは、Windows環境
- Windowsにて、一般的に「Shift_JIS」と呼ばれる文字コードは、正確には「CP932」
- WindowsのIMEにて、「から」を入力して変換される全角のニョロは「全角チルダ」
Shift_JIS / CP932 | Python codec | Unicode | IME 「から」候補 | |
---|---|---|---|---|
符号位置 | 名称 | |||
0x8160 | cp932 | U+FF5E | 全角チルダ | ~ 全 |
shift_jis | U+301C | 波ダッシュ | 〜 環境依存 |
以上より、「shift_jis」を指定すると作成者の意図とは別の文字に変換されるため、「cp932」を指定します。
PyObject変数の解放
Python/C APIの戻り値として使用されるPyObject変数は、最後にPy_DECREFまたはPy_XDECREFにて解放します(実際の解放はPy_FinalizeEx実行時)。Py_DECREFは引数のnull判定無し、Py_XDECREFはnull判定有りです。
対象となるAPIは、Python/C API Reference Manual(https://docs.python.org/3/c-api/index.html)にて「Return value: New reference.」と記載されているAPIです。「Return value: Borrowed reference.」は対象外です。上記ソースでは、PyList_GetItemは対象外になります。
リファレンス
1.3. Pure Embedding
https://docs.python.org/3/extending/embedding.html#pure-embedding
C++からPythonの呼び出しは、このソースを参考にしました。
郵便番号データダウンロード
https://www.post.japanpost.jp/zipcode/download.html
C++のみの方法
ここからはサンプルソースを簡単にするため、文字コードはUTF-8(BOM無し)、Windowsの改行コードはCR+LFまたはLF、UNIX系OSの改行コードはLFを前提とします。その他の文字コードや改行コード、BOM有りに対応する場合は、「【C++】テキストファイルを1行毎に読み取る」を参照してください。
測定用のKEN_ALL.CSVは、予めテキストエディターにてUTF-8(BOM無し)に変換しています。
単純なカンマ区切り
#include <fstream>
#include <sstream>
#include <vector>
std::vector<std::string> row;
std::vector<std::vector<std::string>> data;
std::ifstream ifs("ファイルのパス");
if (!ifs)
{
// オープン失敗
return 1;
}
std::string r;
std::string f;
// 1行毎に読み込む
while (std::getline(ifs, r))
{
std::istringstream iss(r);
// カンマ区切りで読み込む
while (std::getline(iss, f, ','))
{
if (f.front() == '"' && f.back() == '"')
{
// ダブルクォートを除外して格納
row.push_back(f.substr(1, f.size() - 2));
}
else
{
row.push_back(f);
}
}
data.push_back(row);
row.clear();
}
ifs.close();
// フィールドは、data[i][j]にて取り出す。iは行、jは列のインデックス(ゼロ開始)
フィールドに「カンマ」「改行」「括る以外のダブルクォート」が無い、単純なCSVファイルに対応します。KEN_ALL.CSVの読み込みは、約0.3秒です。
正規表現
#include <fstream>
#include <regex>
#include <vector>
std::vector<std::string> row;
std::vector<std::vector<std::string>> data;
std::ifstream ifs("ファイルのパス");
if (!ifs)
{
// オープン失敗
return 1;
}
std::string r;
std::smatch s;
// KEN_ALL.CSVの正規表現
// (読み込むCSVファイルのフォーマットに合わせて書き換える)
std::regex reCsv{R"((\d{5}),\"([\d ]{5})\",\"(\d{7})\",\"(.+?)\",\"(.+?)\",\"(.+?)\",\"(.+?)\",\"(.+?)\",\"(.+?)\",(\d),(\d),(\d),(\d),(\d),(\d))"};
// 1行毎に読み込む
while (std::getline(ifs, r))
{
// 正規表現の判定
if (std::regex_match(r, s, reCsv))
{
for (std::size_t i = 1; i < s.size(); i++)
{
// s.str(0) 1行全体
// s.str(1)~ 各フィールド
row.push_back(s.str(i));
}
}
else
{
// フォーマット不正
continue;
}
data.push_back(row);
row.clear();
}
ifs.close();
// フィールドは、data[i][j]にて取り出す。iは行、jは列のインデックス(ゼロ開始)
テキストから、検索条件に該当するテキストを取り出す場合、正規表現を使用するとソースが短くなります。ただし、テキストの中身や正規表現の書き方によって、実行時間がかかることもあります。KEN_ALL.CSVは4~9列目の桁数が不定で、次のダブルクォートを探すまでの手数がかかる(この現象をバックトラックと言います)ため、読み込みは約2.3秒です。
汎用的なロジック
#include <fstream>
#include <string>
#include <vector>
std::vector<std::string> row;
std::vector<std::vector<std::string>> data;
std::ifstream ifs("ファイルのパス");
if (!ifs)
{
// オープン失敗
return 1;
}
std::string r;
std::string f;
// CSVの1レコード読み取り中フラグ
// フィールドに改行が含まれる場合、ファイルの1行とCSVの1レコードは異なる
bool isRead = false;
// 1行毎に読み込む
while (std::getline(ifs, r))
{
if (isRead)
{
// フィールドの中に改行が有り、改行コードの代わりに印を付ける場合は、ここで付ける。
// 例えば、「[改行]」というテキストを入れる場合、次のコメント解除
//f += u8"[改行]";
}
else
{
isRead = true;
}
// カンマ検索開始位置のインデックス
std::size_t iStart = 0;
while (isRead)
{
// カンマのインデックス
std::size_t iFind = r.find(',', iStart);
if (iFind == std::string::npos)
{
// フィールド読み取り
// (行の最後)
f += r.substr(iStart);
// 行末の空白を削除する場合、次のコメント解除
//f.erase(f.find_last_not_of(" \t") + 1);
isRead = false;
}
else
{
// フィールド読み取り
// (行の最後以外)
f += r.substr(iStart, iFind - iStart);
}
// フィールドがダブルクォートで括られている場合
if (f.front() == '"')
{
// ダブルクォートの直後に改行がある場合の対策として、
// f.size()>1の判定を追加
if (f.size() > 1 && f.back() == '"')
{
// ダブルクォートの削除
f.pop_back();
f.erase(f.begin());
}
else
{
iStart = iFind + 1;
if (isRead)
{
// ダブルクォートで括られた中にカンマがある
f += ',';
continue;
}
else
{
// ダブルクォートで括られた中に改行がある
isRead = true;
break;
}
}
}
// フィールドの中に、ダブルクォートが2個連続する場合、1個を削除
std::size_t pos{std::string::npos};
do
{
pos = f.find("\"\"");
if (pos != std::string::npos)
{
f.erase(pos, 1);
}
}
while (pos != std::string::npos);
// フィールドを格納
row.push_back(f);
f.clear();
// カンマ検索開始位置の更新
iStart = iFind + 1;
}
if (!isRead)
{
// レコードを格納
data.push_back(row);
row.clear();
}
}
ifs.close();
// フィールドは、data[i][j]にて取り出す。iは行、jは列のインデックス(ゼロ開始)
「Ez CSV Viewer」のソースの一部です。フィールドに「カンマ」「改行」「括る以外のダブルクォート」が存在する場合にも対応しています。KEN_ALL.CSVの読み込みは、約0.2秒です。
できるだけRFC4180に準拠するように考えましたが、以下のパターンには対応できませんでした。
"Joan ""the bone"", Anne"
これは「Joan "the bone", Anne
」という、1個のフィールドとして解釈するのが正しいですが、上記ソースでは「Joan "the bone"
」と「 Anne"
」に分離します。ちなみに、ExcelやPythonでは正しく読み込まれます。C++でロジックを考えるのと、C++からPythonを呼び出すのでは、どちらが早いでしょうか。