2026年8月5日

2026年8月5日

シェルスクリプトでCSVを処理する方法:文字コード・引用符・改行コードの落とし穴と対処

シェルスクリプトで CSV を処理するときは、最初に文字コード・BOM・改行コードをそろえ、値にカンマや改行が入らない単純な CSV なら awk や while read、引用符付きの CSV なら gawk --csv や Miller などの CSV 対応ツールを使う、という順で考えると失敗が減ります。この記事では、Excel から受け取った CSV や WP-CLI で書き出した CSV を例に、読み込み・集計・一括更新・書き出しの各場面でつまずく点と対処を説明します。

まず中身を確認する:文字コード・BOM・改行コード

CSV の不具合の多くは、データの中身ではなく「形式のずれ」から起きます。処理を書く前に次の3点を確認します。

file data.csv
head -c 3 data.csv | od -An -tx1    # ef bb bf なら UTF-8 の BOM 付き
head -n 2 data.csv | od -c | head   # 行末に \r があれば CRLF
  • 文字コード: 日本語版の Excel で保存した CSV は、Shift_JIS 系(CP932)であることがよくあります。そのまま UTF-8 として扱うと文字化けし、grep などで検索しても一致しません。
  • BOM: UTF-8 でも先頭に BOM が付いていると、1列目の見出しが見た目は id でも実際は別の文字列になり、列名での指定が効かなくなります。
  • 改行コード: Windows で作られたファイルは行末が CRLF です。最後の列の値に \r が残り、比較が一致しない、出力の表示が崩れる、といった症状になります。

まとめて UTF-8・BOM なし・LF に変換しておくと、以降の処理が単純になります。

iconv -f CP932 -t UTF-8 in.csv \
  | sed '1s/^\xEF\xBB\xBF//' \
  | sed 's/\r$//' > data.csv

元が UTF-8 の場合は iconv の行を外します。sed の \xEF の表記は GNU sed の例で、macOS の BSD sed では動かないことがあります。変換前のファイルは消さずに残しておき、行数(wc -l)が変わっていないことを確認してください。

単純なCSVならawkで集計できる

値にカンマ・引用符・改行が含まれないと分かっている CSV なら、awk -F, で十分です。次の例は、2列目(カテゴリ)ごとに3列目(金額)を合計します。

awk -F, 'NR > 1 { sum[$2] += $3 } END { for (k in sum) printf "%s,%d\n", k, sum[k] }' sales.csv

NR > 1 で見出し行を飛ばしています。見出しを残したまま並べ替えたいときは、見出しと本体を分けて処理します。

{ head -n 1 sales.csv; tail -n +2 sales.csv | LC_ALL=C sort -t, -k3,3nr; } > sorted.csv

LC_ALL=C はロケールによる並び順の違いをなくし、処理も速くします。日本語の並び順が必要な場合は外してください。

引用符付きのCSVは対応ツールで読む

"東京都, 千代田区" のように値の中にカンマがあると、-F, では列がずれます。正規表現で何とかしようとするより、CSV の規則を理解するツールを使う方が安全です。

GNU awk 5.3 以降には CSV モードがあり、引用符の中のカンマや改行、"" によるエスケープを正しく扱います。

gawk --csv 'NR > 1 { print $1 "\t" $3 }' data.csv

gawk --version で版を確認してください(2026年10月現在、古い LTS では 5.3 未満の場合があります)。CSV モードでは区切り文字を変更できないなどの制約があります。

より多機能なのが Miller(コマンド名 mlr)です。列名で指定でき、CSV・TSV・JSON を相互に変換できます。

mlr --icsv --opprint head -n 5 posts.csv                      # 表形式で先頭を確認
mlr --icsv --ocsv filter '$post_status == "publish"' then cut -f ID,post_title posts.csv
mlr --icsv --ocsv count-distinct -f post_author posts.csv     # 投稿者ごとの件数
mlr --icsv --ojson cat posts.csv | jq '.[0]'                   # JSON にして jq へ

Python 製の csvkit(csvcut・csvgrep・csvstat など)も同様の用途に使えます。大きなファイルでは Miller や awk の方が速い傾向があります。

while readで1行ずつ処理するときの注意

