実装ガイド

CSVの文字化けと列ずれが止まる場所をPythonのコードで具体的に潰す手順

2026-10-10 · 最終確認 2026-10-10

この記事で直せるようになること

CSVを開くたびに起きる文字化け・列ずれ・日付表記のゆれ・重複と空欄という4つの詰まりどころは、読み込みから出力までを1本のPythonスクリプトにまとめて、想定外が来たら処理を止めて通知する構成にすることで、毎月同じ手直しを繰り返さずに済むようになります。

FIG 01 — CSVを集計に使える形にするまで
  1. 01入力基幹システムや取引先から届くCSV(Shift_JIS/UTF-8が混在)
  2. 02判定文字コードと区切りを判定して読み込む
  3. 03整形列ずれ・日付・金額・全角を同じ形に揃える
  4. 04検査列数の不一致・重複・空欄を要確認として止める● 人が確認
  5. 05出力Excel/スプレッドシートに貼れるUTF-8(BOM付き)
届いたCSVは、文字コードの判定から始めて、列ずれ・表記のゆれ・重複を順に潰す。確認は人に残す。

場当たり的に直し続けてしまうのは、どのエラーが何を意味しているかを切り分けずに、出てきたエラーメッセージをその都度ネットで調べて済ませているからです。この記事では、文字コードの判定、列ずれの検出、日付や金額の正規化、重複と空欄のフラグ立て、そして定時実行と通知までを、実際に出るエラーメッセージと一緒に順番に見ていきます。最後の章では、CSVの先にある手書き・PDF・写真からのデータ化にどこまで同じ考え方が使えるかも触れます。

文字コードの判定と読み込みで詰まる場所

文字コードの判定は確信度つきで機械的に行い、確信度が低いときだけ人が中身を見て判断する、という手順にすると迷いが減ります。

まず、ファイルの先頭部分を読んで判定します。

import chardet

with open("input.csv", "rb") as f:
    raw = f.read(100000)

result = chardet.detect(raw)
print(result)
# 例: {'encoding': 'SHIFT_JIS', 'confidence': 0.99, 'language': 'Japanese'}

confidenceが0.8を下回るときは、chardetの判定を鵜呑みにせず、先頭3バイトを見てBOMの有無を確認します。UTF-8のBOMはb"\xef\xbb\xbf"です。BOMがあるのにUTF-8として読んでしまうと、先頭の列名に不可視の記号が混ざり、1列目だけ集計で拾えないという現象が起きます。これがBOM絡みの列ずれの典型です。

読み込み時は、まずutf-8-sigで試し、失敗したらcp932にフォールバックする作りにしておくと、判定ミスがあっても処理が止まりません。

import pandas as pd

def read_csv_auto(path):
    try:
        return pd.read_csv(path, encoding="utf-8-sig")
    except UnicodeDecodeError:
        return pd.read_csv(path, encoding="cp932")

df = read_csv_auto("input.csv")

ここで実際に出るエラーを覚えておくと切り分けが早くなります。UnicodeDecodeError: 'utf-8' codec can't decode byte 0x82 in position 123: invalid start byte というメッセージが出たら、それはファイルが実際にはShift_JIS(cp932)である合図です。逆にcp932で読んで文字が化けて見える(意味の通らない漢字が並ぶ)ときは、ファイルが実はUTF-8だったという逆パターンです。Tanidaizのプログラミング備忘録・日記でも、この書き出し側と読み込み側のエンコーディングのすれ違いが文字化けの主な原因として整理されています。

最後に、整形後のデータをExcelで開けるように書き出します。Excelはutf-8(BOMなし)で開くと文字化けするため、utf-8-sigで出力します。

df.to_csv("output.csv", encoding="utf-8-sig", index=False)

freeeのヘルプセンターでも、CSVの文字化け対処として文字コードの指定し直しが案内されています(freee ヘルプセンター)。基幹システム側の出力設定を変えられない場合は、受け取った側のPythonで吸収するしかないため、上のフォールバック構成が実務的です。

列ずれと引用符で止まるCSVの直し方

列ずれは、値の中にカンマや改行が含まれている行を見落として読んでいることが原因であることが多く、該当行だけ切り分けて別ファイルに出す実装にすると処理を止めずに続けられます。

csvモジュールで読むときは、newline=""を指定し忘れるとWindows環境で改行の扱いがずれて空行が増えることがあるため、必ずこの指定をセットで書きます。

import csv

expected_cols = 8
error_rows = []
good_rows = []

