
私が実務で手癖になっているコード8本(Python / GAS / VBA / SQL)
金曜の22時、私は同じCSVを4回開き直していた
客先の会議室、蛍光灯が半分落ちた状態だった。
やることは単純で、支店から集まった売上CSVを1本のテーブルにまとめるだけ。
ファイルは14個。
1個目は普通に開いた。
2個目で文字化けした。
3個目はカンマではなくタブ区切りだった。
4個目はヘッダーが3行目から始まっていて、上2行に「※このファイルは触らないでください」と書いてあった。
私はそこから1時間半、encoding= の引数を書き換えては実行し、書き換えては実行していた。
帰りの電車の中で思ったのは「今日の1時間半は、来月も再来月も同じ形で発生する」ということだった。
その週末に書いたのが、この記事の1本目のコードだ。
ここに出すのは、そういう夜を何度か越えた結果として、私のスニペット置き場から消えずに残っている8本になる。
きれいなコードではない。
ベストプラクティスから外れているものもある。
ただ、私は今も実案件でこれを貼り付けて動かしている。
残っているコードには、必ず「それで殴られた記憶」がくっついている
この記事の値段と、その決め方
有料部分の価格は 1,980円 (タイムセール後)にした。
理由は単純で、ここに載せた8本を自力でゼロから書き、しかも「本番で1回コケてから直す」ところまで到達するのに、私は合計で15時間以上を使っている。
文字コードの総当たり順序、GASの実行ログの持ち方、VBAの結合セルの罠、SQLの日付の穴。
どれも「書けること」ではなく「どこで壊れるかを知っていること」に時間が溶けた部分だ。
時間単価をいくらに見るかは人それぞれだが、私は15時間の踏み抜きを1,980円で渡すのは妥当な線だと考えている。
なお、これを買えば作業時間がどれだけ減る、といった保証は一切しない。
環境も業務も人によって違う。
私が渡せるのは、私の手癖と、その手癖ができた理由だけだ。
1. 文字コードと区切り文字を諦めて総当たりするCSVローダー(Python)
冒頭の金曜の夜が、これを常備するきっかけになった。
支店ごとに使っているツールが違えば、出てくるCSVの文字コードは揃わない。
経理システムから出たものは Shift_JIS 系、BIツールから出たものは UTF-8、誰かが Excel で開いて上書き保存したものは BOM 付き UTF-8。
私はここで、月に3〜4時間を毎月きれいに溶かしていた。
一般には文字コード判定に chardet や charset-normalizer を使うのが定石とされる。
私はそれを外して、総当たりの try で回している
理由は、判定ライブラリが「それらしい答え」を返してきたときに、それが正解かどうかを検証する手間が結局残るからだ。
日本語混じりの短いCSVで euc-jp と cp932 を取り違えられたことがあり、そのときの原因追跡に半日かかった。
候補を自分で並べて、上から順に「読めて、列数が足りて、置換文字が出ていない」ものを採用するほうが、私にとっては挙動が読める。
# 動作確認環境: Python 3.11 / pandas 2.2 以降(3.0 系でも確認)
# 使用ライブラリ: pandas (BSD 3-Clause License)
# 事前準備: pip install pandas
# 必要権限: 対象ファイルの読み取り権限のみ
from pathlib import Path
import pandas as pd
# 順番が命。UTF-8系を必ず先に置く
ENCODING_CANDIDATES = ("utf-8-sig", "utf-8", "cp932", "euc-jp")
SEP_CANDIDATES = (",", "\t", ";", "|")
def read_table_bruteforce(path, min_cols=2, verbose=True):
"""文字コードと区切り文字を総当たりして、最初に成立した組み合わせで読む。"""
path = Path(path)
if not path.exists():
raise FileNotFoundError(path)
tried = []
for enc in ENCODING_CANDIDATES:
for sep in SEP_CANDIDATES:
try:
df = pd.read_csv(
path,
encoding=enc,
sep=sep,
dtype=str, # 数値変換は後段でやる
keep_default_na=False, # 空欄をNaNにしない
engine="python",
)
except (UnicodeDecodeError, pd.errors.ParserError, ValueError) as e:
tried.append(f"{enc} / {sep!r} -> {type(e).__name__}")
continue
if df.shape[1] < min_cols:
tried.append(f"{enc} / {sep!r} -> 列数 {df.shape[1]} で不足")
continue
header_text = "".join(str(c) for c in df.columns)
if "�" in header_text:
tried.append(f"{enc} / {sep!r} -> 置換文字を検出")
continue
df.attrs["source_encoding"] = enc
df.attrs["source_sep"] = sep
if verbose:
print(f"[read] {path.name} enc={enc} sep={sep!r} shape={df.shape}")
return df
raise ValueError(
"どの組み合わせでも読めなかった: {}\n{}".format(path, "\n".join(tried))
)
if __name__ == "__main__":
df = read_table_bruteforce("sample.csv")
print(df.head())
print(df.attrs)ここで一番危ないのは、cp932 を候補の先頭に置いてしまうことだ。
UTF-8 のバイト列は cp932 でも例外を出さずに「読めてしまう」ことがあり、その場合は文字化けした文字列がそのまま後工程に流れる。
例外が飛ばないぶん、気づくのが翌週の集計ミーティングになる。
だから ENCODING_CANDIDATES は必ず UTF-8 系を先頭にする。
それでも化けるケースに備えて、ヘッダー文字列に置換文字 � が混ざっていないかを見ている。
私はここを必ず書き換えてから使う
min_cols を、そのファイルで期待される列数マイナス1くらいまで上げる。
デフォルトの2のままだと、区切り文字を取り違えたまま「1列だけど2列に見える」状態を拾ってしまうことがある。
あとは案件ごとに ENCODING_CANDIDATES を削る。
候補が少ないほど誤爆が減るので、その現場で出てこない文字コードは消す。