住所の正規化とは、同じ住所の書き方のばらつき(表記ゆれ)を、決まった1つの形にそろえることです。郵便番号・電話番号・ふりがなにも同じ考え方を使い、まとめてデータ正規化と呼びます。「123-4567」「〒123-4567」「1234567」を、すべて「1234567」にする作業がその例です。

寄付者のデータは、寄付決済サービス、クラウドファンディング、ネット募金、物品の寄付、自団体のフォームなど、多くの入口から届きます。入口ごとに住所や電話番号の書き方が違うため、そのままCRMに入れると、郵送物の宛名ラベルが崩れる、同じ人が別人として登録される、といったことが起きます。

この記事では、寄付で活動する非営利法人のZoho CRMで、当社が取込前の正規化ルールを設計したときの考え方をまとめます。年1回の年次報告書の郵送という期日があり、そこから逆算してルールを決めました。

なぜ取込の「前」にルールを決めるのか?

仕様を整理した時点で、寄付者データには次の3つの困りごとがありました。

  1. 郵送とメール配信がうまくいかない。 宛名ラベル用に書き出すと、郵便番号にハイフンがあるものとないものが混ざり、住所の欄も入口ごとに分かれ方が違いました
  2. 重複を見つけにくい。 同じメールアドレスでも全角と半角が混ざっていると、別の値として扱われます
  3. 運用の手間が増える。 書き方がそろっていないと、データを使うたびに確認や手直しが必要になります

どれも、データが入ってから直そうとすると手間が増えます。取り込む前に「この項目はこの形にする」と決め、取込のたびに同じ規則で直る仕組みにしておくのが近道です。

ルールを決めるときの出発点は、データを何に使うかです。この案件では、年次報告書のラベルに「氏名・郵便番号・住所・寄付の種類」が出せれば十分でした。使い道から必要な形を決め、それ以上は細かくしない、という順で考えます。

ルール1:住所は何項目で持つ?3項目にする

6つに分かれていた住所の項目を、郵便番号・都道府県・市区町村以下の3項目にまとめて持つ設計を示した図。

元のCRMでは、住所が6つの項目に分かれていました。これを次の3項目にまとめます。

項目 入れるもの 例
郵便番号 記号を除いた半角7桁 1500001
都道府県 そのまま 東京都
市区町村以下 市区町村・町名・番地・建物名をつなげたもの 渋谷区神宮前1-2-3 〇〇ビル101

3項目にした理由は2つあります。

  • 元データが分けられていない。 寄付サービスから届く住所は、「都道府県以降」が1つの欄に入っていることが多く、町名と番地、番地と建物名の境目を機械的に見分けることはできません。細かい項目を用意しても埋まらず、空欄か分け間違いが増えるだけです
  • 使い道に足りる。 宛名ラベルに必要なのは郵便番号・都道府県・それ以下の住所です

建物名を分けられないデータの扱いも論点になりました。手で事前に分ける案もありましたが、仕様では建物名も市区町村以下に含めることにしました。

市区町村以下をつなげるときの細かい決めごとは次のとおりです。

  • 空の項目は飛ばしてつなぐ
  • 番地と建物名の間は半角空白1つで区切る(当社の推奨)
  • 建物名の中にある空白はそのまま残す
  • 番地の数字は半角、区切りは半角ハイフン(-)にそろえる
  • 都道府県が空で届く取込元では、郵便番号から都道府県を補う

区切りのハイフンをそろえるとき、長音の「ー」は置き換えません。「センター」のような建物名まで「セン-タ-」になってしまうためです。置き換えるのは「−」「‐」「-」のようなハイフンに似た記号だけにします。

ルール2:郵便番号は半角7桁

  • 全角数字を半角にする
  • ハイフン・空白・「〒」などの記号を除く
  • 7桁にならなければ、元の値を残す

最後の1行が大事です。6桁や8桁のものを、桁を足したり削ったりして7桁にすると、見た目は正しくても別の地域の番号になるおそれがあります。直せないものは直さず、記録して人が確認します(後述)。

郵便番号から住所を自動で入れる仕組みは、「Zoho CRMで郵便番号から住所を自動入力する」で紹介しています。