with open("input.csv", encoding="utf-8-sig", newline="") as f:
    reader = csv.reader(f)
    header = next(reader)
    for i, row in enumerate(reader, start=2):
        if len(row) != expected_cols:
            error_rows.append((i, row))
        else:
            good_rows.append(row)

print(f"エラー行: {len(error_rows)}件")

行番号iはヘッダーを除いた実データの行として2行目から数えているので、エラーを報告するときはExcelで開いたときの実際の行番号と1つずれないか確認します。

pandasで読む場合は、列数が合わない行に出会うとError tokenizing data. C error: Expected 8 fields in line 15, saw 10のようなエラーで止まります。これが出たら、それは列ずれが起きている合図です。止まったまま終わらせず、該当行だけ別に回収する書き方にします。

bad_lines = []

def capture_bad_line(line):
    bad_lines.append(line)
    return None

df = pd.read_csv(
    "input.csv",
    encoding="utf-8-sig",
    engine="python",
    on_bad_lines=capture_bad_line,
)

if bad_lines:
    with open("列ずれ行.txt", "w", encoding="utf-8-sig") as f:
        for line in bad_lines:
            f.write(str(line) + "\n")

on_bad_lines="skip"とだけ書いて済ませると、ずれた行がそのまま消えて誰にも気づかれなくなるため、必ず回収して別ファイルに出すところまでをセットにします。引用符で囲まれた値の中にカンマがある場合は、quotechar='"'が正しく効いていればcsvモジュール・pandasのどちらも自動で1つの値として扱ってくれるため、ここで詰まる場合は引用符の閉じ忘れ(値の途中でダブルクォートが1つ欠けている)が元データ側にある可能性を疑います。この手の相談はスタック・オーバーフローにも同様の事例が出ています。

和暦・日付・全角数字・金額のゆれを揃える

日付と金額のゆれは、表記パターンを先に洗い出してから、パターンごとに変換ルールを書くほうが、後からの例外対応が少なくて済みます。

入力例表記の種類正規化後
令和8年10月3日和暦2026-10-03
2026/10/3スラッシュ区切り2026-10-03
202610038桁数字2026-10-03
2026/10/3全角数字+スラッシュ2026-10-03
¥1,250円マーク+カンマ区切り1250
1,250円全角数字+円表記1250

全角を半角に揃える処理を先にかけておくと、以降の正規表現がシンプルになります。

import re
import unicodedata
from datetime import date

def to_halfwidth(s):
    return unicodedata.normalize("NFKC", str(s))

def parse_japanese_date(s):
    s = to_halfwidth(s).strip()
    m = re.match(r"令和(\d+)年(\d+)月(\d+)日", s)
    if m:
        year = 2018 + int(m.group(1))  # 令和1年=2019年
        return date(year, int(m.group(2)), int(m.group(3)))
    m = re.match(r"(\d{4})[/\-](\d{1,2})[/\-](\d{1,2})", s)
    if m:
        return date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
    m = re.match(r"(\d{4})(\d{2})(\d{2})$", s)
    if m:
        return date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
    return None

どの正規表現にも一致しない値が来たら、Noneを返してその行を要確認に回すようにします。ここで無理にどれかのパターンに寄せて変換してしまうと、間違った日付のまま集計に混ざり込み、あとで気づけなくなります。

金額もNFKC正規化をかけたあとに記号を取り除きます。

def parse_amount(s):
    s = to_halfwidth(s)
    s = s.replace("¥", "").replace("円", "").replace(",", "").strip()
    if s.startswith("(") and s.endswith(")"):
        return -int(s.strip("()"))
    return int(s)

会計システムから出てくるCSVでは、マイナスの金額が(1,250)のようにカッコで表現されていることがあり、これを見落としてint()に通すとエラーで止まるか、符号が逆のまま集計されてしまいます。カッコ表記の有無は、実際に1度ファイルを開いて目視で確認してからコードに反映させます。

重複行と空欄を「要確認」として可視化する

重複と空欄は、その場で埋めたり消したりせず、専用の列でフラグを立てて別出しし、人が見る範囲をフラグの立った行だけに絞ることが要点です。

df["is_duplicate"] = df.duplicated(subset=["取引先コード", "伝票番号"], keep=False)

required_cols = ["取引先コード", "伝票番号", "金額"]
df["missing_flag"] = df[required_cols].isnull().any(axis=1)

check_df = df[df["is_duplicate"] | df["missing_flag"]]
check_df.to_excel("要確認.xlsx", index=False)

