Blog

試してみたことを書いておく備忘録

スプレッドシートへのデータの書き込み

背景

バッチ処理などで計算結果のデータフレームをデータベースやファイルに保存したいことがあります。そのときに、データフレームをGoogle Drive上のスプレッドシートに保存すると、無料&手軽にデータベース代わりとして使えます。

目的

ローカル環境およびGoogle Coud上で集計したデータフレームをGoogle Drive上のスプレッドシートに保存する方法を試したいと思います。

結論

Docker環境を構築することで、ローカルとGoogle Cloudで同じシステムを構築することができました。

内容

(1)ローカル環境
①環境構築

Dockerを使うと開発環境の構築が楽になります。

version: "3"
services:
  lunch-choice:
    build:
      context: ./
      dockerfile: ./dev/Dockerfile
    container_name: lunch-choice
    volumes:
      - ${PWD}/../:/working
    working_dir: /working
    environment:
      PYTHONPATH: /working
      TZ: Asia/Tokyo
      GOOGLE_APPLICATION_CREDENTIALS: /working/gcp_credentials_key.json
    tty: true
docker-compose.yml

このとき、環境変数 GOOGLE_APPLICATION_CREDENTIALS にGoogle Cloudのサービスアカウントから出力したkey(JSON)のパスを指定します。そうすることで、Google CloudやGoogle Workspaceのサービスに簡単にアクセスすることができます。

②Google Drive上のpreadSheetのデータの読み書き

ローカル環境からスプレッドシートを操作してみます。

まずは、Google APIのPythonライブラリを使って、下記のようなスプレッドシートを操作するクラスを作成します。その中で、スプレッドシートからデータを読み込む、データフレームをスプレッドシートに書き込む、スプレッドシートのデータを削除するメソッドを作成します。

import polars as pl
import google.auth
from googleapiclient.discovery import build


class SpreadSheet:
    def __init__(self, sheet_id: str):
        self.sheet_id=sheet_id
        self.service_sheets = build(
            serviceName="sheets", version="v4", credentials=google.auth.default()[0]
        )

    def read_spreadsheet(self, ranges: str) -> pl.DataFrame:
        """Google Driveに保存されているスプレッドシートからデータを読み込み

        Args:
            ranges (str): スプレッドシートのシート名:セル範囲

        Returns:
            pl.DataFrame: スプレッドシートから取得したデータ
        """
        # リクエスト
        response = (
            self.service_sheets.spreadsheets()
            .values()
            .batchGet(
                spreadsheetId=self.sheet_id,
                ranges=[ranges],
            )
            .execute()
        )

        ranges = response.get("valueRanges", [])

        # レスポンスから列名とデータ部分の抽出
        lst = ranges[0]["values"][1:]
        cols = ranges[0]["values"][0]

        # リストの最大の長さに揃える
        max_len = max(len(sublist) for sublist in lst)
        for sublist in lst:
            while len(sublist) < max_len:
                sublist.append(None)

        return pl.DataFrame(data=lst, schema=cols)
    
    def write_spreadsheet(self, df: pl.DataFrame, ranges: str) -> None:
        """データフレームをスプレッドシートに書き込み

        Args:
            df (pl.DataFrame): 書き込むデータ
            ranges (str): スプレッドシートのシート名:セル範囲
        """
        # スプレッドシートのデータを削除
        self.remove_spreadsheet(ranges=ranges)

        # スプレッドシートに書き込むデータ
        data = [
            {
                "range": ranges,
                "majorDimension": "COLUMNS",
                "values": [
                    [col] + df[col].dt.strftime("%Y-%m-%d").to_list()
                    if col == "date"
                    else [col] + df[col].to_list()
                    for col in df.columns
                ],
            }
        ]

        # リクエスト
        (
            self.service_sheets.spreadsheets()
            .values()
            .batchUpdate(
                spreadsheetId=self.sheet_id,
                body={"value_input_option": "USER_ENTERED", "data": data},
            )
            .execute()
        )

    def remove_spreadsheet(self, ranges: str) -> None:
        """特定のセル範囲に書かれたスプレッドシートのデータを削除

        Args:
            ranges (str): スプレッドシートのシート名!セル範囲
        """
        # リクエスト
        (
            self.service_sheets.spreadsheets()
            .values()
            .clear(
                spreadsheetId=self.sheet_id,
                body={},
                range=ranges,
            )
            .execute()
        )
