🤖

Claude CodeでFirestoreのデータをCSVエクスポートするツールを作ってみた

に公開

はじめに

FirestoreのデータをCSVで出力したいケースは実務でよくあります。
しかし、意外にもシンプルなツールが見つかりませんでした。BigQueryにエクスポートする方法はありますが、ちょっとしたデータ確認には大げさです。また、既存のライブラリも複雑で使いづらいものが多く、シンプルにコピペで動くツールが欲しいと思っていました。

そこで、Claude Codeを使って、コピペで簡単に動作できる Firestore to CSVエクスポートツールを作成しました。

完成したツールの機能

主な機能

  1. コレクション一覧表示

    • プロジェクト内のすべてのコレクションを表示
    • 各コレクションのドキュメント数も表示
  2. CSV出力

    • 指定したコレクションをCSVファイルに出力
    • すべてのフィールドを自動検出
    • ネストしたデータはJSON形式で出力

セットアップと使い方

1. ファイルの準備

新しいディレクトリを作成し、以下の2つのファイルを作成します。

# ディレクトリ作成
mkdir firestore-csv-export
cd firestore-csv-export

2. main.goファイルを作成

以下のコードをmain.goとして保存します。

package main

import (
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"strconv"
	"time"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go/v4"
	"google.golang.org/api/iterator"
)

func main() {
	// コマンドライン引数のチェック
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	// モード判定
	if os.Args[1] == "-l" || os.Args[1] == "--list" {
		// コレクション一覧表示モード
		if len(os.Args) != 3 {
			printUsage()
			os.Exit(1)
		}
		projectID := os.Args[2]
		if err := listCollections(projectID); err != nil {
			fmt.Fprintf(os.Stderr, "エラー: %v\n", err)
			os.Exit(1)
		}
	} else {
		// CSV出力モード
		if len(os.Args) != 3 {
			printUsage()
			os.Exit(1)
		}
		projectID := os.Args[1]
		collectionName := os.Args[2]
		if err := exportToCSV(projectID, collectionName); err != nil {
			fmt.Fprintf(os.Stderr, "エラー: %v\n", err)
			os.Exit(1)
		}
	}
}

func printUsage() {
	fmt.Fprintf(os.Stderr, "使用方法:\n")
	fmt.Fprintf(os.Stderr, "  %s -l <GCPプロジェクトID>                # コレクション一覧表示\n", os.Args[0])
	fmt.Fprintf(os.Stderr, "  %s <GCPプロジェクトID> <コレクション名>   # CSV出力\n", os.Args[0])
}

func initializeFirestore(ctx context.Context, projectID string) (*firestore.Client, error) {
	conf := &firebase.Config{ProjectID: projectID}
	app, err := firebase.NewApp(ctx, conf)
	if err != nil {
		return nil, fmt.Errorf("Firebase初期化エラー: %w", err)
	}

	client, err := app.Firestore(ctx)
	if err != nil {
		return nil, fmt.Errorf("Firestore接続エラー: %w", err)
	}

	return client, nil
}

func listCollections(projectID string) error {
	ctx := context.Background()

	client, err := initializeFirestore(ctx, projectID)
	if err != nil {
		return err
	}
	defer client.Close()

	collections := client.Collections(ctx)
	collectionRefs, err := collections.GetAll()
	if err != nil {
		return fmt.Errorf("コレクション取得エラー: %w", err)
	}

	if len(collectionRefs) == 0 {
		fmt.Println("コレクションが存在しません")
		return nil
	}

	fmt.Println("📋 利用可能なコレクション一覧:")
	fmt.Println("----------------------------------------")

	for i, col := range collectionRefs {
		// ドキュメント数を取得
		count := 0
		iter := col.Documents(ctx)
		for {
			_, err := iter.Next()
			if err == iterator.Done {
				break
			}
			if err != nil {
				fmt.Printf("%d. %s (件数取得エラー)\n", i+1, col.ID)
				continue
			}
			count++
			if count >= 1000 {
				// 1000件以上は概算表示
				fmt.Printf("%d. %s (1000件以上)\n", i+1, col.ID)
				break
			}
		}
		if count < 1000 {
			fmt.Printf("%d. %s (%d件)\n", i+1, col.ID, count)
		}
	}

	fmt.Println("----------------------------------------")
	fmt.Printf("合計: %d個のコレクション\n", len(collectionRefs))

	return nil
}