keep=Falseを指定しているのは、重複している行の片方だけでなく両方を一緒に表示するためです。keep="first"のままにすると、2件目以降だけがフラグされ、比較対象の1件目が要確認シートに出てこず、何と重複しているのか分からなくなります。

空欄は、前の行の値で埋める(forward fill)や0で埋めるといった推測をコードの中でやってしまうと、本来は入力漏れだったものが正しい値として集計に混ざります。missing_flagを立てるだけにとどめ、埋めるかどうかの判断は要確認シートを見た人に委ねます。PyQのpandasドリルでも、読み込み時のデータの欠けや化けを見つけてから対処する順番が解説されています(つとむ先生のpandasドリル)。

この手順を定時実行にして、想定外が来たら止めて通知する

ここまでの、読み込み・列ずれ検出・正規化・重複空欄チェックを1本のスクリプトにまとめ、想定外のエラーが起きたら処理を止めてLINEに通知する構成にすると、月初に手動でファイルを開く作業自体がなくなります。

私が自分の定時処理で使っているのは、Windowsのタスクスケジューラ+Pythonでスクリプトを動かし、整形後のデータはSupabaseに保存、異常時の通知はLINEに送るという組み合わせです。公開や送信が絡む処理では、最終判断は人が行う運用にしています。

import sys
import requests

LINE_ACCESS_TOKEN = "xxxxx"
LINE_USER_ID = "xxxxx"

def notify_line(message):
    requests.post(
        "https://api.line.me/v2/bot/message/push",
        headers={
            "Authorization": f"Bearer {LINE_ACCESS_TOKEN}",
            "Content-Type": "application/json",
        },
        json={
            "to": LINE_USER_ID,
            "messages": [{"type": "text", "text": message}],
        },
    )

try:
    df = read_csv_auto("input.csv")
    if len(error_rows) > 0:
        raise ValueError(f"列ずれ行が{len(error_rows)}件あります")
    # ここで正規化・重複空欄チェックを実行
except Exception as e:
    notify_line(f"CSV処理が停止しました: {e}")
    sys.exit(1)

タスクスケジューラの設定では、次の順で詰まりやすい箇所があります。

  1. タスクスケジューラを開き「基本タスクの作成」からトリガーを「毎月1日 9:00」などに設定する
  2. 「操作」で「プログラムの開始」にpython.exeのフルパスを指定し、「引数の追加」にスクリプトのパスを書く(ここでpython.exeのパスを省略するとタスクは成功と表示されるのに何も実行されない)
  3. 「開始(オプション)」にスクリプトが置いてあるフォルダのパスを指定する(これを空欄のままにすると、相対パスで読み込んでいるCSVファイルが見つからずエラーで止まる)
  4. 「条件」タブで「コンピューターをAC電源で使用している場合のみ…」のチェックを外す(ノートPCで実行する場合、ここを外し忘れると電源接続時しか動かない)
  5. 一度「実行」ボタンで手動テストし、ログまたはLINE通知が届くことを確認してから実運用に切り替える

定時処理が止まらずに回り続ける例として、私が運用している日本株の監視ボードは、毎営業日、データの収集から採点、公開までを無人で動かしています。こちらは公開までを自動化していますが、CSVの整形から請求書や報告書の送信につながるような業務では、最後の送信判断だけは人が行う形にしています。自動化する範囲と、人の目を残す範囲を分けて設計しておくことが、止めて通知する仕組みを作るときの前提になります。

手書き・PDF・写真からのデータ化まで広げる場合

CSVの整形で使った「読み取り→自動判定→要確認の可視化」という構成は、手書きの記録やPDF、写真からのデータ化にもそのまま応用できます。

私は2026年7月から、手書きの点検記録をデータ化する作業を受託で続けています。週10時間ほどの規模で、読み取りのあとに人が確認を挟む構成にしており、CSVの処理と同じく、機械による読み取りと人の確認を分けて組み立てています。

この構成を試せる形として、PDFや写真をExcelに貼れるデータに変換するAI転記ツールを公開しています。アップロードしたファイルは保存しない作りにしており、読み取った結果をそのまま集計用のExcelに貼り付けて使える形で出力します。CSVの整形で要確認フラグを立てたのと同じように、読み取り結果をそのまま信用せず、人が最後に目を通す前提で使う道具です。手元の点検記録やPDF帳票で詰まっている場合は、まずこのツールで1件試してみて、読み取り精度がどこまで実用になるかを確認するところから始められます。

同じような作業を自動にできるかどうか、無料でお返しします。いまのファイルと手順を見せてください。

お問い合わせ →