Open8

DuckDBでGoogle Sheetsのデータをクエリしてみる

FKeisukeFKeisuke

公開されているスプレッドシートへのクエリは特に難しくなさそう
参考: https://www.arecadata.com/sql-for-google-sheets-with-duckdb/

SELECT *
FROM read_csv_auto('https://docs.google.com/spreadsheets/export?format=csv&id=1GuEPkwjdICgJ31Ji3iUoarirZNDbPxQj_kf7fd4h4Ro', normalize_names=True);

おそらくexport?format=csvの部分がミソ
これを行った上でread_csvで読み込む

FKeisukeFKeisuke

Google Cloudアカウント側での準備

  1. サービスアカウントを作成し、認証情報のjsonファイルを出力しておく
    • 一旦人間のアドホックな分析ではなく、定期的なETL(ELT)ワークフロー内での実行を想定したためサービスアカウントを利用
  2. APIライブラリから「Google Sheets API」を有効化しておく
  3. 参照したいスプレッドシート側でサービスアカウントのメールアドレスに対して共有設定を行っておく
FKeisukeFKeisuke

実行

Python(rye)で実行するものとする

依存

    "duckdb>=1.1.1",
    "google-auth>=2.35.0",
    "google-auth-oauthlib>=1.2.1",
    "google-auth-httplib2>=0.2.0",
    "google-api-python-client>=2.147.0",

スクリプト

import sys
from argparse import ArgumentParser
from pathlib import Path
from typing import NamedTuple

import duckdb
from google.auth.transport.requests import Request
from google.oauth2.service_account import Credentials

ScriptArgs = NamedTuple("ScriptArgs", [("secret_json_path", Path), ("sheet_id", str)])


def valid_path(path_str: str) -> Path:
    if (p := Path(path_str).resolve()) and p.exists() and p.is_file():
        return p
    raise ValueError(f"`{path_str}` is invalid")


def parse_args(argv: list[str]) -> ScriptArgs:
    parser = ArgumentParser()
    parser.add_argument("--secret-json-path", type=valid_path, required=True)
    parser.add_argument("--sheet-id", type=str, required=True)
    args = parser.parse_args(argv)
    return ScriptArgs(**vars(args))


SCOPES = [
    "https://www.googleapis.com/auth/spreadsheets.readonly",
    "https://www.googleapis.com/auth/drive.readonly",
]


def get_access_token(secret_json_path: Path) -> str | None:
    try:
        # サービスアカウントの認証情報を読み込む
        credentials = Credentials.from_service_account_file(
            secret_json_path, scopes=SCOPES
        )

        # 認証情報が有効でない場合は更新
        if not credentials.valid:
            credentials.refresh(Request())

        # アクセストークンを取得
        access_token = credentials.token
        print("Succeeded to get access token")
        return access_token
    except Exception as e:
        print(e)
        return None


def add_secret(access_token: str) -> bool:
    try:
        duckdb.sql(f"""
            CREATE OR REPLACE SECRET http (
                TYPE HTTP,
                EXTRA_HTTP_HEADERS MAP {{
                    'Authorization': 'Bearer {access_token}'
                }}
            );
        """)
        print("Succeeded to add secret")
        return True
    except Exception as e:
        print(e)
        return False


def query_spreadsheet(sheet_id: str) -> duckdb.DuckDBPyRelation:
    try:
        url = f"https://docs.google.com/spreadsheets/export?format=csv&id={sheet_id}"
        res = duckdb.sql(f"""
            SELECT
                *
            FROM read_csv_auto('{url}');
        """)
        return res
    except Exception as e:
        print(e)
        return None


def main(args: ScriptArgs) -> int:
    if not (access_token := get_access_token(args.secret_json_path)):
        print("Failed to get access token")
        return 1

    if not (_ := add_secret(access_token)):
        print("Failed to add secret")
        return 1

    if not (result := query_spreadsheet(args.sheet_id)):
        print("Failed to fetch spread sheet data")
        return 1

    print(result)
    return 0


if __name__ == "__main__":
    args = parse_args(sys.argv[1:])
    sys.exit(main(args))

FKeisukeFKeisuke

実行結果

  • --secret-json-pathはサービスアカウント作成時に出力した認証情報
  • --sheet-idは参照したいスプレッドシートのID。URLhttps://docs.google.com/spreadsheets/d/{{ SPREAD_SHEET_ID }}/edit?gid=9999999999#gid=9999999999SPREAD_SHEET_ID部分