google_cloud_test.py

データを読み書きするスプレッドシートのシートIDを取得します。スプレッドシートを作成し、URLのd/から/editの間の文字列がシートID(下図の白い部分)です。

スプレッドシートにデータを書き込むスクリプトをgoogle_cloud_test.pyと同じディレクトリに作成します。

from google_cloud_test import SpreadSheet

def main():
	# インスタンスの作成
	ss = SpreadSheet(sheet_id="*******************")
	
	# データフレームの作成
	df_write = pl.DataFrame(
	    {
	        "foo": [1, 2, 3],
	        "bar": [6, 7, 8],
	        "ham": ["a", "b", "c"],
	    }
	)
	
	# スプレッドシートへのデータの書き込み
	ss.write_spreadsheet(df=df_write, ranges="シート1!A1:C4")

if __name__ == "__main__":
    main()
write_spreadsheet.py

上記のスクリプトを実行します。

$ python write_spreadsheet.py

スプレッドシートにデータが書き込まれています。

次に、スプレッドシートからデータを読み込むスクリプトをgoogle_cloud_test.pyと同じディレクトリに作成します。

from google_cloud_test import SpreadSheet

def main():
	# インスタンスの作成
	ss = SpreadSheet(sheet_id="*******************")

	# スプレッドシートからデータの読み込み
	df_read = ss.read_spreadsheet(ranges="シート1!A1:C4")

	print(df_read)

if __name__ == "__main__":
    main()
read_spreadsheet.py

上記のスクリプトを実行するとデータを取得することができます。

$ python read_spreadsheet.py

shape: (3, 3)
┌─────┬─────┬─────┐
│ foo ┆ bar ┆ ham │
│ --------- │
│ str ┆ str ┆ str │
╞═════╪═════╪═════╡
│ 123   │
│ 678   │
│ a   ┆ b   ┆ c   │
└─────┴─────┴─────┘

(2)Google Cloud

ローカル環境で作成したスクリプトをGoogle Cloudで実行します。ローカル環境ではDockerを使って開発環境を構築していますので、Dockerを使うことでGoogle Cloudでも同じ環境でスクリプトを実行することができます。

Google Cloud上では下記のサービスを使ってスクリプトを実行します。開発の流れは下記の通りです。

  1. GitHubにコードをプッシュ
  2. Cloud BuildでDockerイメージをビルド
  3. DockerイメージがArtifact Registryに保存
  4. Cloud Run Jobsでスクリプトを実行

①GitHubにコードをプッシュ

ローカル環境ではサンプルのプログラムで説明しましたが、Google Cloudでは下記のリポジトリで説明します。

②Cloud BuildでDockerイメージをビルド

下のGoogle Build用のyamlファイルをリポジトリに保存することで、GitHubでプルリクが承認されたコードを自動的にGoogle Cloudに保存することができます。

上のyamlファイル実行するためにGoogle Buildのトリガーを設定します。

名前 lunch-choice
リージョン asia-northeast1
イベント ブランチにpushする
ソース 第1世代
リポジトリ interh852/lunch-choice(GitHubアプリ)
ブランチ ^main$
構成の形式 Cloud Build構成ファイル
ロケーション リポジトリ
Cloud Build 構成ファイルの場所 gcloud/cloudbuild_prod.yaml
代入変数 _IMAGE
代入値 asia-northeast1-docker.pkg.dev/プロジェクト名/リポジトリ名/イメージ名:latest

③DockerイメージがArtifact Registryに保存

Artifact RegistryにDockerイメージを保存するリポジトリを作成するとCloud Buildでビルドしたイメージが保存されます。

④Cloud Run Jobsでスクリプトを実行

Cloud Run Jobsを使うことでDockerコンテナの特定のスクリプトを実行することができます。

コンテナイメージのURL asia-northeast1-docker.pkg.dev/プロジェクト名/リポジトリ名/イメージ名:latest
コンテナコマンド /working/script/run.sh -o update_this_week
CPU 1
メモリ 512MiB

まとめ

ローカルでもGoogle Cloud上でもDockerコンテナを実行することができました。