導入(問題提起)
Excelで長年運用してきた台帳や受発注、顧客リストを業務システムに取り込みたい。しかし、いざ取り込もうとすると「列が毎回変わる」「見出しが2段」「結合セルでレコードが崩れる」「同じ顧客が3通りの表記」など、移行前に“地ならし”が必要になります。ここでつまずくと、Web移行プロジェクトは簡単に遅延・品質劣化します。
本記事は、小規模〜中小規模のWebシステム開発において、Excel資産を失敗なく取り込むための現実的な手順と、Pythonで作る最小の取込・クレンジング基盤を解説します。キーワードは「受入仕様」「一時テーブル」「自動検証」「名寄せ」「再実行可能」です。
課題の詳細説明
Excel由来データの“よくある困りごと”を体系化します。
- 構造の揺れ
- データ型の不整合
- マスタ未整備
- 参照整合の欠落
- 運用課題
- 列名・順序が月ごとに変わる、見出しが2段、メモ列が途中に挟まる - 結合セルや空行、罫線ベースのレイアウトで表が複数存在
- 数値が文字列化、日付がシリアル/文字/和暦の混在、小数点やカンマ問題
- 部署名/商品名/取引先名の表記ゆれ(例:株式会社有→(株)有→有) - 郵便番号・電話・メールなどフォーマット崩れ
- 外部キーがなく“名前”で紐づけている、同名異人/異社の混在
- 取り込みのたびに人手修正が発生、ミスが埋もれる、リトライが面倒
解決方法
次の原則で“壊れにくい”移行基盤を作ります。
- 受入仕様書を作る
- 物理前処理:Excel→CSVへ正規化
- クリーニングの標準化
- 名寄せと参照整合
- 段階ロード
- ログ/検証レポート
- 技術基盤
- 列/型/必須/一意/参照元を定義。Excel提出者にも共有し、将来の揺れを抑制。
- ヘッダ1行、データ開始行を固定。結合セルは展開、空行は削除。
- 欠損補完(既定値/推定)、前後空白トリム、全角半角統一、改行除去 - 正規表現で電話/郵便番号/メール形式を検証し、正規化
- 表記ゆれ辞書(“(株)有”→“株式会社有”)を用意し、辞書にないものは“仮マスタ”に退避 - マスタ未登録は一時IDを払い出し、後段の人手確認で確定
- アップロード→一時テーブル→検証/差分表示→本番反映の順で段階適用
- 取込件数、スキップ理由、重複ID、型変換件数を記録。毎回ダウンロード可能に
- Python Webアプリでジョブ化(Pandas/SQLite→将来はPostgreSQL)。再実行可能なスクリプトにする
具体例
以下は実務で“効く”正規化・検証パターンです。
- 電話番号:
[^0-9]除去→10/11桁のみ許可→市外局番/携帯の先頭桁で判定 - 郵便番号:
\d{3}-?\d{4}に正規化→外部APIで住所補完(任意) - メール:RFC準拠の簡易検証→ドメインMX確認(任意)
- 日付:
datetime.strptimeで標準化→和暦は西暦に変換→タイムゾーン明示 - 金額:カンマ除去→数値化→負値/桁あふれチェック
- 商品名:辞書化(別表)→
JOINで標準名付与→未知語は仮マスタ
技術的な解説
最小構成で“壊れにくいETL”をPythonで組みます。UIは既存のWebシステム開発(Bottle/FastAPI)にボタンを追加するだけでOKです。
データモデル(SQLite例)
CREATE TABLE import_job (
id INTEGER PRIMARY KEY,
filename TEXT NOT NULL,
status TEXT NOT NULL, -- uploaded | validated | applied | failed
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE staging_customer (
job_id INTEGER,
row_no INTEGER,
name TEXT, phone TEXT, email TEXT, zip TEXT, address TEXT,
valid INTEGER, reason TEXT
);
CREATE TABLE customer (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT, email TEXT,
zip TEXT, address TEXT
);
取込の骨子(Pandas)
import pandas as pd
import re
def normalize_phone(s: str) -> str:
if not isinstance(s, str):
return ''
digits = re.sub(r'[^0-9]', '', s)
return digits if len(digits) in (10, 11) else ''
def normalize_zip(s: str) -> str:
if not isinstance(s, str):
return ''
m = re.match(r'^(\d{3})-?(\d{4})$', s)
return f"{m.group(1)}-{m.group(2)}" if m else ''
def validate_row(row):
reasons = []
if not row['name']:
reasons.append('name required')
if row['email'] and '@' not in row['email']:
reasons.append('email invalid')
return (len(reasons) == 0, ';'.join(reasons))
def clean_df(df: pd.DataFrame) -> pd.DataFrame:
df = df.rename(columns={c: c.strip().lower() for c in df.columns})
# 想定列を抽出(なければ空列を作る)
cols = ['name','phone','email','zip','address']
for c in cols:
if c not in df:
df[c] = ''
df['phone'] = df['phone'].map(normalize_phone)
df['zip'] = df['zip'].map(normalize_zip)
df['name'] = df['name'].fillna('')
df['email'] = df['email'].fillna('')
# 行検証
flags, reasons = [], []
for _, r in df.iterrows():
ok, why = validate_row(r)
flags.append(1 if ok else 0)
reasons.append(why)
df['valid'] = flags
df['reason'] = reasons
return df[cols + ['valid','reason']]
ルーティング(Bottle/FastAPI いずれでも)
from bottle import Bottle, request
import pandas as pd
app = Bottle()
@app.post('/import/customer')
def import_customer():
upload = request.files.get('file')
df = pd.read_excel(upload.file) # 事前にテンプレ提供が望ましい
cleaned = clean_df(df)
# stagingへ保存 → 検証レポートをUI表示 → OKなら本番へ適用
job_id = save_staging('customer', cleaned)
return {'job_id': job_id, 'valid_rate': float(cleaned['valid'].mean())}
名寄せ(表記ゆれ辞書)
CREATE TABLE name_alias (
key TEXT PRIMARY KEY, -- 正規化キー(カナ/ローマ字など)
label TEXT NOT NULL -- 正式名称
);
辞書はCSVで管理し、運用中も随時追加できるようにします。未知語は“仮マスタ”に蓄積し、後から人が正式名称を割当て、再実行で反映します。
導入の流れ
- 受入仕様テンプレを定義し、提出元に配布(列定義/型/禁止事項/サンプル)
- 一時テーブルと検証レポートUIを先に作る(本番反映は後回し)
- 代表データでPoC(100〜1000行)を実施、検証観点と名寄せ辞書を整備
- 本番適用の“条件”を明文化(有効率90%以上/致命エラー0件など)
- 定期取込の自動化(スケジュール/ジョブ/通知)。失敗時は差分を保全
- 将来のスケール:SQLite→PostgreSQL、キュー導入、非同期化
まとめ
Excelの“揺れるデータ”を前提に、受入→検証→反映の3段構えにすることで、移行は劇的に安定します。まずはPythonで最小のETLを作り、Excel Web化の延長で再実行可能な仕組みを定着させましょう。小さく始め、大きく間違えない——それが中小企業のWebシステム開発を成功させるコツです。
問い合わせ導線
Webシステム開発のご相談は monou まで → /contact