生成AIが作ったCSVは、Excelで開く前に一度、式として解釈されるセルを潰しています。目視で怪しい文字列を探す運用は、件数が増えると必ず漏れます。ここは地味に効きます。
CSVのセルが =、+、-、@ で始まると、表計算ソフトは値ではなく数式として扱うことがあります。自由記述や会社名を生成AIで整形し、そのまま営業リストやアンケート一覧にする流れでは、意図しない文字列まで一緒にExcelへ渡ります。
数式インジェクションは、開いた人の画面で起きる
生成AIに「CSVで返して」と頼むこと自体が危ないわけではありません。問題は、出力を開く工程に防御がないことです。数百行の中から先頭記号だけを探すのは現実的ではないし、先頭に空白やタブが付いた値もあります。
この記事用に5行のCSVを流したところ、=1+1、空白付きの +1、タブ付きの @cmd、-10 をすべて文字列にできました。普通のメモはそのままです。
この流れを先に固めると、目視確認が最後の防波堤になりません。
Excelへ渡す直前に一律で文字列化するPython
次のスクリプトは標準ライブラリだけで動きます。入力CSVの全セルを読み、先頭の空白・タブ・改行を除いた最初の文字が4記号のどれかなら、先頭へアポストロフィを付けます。Excelではアポストロフィは表示されず、セルを文字列として扱えます。
# sanitize_csv.py
import csv
import sys
from pathlib import Path
DANGEROUS_PREFIXES = ("=", "+", "-", "@")
def escape_cell(value: str) -> str:
probe = value.lstrip(" \t\r\n")
if probe.startswith(DANGEROUS_PREFIXES):
return "'" + value
return value
def clean_csv(source: Path, destination: Path) -> None:
with source.open("r", encoding="utf-8-sig", newline="") as src, \
destination.open("w", encoding="utf-8-sig", newline="") as dst:
reader = csv.reader(src)
writer = csv.writer(dst, lineterminator="\n")
for row in reader:
writer.writerow([escape_cell(cell) for cell in row])
if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit("使い方: python3 sanitize_csv.py 入力.csv 出力.csv")
clean_csv(Path(sys.argv[1]), Path(sys.argv[2]))
実行はこれだけです。
python3 sanitize_csv.py ai_output.csv checked.csv
検証時に checked.csv を読み直すと、次の値になりました。先頭のアポストロフィが付いた4件だけ、Excelでは文字列として開かれます。
['A社', "'=1+1"]
['B社', "' +1"]
['C社', "'\t@cmd"]
['D社', "'-10"]
['E社', '通常のメモ']
utf-8-sig を使っているのは、Excelで文字化けしにくいUTF-8のBOM付きCSVとして出すためです。引用符やカンマを含むセルも、csv モジュールに任せれば崩れません。
使う場所を間違えない
この処理は、AIの出力を人が確認したり配布したりする最終CSV向けです。たとえば金額のマイナス値をExcelで再計算したい列まで対象にすると、-10 も文字列になります。数値計算用の固定列があるなら、その列は別に出すか、自由記述の列だけにこの処理をかけます。
式を含むCSVを「たぶん大丈夫」と開かないこと。生成AIの出力に限らず、外から来た表をExcelへ渡すなら、保存前の一段を決めておくと運用が安定します。僕は、CSVを作る担当と開く担当が違う案件ほど、このスクリプトを間に置きます。