CSV の各行をもとに WP-CLI などを呼び出す場合は while read を使いますが、落とし穴がいくつかあります。次は、post_id,meta_key,meta_value の CSV でカスタムフィールドを一括更新するスクリプトです。

#!/usr/bin/env bash
set -euo pipefail
csv="${1:?usage: $0 file.csv}"
dry_run="${DRY_RUN:-1}"

line_no=1
while IFS=, read -r -u 3 post_id meta_key meta_value || [[ -n "${post_id:-}" ]]; do
  line_no=$((line_no + 1))
  meta_value="${meta_value%$'\r'}"
  if [[ ! "$post_id" =~ ^[0-9]+$ ]]; then
    echo "line ${line_no}: invalid post_id '${post_id}'" >&2
    continue
  fi
  if [[ "$dry_run" == "1" ]]; then
    printf 'DRY-RUN: wp post meta update %s %q %q\n' "$post_id" "$meta_key" "$meta_value"
  else
    wp post meta update "$post_id" "$meta_key" "$meta_value"
  fi
done 3< <(tail -n +2 "$csv")

このスクリプトで対策している点は次のとおりです。

  • **-r**: バックスラッシュをエスケープとして解釈させない。付けないと C:\path のような値が壊れます。
  • **|| [[ -n ... ]]**: ファイル末尾に改行がないと、最後の1行が read の失敗扱いになり処理されません。その行も拾うための書き方です。
  • **-u 3 と 3<**: ループの中で呼んだコマンドが標準入力を読むと、CSV の残りの行を食べてしまい、途中でループが終わります。CSV を別のファイル記述子から読むことでこれを防ぎます。
  • **${meta_value%$'\r'}**: 変換し忘れた CRLF の \r を最後の列から取り除きます。
  • 入力の検証と DRY_RUN: ID が数字でない行はとばし、既定では実行せずに内容を表示するだけにしています。

IFS=, による分割は引用符を理解しないため、値にカンマを含む可能性があるなら、先に mlr --icsv --otsv cat data.csv で TSV に変換し、IFS=$'\t' で読む方が安全です。また、最後の変数(ここでは meta_value)には残りの列がすべて入るので、列数が想定より多い行がないかも事前に確認します。

本番で DRY_RUN=0 にする前に、wp db export でデータベースのバックアップを取り、ステージング環境で数行だけのファイルを使って結果を確かめてください。

CSVを書き出すときの注意

WordPress のデータを CSV にするなら、WP-CLI の --format=csv が使えます。

wp post list --post_type=post --fields=ID,post_title,post_date --format=csv > posts.csv

自分で CSV を組み立てる場合は、値に含まれるカンマや引用符をエスケープする必要があります。printf '%s,%s\n' で並べるだけでは、タイトルにカンマが入った時点で列が崩れます。JSON から作るなら jq の @csv に任せるのが簡単です。

jq -r '.[] | [.id, .title.rendered] | @csv' posts.json > posts.csv

Excel で開くことが前提なら、さらに2点を考えます。

  • 文字コード: UTF-8 の CSV は、BOM がないと日本語が文字化けして表示される場合があります。先頭に BOM(printf '\xEF\xBB\xBF')を付けるか、受け取る側の環境に合わせて CP932 に変換します。
  • CSV インジェクション: =・+・-・@ で始まる値は、表計算ソフトで数式として解釈されることがあります。ユーザーが入力した値(問い合わせ内容や名前など)を書き出す場合は、先頭に ' を付けるなどの無害化を検討してください。

氏名やメールアドレスを含む CSV は個人情報です。作業後は不要になったファイルを削除し、公開ディレクトリには置かないようにします。

まとめ

  • 処理の前に文字コード・BOM・改行コードを確認し、UTF-8・BOM なし・LF にそろえる。
  • 単純な CSV は awk -F,、引用符付きの CSV は gawk --csv や Miller で読む。
  • while read では -r、末尾行の取りこぼし対策、別のファイル記述子からの読み込み、\r の除去を押さえる。
  • 一括更新は DRY_RUN と入力検証を入れ、バックアップとステージングでの確認を先に行う。
  • 書き出しは --format=csv や jq の @csv に任せ、Excel 向けには BOM と数式の無害化を考える。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