ルール3:電話番号はハイフンなしの10桁か11桁

  • 全角数字を半角にする
  • ハイフン・空白・括弧を除く
  • 先頭の「+81」を「0」に置き換える(海外の書き方で届く場合)
  • 10桁か11桁にならなければ、元の値を残す

もともとのデータでは、市外局番が「03」のものは2桁、それ以外は3桁で区切る、という書き方が混ざっていました。区切りの位置は番号によって違うため、ハイフンは入れずに数字だけで持つほうが、比べるのも検索するのも簡単です。

ルール4:ふりがなは全角カタカナで1項目

  • 「セイ」「メイ」を全角カタカナにそろえる(半角カタカナ・ひらがなを直す)
  • セイとメイをつなげて「ふりがな」1項目にも入れる

取込元によっては半角カタカナで届き、手入力ではひらがなも混ざります。どれも全角カタカナに直します。半角の「ガ」は2文字(「カ」と濁点)なので、1文字ずつ直す前に「ガ→ガ」のように2文字の組を先に直すのがこつです。

セイとメイの間に空白を入れるかどうかは、どちらかに決めてそろえます。並べ替えや検索に使うなら空白なし、宛名で読みやすくするなら空白あり、と使い道で決めます。

ルール5:項目の種類ごとに全角・半角を決める

種類 そろえ方 対象の例
数字 半角 郵便番号・電話番号・金額・日付
カタカナ 全角 セイ・メイ・法人名のフリガナ
英数字 半角 メールアドレス・寄付サービスの会員番号
氏名の区切り 全角空白 姓と名の間
住所の空白 不要なものを除く 前後の空白など

「メールアドレスの全角を半角に直す」は地味ですが効きます。全角の「@」や「.」が混ざったアドレスは、メール配信で届かないだけでなく、重複チェックでも別の人として扱われます。

ルール6:氏名が1つの欄で届くときの分け方

取込元によっては、姓と名が1つの欄に入って届きます。この場合は次の順で分けます。

  1. 空白(半角・全角どちらでも)があれば、その位置で分ける。1つ目を姓、残りを名にする
  2. 空白がなく3文字以上なら、先頭2文字を姓、残りを名にする
  3. 2文字以下なら、全体を姓にして名は空にする

(姓と名から氏名を組み立てる逆向きの処理は「Zoho CRMで氏名を姓・名から自動入力する構成案」で扱っています)

2の規則は割り切りです。名字が1文字や3文字の人(例:「佐々木」)は正しく分かれません。仕様では、分け間違いは月1回の点検で人が直す運用にしました。宛名ラベルで姓と名を続けて印字するなら、分け間違いの実害は比較的小さいと当社は考えています。どこまでを機械に任せ、どこから人が直すかを先に決めておくと、規則を複雑にしすぎずに済みます。

ルール7:匿名・募金箱のデータは決まった名前で受ける

ネット募金などでは、寄付者が匿名を選べます。氏名の欄が空、または「匿名」「-」で届いたものは、「(取込元の名前)匿名」という決まった姓で登録し、名は空にします。取込元ごとに分けておけば、どこからの匿名寄付かが後から集計できます。

募金箱のように個人の情報がないものは、姓を「募金箱」、ふりがなを「ボキンバコ」と固定します。寄付の合計額には含めつつ、寄付者数を数える集計(認定NPO法人のパブリック・サポート・テストなど)を行う場合はその対象から外せるよう、対象外の印を付けておきます。

ルール8:どの住所をどこに持つか

同じ人でも、自宅・勤務先・郵送物の送り先と、住所がいくつもあります。この案件では次のように決めました。

  • 連絡先には、郵送物の送り先の住所だけを持つ(ラベル印刷に使う)
  • 取引先(法人)には、法人の住所を持つ
  • 寄付の記録には、寄付したときの住所を履歴として残す(寄付者名簿に使う)

送り先の住所が指定されていればそれを優先し、なければ通常の住所を送り先とします。新しい寄付データが届いたときは、既存の「情報更新日」と寄付日を比べ、寄付日のほうが新しい場合だけ連絡先の住所を書き換えます。古い寄付データを後から取り込んだときに、新しい住所が古い住所で上書きされるのを防ぐためです。

