はじめに
弊社ではクラウドインフラとしてGoogle Cloudを使用する機会が多いです。その中でもCloud Run Functions(旧Cloud Functions)を使用することが多いのですが、先日WebアプリのフロントエンドからPOSTリクエストを送り、Cloud Run FunctionsでExcelファイル出力を実装したため今回はその内容を共有します。
機能のイメージ
以下のようなWebアプリ(※1)の「Excel出力」ボタンを押すと、画面上で設定したデータがHTTPリクエスト(※2)でCloud Run Functionsに送信され、「simulation.xlsx」(※3)がダウンロードされます。
なお、今回はデジタル広告のシミュレーションを想定して、予算やクリック数などをWebアプリ上でシミュレーション後、Excel出力するという前提で記事を書いております。
※2:HTTPリクエストの主な構成要素
- HTTPメソッド
- OPTIONS / POST
- HTTPリクエストボディ
- application/json
- target
- 性別
- 年代
- simulation
- 合計
- 予算やクリック数など
- 合計
- media
- media名(Metaなど)
- menu名(facebookなど)
- 予算やクリック数など
- menu名(facebookなど)
- media名(Metaなど)
- target
- application/json
HTTPリクエストボディ(サンプル)
{
"target":{
"gender":"男性",
"ages":["20代","30代"]
},"simulation":{
"all":{
"budget":1000000
}
},"media":{
"Meta広告":{
"Facebook":{
"budget":333333
}
},"Google広告":{
"GDN":{
"budget":333333
}
}
}
}
OPTIONSメソッドとは、以下の条件を持たす通信で発生するプリフライトリクエストです。
本プログラムでは、以下の2の項目を扱っているため、OPTIONSメソッドが送信されます。
- メソッドが GET / HEAD / POST 以外
- POST であっても Content-Type が application/json
- カスタムヘッダーがある(Authorization など)
プリフライトリクエストについてはこちらを参照ください。
使用環境・バージョン等
dev | version |
---|---|
Python | 3.11 |
Cloud Run Functions | 第2世代 |
Flask | 2.2.5 |
XlsxWriter | 3.2.2 |
-
Cloud Run Functions
- Googleが提供するサーバーレスのコンピューティングサービス
- HTTPトリガーで構築
本記事ではCloud Run Functionsは[未認証の呼び出しを許可する]設定としています。必要に応じて認証は設定ください。
参考 -
Flask
- 小規模向けのWebアプリケーションフレームワーク
- Cloud Run Functionsで関数をデプロイする際、自動的にインストールされる
Flaskのバージョンはrequirements.txtで明示的にしているわけではなく、こちらを参照しています。
2025年5月14日時点の情報ですので、今後変更される場合はございます。 -
XlsxWriter
- Excelファイルへの書き込みをサポートするPythonモジュール
全体構造
本記事で作成したCloud Run Functionsのフォルダ構成は以下の通りになっています。
.
├── main.py
├── config.py
├── make_excel_sheet.py
└── requirements.txt
プログラム内容
main.py
メインの処理です。
main関数がCloud Run Functionsのエントリーポイントとなります。
from flask import make_response, jsonify, send_file
import make_excel_sheet as mes
import config as cf
# CORS対応用ヘッダ追加関数
def cors_response(response):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
return response
def main(request):
# OPTIONSメソッドへの応答(CORSプリフライト)
if request.method == "OPTIONS":
return cors_response(make_response('', 204))
try:
json_data = request.get_json()
if not json_data:
return jsonify({"error": "No JSON received"}), 400
# Excelファイルを生成し、responseで保持
response = send_file(
mes.make_excel_sheet(json_data),
download_name=cf.FILE_NAME
)
return cors_response(response)
except Exception as e:
print("❌ エラー:", e)
return cors_response(jsonify({"error": str(e)}), 500)
config.py
プログラム全体で使用する定数をまとめたファイルです。
FILE_NAME = 'simulation.xlsx'
START_COL = 1
ALL_COL = 5
START_ROW = 3
TARGET_KEY = 'target'
MEDIA_KEY = 'media'
SIMULATION_KEY = 'simulation'
TITLE = 'デジタル広告シミュレーション'
ALL = '合計'
MEDIA_COL = {
'media': '媒体',
'menu': 'メニュー',
}
EXCEL_COL = {
'性別' : 'gender',
'年代' : 'ages',
'予算' : 'budget',
'インプレッション' : 'impression',
'CPM' : 'cpm',
'クリック数' : 'click',
'CTR' : 'ctr',
'CPC' : 'cpc',
}
項目名 | 説明 |
---|---|
FILE_NAME | 作成するExcelファイルの名前 |
START_COL | 先頭の列数 |
ALL_COL | 合計値を入力する際の先頭の列数 |
START_ROW | 先頭の行数 |
TARGET_KEY | 性別と年代データの抽出用文字列 |
MEDIA_KEY | 予算やインプレッションデータなどの抽出用文字列 |
SIMULATION_KEY | 合計値データの抽出用文字列 |
TITLE | タイトル用の文字列 |
ALL | 合計値入力行用の文字列 |
MEDIA_COL | カラム用の文字列(メディアとメニュー) |
EXCEL_COL | カラム用の文字列(シミュレーション算出結果) |
make_excel_sheet.py
Excelファイルを作成するプログラムをまとめたファイルです。
make_excel_sheet関数がmain関数内で使用されます。引数と戻り値は以下の通りです。
- 引数:リクエストボディから取得したデータ(JSON形式)
- 戻り値:バイナリ形式のExcelファイルデータ
import config as cf
import io
import xlsxwriter as xw
def make_excel_sheet(json_data):
# メモリ上のバッファ
output = io.BytesIO()
# Excel作成と編集
workbook = xw.Workbook(output)
worksheet = workbook.add_worksheet()
end_col = len(cf.MEDIA_COL) + len(cf.EXCEL_COL)
worksheet.set_column(cf.START_COL, end_col, 15)
# タイトルとカラム追加
row = cf.START_ROW
column = cf.START_COL
key_col = list(cf.EXCEL_COL.keys())
add_excel = [(1, 1, cf.TITLE), (row, column, cf.MEDIA_COL['media']), (row, column + 1, cf.MEDIA_COL['menu'])]
column += 2
for num in range(len(key_col)):
add_excel.append((row, column, key_col[num]))
column += 1
column = cf.START_COL
# メディアデータ整理
result_dict = {}
for ad_num in list(cf.EXCEL_COL.values()):
json_values = get_filtered_values(json_data, ad_num)
for k, v in json_values:
result_dict[k] = v
# target
target = {k: v for k, v in result_dict.items() if cf.TARGET_KEY in k}
for key, value in target.items():
if isinstance(value, list):
list_pos = key
target[list_pos]= ",".join(target[list_pos])
# media
for media_key in json_data[cf.MEDIA_KEY]:
menus = json_data[cf.MEDIA_KEY][media_key]
for menu_key in menus:
prefix = f"{cf.MEDIA_KEY}.{media_key}.{menu_key}."
menu = [v for full_key, v in result_dict.items() if full_key.startswith(prefix)]
# Excel書き込み
row += 1
for value in [media_key] + [menu_key] + list(target.values()) + menu:
add_excel.append((row, column, value))
column += 1
column = cf.START_COL
# simulation
row += 1
add_excel.append((row, cf.START_COL, cf.ALL))
all_col = cf.ALL_COL
all_menu = {k: v for k, v in result_dict.items() if cf.SIMULATION_KEY in k}
for value in all_menu.values():
add_excel.append((row, all_col, value))
all_col += 1
# Excel出力
for output_row, output_col, value in add_excel:
worksheet.write(output_row, output_col, value)
workbook.close()
output.seek(0)
return output
def get_filtered_values(d, keyword, parent_key=''):
json_values = []
for k, v in d.items():
full_key = f"{parent_key}.{k}" if parent_key else k
if isinstance(v, dict):
json_values.extend(get_filtered_values(v, keyword, full_key))
else:
if k == keyword:
json_values.append((full_key, v))
return json_values
requirements.txt
使用モジュールをまとめたファイルです。
XlsxWriter==3.2.2
詳細解説・処理の流れ
OPTIONSメソッドへの対応処理
WebアプリとCloud Run Functionsが異なるドメインにあるため、クロスドメイン通信によるCORS認証エラーが発生します。これを防ぐため、OPTIONSメソッドへのレスポンスにCORS認証用ヘッダーを付与するcors_response 関数を実行しています。
今回追加したヘッダーは以下の2つですが、必要に応じて付け足してください。
- Access-Control-Allow-Origin:リクエスト元のオリジン名
- Access-Control-Allow-Headers:リクエスト元のヘッダー
def cors_response(response):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
return response
def main(request):
# OPTIONSメソッドへの応答(CORSプリフライト)
if request.method == "OPTIONS":
return cors_response(make_response('', 204))
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ワイルドカード「*」は、オリジンに対して制限が設けられていない状態なので、必要に応じて対象ドメインをしてしてください。
CORS
CORSは、Cross Origin Resource Sharingという、異なるオリジンサーバへアクセスする際に発生するセキュリティ機能です。信頼のないオリジンサーバへの接続には、認証情報を抜き取られて悪用される危険性があるため、設定したHTTPメソッド(今回は「POST」)よりも先にCORS認証用HTTPメソッド「OPTIONS」が送信されます。もしCORS認証用ヘッダーがレスポンスに含まれていない場合、接続が認められずに切断されます。
POSTメソッドへの対応処理
リクエストに含まれるJSONデータを確認した後、make_excel_sheet.pyのmake_excel_sheet関数に渡します。
def main(request):
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
try:
json_data = request.get_json()
if not json_data:
return jsonify({"error": "No JSON received"}), 400
# Excelファイルを生成し、responseで保持
response = send_file(
mes.make_excel_sheet(json_data),
download_name=cf.FILE_NAME
)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
except Exception as e:
print("❌ エラー:", e)
return cors_response(jsonify({"error": str(e)}), 500)
Excelファイル作成(make_excel_sheet.py)
メモリ(RAM)上にExcelファイルを記録するバッファ(output変数)を作成します。
# メモリ上のバッファ
output = io.BytesIO()
xlsxwriterモジュール(xw)を用いて、Excelファイルとシートを作成します。
その後、データが出力される列の列幅(列B~K)を15に変更しています。
# Excel作成と編集
workbook = xw.Workbook(output)
worksheet = workbook.add_worksheet()
end_col = len(cf.MEDIA_COL) + len(cf.EXCEL_COL)
worksheet.set_column(cf.START_COL, end_col, 15)
add_excel変数は、Excelシートに記載する内容をまとめるためのリスト型変数です。
この変数にHTTPリクエストで受け取った情報をもとに整理したデータを入れていく処理を以下で行っています。
なお、処理の途中で使用しているget_filtered_values関数は、json_data変数の階層型構造をkeyとvalueが対になる構造として整える関数です。 再帰的に関数を呼び出すことで、valueがある階層までのパスを記録可能としました。 出力例のkeyは、valueに辿り着くまでに経由した入力値のkeyを「.」で区切り、繋ぎ合わせたものとなっています。
get_filtered_values関数
def get_filtered_values(d, keyword, parent_key=''):
json_values = []
for k, v in d.items():
full_key = f"{parent_key}.{k}" if parent_key else k
if isinstance(v, dict):
json_values.extend(get_filtered_values(v, keyword, full_key))
else:
if k == keyword:
json_values.append((full_key, v))
return json_values
- 入力値(サンプル)
{
"target":{
"gender":"男性",
"ages":["20代","30代"]
},"simulation":{
"all":{
"budget":1000000
}
},"media":{
"Meta広告":{
"Facebook":{
"budget":333333
}
},"Google広告":{
"GDN":{
"budget":333333
}
}
}
}
- 出力例(サンプル)
{
'target.gender': '男性',
'target.ages': ['20代', '30代'],
'simulation.all.budget': 1000000,
'media.Meta広告.Facebook.budget': 333333,
'media.Google広告.GDN.budget': 333333,
}
# タイトルとカラム追加
row = cf.START_ROW
column = cf.START_COL
key_col = list(cf.EXCEL_COL.keys())
add_excel = [(1, 1, cf.TITLE), (row, column, cf.MEDIA_COL['media']), (row, column + 1, cf.MEDIA_COL['menu'])]
column += 2
for num in range(len(key_col)):
add_excel.append((row, column, key_col[num]))
column += 1
column = cf.START_COL
# メディアデータ整理
result_dict = {}
for ad_num in list(cf.EXCEL_COL.values()):
json_values = get_filtered_values(json_data, ad_num)
for k, v in json_values:
result_dict[k] = v
# target
target = {k: v for k, v in result_dict.items() if cf.TARGET_KEY in k}
for key, value in target.items():
if isinstance(value, list):
list_pos = key
target[list_pos]= ",".join(target[list_pos])
# media
for media_key in json_data[cf.MEDIA_KEY]:
menus = json_data[cf.MEDIA_KEY][media_key]
for menu_key in menus:
prefix = f"{cf.MEDIA_KEY}.{media_key}.{menu_key}."
menu = [v for full_key, v in result_dict.items() if full_key.startswith(prefix)]
# Excel書き込み
row += 1
for value in [media_key] + [menu_key] + list(target.values()) + menu:
add_excel.append((row, column, value))
column += 1
column = cf.START_COL
# simulation
row += 1
add_excel.append((row, cf.START_COL, cf.ALL))
all_col = cf.ALL_COL
all_menu = {k: v for k, v in result_dict.items() if cf.SIMULATION_KEY in k}
for value in all_menu.values():
add_excel.append((row, all_col, value))
all_col += 1
add_excel変数のデータをExcelシートに書き込み、output変数ごとmain.py
に返します。
# Excel出力
for output_row, output_col, value in add_excel:
worksheet.write(output_row, output_col, value)
workbook.close()
output.seek(0)
return output
Excelファイル送信
受け取ったExcelファイルとファイル名を含んだレスポンスにCORS認証用ヘッダーを付与し、Webアプリに送信します。
def cors_response(response):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Headers"] = "Content-Type"
return response
def main(request):
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Excelファイルを生成し、responseで保持
response = send_file(
mes.make_excel_sheet(json_data),
download_name=cf.FILE_NAME
)
return cors_response(response)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
まとめ
今回は、バッファを用いたCloud Run Functions上でのExcelファイル出力についてご紹介しました。そこまで多くのAPIを開発する必要のない場合は、今回のようにFaaSで開発することも1つの手段だと思います。Google Cloudを用いて簡易的なファイル作成APIを開発したい際など、ぜひお試しください!