func exportToCSV(projectID, collectionName string) error {
	ctx := context.Background()

	client, err := initializeFirestore(ctx, projectID)
	if err != nil {
		return err
	}
	defer client.Close()

	collection := client.Collection(collectionName)
	iter := collection.Documents(ctx)
	defer iter.Stop()

	// 最初のドキュメントでフィールド名を収集
	fieldSet := make(map[string]bool)
	var docs []*firestore.DocumentSnapshot

	for {
		doc, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return fmt.Errorf("ドキュメント取得エラー: %w", err)
		}

		docs = append(docs, doc)
		data := doc.Data()
		for fieldName := range data {
			fieldSet[fieldName] = true
		}

		// 最初の100件でフィールド構造を把握
		if len(docs) >= 100 {
			break
		}
	}

	if len(docs) == 0 {
		fmt.Printf("⚠️  コレクション '%s' にドキュメントが存在しません\n", collectionName)
		return nil
	}

	// フィールド名をソート
	var fieldNames []string
	for fieldName := range fieldSet {
		fieldNames = append(fieldNames, fieldName)
	}
	sort.Strings(fieldNames)

	// CSV作成
	filename := fmt.Sprintf("%s_%s.csv", collectionName, time.Now().Format("20060102_150405"))
	file, err := os.Create(filename)
	if err != nil {
		return fmt.Errorf("ファイル作成エラー: %w", err)
	}
	defer file.Close()

	writer := csv.NewWriter(file)
	defer writer.Flush()

	// ヘッダー出力
	headers := append([]string{"document_id"}, fieldNames...)
	if err := writer.Write(headers); err != nil {
		return fmt.Errorf("ヘッダー書き込みエラー: %w", err)
	}

	// データ出力(収集済みドキュメント)
	count := 0
	for _, doc := range docs {
		record := make([]string, len(fieldNames)+1)
		record[0] = doc.Ref.ID

		data := doc.Data()
		for i, fieldName := range fieldNames {
			record[i+1] = convertToString(data[fieldName])
		}

		if err := writer.Write(record); err != nil {
			return fmt.Errorf("データ書き込みエラー: %w", err)
		}
		count++
	}

	// 残りのドキュメントを処理
	for {
		doc, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return fmt.Errorf("ドキュメント取得エラー: %w", err)
		}

		record := make([]string, len(fieldNames)+1)
		record[0] = doc.Ref.ID

		data := doc.Data()
		for i, fieldName := range fieldNames {
			record[i+1] = convertToString(data[fieldName])
		}

		if err := writer.Write(record); err != nil {
			return fmt.Errorf("データ書き込みエラー: %w", err)
		}
		count++
	}

	fmt.Printf("✅ %s コレクションのCSV出力が完了しました\n", collectionName)
	fmt.Printf("📄 出力ファイル: %s (%d件)\n", filename, count)

	return nil
}

func convertToString(value interface{}) string {
	if value == nil {
		return ""
	}

	switch v := value.(type) {
	case string:
		return v
	case int, int32, int64:
		return fmt.Sprintf("%d", v)
	case float32, float64:
		return fmt.Sprintf("%f", v)
	case bool:
		return strconv.FormatBool(v)
	case time.Time:
		return v.Format(time.RFC3339)
	case []interface{}, map[string]interface{}:
		jsonBytes, _ := json.Marshal(v)
		return string(jsonBytes)
	case *firestore.DocumentRef:
		return v.Path
	default:
		return fmt.Sprintf("%v", v)
	}
}

3. go.modファイルを作成

以下の内容でgo.modファイルを作成します。

module firestore-csv-export

go 1.23

require (
	cloud.google.com/go/firestore v1.14.0
	firebase.google.com/go/v4 v4.14.1
	google.golang.org/api v0.152.0
)

4. 依存関係のインストール

# 依存関係をダウンロードしてgo.sumファイルを生成
go mod tidy

このコマンドを実行すると、必要なパッケージがダウンロードされ、go.sumファイルが自動生成されます。

5. Google Cloud認証の設定

方法1: サービスアカウントキーを使用

export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json"

方法2: gcloud CLIでログイン

gcloud auth application-default login

必要な権限

  • Cloud Datastore User または
  • Firebase Admin

6. プログラムの実行

コレクション一覧を表示

go run main.go -l <プロジェクトID>

# 例
go run main.go -l my-project-id

出力例:

📋 利用可能なコレクション一覧:
----------------------------------------
1. users (150件)
2. orders (89件)
3. products (45件)
----------------------------------------
合計: 3個のコレクション

CSV出力

go run main.go <プロジェクトID> <コレクション名>

# 例
go run main.go my-project-id users

出力例:

✅ users コレクションのCSV出力が完了しました
📄 出力ファイル: users_20240915_143022.csv (150件)

出力仕様

ファイル名形式

<コレクション名>_<日時>.csv

例:users_20240915_143022.csv

データ型の変換ルール

Firestoreデータ型 CSV出力形式
string そのまま "Hello World"
number 数値 123.45
boolean true/false true
timestamp RFC3339形式 2024-09-15T14:30:22Z
array JSON配列 ["item1","item2"]
map JSONオブジェクト {"key":"value"}
reference パス文字列 projects/my-project/databases/(default)/documents/users/abc123
null 空文字 (空)

Discussion