直せない値は「元のまま残して記録する」

ここまでのルールで何度か出てきたとおり、決まった形にならない値は直しません。代わりに、次の項目を持つ記録用の表(カスタムモジュール)に1行ずつ残します。

項目 内容
発生日時 処理した日時
モジュール名・データID どのデータか
エラーの種類 郵便番号・電話番号・住所・氏名の分割 など
元の値 直せなかった値
対応状況 未対応・対応中・対応済み

月に1回、「未対応」を種類ごとに見て、元データを確かめて手で直します。同じ種類が増えていれば、ルールの追加を検討します。直せないデータが目に見える形で残ることが、ルールを育てる材料になります。

正規化はDataPrepとCRMの関数のどちらで行う?

置き場所 担当する処理 理由
CRMのワークフロー+関数 郵便番号・電話・ふりがな・全角半角・住所の結合 手入力・フォーム・取込のどれから入っても同じ規則で直る
取込前の加工(Zoho DataPrep) 氏名の分け方、匿名の書き方、都道府県の補完など取込元ごとの癖 取込元によって規則が違う

CRMの関数は、データの作成時と、郵便番号・電話番号・住所・氏名の項目が変わったときに動かします。作成時に重複チェックも行うなら、正規化と同じ関数の続きで行います。別々のワークフローに分けると、どちらが先に動くかを決められず、正規化の前の値で比べてしまうおそれがあるためです。DataPrepの基本的な使い方は「Zoho DataPrepでデータをクレンジング」で紹介しています。

Delugeのコード例

連絡先の作成時・編集時にワークフローから呼ぶ関数の例です。項目のAPI名は一般的な名前に置き換えています。お使いのCRMの項目名に合わせて変えてください。関数の1行目(関数名と引数)は、関数を作る画面で引数(ここでは連絡先のIDを文字列で受け取る contactId)を設定すると入ります。ワークフローから呼ぶときは、引数に連絡先のIDを割り当てます。直せなかった値は、記録用の複数行テキスト項目(Normalize_Note)と確認用のチェックボックス(Needs_Review)に残します。

