Claude CodeでEC-CUBEの商品CSV取り込みエラーを直す — 列・規格・文字コードを検証
EC-CUBE 4.xの商品CSVが「フォーマットが一致しません」で弾かれる原因を、取り込みコードの検査順に沿って切り分けます。事前点検のスクリプトと、Claude Codeに直させる指示も載せます。
EC-CUBEの管理画面で商品CSVを取り込むと、「CSVのフォーマットが一致しません」とだけ出て止まることがあります。原因は、ヘッダーの列名、1行の列数、セルの値、ファイルの文字コードのどれかです。エラー文は行番号までしか教えてくれないので、数百行のCSVを目で追うのは割に合いません。
先に結論を書きます。取り込みコードの検査順をそのままスクリプトにし、アップロード前に回せば、エラー行は原因別に一覧できます。直す作業はClaude Codeにスクリプトを繰り返し実行させ、エラーが0件になるまで続けさせます。この記事は、EC-CUBE 4.0〜4.3の取り込みコードを読んで作った点検スクリプトと、その使い方をまとめたものです。
取り込みは3段階で検査され、エラーの出方が変わる
EC-CUBE 4.3の商品CSV取り込み(CsvImportControllerのcsvProduct)は、次の順に検査します。どの段階で止まったかで、見る場所が変わります。
商品CSV取り込みの検査順
- 1
ファイル全体
文字コードを判定してファイルを読み、必須の列名がヘッダーにそろっているかを見ます。足りなければ「CSVのフォーマットが一致しません」で終わります。データ行が0なら「CSVデータが存在しません」です。
- 2
1行ごとの列数
各行の列数がヘッダーの列数と違うと、「N行目のCSVフォーマットが一致しません」で処理が止まります。以降の行は検査されません。
- 3
セルの値
公開ステータス、販売種別、在庫数、価格などを1つずつ検査し、エラーを溜めていきます。商品名が空の行はその時点で処理が止まりますが、それ以外の値の問題は複数行をまとめて報告できます。
エラーが出た時点で、取り込みのトランザクションはロールバックされます。直して再アップロードすれば、同じCSVをやり直せます。
必須の列名は4つです。公開ステータス(ID)、商品名、販売種別(ID)、販売価格で、ヘッダー行に文字列として一致する必要があります。ほかの列は任意です。列順はファイル側のヘッダーで解釈されるため、並び替えそのものはエラー原因になりません。
4.0〜4.3で商品CSVの何が変わったか
バージョン違いで取り込みエラーが出るのは、旧バージョンで書き出したCSVを新しい環境に入れるときです。EC-CUBEの取り込みコードを4.0、4.1、4.2、4.3で比べると、ヘッダー定義の差は1列だけでした。
| バージョン | 列数 | 差分 |
|---|---|---|
| 4.0 / 4.1 | 列数24列 | 差分税率が最後の列 |
| 4.2 / 4.3 | 列数25列 | 差分末尾に商品規格表示フラグが増える |
商品規格表示フラグは任意列で、0:非表示 1:表示を指定し、未指定なら更新しません。つまり4.1までのCSVに列を足さなくても、列名の検査は通ります。点検スクリプトは、定義にない列があればエラー扱いで報告します。
列の増減より効いてくるのは、取り込み時の処理の変化です。
- 文字コード: 4.3は先頭1行がUTF-8として妥当でなければ、SJIS-winのストリームフィルターをかけて読みます。4.0の
CsvImportServiceにあるのは、Windows版PHP 7の環境だけ列ごとにSJIS-winからUTF-8へ変換する処理でした。 - 説明欄のHTML: 4.3は
商品説明(一覧)、商品説明(詳細)、フリーエリアをHTMLPurifierに通してから保存します。4.0のコードには、この処理がありません。 - 画面上の説明文: 4.3の取り込み画面には、
販売価格について「未指定の場合、非表示として扱います」という説明があります。ところが新規登録も更新も、コードは販売価格が空だと「設定されていません」のエラーを積みます。説明文を信じて空欄にすると弾かれます。
価格を後で決める商品を空欄で入れたいときも、0など具体値を入れる必要があります。
点検スクリプトで原因別にエラー行を出す
ここからは、取り込みコードの検査をPythonで写した点検スクリプトです。検査するのは、文字コード、ヘッダー、列数、必須項目、価格・在庫の形式、規格分類の組み合わせです。IDが実在するかはDBを見ないと分からないので、書式(数字かどうか)までを見ます。
validate_products.py の全文(Python 3、標準ライブラリのみ)
#!/usr/bin/env python3
"""EC-CUBE 4.3 の商品CSVを、取り込み前に点検する。"""
import csv, io, re, sys
from collections import Counter
# (ヘッダー名, 必須か) EC-CUBE 4.3 の定義順
COLUMNS = [
("商品ID", False), ("公開ステータス(ID)", True), ("商品名", True),
("ショップ用メモ欄", False), ("商品説明(一覧)", False),
("商品説明(詳細)", False), ("検索ワード", False), ("フリーエリア", False),
("商品削除フラグ", False), ("商品画像", False), ("商品カテゴリ(ID)", False),
("タグ(ID)", False), ("販売種別(ID)", True), ("規格分類1(ID)", False),
("規格分類2(ID)", False), ("発送日目安(ID)", False), ("商品コード", False),
("在庫数", False), ("在庫数無制限フラグ", False), ("販売制限数", False),
("通常価格", False), ("販売価格", True), ("送料", False), ("税率", False),
("商品規格表示フラグ", False),
]
NAMES = [c for c, _ in COLUMNS]
REQUIRED = [c for c, r in COLUMNS if r]
def decode(raw: bytes):
first = raw.split(b"\n", 1)[0] # EC-CUBEと同じく先頭1行で判定
try:
first.decode("utf-8")
return raw.decode("utf-8"), "UTF-8"
except UnicodeDecodeError:
return raw.decode("cp932"), "SJIS-win(cp932)"
def num_ok(s): # 0以上の数値(カンマは取り除いて判定)
return re.fullmatch(r"\d+(\.\d+)?", s.replace(",", "")) is not None
def main(path):
text, enc = decode(open(path, "rb").read())
print(f"文字コード: {enc}")
rows = list(csv.reader(io.StringIO(text, newline="")))
header, body = rows[0], rows[1:]
errs = []
missing = [h for h in REQUIRED if h not in header]
if missing:
errs.append((1, "ヘッダー", f"必須列がありません: {', '.join(missing)}"))
for h, n in Counter(header).items():
if n > 1: errs.append((1, h, "ヘッダーが重複しています"))
unknown = [h for h in header if h not in NAMES]
if unknown:
errs.append((1, "ヘッダー", f"定義にない列: {', '.join(unknown)}"))
absent = [h for h in NAMES if h not in header and h not in REQUIRED]
if absent: print(f"任意列が無い: {', '.join(absent)}")
if not missing:
for i, r in enumerate(body, start=2): # ヘッダーを1行目として数える
if len(r) != len(header):
errs.append((i, "列数", f"{len(r)}列(ヘッダーは{len(header)}列)"))
continue
d = dict(zip(header, r))
g = lambda k: d.get(k, "").strip()
if g("商品ID") and not g("商品ID").isdigit():
errs.append((i, "商品ID", "数字以外です"))
if not g("公開ステータス(ID)").isdigit():
errs.append((i, "公開ステータス(ID)", "空か数字以外です"))
if not g("商品名"): errs.append((i, "商品名", "空です"))
if not g("販売種別(ID)").isdigit():
errs.append((i, "販売種別(ID)", "空か数字以外です"))
if not g("販売価格"):
errs.append((i, "販売価格", "空です"))
elif not num_ok(g("販売価格")):
errs.append((i, "販売価格", f"0以上の数値ではありません: {g('販売価格')!r}"))
if g("通常価格") and not num_ok(g("通常価格")):
errs.append((i, "通常価格", f"数値ではありません: {g('通常価格')!r}"))
unl = g("在庫数無制限フラグ")
if unl in ("", "0"):
s = g("在庫数").replace(",", "")
if not s: errs.append((i, "在庫数", "無制限でないのに空です"))
elif not s.isdigit():
errs.append((i, "在庫数", f"0以上の整数ではありません: {g('在庫数')!r}"))
elif unl != "1":
errs.append((i, "在庫数無制限フラグ", f"0か1以外です: {unl!r}"))
c1, c2 = g("規格分類1(ID)"), g("規格分類2(ID)")
if c1 and c1 == c2:
errs.append((i, "規格分類", "分類1と分類2が同じです"))
if c2 and not c1:
errs.append((i, "規格分類2(ID)", "分類1が空なのに指定されています"))
for line, col, msg in errs:
print(f"{line}行目 [{col}] {msg}")
print(f"エラー {len(errs)} 件 / データ {len(body)} 行")
sys.exit(1 if errs else 0)
main(sys.argv[1])終了コードはエラーがあれば1、なければ0です。この性質が、後でClaude Codeに繰り返させるときの終了条件になります。
手元で試すために、Shift_JIS(cp932)で6行のCSVを作りました。最初の行は正常で、残り5行にそれぞれ別の問題を仕込んであります。実行した結果は次のとおりです(Python 3.12.2)。
python3 validate_products.py products.csv文字コード: SJIS-win(cp932)
任意列が無い: 商品規格表示フラグ
3行目 [商品名] 空です
4行目 [販売価格] 0以上の数値ではありません: '1,980円'
5行目 [在庫数] 無制限でないのに空です
6行目 [規格分類] 分類1と分類2が同じです
7行目 [公開ステータス(ID)] 空か数字以外です
エラー 5 件 / データ 6 行行番号はヘッダーを1行目として数えます。表計算ソフトの行番号と一致するので、そのまま直しに行けます。24列で作ったCSVなので、商品規格表示フラグが無い旨も出ています。これは4.2以降では任意列なので、エラーではなく情報扱いです。
販売価格の1,980円は、取り込み側のコードがカンマだけを取り除いてから0以上の数値かを判定するため、円が残って弾かれる形です。カンマ区切りの1,980は通ります。
エラー文から直し方を引く早見表
画面に出るメッセージは、messages.ja.yamlのadmin.common.csv_invalid_*にまとまっています。%name%の部分にはヘッダーの列名が入ります。
| 画面のメッセージ | 起きる条件 | 直し方 |
|---|---|---|
| CSVのフォーマットが一致しません | 起きる条件必須の4列名のどれかがヘッダーに無い、または読み込み自体に失敗 | 直し方ヘッダーの列名を日本語の定義どおりに直す。文字コードと区切り文字を確かめる |
| CSVデータが存在しません | 起きる条件ヘッダーだけでデータ行が0 | 直し方出力時に行が落ちていないか見る |
| N行目のCSVフォーマットが一致しません | 起きる条件行の列数がヘッダーと違う | 直し方セル内のカンマや改行が引用符で囲まれているか見る |
| N行目の〜が設定されていません | 起きる条件公開ステータス、販売種別、販売価格、在庫数が空 | 直し方空欄を具体値で埋める |
| N行目の〜が存在しません | 起きる条件IDが数字でない、IDに対応するマスタが無い、商品名が空 | 直し方管理画面のマスタ設定でIDを引き直す |
| N行目の〜は0以上の数値を設定してください | 起きる条件価格・在庫・販売制限数・送料・税率の書式不正 | 直し方通貨記号や全角数字を除く |
| N行目の〜と〜には同じ値を使用できません | 起きる条件規格分類1と2が同じ | 直し方分類の組み合わせを見直す |
| N行目の〜は関連するデータがあるため削除できません | 起きる条件商品削除フラグ付きの商品が、他のデータから参照されている | 直し方削除をやめ、非公開に切り替える |
「商品名が空」は「設定されていません」ではなく「存在しません」のメッセージで返ります。メッセージの文面から原因を逆引きすると、迷いやすい箇所です。
送料と税率の列には、もう一つ癖があります。4.3のコードは、送料を店舗設定の商品別送料設定が有効なときだけ、税率を商品別税率設定が有効なときだけ読みます。無効なら、列に不正な値があっても「0以上の数値を設定してください」は出ず、値は反映もされません。税率の列を直したのに商品に反映されない場合は、先に管理画面の店舗設定を確かめてください。
列数が合わない行の見つけ方
「N行目のCSVフォーマットが一致しません」が出た場合、その行から後ろは検査されません。1行直して再アップロードするたびに次の行で止まる、という繰り返しになりがちです。点検スクリプトは全行の列数を一度に見るので、この往復を避けられます。
セル内のカンマや改行を引用符で囲み損ねた行は、列数がずれます。最初のエラー行を直したら、もう一度実行して件数が減るかを確かめてください。
規格ありの商品で踏む落とし穴
落とし穴
規格分類を入れた行は在庫・価格の行が増える
規格分類1を指定した行は、取り込み側で「規格なしの行」を非表示にして、分類付きの行を作る処理に入ります。分類1と分類2を同じにした行や、分類2だけ指定した行は、この段階で弾かれます。
文字コードはUTF-8で書き出し直すのが確実
4.3は先頭1行がUTF-8として読めなければ、SJIS-winと見なして読みます。Shift_JISで保存されたCSVも、4.3では取り込める作りです。
ただし判定は先頭の1行だけです。ヘッダーがUTF-8として読めても、データ行にSJISの文字が混ざっている複合ファイルは、判定の外になります。4.0のコードにあるSJIS変換はWindows版PHP 7向けだけなので、旧版の環境に入れるCSVはUTF-8に統一して書き出す方が確実です。
Shift_JISとcp932の違いで一部の文字が欠ける問題は、取り込みに限らずPythonでも起きます。判定の考え方は、Claude CodeでShift_JISを取得して文字化けさせないにあります。
Claude Codeに「エラー0件になるまで」を任せる
点検スクリプトが終了コードを返すので、Claude Codeには「実行し、エラー行を直し、もう一度実行する」を繰り返させられます。非対話モードのclaude -pを使えば、ターミナルから1コマンドで回せます。
claude -p "products.csvをvalidate_products.pyで点検し、エラー行を直して \
products_fixed.csvに保存して。エラーが0件になるまで繰り返す。\
元のproducts.csvは書き換えない。直せない行は行番号と理由を列挙する" \
--allowedTools "Bash(python3 validate_products.py *),Read,Edit,Write"--allowedToolsに載せたツールは、確認なしで実行されます。ここでは点検スクリプトの実行だけをBashで許可し、ほかのシェルコマンドは許可していません。任意のコマンドを通すBash単体より範囲が狭く、無人実行でも事故が起きにくくなります。
プロジェクトのCLAUDE.mdに、直してよい範囲を書いておくと、再実行でも判断がぶれません。
## EC-CUBE商品CSV
- 点検は `python3 validate_products.py <csv>`。終了コード0が完了条件
- 直してよいのは書式のみ(カンマ除去、全角数字の半角化、通貨記号の除去)
- 商品名・価格・在庫・ID・規格分類の値そのものは推測で埋めない。
直せない行は行番号と理由を報告して止まる
- 元のCSVは上書きしない。修正版は別名で保存する最後の2行が大事です。空の販売価格を0で埋めると、取り込みは通っても、価格0円の商品として登録されます。エラーを消すことと正しいデータにすることは別なので、値の判断が要る行は人に戻す作りにします。
構造化された結果が欲しい場合は、--output-format jsonと--json-schemaで、直せた行・直せなかった行をJSONで受け取れます。応答のstructured_output欄に入るので、後続のスクリプトに渡しやすくなります。
点検スクリプトが見ない範囲
取り込みの挙動は、EC-CUBE 4.0〜4.3のソースコードの検査条件にもとづきます。点検スクリプトはその条件を写したもので、DBの内容(マスタIDの実在、カテゴリ・タグ・画像ファイルの存在)までは確認しません。
そのため、スクリプトが0件でも、取り込み側が「存在しません」を返すことはあります。そのときは、画面のメッセージに出る列名と行番号を、スクリプトの出力の隣に並べて原因を絞ります。マスタIDは、管理画面の設定画面の一覧から引き直せます。
次の運用にすると、再発が減ります。
- 旧環境から書き出したCSVを、新しいバージョンの点検スクリプトにかける
- 書式の問題はClaude Codeに直させ、値の判断が要る行は人が決める
- ステージングで取り込み、エラーが0件で、商品ページの表示を見比べてから本番に入れる
まとめ
EC-CUBEの商品CSVは、ヘッダーの必須4列、1行の列数、セルの値の順に検査され、エラーが1件でもあれば全体がロールバックされます。4.0から4.3までの列定義の差は商品規格表示フラグの1列で、手間がかかるのは列よりも、価格が空だと弾かれる点や、先頭1行だけで決まる文字コード判定のような実装の癖です。
点検スクリプトを終了コード付きで用意しておけば、Claude Codeの-p実行で「エラー0件」を完了条件にできます。書式の修正はClaude Codeに任せ、価格や在庫のような値の判断は人に残すのが、データを壊さない分担です。非対話実行の基本は、claude -pオプションの解説にまとめてあります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。