「先週納品したCSVを先方の担当者がExcelで開いたら、商品名が全部文字化けしていると連絡が来ました」「出荷指示のCSVで、007だった商品コードが7になっていて、別の商品が出荷される事故が起きました」——データのエクスポート機能やインポート機能を受託で作っていると、納品後にこの種の連絡が必ず飛んできます。開発側のローカルでは何の問題もなく開けていたファイルが、受け取った相手の環境では平然と壊れる。CSVは「カンマで区切るだけの単純なテキスト」だと思われがちですが、「誰かに渡すCSV」になった瞬間、文字コード・改行・エスケープ・Excelの勝手な変換が絡み合う地雷原に変わります。
やっかいなのは、この事故が開発中にはまず再現しないことです。問題は「相手が何で開くか」を制御できないところにある。本記事では、受託でエクスポート/インポート機能を作るときにCSVのどこで事故が起き、どう仕様を合意し、どこからCSVをやめるべきかを判断軸として整理します。
Excelで開くと、CSVは黙って書き換えられる
まず最大の事故源は、受け取った相手がCSVをExcelで開くことです。Excelはダブルクリックで開いたCSVの値を、こちらの意図を確認せず「賢く」変換します。これが事故の大半を生みます。
代表的なのが先頭0落ちです。007や郵便番号0150001のように先頭が0の文字列は、Excelが数値とみなして0を削ります。商品コードや電話番号、郵便番号といった「数字に見えるが実体は文字列」のデータは、これでまるごと別の値に化けます。同様に、長い数値は1.23457E+12のような指数表記に化け、1-2や2026/3/5は日付に自動変換され、=で始まる値は数式として解釈されます。
この「=で始まると数式扱い」は、見た目の問題にとどまりません。攻撃者が入力欄に=cmd|...のような文字列を仕込み、それがそのままCSVに出力されると、受け取った人がExcelで開いた瞬間に実行されかねない——CSVインジェクションです。サイボウズのバグバウンティガイドラインでも、= + - @で始まるセルが式として解釈される点が明記されており、ユーザー入力をCSVに書き出す機能では無視できないリスクです。
# 危険: ユーザーが氏名欄に入力した値をそのまま出力
name,memo
=1+1,テスト
+44-20-...,海外連絡先
# 対策: 式の起点になる文字で始まるセルの先頭に ' を付与してエスケープ
name,memo
'=1+1,テスト
'+44-20-...,海外連絡先
エクスポート機能を作る側は、「数値に見えるが文字列として扱いたい列」と「ユーザー入力が混ざる列」を洗い出し、出力時にエスケープやクォートで防御しておく必要があります。相手のExcel設定を変えてもらう運用に頼ると、必ずどこかで抜けます。
文字コードと改行 — 「自分の環境では開けた」が通用しない領域
次の事故源は文字コードです。日本語CSVで最も多いトラブルがこれで、原因はExcelの仕様に集約されます。Windows版ExcelはCSVをダブルクリックで開くとき、ファイルをShift_JIS(CP932)とみなして読み込む。そのためBOMなしUTF-8で書き出したCSVをExcelで開くと日本語が「譁・喧縺・」のように化けます。逆にBOM付きUTF-8で出力するとExcelは正しく開けますが、今度はBOMを想定しない他システムのインポート処理が先頭3バイト(EF BB BF)を読み込み、ヘッダの1列目を壊すことがあります。
つまり、「Excelで正しく開ける」と「他システムが正しく取り込める」が、文字コードのレベルで両立しないことがある。出力先がどちらなのかで正解が変わります。
| 出力エンコーディング | Excel(ダブルクリック) | 他システムの取り込み |
|---|---|---|
| Shift_JIS | 正しく開ける | UTF-8前提だと文字化け、表現できない文字で破綻 |
| UTF-8(BOMなし) | 文字化けしやすい | 正しく取り込めることが多い |
| UTF-8(BOM付き) | 正しく開ける | BOMで先頭列が壊れることがある |
改行コードも同じ構図です。RFC 4180は改行をCRLFと定めており、CRLFにしておけば多くの処理系で安全に通りますが、LFしか想定しない受け手も実在し、ここも相手次第です。
文字コードと改行は「開発者のローカルでは事故が再現しない」典型でもあります。開発機がmacOSでUTF-8の閲覧ツールを使っていれば、Shift_JIS前提のWindows Excelで何が起きるかは見えない。だからこそ、後述するサンプル先行が効きます。
RFC 4180 — カンマと改行とクォートのエスケープ
CSVには国際標準としてRFC 4180があり、エクスポート機能を作るならここは押さえたい。要点はシンプルで、フィールド内にカンマ・改行(CRLF)・ダブルクォートのいずれかを含む場合は、そのフィールド全体をダブルクォートで囲み、フィールド内のダブルクォートは2つ重ねてエスケープする、というものです。
# 壊れるCSV: 住所にカンマ、備考に改行とダブルクォートが入っている
id,address,memo
1,東京都港区1-2,3,担当は"鈴木"さん
改行も入っている
# RFC 4180 準拠: カンマ・改行・クォートを含むフィールドを "" で囲み、" は "" にエスケープ
id,address,memo
1,"東京都港区1-2,3","担当は""鈴木""さん
改行も入っている"
自前で文字列連結してCSVを組み立てると、このエスケープを忘れます。住所のカンマで列がずれ、備考欄の改行で1レコードが2行に分裂し、受け手のインポートが沈黙のうちに崩れる。CSVの出力は言語標準やライブラリのCSVライターに任せ、自前でString.join(",")しないのが鉄則です。読み込む側もsplit(",")で済ませると、クォート内のカンマで確実に破綻します。システム間でデータをどう正しく受け渡すかという観点は、CDC(変更データキャプチャ)によるシステム間連携の記事 で扱った「連携の壊れにくさ」とも地続きです。
ヌル値の表現も合意が要ります。空文字""とNULLは違うのか、NULLを\Nなどの文字列で表すのか。ここを決めずに渡すと、受け手が空欄をゼロや空文字に勝手に倒し、金額や数量がずれます。
列が増減した瞬間、受け手は静かに壊れる
CSVのもう一つの怖さは、スキーマが本文に埋め込まれていないことです。「3列目が単価」という前提は、ヘッダ行のラベルか口頭の取り決めにしか存在しません。だから、こちらが善意で列を1つ追加しただけで、列番号でパースしている受け手のシステムは静かにずれて壊れる。型が違う列に値が流し込まれても、CSVは何も警告しません。
ここを防ぐのが、ヘッダ契約とスキーマ合意です。列名・順序・型・必須/任意・文字コード・改行・エスケープ規則を、受け渡し前に文書で握る。そのうえでフォーマットを変える可能性があるなら、ファイル名やヘッダにバージョンを持たせる(export_v2_20260626.csvなど)。こうしておけば受け手は「v1なら旧パーサ、v2なら新パーサ」と切り替えられ、列追加が即事故になりません。マスタ側の項目をどう定義し型を揃えるかという根本は、業務データのマスタ設計の記事 で扱った設計思想がそのまま効いてきます。
加えて巨大CSVのメモリ問題も避けられません。数百万行を丸ごとメモリに読み込む実装は本番規模で簡単に破綻するため、出力も取り込みも1行ずつ流すストリーミング処理を前提に組むべきです。
あるアパレル卸の受託で、出荷CSVが事故になった話
具体例を挙げます。あるアパレルの中堅卸(社名は伏せます)から、基幹システムの出荷指示CSVを取引先の倉庫会社のWMS(倉庫管理システム)に取り込んでもらう連携機能を受託しました。最初の納品では基幹データをそのままUTF-8(BOMなし)で出力していました。倉庫会社の担当者が中身を確認しようとExcelで開いたところ、商品名が全部文字化けし、さらに0078-0042のようなSKUコードの先頭0が落ちて別商品のコードと衝突した。実出荷の直前に発覚し、危うく誤出荷が起きるところでした。
問題は二つ重なっていました。倉庫会社の担当者は受領後にExcelで目視確認する運用だったため、UTF-8 BOMなしでは化ける。加えてSKUの先頭0落ちで、これはExcelの自動変換が原因でした。私たちが取った打ち手は、1件分のサンプルCSVを先に渡し、相手のWMSと相手のExcelの両方で開いてもらってから本実装に入ること。サンプルの段階で、WMS側はUTF-8 BOMなしを正しく取り込めるが目視用にExcelで開くと化ける、という食い違いが見えました。
最終的には、WMS取り込み用の正本CSVはUTF-8 BOMなしで出し、SKUなどの文字列列はエスケープして0落ちを防ぎ、列名・順序・型・ヌルの扱いをヘッダ契約として一枚の仕様書に固定し、ファイル名にバージョンを持たせました。結果、以後の取引先追加でも同じ契約をベースに展開でき、文字化けと0落ちの問い合わせはゼロに。いちばん効いたのは技術的な小細工ではなく、「相手が何で開くか」をサンプルで先に潰したことでした。
CSVをやめる、という分岐を持っておく
最後に、受託で一番価値が出るのは「CSVを正しく作る」ことと同じくらい、「CSVをやめる判断」を提示できることです。CSVは人間が読めてどこでも開ける反面、型もスキーマも持たず、エスケープと文字コードの事故が構造的に避けられません。
相手が機械処理しかしないなら選択肢は広がります。1行1JSONのJSON Linesは行単位でストリーミングでき、型と入れ子を表現でき、文字コード/エスケープ地獄から解放される。分析用途や大容量なら列指向のParquetが効き、CSVと比べてファイルサイズが数分の一、クエリも桁違いに速いという比較もあります。リアルタイム性や双方向のやり取りが要るならファイルを配るのをやめてAPI連携に振る。相手が非エンジニアで少量を目で見たいだけなら、いっそGoogleスプレッドシートを共有するほうが文字化けの心配がない——Googleスプレッドシート自動化の記事 で扱った共有運用とも相性がよい領域です。
正解は相手次第です。受託では「Excelで開く人間が受け手」「機械が定期取り込みする受け手」「大量データを分析する受け手」を見分け、CSVを磨くのか別フォーマットやAPIに逃がすのかを最初に切り分けることが、後の事故を丸ごと消します。
データのエクスポート機能を作ったら先方で文字化けする、インポートが特定のファイルで黙って壊れる、取引先ごとに微妙に違うCSV仕様の保守で消耗している、そもそもCSVで渡し続けるべきか相談したい——そうしたお悩みがあれば、グリームハブのお問い合わせからご相談ください。受け手が何で開くのかを起点に、文字コードとエスケープの設計、ヘッダ契約とバージョニング、そしてCSVを続けるべきか別の連携手段に切り替えるべきかの見立てまで含めて、壊れないデータ受け渡しをご一緒に組み上げます。