void automation.NormalizeContact(string contactId)
{
	rec = zoho.crm.getRecordById("Contacts",contactId.toLong());
	if(rec.get("id") == null)
	{
		info "連絡先が取得できませんでした: " + contactId;
		return;
	}
	upd = Map();
	notes = List();
	// ---- 変換表 ----
	zenNum = "0,1,2,3,4,5,6,7,8,9".toList(",");
	zenAlpha = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,@,.,_,+,-".toList(",");
	hanAlpha = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,@,.,_,+,-".toList(",");
	// 半角カナ→全角カナ(濁点・半濁点の2文字の組を先に並べる)
	hanKana = "ガ,ギ,グ,ゲ,ゴ,ザ,ジ,ズ,ゼ,ゾ,ダ,ヂ,ヅ,デ,ド,バ,ビ,ブ,ベ,ボ,ヴ,パ,ピ,プ,ペ,ポ,ヲ,ァ,ィ,ゥ,ェ,ォ,ャ,ュ,ョ,ッ,ー,ア,イ,ウ,エ,オ,カ,キ,ク,ケ,コ,サ,シ,ス,セ,ソ,タ,チ,ツ,テ,ト,ナ,ニ,ヌ,ネ,ノ,ハ,ヒ,フ,ヘ,ホ,マ,ミ,ム,メ,モ,ヤ,ユ,ヨ,ラ,リ,ル,レ,ロ,ワ,ン".toList(",");
	zenKana = "ガ,ギ,グ,ゲ,ゴ,ザ,ジ,ズ,ゼ,ゾ,ダ,ヂ,ヅ,デ,ド,バ,ビ,ブ,ベ,ボ,ヴ,パ,ピ,プ,ペ,ポ,ヲ,ァ,ィ,ゥ,ェ,ォ,ャ,ュ,ョ,ッ,ー,ア,イ,ウ,エ,オ,カ,キ,ク,ケ,コ,サ,シ,ス,セ,ソ,タ,チ,ツ,テ,ト,ナ,ニ,ヌ,ネ,ノ,ハ,ヒ,フ,ヘ,ホ,マ,ミ,ム,メ,モ,ヤ,ユ,ヨ,ラ,リ,ル,レ,ロ,ワ,ン".toList(",");
	hira = "ぁ,あ,ぃ,い,ぅ,う,ぇ,え,ぉ,お,か,が,き,ぎ,く,ぐ,け,げ,こ,ご,さ,ざ,し,じ,す,ず,せ,ぜ,そ,ぞ,た,だ,ち,ぢ,っ,つ,づ,て,で,と,ど,な,に,ぬ,ね,の,は,ば,ぱ,ひ,び,ぴ,ふ,ぶ,ぷ,へ,べ,ぺ,ほ,ぼ,ぽ,ま,み,む,め,も,ゃ,や,ゅ,ゆ,ょ,よ,ら,り,る,れ,ろ,ゎ,わ,を,ん".toList(",");
	kata = "ァ,ア,ィ,イ,ゥ,ウ,ェ,エ,ォ,オ,カ,ガ,キ,ギ,ク,グ,ケ,ゲ,コ,ゴ,サ,ザ,シ,ジ,ス,ズ,セ,ゼ,ソ,ゾ,タ,ダ,チ,ヂ,ッ,ツ,ヅ,テ,デ,ト,ド,ナ,ニ,ヌ,ネ,ノ,ハ,バ,パ,ヒ,ビ,ピ,フ,ブ,プ,ヘ,ベ,ペ,ホ,ボ,ポ,マ,ミ,ム,メ,モ,ャ,ヤ,ュ,ユ,ョ,ヨ,ラ,リ,ル,レ,ロ,ヮ,ワ,ヲ,ン".toList(",");
	// セイとメイの区切り。空白ありにするなら " "
	KANA_SEP = "";
	// ---- 1. 郵便番号:半角7桁。ならなければ元の値を残す ----
	zipRaw = ifnull(rec.get("Zip_Code"),"").toString().trim();
	if(zipRaw != "")
	{
		zip = zipRaw;
		for each index i in zenNum
		{
			zip = zip.replaceAll(zenNum.get(i),i.toString());
		}
		zip = zip.replaceAll("[^0-9]","");
		if(zip.length() == 7)
		{
			if(zip != zipRaw)
			{
				upd.put("Zip_Code",zip);
			}
		}
		else
		{
			notes.add("郵便番号が7桁になりません: " + zipRaw);
		}
	}
	// ---- 2. 電話番号:+81を0に、ハイフンなしの10桁か11桁 ----
	telRaw = ifnull(rec.get("Phone"),"").toString().trim();
	if(telRaw != "")
	{
		tel = telRaw;
		for each index i in zenNum
		{
			tel = tel.replaceAll(zenNum.get(i),i.toString());
		}
		tel = tel.replaceAll("+","+");
		tel = tel.replaceAll("[^0-9+]","");
		if(tel.startsWith("+810"))
		{
			// 「+81(0)3-…」の書き方
			tel = "0" + tel.subString(4,tel.length());
		}
		else if(tel.startsWith("+81"))
		{
			tel = "0" + tel.subString(3,tel.length());
		}
		tel = tel.replaceAll("[^0-9]","");
		if(tel.length() == 10 || tel.length() == 11)
		{
			if(tel != telRaw)
			{
				upd.put("Phone",tel);
			}
		}
		else
		{
			notes.add("電話番号が10桁・11桁になりません: " + telRaw);
		}
	}
	// ---- 3. メールアドレス:全角英数字・記号を半角に ----
	mailRaw = ifnull(rec.get("Email"),"").toString().trim();
	if(mailRaw != "")
	{
		mail = mailRaw;
		for each index i in zenAlpha
		{
			mail = mail.replaceAll(zenAlpha.get(i),hanAlpha.get(i));
		}
		for each index i in zenNum
		{
			mail = mail.replaceAll(zenNum.get(i),i.toString());
		}
		mail = mail.toLowerCase();
		if(mail != mailRaw)
		{
			upd.put("Email",mail);
		}
	}
	// ---- 4. ふりがな:全角カタカナにそろえ、セイ+メイを1項目に ----
	kanaFields = {"Last_Name_Kana","First_Name_Kana"};
	kanaValues = List();
	for each f in kanaFields
	{
		raw = ifnull(rec.get(f),"").toString().trim();
		v = raw;
		for each index i in hanKana
		{
			v = v.replaceAll(hanKana.get(i),zenKana.get(i));
		}
		for each index i in hira
		{
			v = v.replaceAll(hira.get(i),kata.get(i));
		}
		if(v != raw)
		{
			upd.put(f,v);
		}
		kanaValues.add(v);
	}
	furigana = kanaValues.get(0);
	if(kanaValues.get(0) != "" && kanaValues.get(1) != "")
	{
		furigana = kanaValues.get(0) + KANA_SEP + kanaValues.get(1);
	}
	else if(kanaValues.get(0) == "")
	{
		furigana = kanaValues.get(1);
	}
	// セイ・メイが両方空のときは、既存のふりがなを空で上書きしない
	if(furigana != "" && furigana != ifnull(rec.get("Furigana"),"").toString())
	{
		upd.put("Furigana",furigana);
	}
	// ---- 5. 市区町村以下:空の項目を飛ばしてつなぎ、数字・英字・ハイフンを半角に ----
	parts = {"City","Town","Banchi"};
	addr = "";
	for each p in parts
	{
		addr = addr + ifnull(rec.get(p),"").toString().trim();
	}
	// 建物名は半角空白1つで区切って後ろにつなぐ(建物名の中の空白はそのまま)
	building = ifnull(rec.get("Building"),"").toString().trim();
	if(building != "")
	{
		if(addr != "")
		{
			addr = addr + " ";
		}
		addr = addr + building;
	}
	if(addr != "")
	{
		for each index i in zenNum
		{
			addr = addr.replaceAll(zenNum.get(i),i.toString());
		}
		for each index i in zenAlpha
		{
			addr = addr.replaceAll(zenAlpha.get(i),hanAlpha.get(i));
		}
		// ハイフンに似た記号だけを置き換える(長音「ー」は残す)
		addr = addr.replaceAll("[−‐-―]","-");
		if(addr != ifnull(rec.get("Address_Below"),"").toString())
		{
			upd.put("Address_Below",addr);
		}
	}
	// ---- 6. 直せなかった値を残す ----
	if(notes.size() > 0)
	{
		upd.put("Needs_Review",true);
		upd.put("Normalize_Note",zoho.currenttime.toString("yyyy-MM-dd HH:mm") + " " + notes.toString("\n"));
	}
	// ---- 7. 変わった項目だけ更新する ----
	if(upd.size() > 0)
	{
		opt = Map();
		opt.put("trigger",List());
		res = zoho.crm.updateRecord("Contacts",contactId.toLong(),upd,opt);
		info res;
	}
}

