Как стать автором
Обновить

Google sheets with C++

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров2.5K

Привет, пишу тг бота на C++. Недавно мне требовалось подключить выгрузку данных из бд в гугл таблицы, но годного гайда как это сделать я не нашел. Поэтому будет простенький гайд. Расписываю как читать и записывать данные в гугл таблицы. Использовал библиотеки rapidjson, libcurl и jwt-cpp.

1. Создание сервисного аккаунта

Переходим в Google cloud console открываем выпадающее меню слева и переходим наводимся на вкладку APIs & Services и переходим в Credentials.

Дальше нажимаем Create credentials выбираем Service account. После этого переходим в настройки этого аккаунта на вкладу Keys. Нажимаем Add key -> Create new key -> JSON. JSON файл загрузиться на компьютер.

Этот файлик выглядит примерно так:

{
  "type": "service_account",
  "project_id": "inxbot",
  "private_key_id": "60c19ce53e578c63a679a300024ae29678053a2a",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBPiM67EIHEV+p\nnxSn2cfIUUocbeptKzaF+6HWjIer1+ijhdwEPnYXOAJFJoSQlAJkQeYdTHAHrWHo\n/r+M/QV3hnBk/EljkpEHYTezROFETG02x4LR1PWy0JZceRUYs8EKx0Z1IMXO7JLL\n2n3ePagX3QKMhYn3YDVvenMCvTQZswg1sRxQzZdIIu6Zc/8x8wIp7AZraSxBokPU\nfjACOr9OgyO2reft8fSU2rPUFAfhVXkeW+tbbGqasDXao08koV7Ng0B4sO5PHFik\nA6PMgFftXpSAybBiyVg205UauUhQ6GX8tlegfxSMvK4HITcWuLCQF1N1XFKrsihJ\nUqay+AJ5AgMBAAECggEAEmXXldMLgD/Ds1L9dPOThTtUCnvef1FHah8oa9RKUG6I\nbBUg+th1hsVi1fGSWm91ANWTUoPGAL0dnsCuPP6T6CrJBZxNYmkAXMsBMhSjO8zo\n8vewuUiEsecRAbHXc9gdT2jkiN87ZfV5DmZ5aFDHcaothXJrTb1/qOLoE0EvNij+\nJISdNV4VvCKyh44SYRAeUCUx6Tk071QNLapWxURCVuJZxLAZdatVztuxgSyMjqVc\n/u6ZMDdBwrJX7POPZItQZAYjNiz+5ytK/+a3pb3A4sij7cwfkqrEjy9Vnt6K567c\n7aZ77Zf7bMbPAQaEqs7sc1GgSZidt8hT+lpGSIO6WQKBgQD/ROQ+aQEBNYEY+Iza\nh9w2QvZzl3pS6dohLXeq1tSS4Pn7hddhaMniY2gBUC/UdLxaUSHncYneCWZj17wL\nqUxp8tNuYbmFx7SwN0hf4gRVRs9VHliot0ShJehatXIUdQtZGem6ZeBCMiU6Ahl6\nw0DoeSmXmXKMjA/0nqa7V8uEbQKBgQDBy8gZIaiPAbskfwzE+rtEGysSrWbp5YmL\novaAQyIU6ZQ1QcSRuwPyLsK1mQokpVLp/D3tuX5bf7QEywXGZpqQkNAkTIAszfd1\nFu5dZYjD3/U8nh2vXSOhoWLV3cs+MFn/1pOE6okd22RxVszKKPAMGwdFHLjqhTgm\n4PaGX0F2vQKBgQD7QdkTIRHPGdXvOT6THuT/N9wTHMdXczy0KWe/bWOhCvajvzKn\ne5gzUDsgB8h8a0a182PWxme4eDIjFkbOh55iLpQz8tbEOjS8uj1vuy0erusG0i7c\nKj/QClrxZeVbA3XfBuWLBhm9rlP8C/L+Zc+uwwvQX0Zj1r0hv0NH5Xu55QKBgGEy\nuguQzB/duGUXq+nn76C7F6FogyNw0snMVGIEScW/WWxUriiBafs/5Lx8I5A5sxpU\nPyNKU8vEgx+cTCs0LvWvDBYIoIjYTPLXK3qAgFXtr+xVSYI47rQUY6PseBGa4qjk\nkFyr3VWyQfBunowOeEc6Dy1kuII3ivBFCKBgik7pAoGAUnhxLBrSuBh4dlwKgTZp\n9qsnRnVRyrHvXipHpeizZAyvEGn1+5jHHFa6OYgX/7k90hRGmY0v27HcFgKwiqZX\nGJ4LovYF5kYuZ6EYs1kdLP5Ha8f6snMr3N1f9aKawBE5Lg3OhDJeNJAxaOkOcJTR\nUoZfBrgvw6u6H/Kr0KMo/Ws=\n-----END PRIVATE KEY-----\n",
  "client_email": "ura-ia-na-habre@inxbot.iam.gserviceaccount.com",
  "client_id": "116600111704528514606",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ura-ia-na-habre%40inxbot.iam.gserviceaccount.com",
  "universe_domain": "googleapis.com"
}

