🐷

C++でもGoogleSheetsを使いたい!(Firebase未使用)

2022/08/08に公開

まずGoogleSheetsを導入するためには

そもそも論、GoolgeSheetsというか、GoogleのAPIを使うためには、セキュリティ的保護がされており、それを通るために、複数の認証法が用意されています。

今回は、「OAuth」を使用しますが、一応、すべての認証方法について説明しておきます。

APIKey

APIKeyは、他の認証方法(OAuthやサービスアカウント)に比べて、制限が多いです。
使うAPIによって、様々ですが、GoogleSheetsの場合は、書き込みが出来ません。

認証方法

使い方は簡単で、GoogleSheetsの場合は、URL中にクエリストリング(http://example.com/hogehoge?key=APIKEY)として、書き込んでおけば、これだけで認証が完了します。

サービスアカウント

正直これについては、調べて使おうとしたのですが、公式説明にC++でも実装できるぐらいの細かい技術的な話が載っていなかったため、詳しく説明できません。
GoogleSheetsの場合は、これによって生成されるメールアドレスを、1ユーザーのように登録しておくことで、スプレッドシートを操作できるようになります。

使う利点・場面

私は、Botを動作させる際に使っていて、開発者個人のアカウントの権限に基づくものではなく、Bot独自のアカウントの権限で、動かすことができます。なので、開発者のアカウントの権限が「閲覧者」であっても、Botのアカウントの権限が、「編集者」ならば、書き込むことができます。(逆に言うと、Botが「閲覧者」なら書き込めないけど)

OAuth

使う利点・場面

今回は、説明上、これを採用します。が、説明します。
OAuthは、利便性的な観点で話すと、これを経由してログインした人のアカウントの権限に基づいて、操作できるものです。なので、サービスアカウントがBotなどの自動操作向けであるのに対して、OAuthは人間が手動で起動させるタイプのソフトウェアに向いています。

認証方法

HTTPのPOSTを用いて認証します。
認証コードの仕組みなどが、あります。でも、さほど難しくはないです。この記事を読むうえで、一度、公式説明に、目を通しておくと、すごく理解しやすいかと思います。というか、絶対に目を通して置けというレベル。不具合があったときに超助かるので。

OAuthを使ってログインしてみよう

一応、これの説明する上で、OAuthライブラリを使わず、実装する方法まで調べてありますが、C++にOAuthライブラリがありましたので、そっちを使います。http通信機能とセットになっているライブラリなのでOAuthの導入は楽です。

1.cpprestsdkを入れる

超大手のcpprestsdkを入れます。今回は、VS2022で開発するので、vcpkgで入れました。

vcpkgの公式ページとインストール方法は、ここにあるので、適当に入れておきます。

2.OAuthログインする部分を作る

作る…と言っても、実は、cpprestsdkにサンプルとして、OAuth2クライアントのソースコードが提供されています
今回は、これを使って、ログインしたいと思います…と言いたいのですが、このソースコードに書き足すことは1つしかないので、それだけ書いて置きます。あとは、サンプルのソースコードを丸コピしてください。

class Google_session : public oauth2_session_sample
{
public:
Google_session()
 : oauth2_session_sample(U("Google"),
    GoogleOAuth::client_id,
    GoogleOAuth::client_secret,
    GoogleOAuth::auth_uri,
    GoogleOAuth::token_uri,
    GoogleOAuth::redirect_uris)
{
 /// これを追記
 m_oauth2_config.set_scope(L"https://www.googleapis.com/auth/spreadsheets");
}

};

今回、「m_oauth2_config.set_scope」だけを追記しました。なお、ここに書く内容は、「作業する上で必要な権限」という話のものです。今回は、スコープ一覧は、ここに書いてありますので、事前にGoogle Cloudで生成したOAuth情報に基づいて、記入してください。

ちなみに、当然ですが、githubなど上のソースコード保存の中に、client_idなどが入ったものは入れないでね。

3.データを取得する。

spreadsheets.getを使って、スプレッドシートのデータを見たいと思います。
ということで、下記のようなコードをパパっと書きます。

ちなみに、ソースコード中に「s_TextConvert」という関数がありますが、これは、std::stringからstd::wstringへの変換やその逆の変換をするための関数です。要らない人は、外したりしてください。
凄くいい説明をしているページがありますので、ぜひ、参考にしてください。

Google_DataSheet Google_session::getCells(std::string SheetsID, std::string TableName, std::wstring cell) {

	web::uri t_uri = std::wstring(U("/v4/spreadsheets/") + s_TextConvert(SheetsID));

	uri_builder builder = uri_builder(t_uri);

	builder.append_query(U("ranges"), s_TextConvert(TableName) + U("!") + cell);
	builder.append_query(U("includeGridData"), U("true"));

	auto text_url = (U("https://sheets.googleapis.com") + builder.to_string());

	http_client client(text_url, m_http_config);

	http_request requestData(methods::GET);
	requestData.headers().add(L"Accept", L"application/json");

	int error_code = 0;

	auto requestTask = client.request(requestData).then([&](http_response response) {
		std::cout << "[GoogleSheet] Received response status code: " << response.status_code() << " <= SheetID : " << SheetsID << std::endl;
		error_code = response.status_code();

		return response.extract_json();
		}).then([&](json::value body_value) ->Google_DataSheet {

			if (error_code != 200) return Google_DataSheet();

			auto test_getString = [&](std::wstring key) -> std::wstring {
				json::value value = body_value.at(key);
				if (value.is_string() == false) {
					return U("");
				}
				return value.as_string();
			};
			Google_DataSheet cellData = Google_DataSheet();
			try {
				// sheet情報を取る。
				cellData.spreadsheetId = s_TextConvert(test_getString(U("spreadsheetId")));

				cellData.SheetName = s_TextConvert(std::wstring(body_value.at(U("properties")).at(L"title").as_string()));

				cellData.tableName = s_TextConvert(std::wstring(body_value.at(U("sheets")).as_array().at(0).at(U("properties")).at(U("title")).as_string()));

				// cell 情報を取る
				auto cells = body_value.at(U("sheets")).as_array().at(0).at(U("data")).as_array().at(0).at(U("rowData")).as_array();
				for (int i = 0; i < cells.size(); i++) {
					if ( !cells.at(i).at(U("values")).as_array().at(0).at(U("formattedValue")).is_string() ) {
						break;
					}

					cellData.cells.push_back(s_TextConvert(std::wstring(cells.at(i).at(U("values")).as_array().at(0).at(U("formattedValue")).as_string())));
				}

				return cellData;
			}
			catch (const std::exception& e)
			{
				return cellData;
			}
			return cellData;
			});

		try
		{
			requestTask.wait();
			if (requestTask.get().cells.size() == 0) {
				std::cout << "[GoogleSheets HTTP ERROR1] : " << SheetsID << std::endl;
			}
			else {
				//std::cout << "[GoogleSheets HTTP OK] : " << SheetsID << std::endl;
			}
			Sleep(1000);
			return requestTask.get();
		}
		catch (const std::exception& e)
		{
			std::cout << "[GoogleSheets HTTP ERROR2] : " << SheetsID << std::endl;

			Sleep(1000);
			return requestTask.get();
		}

		return Google_DataSheet();
}

解説

解説するまでもないですが、下記のソースコードでは、URLの生成を行っています。builder.append_queryをすると、綺麗にクエリストリングを登録することできます。(簡単な仕組みだから、string操作でもいいけど、こっちの方が綺麗に見えるよね)

web::uri t_uri = std::wstring(U("/v4/spreadsheets/") + s_TextConvert(SheetsID));

uri_builder builder = uri_builder(t_uri);

builder.append_query(U("ranges"), s_TextConvert(TableName) + U("!") + cell);
builder.append_query(U("includeGridData"), U("true"));

下記のソースコードでは、http通信した後、bodyをjsonにして情報を抜いています。
レスポンスの作りに関しては、これを読んでください。

// sheet 情報を取る
cellData.spreadsheetId = s_TextConvert(test_getString(U("spreadsheetId")));

cellData.SheetName = s_TextConvert(std::wstring(body_value.at(U("properties")).at(L"title").as_string()));

cellData.tableName = s_TextConvert(std::wstring(body_value.at(U("sheets")).as_array().at(0).at(U("properties")).at(U("title")).as_string()));

// cell 情報を取る
auto cells = body_value.at(U("sheets")).as_array().at(0).at(U("data")).as_array().at(0).at(U("rowData")).as_array();
for (int i = 0; i < cells.size(); i++) {
	if ( !cells.at(i).at(U("values")).as_array().at(0).at(U("formattedValue")).is_string() ) {
		break;
	}
}

これで、データを取得できました。
それでは、おまけで、データの追加をしてみましょう。

4.データを追加する

今回は、spreadsheets.values.appendを使って、一列にデータを追加していくという処理を書きました。
cell変数に値を3つ追加すると、横一列に3つの値が追加されるという仕組みです。

bool Google_session::AddCells(std::string SheetsID, std::string TableName,  std::vector<std::wstring> cell) {

	web::uri t_uri = std::wstring(U("/v4/spreadsheets/") + s_TextConvert(SheetsID) + U("/values/") + U("A1:append"));

	uri_builder builder = uri_builder(t_uri);

	builder.append_query(U("valueInputOption"), U("USER_ENTERED"));
	builder.append_query(U("insertDataOption"), U("INSERT_ROWS"));

	auto text_url = (U("https://sheets.googleapis.com") + builder.to_string());

	std::wcout << text_url << std::endl;

	http_client client(text_url, m_http_config);

	http_request requestData(methods::POST);
	requestData.headers().add(L"Accept", L"application/json");
	requestData.headers().add(L"Content-Type", L"application/json");

	//  ***************  Dataset  *************** 
	auto values = json::value::array(cell.size());
	for (int i = 0; i < cell.size(); i++) {
		values.as_array().at(i) = json::value::string(cell.at(i));

	}

	auto values_out = json::value::array(1);
	values_out.as_array().at(0) = values;

	std::vector<std::pair<::utility::string_t, json::value>> fields;
	fields.push_back(std::pair<::utility::string_t, json::value>(U("majorDimension"), json::value::string(U("ROWS"))));
	fields.push_back(std::pair<::utility::string_t, json::value>(U("range"), json::value::string(U("A1"))));
	fields.push_back(std::pair<::utility::string_t, json::value>(U("values"), values_out));

	auto top = json::value::object(fields);

	//std::wcout << top.to_string() << std::endl;
	//  ***************************************** 

	requestData.set_body(top);

	int error_code = 0;

	auto requestTask = client.request(requestData).then([&](http_response response) {
		std::cout << "[GoogleSheet] Received response status code: " << response.status_code() << " <= SheetID : " << SheetsID << std::endl;
		error_code = response.status_code();

		return response.extract_json();
		}).then([&](json::value body_value) -> bool {

			if (error_code != 200) return false;

			auto test_getString = [&](std::wstring key) -> std::wstring {
				json::value value = body_value.at(key);
				if (value.is_string() == false) {
					return U("");
				}
				return value.as_string();
			};

			try {
				// 本当は何か取っておくべきなんだろうけど、メンドイので、やりません☆
				return true;
			}
			catch (const std::exception& e)
			{
				//std::cout << e.what() << std::endl;
				return false;
			}
			return false;
			});
		try
		{
			requestTask.wait();
			if (requestTask.get() == false) {
				std::cout << "[GoogleSheets HTTP ERROR1] : " << SheetsID << std::endl;
			}
			else {
				//std::cout << "[GoogleSheets HTTP OK] : " << SheetsID << std::endl;
			}
			Sleep(1000);
			return true;
		}
		catch (const std::exception& e)
		{
			std::cout << "[GoogleSheets HTTP ERROR2] : " << SheetsID << " : HTTP Code" << e.what() << std::endl;

			Sleep(1000);
			return false;
		}

		return false;
}

解説

ここでは、Get時にはなかったBodyの設定を行っています。適切に設定すれば動きます。

//  ***************  Dataset  *************** 
auto values = json::value::array(cell.size());
for (int i = 0; i < cell.size(); i++) {
	values.as_array().at(i) = json::value::string(cell.at(i));

}

auto values_out = json::value::array(1);
values_out.as_array().at(0) = values;

std::vector<std::pair<::utility::string_t, json::value>> fields;
fields.push_back(std::pair<::utility::string_t, json::value>(U("majorDimension"), json::value::string(U("ROWS"))));
fields.push_back(std::pair<::utility::string_t, json::value>(U("range"), json::value::string(U("A1"))));
fields.push_back(std::pair<::utility::string_t, json::value>(U("values"), values_out));

auto top = json::value::object(fields);

//std::wcout << top.to_string() << std::endl;
//  *****************************************

データを削除する

最後にデータを削除してみましょう。
削除は、spreadsheets.batchUpdateのdeleteDimensionを使います。
indexを入れると、その値の行が削除されるという仕組みです。

bool Google_session::RemoveColumn(std::string SheetsID, std::string TableName, int index) {

	web::uri t_uri = std::wstring(U("/v4/spreadsheets/") + s_TextConvert(SheetsID) + U(":batchUpdate"));

	uri_builder builder = uri_builder(t_uri);

	auto text_url = (U("https://sheets.googleapis.com") + builder.to_string());

	std::cout << s_TextConvert( text_url ) << std::endl;

	http_client client(text_url, m_http_config);

	http_request requestData(methods::POST);
	requestData.headers().add(L"Accept", L"application/json");
	requestData.headers().add(L"Content-Type", L"application/json");

	//  ***************  Dataset  *************** 


	std::vector<std::pair<::utility::string_t, json::value>> fields;
	fields.push_back(std::pair<::utility::string_t, json::value>(U("dimension"), json::value::string(U("ROWS"))));
	fields.push_back(std::pair<::utility::string_t, json::value>(U("startIndex"), index ));
	fields.push_back(std::pair<::utility::string_t, json::value>(U("endIndex"), index + 1));

	std::vector<std::pair<::utility::string_t, json::value>> range;
	range.push_back(std::pair<::utility::string_t, json::value>(U("range"), json::value::object(fields)));


	std::vector<std::pair<::utility::string_t, json::value>> deleteDimension ;
	deleteDimension.push_back(	std::pair<::utility::string_t, json::value>(U("deleteDimension"), json::value::object(range)) );

	auto top_requests = json::value::array(1);
	top_requests.at(0) = json::value::object(deleteDimension);

	std::vector<std::pair<::utility::string_t, json::value>> top_requests_vec;
	top_requests_vec.push_back(std::pair<::utility::string_t, json::value>(U("requests"), top_requests));

	auto top = json::value::object(top_requests_vec);

	//std::cout << s_TextConvert( top.to_string() ) << std::endl;
	//  ***************************************** 

	requestData.set_body(top);

	int error_code = 0;

	auto requestTask = client.request(requestData).then([&](http_response response) {
		std::cout << "[GoogleSheet] Received response status code: " << response.status_code() << " <= SheetID : " << SheetsID << std::endl;
		error_code = response.status_code();

		return response.extract_json();
		}).then([&](json::value body_value) -> bool {

		
			if (error_code != 200) return false;

			auto test_getString = [&](std::wstring key) -> std::wstring {
				json::value value = body_value.at(key);
				if (value.is_string() == false) {
					return U("");
				}
				return value.as_string();
			};

			try {
				// 本当は何か取っておくべきなんだろうけど、メンドイので、やりません☆
				return true;
			}
			catch (const std::exception& e)
			{
				//std::cout << e.what() << std::endl;
				return false;
			}
			return false;
		});

	try
	{
		requestTask.wait();
		if (requestTask.get() == false) {
			std::cout << "[GoogleSheets HTTP ERROR1] : " << SheetsID << std::endl;
		}
		else {
			//std::cout << "[GoogleSheets HTTP OK] : " << SheetsID << std::endl;
		}
		Sleep(1000);
		return true;
	}
	catch (const std::exception& e)
	{
		std::cout << "[GoogleSheets HTTP ERROR2] : " << SheetsID << " : HTTP Code" << e.what() << std::endl;

		Sleep(1000);
			
		return false;
	}
		
	return false;
}

オワリ

簡単だったね!OAuthとか言うと、なんかめっちゃ難解な感があったけど、手を動かして見ると案外スッキリしたつくりだったね!
pythonやjsのGoogleSheetsのライブラリを使ったことがあるけど、あるけど…あるだけど…ぶっちゃけてしまうと、こっちの直接httpを叩く方が、しっくりきて、つかいやすい…とおもってしまった…なんか超敗北感があるのであった…。
あと、spreadsheets.batchUpdateの機能が多いので、割と…割と、http叩いた方が、バリエーションが豊かな気がしなくもない…(´ε`;)ウーン…

Discussion