書くときのポイントは3つです。

  • 変わった項目だけ更新します。 値が同じなら更新しないので、関数の更新がきっかけでワークフローが繰り返し動くのを防げます。更新時に trigger を空にしているのも同じ目的です
  • 直せない値は上書きしません。 notes に理由を積み、確認用の項目に残すだけにしています。記録用のカスタムモジュールに1行ずつ作る形にしてもかまいません
  • 住所の結合は、元の細かい項目が残っている間の移行用です。 新しい取込で「市区町村以下」に直接入るようになったら、5の処理は外せます

既存データを一括で直す手順

新しい取込に規則を効かせる前に、すでに入っているデータを直します。

  1. 連絡先・見込み客・取引先と、寄付・申込のカスタムモジュールを、すべてCSVで書き出して日付付きで保管する
  2. 書き出した件数と、CRM上の件数が合っているかを確かめる
  3. サンドボックス(本番と切り離した検証用の環境)で、10件ほどに一括処理を試す
  4. 直った値と、記録に残った「直せない値」を目で確かめる
  5. 本番では、モジュールごとに200件ずつ区切って処理し、区切りごとに記録を見る。おかしな結果が出たらそこで止める
  6. 最後に、無作為に選んだデータを開いて確かめる

1のバックアップを取らずに始めないことが一番大切です。正規化は元の値を書き換える処理なので、規則の誤りに後から気づいたとき、戻せる元データが必要になります。