$ rye run python -m src.test_duckdb_gsheets.main --secret-json-path=./secret/hoge.json --sheet-id=fuga
Succeeded to get access token
Succeeded to add secret
https://docs.google.com/spreadsheets/export?format=csv&id=fuga
┌──────────┬────────────────┬────────────────┬──────────┬──────────┬─────────┬──────────┬───┬──────────────────┬──────────────────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐
│ 資料区分 │ 都道府県コード │ 警察署等コード │ 本票番号 │ 事故内容 │ 死者数  │ 負傷者数 │ … │ 曜日(発生年月日) │ 祝日(発生年月日) │ 認知機能検査経過日…  │ 認知機能検査経過日…  │ 運転練習の方法(当…  │ 運転練習の方法(当…  │
│  int64   │     int64      │    varchar     │ varchar  │  int64   │ varchar │ varchar  │   │      int64       │      int64       │       varchar        │       varchar        │        int64         │        int64         │
├──────────┼────────────────┼────────────────┼──────────┼──────────┼─────────┼──────────┼───┼──────────────────┼──────────────────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
│        110 │ 059            │ 0001     │        2 │ 000     │ 001      │ … │                739999                 │ 0000                 │                    50 │
│        110 │ 059            │ 0002     │        2 │ 000     │ 001      │ … │                639999                 │ 0000                 │                    10 │
│        110 │ 059            │ 0003     │        2 │ 000     │ 001      │ … │                239999999911 │
│        110 │ 059            │ 0004     │        2 │ 000     │ 001      │ … │                739999999919 │
│        110101            │ 0001     │        2 │ 000     │ 001      │ … │                729999999911 │
│        110101            │ 0002     │        2 │ 000     │ 001      │ … │                639999                 │ 0000                 │                    90 │
│        110101            │ 0003     │        2 │ 000     │ 001      │ … │                439999999919 │
│        110101            │ 0004     │        2 │ 000     │ 001      │ … │                639999                 │ 0000                 │                    90 │
│        110101            │ 0005     │        2 │ 000     │ 001      │ … │                639999999919 │
│        110101            │ 0006     │        2 │ 000     │ 001      │ … │                539999999999 │
│        · │              · │  ·             │  ·       │        · │  ·      │  ·       │ · │                · │                · │  ·                   │  ·                   │                    · │                    · │
│        · │              · │  ·             │  ·       │        · │  ·      │  ·       │ · │                · │                · │  ·                   │  ·                   │                    · │                    · │
│        · │              · │  ·             │  ·       │        · │  ·      │  ·       │ · │                · │                · │  ·                   │  ·                   │                    · │                    · │
│        110103            │ 0005     │        2 │ 000     │ 001      │ … │                739999999911 │
│        110103            │ 0006     │        2 │ 000     │ 001      │ … │                639999999991 │
│        110103            │ 0007     │        2 │ 000     │ 001      │ … │                439999999991 │
│        110103            │ 0008     │        2 │ 000     │ 001      │ … │                219999999991 │
│        110103            │ 0009     │        2 │ 000     │ 001      │ … │                339999                 │ 0000                 │                    90 │
│        110103            │ 0010     │        2 │ 000     │ 001      │ … │                439999999911 │
│        110103            │ 0011     │        2 │ 000     │ 002      │ … │                329999999919 │
│        110103            │ 0012     │        2 │ 000     │ 001      │ … │                43 │ 0189                 │ 999991 │
│        110103            │ 0013     │        2 │ 000     │ 001      │ … │                239999999991 │
│        110103            │ 0014     │        2 │ 000     │ 001      │ … │                129999999911 │
├──────────┴────────────────┴────────────────┴──────────┴──────────┴─────────┴──────────┴───┴──────────────────┴──────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┤
│ 100 rows (20 shown)                                                                                                                                                                                   68 columns (13 shown) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
FKeisukeFKeisuke

DuckDB CLIからの実行

Google Cloudがよくわからん状態なので間違えてる部分があるかもしれない…

雑に試すとしたらこちらでアクセストークンを払い出して
https://developers.google.com/oauthplayground/

pythonでなくduckdbのcliから実行することもできた

$ duckdb
v1.1.1 af39bd0dcf
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D CREATE OR REPLACE SECRET http (
      TYPE HTTP,
      EXTRA_HTTP_HEADERS MAP {
          'Authorization': 'Bearer {{ ACCESS_TOKEN }}'
      }
  );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘

※ {{ ACCESS_TOKEN }}部分を取得したトークンに変更してSecretを作成

D SELECT *
  FROM read_csv('https://docs.google.com/spreadsheets/export?format=csv&id={{ SPREAD_SHEET_ID }}');
┌──────────┬────────────────┬────────────────┬──────────┬──────────┬─────────┬──────────┬───┬──────────────────┬──────────────────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐
│ 資料区分 │ 都道府県コード │ 警察署等コード │ 本票番号 │ 事故内容 │ 死者数  │ 負傷者数 │ … │ 曜日(発生年月日) │ 祝日(発生年月日) │ 認知機能検査経過日…  │ 認知機能検査経過日…  │ 運転練習の方法(当…  │ 運転練習の方法(当…  │
│  int64   │     int64      │    varchar     │ varchar  │  int64   │ varchar │ varchar  │   │      int64       │      int64       │       varchar        │       varchar        │        int64         │        int64         │
├──────────┼────────────────┼────────────────┼──────────┼──────────┼─────────┼──────────┼───┼──────────────────┼──────────────────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
│        110 │ 059            │ 0001     │        2 │ 000     │ 001      │ … │                739999                 │ 0000                 │                    50 │
│        110 │ 059            │ 0002     │        2 │ 000     │ 001      │ … │                639999                 │ 0000                 │                    10 │
│        110 │ 059            │ 0003     │        2 │ 000     │ 001      │ … │                239999999911 │
│        110 │ 059            │ 0004     │        2 │ 000     │ 001      │ … │                739999999919 │
│        110101            │ 0001     │        2 │ 000     │ 001      │ … │                729999999911 │
│        110101            │ 0002     │        2 │ 000     │ 001      │ … │                639999                 │ 0000                 │                    90 │
│        110101            │ 0003     │        2 │ 000     │ 001      │ … │                439999999919 │
│        110101            │ 0004     │        2 │ 000     │ 001      │ … │                639999                 │ 0000                 │                    90 │
│        110101            │ 0005     │        2 │ 000     │ 001      │ … │                639999999919 │
│        110101            │ 0006     │        2 │ 000     │ 001      │ … │                539999999999 │
│        110101            │ 0007     │        2 │ 000     │ 001      │ … │                539999999991 │
│        110101            │ 0008     │        2 │ 000     │ 001      │ … │                339999999991 │
│        110101            │ 0009     │        2 │ 000     │ 001      │ … │                539999                 │ 0000                 │                    90 │
│        110101            │ 0010     │        2 │ 000     │ 001      │ … │                739999999951 │
│        110101            │ 0011     │        2 │ 000     │ 001      │ … │                239999                 │ 0000                 │                    90 │
│        110101            │ 0012     │        2 │ 000     │ 002      │ … │                139999999991 │
│        110101            │ 0013     │        2 │ 000     │ 001      │ … │                129999999999 │
│        110101            │ 0014     │        2 │ 000     │ 001      │ … │                339999999959 │
│        110101            │ 0015     │        2 │ 000     │ 001      │ … │                539999999999 │
│        110101            │ 0016     │        2 │ 000     │ 001      │ … │                239999999991 │
│        · │              · │  ·             │  ·       │        · │  ·      │  ·       │ · │                · │                · │  ·                   │  ·                   │                    · │                    · │
│        · │              · │  ·             │  ·       │        · │  ·      │  ·       │ · │                · │                · │  ·                   │  ·                   │                    · │                    · │
│        · │              · │  ·             │  ·       │        · │  ·      │  ·       │ · │                · │                · │  ·                   │  ·                   │                    · │                    · │
│        110102            │ 0037     │        2 │ 000     │ 003      │ … │                439999999911 │
│        110102            │ 0038     │        2 │ 000     │ 001      │ … │                439999999919 │
│        110102            │ 0039     │        2 │ 000     │ 001      │ … │                539999                 │ 0000                 │                    10 │
│        110102            │ 0040     │        2 │ 000     │ 001      │ … │                23 │ 0797                 │ 999991 │
│        110102            │ 0041     │        2 │ 000     │ 001      │ … │                339999999991 │
│        110102            │ 0042     │        2 │ 000     │ 001      │ … │                639999999995 │
│        110103            │ 0001     │        2 │ 000     │ 001      │ … │                339999999999 │
│        110103            │ 0002     │        2 │ 000     │ 001      │ … │                139999                 │ 0000                 │                    90 │
│        110103            │ 0003     │        2 │ 000     │ 001      │ … │                239999999911 │
│        110103            │ 0004     │        2 │ 000     │ 001      │ … │                239999999919 │
│        110103            │ 0005     │        2 │ 000     │ 001      │ … │                739999999911 │
│        110103            │ 0006     │        2 │ 000     │ 001      │ … │                639999999991 │
│        110103            │ 0007     │        2 │ 000     │ 001      │ … │                439999999991 │
│        110103            │ 0008     │        2 │ 000     │ 001      │ … │                219999999991 │
│        110103            │ 0009     │        2 │ 000     │ 001      │ … │                339999                 │ 0000                 │                    90 │
│        110103            │ 0010     │        2 │ 000     │ 001      │ … │                439999999911 │
│        110103            │ 0011     │        2 │ 000     │ 002      │ … │                329999999919 │
│        110103            │ 0012     │        2 │ 000     │ 001      │ … │                43 │ 0189                 │ 999991 │
│        110103            │ 0013     │        2 │ 000     │ 001      │ … │                239999999991 │
│        110103            │ 0014     │        2 │ 000     │ 001      │ … │                129999999911 │
├──────────┴────────────────┴────────────────┴──────────┴──────────┴─────────┴──────────┴───┴──────────────────┴──────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┤
│ 100 rows (40 shown)                                                                                                                                                                                   68 columns (13 shown) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

※ {{ SPREAD_SHEET_ID }}部分は参照したいスプレッドシートのIDに変更してクエリを実行