Далее переходим в свою гугл таблицу -> Настройки доступа -> Вписываем email нашего аккаунта (поле client_email в json) -> Права редактор или читатель в зависимости от ваших целей. За гугл таблицу можно не бояться, гугл автоматически сохраняет все версии таблицы во вкладке Файл -> История версий -> Смотреть историю версий.

2. C++ код

Код пояснять практически не буду, если что-то непонятно chat про всё спокойно расскажет.

Авторизация по протоколу OAuth 2.0 с помощью JWT

Метод для парсинга JSON файла. В SERVICE_ACCOUNT_FILE лежит путь к файлу.

rapidjson::Document loadServiceAccountKey() {
        std::ifstream file(SERVICE_ACCOUNT_FILE);
        std::stringstream buffer;
        buffer << file.rdbuf();
        rapidjson::Document doc;
        doc.Parse(buffer.str().c_str());
        return doc;
    }

Метод для получения jwt токена. jwt::create не будет работать без трэитов, я взял их из репозитория библиотеки на гитхабе. Так же тут очень важная строчка ".set_payload_claim("scope", jwt::claim(std::string("https://www.googleapis.com/auth/spreadsheets")))" - указывает сервис в который мы получаем аутентификацию.

std::string getJwtToken() {
        rapidjson::Document doc = loadServiceAccountKey();
        std::string privateKey = doc["private_key"].GetString();
        std::string clientEmail = doc["client_email"].GetString();

        auto now = std::chrono::system_clock::now();
        auto exp = now + std::chrono::minutes(60);
        auto rsa = jwt::algorithm::rs256("", privateKey, "", "RS256");

        std::string jwt = jwt::create()
        .set_issuer(clientEmail)
        .set_audience("https://oauth2.googleapis.com/token")
        .set_issued_at(now)
        .set_expires_at(exp)
        .set_payload_claim("scope", jwt::claim(std::string("https://www.googleapis.com/auth/spreadsheets")))
        .set_type("JWT")
        .set_algorithm("RS256")
        .sign(rsa);

        return jwt;
    }

Пример трэйта. Я использовал этот трейт для библиотеки nlohmann-json. Свой писать на rapid времени не было.

#ifndef JWT_CPP_NLOHMANN_JSON_TRAITS_H
#define JWT_CPP_NLOHMANN_JSON_TRAITS_H

#include "jwt-cpp/jwt.h"
#include "nlohmann/json.hpp"

namespace jwt {
	/**
	 * \brief Namespace containing all the json_trait implementations for a jwt::basic_claim.
	*/
	namespace traits {
		/// basic_claim's JSON trait implementation for Modern C++ JSON
		struct nlohmann_json {
			using json = nlohmann::json;
			using value_type = json;
			using object_type = json::object_t;
			using array_type = json::array_t;
			using string_type = std::string; // current limitation of traits implementation
			using number_type = json::number_float_t;
			using integer_type = json::number_integer_t;
			using boolean_type = json::boolean_t;

			static jwt::json::type get_type(const json& val) {
				using jwt::json::type;

				if (val.type() == json::value_t::boolean) return type::boolean;
				// nlohmann internally tracks two types of integers
				if (val.type() == json::value_t::number_integer) return type::integer;
				if (val.type() == json::value_t::number_unsigned) return type::integer;
				if (val.type() == json::value_t::number_float) return type::number;
				if (val.type() == json::value_t::string) return type::string;
				if (val.type() == json::value_t::array) return type::array;
				if (val.type() == json::value_t::object) return type::object;

				throw std::logic_error("invalid type");
			}

			static json::object_t as_object(const json& val) {
				if (val.type() != json::value_t::object) throw std::bad_cast();
				return val.get<json::object_t>();
			}

			static std::string as_string(const json& val) {
				if (val.type() != json::value_t::string) throw std::bad_cast();
				return val.get<std::string>();
			}

			static json::array_t as_array(const json& val) {
				if (val.type() != json::value_t::array) throw std::bad_cast();
				return val.get<json::array_t>();
			}

			static int64_t as_integer(const json& val) {
				switch (val.type()) {
				case json::value_t::number_integer:
				case json::value_t::number_unsigned: return val.get<int64_t>();
				default: throw std::bad_cast();
				}
			}

			static bool as_boolean(const json& val) {
				if (val.type() != json::value_t::boolean) throw std::bad_cast();
				return val.get<bool>();
			}

			static double as_number(const json& val) {
				if (val.type() != json::value_t::number_float) throw std::bad_cast();
				return val.get<double>();
			}