テストケースの例

入力と期待する結果を表にしておくと、規則を変えたときに同じ表で確かめ直せます。

項目 入力 期待する結果
郵便番号 123-4567 1234567
郵便番号 〒100-0001 1000001
郵便番号 123456(6桁) 123456(元のまま・記録に残る)
電話番号 03−1234−5678 0312345678
電話番号 +81-3-9876-5432 0398765432
電話番号 090-1234(桁不足) 090-1234(元のまま・記録に残る)
メール test@example.com test@example.com
セイ・メイ ヤマダ/タロウ ヤマダ/タロウ
セイ・メイ たなか/はなこ タナカ/ハナコ
住所 渋谷区1−2−3 渋谷区1-2-3
住所 5F A−101 5F A-101
氏名(1つの欄) 山田 太郎 姓:山田/名:太郎
氏名(1つの欄) 山 姓:山/名:空

ハイフンに似た記号は「−」「‐」「-」「-」の4種類と、長音の「ー」「ー」をそれぞれ入れて、ハイフンだけが置き換わり長音が残ることも確かめます。

取込前のチェックリスト

  • データの使い道(ラベル・メール・名簿など)と、それに必要な項目を書き出した
  • 住所を何項目で持つかを決めた(元データが分けられているかを見て決める)
  • 郵便番号・電話番号の桁と、ならなかったときの扱い(元のまま残す)を決めた
  • ふりがなの文字種と、セイ・メイの区切りの有無を決めた
  • 項目の種類ごとに全角・半角を決めた
  • 氏名が1つの欄で届く取込元の分け方と、分け間違いを誰が直すかを決めた
  • 匿名・募金箱など、個人の情報がないデータの登録の仕方を決めた
  • 連絡先・取引先・寄付の記録に、どの住所を持つかを決めた
  • 直せない値を残す場所と、確認する頻度を決めた
  • 処理をCRMの関数と取込前の加工のどちらに置くかを、項目ごとに分けた
  • 一括修正の前にバックアップを取り、件数を確かめた

ルールが決まれば、次は重複の扱いを決めます。そろえた値を使ってどう重複を見つけ、どこまでを自動にするかを考える段階です。名寄せの進め方の別の例は「顧客データ統合の進め方」で紹介しています。データの表記ルールの整備は、Zoho導入支援・コンサルティングでご相談いただけます。

よくある質問

住所は都道府県・市区町村・町名・番地・建物名と細かく分けたほうが便利ではありませんか?

分けた項目が埋まるなら便利です。ただ、寄付サービスから届く住所は1つの欄にまとめて入っていることが多く、機械的には正しく分けられません。宛名ラベルに必要なのは郵便番号・都道府県・それ以下の住所なので、3項目にそろえるほうが、空欄や分け間違いが起きにくくなります。

郵便番号が7桁にならないデータは、どう扱えばよいですか?

記号や全角数字を除いても7桁にならない場合は、元の値をそのまま残します。そのうえで、どの項目がなぜ直せなかったかを記録し、担当者が元データを見て直します。桁を補ったり削ったりして自動で直すと、誤った住所に郵送物が届くおそれがあります。

ふりがなはひらがなとカタカナのどちらにそろえるべきですか?

どちらでも構いませんが、1つに決めることが大切です。この記事の例では全角カタカナにそろえ、半角カタカナやひらがなで届いたものもカタカナへ直します。

正規化はZoho DataPrepとCRMの関数のどちらで行うべきですか?

両方に役割を分けます。郵便番号・電話・ふりがなのように、どの入口から入っても同じ規則で直せるものはCRMの関数に置きます。氏名が1つの欄で届く、匿名の書き方が独特など、取込元ごとの癖は取込前の加工(DataPrep)で直します。