Day 175|「Box → BigQuery」を実走:CSV取得→型変換→ストリーミング挿入の一本化

こんにちは、こんです🦊
100日チャレンジ2回目のWeek 11 / Day 4(通算 Day 175)です。

Day 172でスコープを定め、Day 173でBQスキーマBox OAuthを用意、Day 174で認可突破&未処理判定まで到達。今日はついに「Box→BQの実データ流し込み」を通しました。対象はまず返品CSV(ec_returns)です。仕様書の進行プランでは、まさにDay 4:BigQueryへのデータロード**に相当する工程(Advanced Service有効化→insertAll実装)になります。


🎯 今日のゴール

  • Boxの files/{file_id}/content からCSV本文を取得

  • ヘッダ正規化・型変換(日付/数値/文字)を行い、ec_returns のスキーマへ合わせる

  • BigQuery.Tabledata.insertAll によるストリーミング挿入を成功させる

  • 冪等化と失敗ハンドリング(処理済みID記録・失敗行のDLQ化)を下地だけでも用意する
    (この進め方はプロジェクト計画でのDay 3〜5の道筋に沿っています)


🔧 実装ログ

1) BigQuery Advanced Serviceを有効化

GASのサービスからBigQuery APIを追加し、BigQuery.Tabledata.insertAll が直接使える状態に。GCP側の紐付けプロジェクトも再確認しました(プロジェクト1の手順どおり)。


2) CSV本文の取得(Box API)

function fetchCsvText(fileId) {
  const svc = getBoxService_();
  if (!svc.hasAccess()) throw new Error("Box認証が必要です");
  const url = `https://api.box.com/2.0/files/${fileId}/content`;
  const res = UrlFetchApp.fetch(url, {
    headers: { Authorization: `Bearer ${svc.getAccessToken()}` },
    muteHttpExceptions: true
  });
  return res.getContentText(); // CSV生テキスト
}

Day 174で作った未処理ファイル抽出(file_idベース)とつなぎ、CSV本文を順次取得します。


3) ヘッダ正規化&型変換

Utilities.parseCsv()で2次元配列にし、見出し行をキー化。列名の揺れに備え、軽い正規化(小文字化・空白/記号の置換)を実装。
型は ec_returns のスキーマに合わせてDATE/INT64/STRINGへ変換します(Day 173で定めた最小スキーマを忠実に適用)。

function normalizeHeader(h) {
  return h.trim().toLowerCase().replace(/\s+/g,'_').replace(/[^\w]/g,'');
}

function castToSchema(rowObj) {
  return {
    return_date: Utilities.formatDate(new Date(rowObj.return_date), Session.getScriptTimeZone(), 'yyyy-MM-dd'),
    sku: String(rowObj.sku||'').trim(),
    qty: Number(rowObj.qty||0),
    reason: String(rowObj.reason||'').trim(),
    order_id: String(rowObj.order_id||'').trim(),
    customer_id: String(rowObj.customer_id||'').trim()
  };
}

4) insertAll(ストリーミング挿入)

1レコード=1 json 行で投入。エラー行は回収し、DLQ(dead-letter)シートへ退避、再送できるようメモ化。

function insertRowsToBQ(dataset, table, rows) {
  const body = {
    kind: "bigquery#tableDataInsertAllRequest",
    rows: rows.map(r => ({ json: r }))
  };
  const projectId = Session.getActiveUser().getEmail() ? 
                    PropertiesService.getScriptProperties().getProperty('GCP_PROJECT_ID') : 'YOUR_PROJECT';
  const res = BigQuery.Tabledata.insertAll(body, projectId, dataset, table);
  const errors = (res.insertErrors || []).map(e => ({ index: e.index, errors: e.errors }));
  return errors; // 空なら成功
}

5) 冪等化とログ

  • 処理済みレジストリ:PROCESSED_IDS に file_id を蓄積。既処理の再投入を遮断(Day 174の仕組みを継承)。

  • DLQ(再送口):DLQ_returns シートへ失敗行+理由を追記。次回バッチで修正→再送の入口に。

  • サマリログ:処理件数 / 失敗件数 / 処理時間をLoggerと管理シートに記録。
    この“止まらない設計”は、Box Data Weaver の運用コンセプト(毎朝まわる・人手ゼロ)に直結します。


🧪 スモーク結果(返品CSV → ec_returns)

  • 最初のテストCSV(小さめ)を投入し、全件成功(数十行)

  • 型揺れ(qtyの空文字)を2件検知 → 0→NULL扱いはやめ、0固定に暫定統一(今週はスピード優先。来週の改善バックログでNULL/0の方針を決める)

  • created_at はBQ側既定値で自動付与(監査/再取込の道を確保)

この“1本目が通る瞬間”で、Week 11のコア価値(手作業ゼロの朝更新)が見えました。


💡 作ってわかったこと

  • insertAllは“小さく速く”:1,000行程度までなら1ショット、それ以上はバッチ分割が安定。

  • 列名の揺れ耐性が効く:ヘッダ正規化+簡易マッピングで「CSVのちょい違い」に折れない。

  • 失敗の逃し道=DLQ:落ちたレコードを見える場所へ逃がすと、運用が一気に現実的。
    (これらはプロジェクト設計の「通し→自動→可視化」の流れと一致)


🔭 明日の予定(Day 176)

  • T-sort稼働CSV → tsort_logs の流し込み(型:TIMESTAMP/STRING/INT64)

  • バッチングとレート設計(API呼び出し間隔・行分割)

  • “毎朝ジョブ”の通し(Day 5の自動実行セットアップ手前まで進める)


✍️ まとめ

Day 175は、BoxのCSVを読み→型を整え→BQへストリーミング挿入の“幹”を実走。
未処理判定・冪等化・DLQまで入ったことで、止まる/詰まるのリスクに初手から備えられました。Week 11のゴール(置けば朝に溜まる)へ、確実に前進しています。


#100日チャレンジ #Week11 #Day175 #DX実験室
#BoxDataWeaver #BoxAPI #BigQuery #GoogleAppsScript #データパイプライン #業務改善

いいなと思ったら応援しよう!