			static bool parse(json& val, std::string str) {
				val = json::parse(str.begin(), str.end());
				return true;
			}

			static std::string serialize(const json& val) { return val.dump(); }
		};
	} // namespace traits
} // namespace jwt

#endif

Метод для получения Acess токена. При написании запросов на curl обращаю внимание на строчку "curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);" логирование очень помогает, если есть какие-то проблемы с запросом. Acess токен нужно всегда получают через post запросы.

std::string getAcessToken() {
        CURL* curl = curl_easy_init();

        //TODO: Возможно нужно как-то обработать этот случай
        if(!curl) return "";
        std::string postData = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + getJwtToken();
        std::string readBuffer;

        curl_easy_setopt(curl, CURLOPT_URL, "https://oauth2.googleapis.com/token");
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str());
        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); // для логирования
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);

        CURLcode res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            std::cerr << "Ошибка CURL: " << curl_easy_strerror(res) << std::endl;
            curl_easy_cleanup(curl);
            return "";
        }
        curl_easy_cleanup(curl);

        rapidjson::Document doc_res;
        doc_res.Parse(readBuffer.c_str());

        if (!(doc_res.HasMember("access_token") && doc_res["access_token"].IsString())) {
            std::cerr << "Ошибка: Не найден access_token в ответе!" << std::endl;
            return "";
        }

        return doc_res["access_token"].GetString();
    }

Запись данных.

static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) {
        output->append((char*)contents, size * nmemb);
        return size * nmemb;
    }

Получение данных из таблицы. Простой пример в этом случае получим данные из D столбца.

void updateDbWithSheets() {
        std::string accessToken = getAcessToken();
        std::string range = "clients!D:D";
        std::string url = "https://sheets.googleapis.com/v4/spreadsheets/" + SPREADSHEET_ID + "/values:batchGet?ranges=" + range;

        // Настройка cURL
        CURL* curl = curl_easy_init();
        if (curl) {
            curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
            
            struct curl_slist* headers = NULL;
            headers = curl_slist_append(headers, ("Authorization: Bearer " + accessToken).c_str());
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
            
            std::string readBuffer;
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
            
            CURLcode res = curl_easy_perform(curl);
            
            if (res != CURLE_OK) {
                std::cerr << "Ошибка CURL: " << curl_easy_strerror(res) << std::endl;
            } else {
                std::cout << "Ответ от Google Sheets API:\n" << readBuffer << std::endl;
            }
            
            curl_easy_cleanup(curl);
            curl_slist_free_all(headers);
        }
    }

Запись данных в таблицу. В гугл таблицы данные отправляются put запросом. Передаете range , если нужно несколько ячеек из разных участков таблицы, спросите у гпт или посмотрите как прописывается в google sheets api. Это не сложно изменить.

void updateSheetsWithDb() {
        CURL* curl = curl_easy_init();
        if(curl) {
            const std::string accessToken = getAcessToken();
            const std::string range = "clients!A3:AD3";
            std::string url = "https://sheets.googleapis.com/v4/spreadsheets/" + SPREADSHEET_ID + "/values/" + range + "?valueInputOption=RAW";
            

            const std::string jsonData = R"({"range":"clients!A3:AD3","majorDimension":"ROWS","values":[["01.03.25","Артем Дадерко", "Tg", "id", "90", null, null, null, "80"]]})";

            struct curl_slist* headers = NULL;
            headers = curl_slist_append(headers, ("Authorization: Bearer " + accessToken).c_str());
            headers = curl_slist_append(headers, "Content-Type: application/json");
            headers = curl_slist_append(headers, "Accept: application/json");

            curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
            curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonData.c_str());
            //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

            CURLcode res = curl_easy_perform(curl);

            if(res != CURLE_OK) {
                std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
            }
                
            curl_easy_cleanup(curl);
            curl_slist_free_all(headers);
        }
    }

3. Заключение

По сути этих методов хватает чтобы получить и отправить данные без обработки. Этот гайд написал лишь для быстрого старта. Я несколько дней не мог понять в чем моя ошибка при аутентификации и надеюсь с помощью этих наработок вам не придется тратить время на решение таких проблем. Спасибо.

Теги:
Хабы:
+9
Комментарии7

Публикации

Истории

Работа

Программист C++
96 вакансий
QT разработчик
5 вакансий

Ближайшие события

19 марта – 28 апреля
Экспедиция «Рэйдикс»
Нижний НовгородЕкатеринбургНовосибирскВладивостокИжевскКазаньТюменьУфаИркутскЧелябинскСамараХабаровскКрасноярскОмск
24 апреля
VK Go Meetup 2025
Санкт-ПетербургОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
14 мая
LinkMeetup
Москва
5 июня
Конференция TechRec AI&HR 2025
МоскваОнлайн
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область