営業リストの名寄せを自動化する — 会社名の表記ゆれを正規化して重複を消す
リストは、増えるほど汚れる
見込み客リストを集め始めた頃は、100社が100行でした。ところが半年も経つと、同じ会社が3行に増えている。
原因は、集める経路が複数あることです。展示会で名刺を読み取り、Webフォームから落とし、ニュース記事から会社名を拾い、商談相手から紹介をもらう。同じ会社でも、経路ごとに違う字面で記録されます。
「株式会社」を書く人と書かない人、「(株)」と略す人、全角と半角が混ざった人。 これが人間の目では同じに見えても、シートの上では別人です。結果として、既にアプローチ済みの会社に初回メールを送る、という事故が起きます。
この記事では、会社名を正規化して重複を機械的に消す手順を書きます。使うのはスプレッドシートとApps Scriptだけです。
表記ゆれは3種類に分けて考える
闇雲に正規表現を足すと、正規化のルールが膨らんで保守できなくなります。まず、実際に起きるゆれを3つに分類します。
① 法人格のゆれ。 「株式会社」「(株)」「(株)」「有限会社」「合同会社」「株式会社(登記上は前株/後株)」など。同じ会社でも書き手によって変わります。
② 文字種のゆれ。 全角の英数字と半角、全角スペースと半角スペース、長音・中黒・カッコの種類。目視では気づけない段差です。
③ 付随語のゆれ。 「〜事業部」「〜製作所」「〜グループ」のような組織名が後ろに付いたり付かなかったりするもの。これは正規化では消せません。
①②は機械で潰せます。③は機械では潰せないので、候補を出すだけにして人間が判断する——ここを分けるのが設計の要点です。
正規化キーを作る
まず、比較用の文字列(正規化キー)を別列に作ります。元の会社名は絶対に上書きしません。元データを壊すと、後から「本当は何と書いてあったか」を確認できなくなるからです。
/**
* 会社名から比較用の「正規化キー」を作る
* 元の値を壊さず、隣の列に書き出す方式
*/
function makeNormKey_(raw) {
if (!raw) return '';
var s = String(raw);
// ① 文字種のゆれ: 全角英数記号 → 半角
s = s.replace(/[A-Za-z0-9]/g, function (c) {
return String.fromCharCode(c.charCodeAt(0) - 0xFEE0);
});
// ② 空白のゆれ: 全角・半角・連続をすべて除去
s = s.replace(/[\s\u3000]/g, '');
// ③ 法人格のゆれ: 表記を除去(前株・後株の両方に対応)
var legalForms = [
'株式会社', '有限会社', '合同会社', '合資会社', '合名会社',
'一般社団法人', '公益社団法人', '一般財団法人', '公益財団法人',
'\\(株\\)', '(株)', '\\(有\\)', '(有)', '\\(同\\)', '(同)'
];
legalForms.forEach(function (f) {
s = s.replace(new RegExp(f, 'g'), '');
});
// ④ 記号のゆれ: 中黒・長音・ドット・カッコを除去
s = s.replace(/[・・\.\-\u30FB\uFF0E\uFF0D]/g, '');
s = s.replace(/[()()\[\]「」『』]/g, '');
// ⑤ 大文字小文字を統一
s = s.toLowerCase();
return s.trim();
}
/**
* シート全体に正規化キー列を付与する
* A列=会社名 / B列=正規化キー(新設) / C列以降=既存データ
*/
function addNormKeyColumn() {
var sh = SpreadsheetApp.getActiveSheet();
var last = sh.getLastRow();
if (last < 2) return;
// ヘッダー
sh.getRange(1, 2).setValue('正規化キー').setFontWeight('bold');
var names = sh.getRange(2, 1, last - 1, 1).getValues();
var keys = names.map(function (r) { return [makeNormKey_(r[0])]; });
sh.getRange(2, 2, keys.length, 1).setValues(keys);
}
重複を検出して印を付ける
正規化キーが揃ったら、同じキーを持つ行を探して印を付けます。削除はしません。 印を見て人間が判断します。同じキーでも、実際には別会社(例: 「山田工業」が2社存在する)というケースは現実にあります。
/**
* 正規化キーが重複する行に印を付ける(削除はしない)
* 印: 「重複: 3行目」のように、先に現れた行番号を書く
*/
function markDuplicates() {
var sh = SpreadsheetApp.getActiveSheet();
var last = sh.getLastRow();
if (last < 2) return;
var names = sh.getRange(2, 1, last - 1, 1).getValues();
var keys = sh.getRange(2, 2, last - 1, 1).getValues();
// キー → 最初に出現した行番号
var firstSeen = {};
var flags = [];
for (var i = 0; i < keys.length; i++) {
var k = keys[i][0];
var rowNo = i + 2; // シート上の実際の行番号
if (k === '') {
flags.push(['']); // 会社名が空の行は判定対象外
continue;
}
if (firstSeen[k] === undefined) {
firstSeen[k] = rowNo;
flags.push(['']); // 初出は印なし
} else {
flags.push(['重複: ' + firstSeen[k] + '行目と同一']);
}
}
// 重複フラグ列(D列)をヘッダーごと書き込む
sh.getRange(1, 4).setValue('重複判定').setFontWeight('bold');
sh.getRange(2, 4, flags.length, 1).setValues(flags);
// 印が付いた行を赤系に塗って目立たせる
var col = sh.getRange(2, 4, flags.length, 1).getValues();
for (var j = 0; j < col.length; j++) {
var cell = sh.getRange(j + 2, 1, 1, 4);
if (col[j][0] === '' && names[j][0] === '') {
continue;
}
if (col[j][0] !== '') {
cell.setBackground('#fce8e6');
} else {
cell.setBackground(null);
}
}
}
機械で潰せないゆれは、候補として出す
「〜製作所」が付く/付かないのような③のゆれは、正規化では拾えません。そこで、前方一致で候補を提示する関数を足します。判定は人間、列挙は機械です。
/**
* 前方一致で「似ている行」を候補として列挙する
* 正規化では拾えない組織名のゆれ(例: 「山田工業」と「山田工業製作所」)を人間が判断するための材料を出す
*/
function listSimilarPairs() {
var sh = SpreadsheetApp.getActiveSheet();
var last = sh.getLastRow();
if (last < 2) return;
var keys = sh.getRange(2, 2, last - 1, 1).getValues()
.map(function (r) { return r[0]; });
var out = [];
for (var i = 0; i < keys.length; i++) {
if (!keys[i]) continue;
for (var j = i + 1; j < keys.length; j++) {
if (!keys[j]) continue;
// 短い方が長い方の先頭に含まれ、かつ長さが3文字以上違う場合のみ候補に出す
var a = keys[i], b = keys[j];
var short = a.length <= b.length ? a : b;
var long = a.length <= b.length ? b : a;
if (short.length >= 2 && long.indexOf(short) === 0 && long.length - short.length >= 3) {
out.push([i + 2, a, j + 2, b]);
}
}
}
if (out.length === 0) {
Logger.log('前方一致の候補はありませんでした');
return;
}
Logger.log('要確認の候補 ' + out.length + '件:');
out.forEach(function (r) {
Logger.log(' ' + r[0] + '行目「' + r[1] + '」 / ' + r[2] + '行目「' + r[3] + '」');
});
// 候補を別シートに書き出して、あとから確認できるようにする
var ss = SpreadsheetApp.getActiveSpreadsheet();
var name = '要確認候補';
var ws = ss.getSheetByName(name) || ss.insertSheet(name);
ws.clear();
ws.getRange(1, 1, 1, 4).setValues([['行A', '正規化キーA', '行B', '正規化キーB']])
.setFontWeight('bold');
ws.getRange(2, 1, out.length, 4).setValues(out);
}
つまずきやすい3点
実装は短いのに、運用で必ず引っかかる箇所があります。先に潰しておきます。
1つ目は、法人格を消す順序です。 全角を半角に変換する処理を先に走らせないと、「(株)」の全角カッコが半角になり、法人格リストの全角表記と一致しません。上のコードで文字種の変換を最初に置いているのはこのためです。順序を入れ替えると、後ろ株だけが消えずに残ります。
2つ目は、正規化キーが空になる行です。 会社名が空の行、法人格のみが入った行は、キーが空文字になります。これを重複判定にかけると、空欄同士が全部「重複」と判定されてしまいます。上のコードで k === '' を除外しているのはこの理由です。除外を忘れると、会社名未入力の行が大量に赤くなって、本当の重複が埋もれます。
3つ目は、同じキーでも別会社があり得るという点です。 「山田工業」という会社が日本に2社あっても不思議ではありません。だから削除はせず、印を付けて人間に渡す設計にしています。名寄せの自動化とは「消す作業の自動化」ではなく「判断が必要な行を集める作業の自動化」——ここを取り違えると、消してはいけない行を消します。
運用に乗せるときの順序
いきなり全件を処理しないでください。次の順で進めると、事故なく入ります。
- コピーしたシートで試す。 本番シートでいきなり走らせない。
addNormKeyColumn()を実行して、正規化キーが意図どおりか先頭20行を目視する。ここで正規化のルール漏れが見つかります。markDuplicates()を実行し、赤くなった行を確認する。「重複」の印が付いても、消すかどうかは別の判断です。listSimilarPairs()で前方一致の候補を出し、別シートで1件ずつ判断する。- 問題がなければ本番シートに同じコードを適用する。
正規化のルールは、運用しながら育てるものです。最初から完璧を狙わず、目視で見つかったゆれを法人格のリストや記号のリストに1行足していく方が、結果的に速く安定します。
一度作れば、以後は追加時に効く
この仕組みの価値は、初回の一掃よりも以後の追加時に自動で効く点にあります。
新しくリストに会社を足したとき、正規化キーが既存行と一致すれば、その場で重複の印が付きます。「これ、前にアプローチした会社では」と気づくのが、メールを送る前になるわけです。
見込み客リストは、集める仕組みを作った時点で「集める仕事」は終わります。そのあとに残るのは、集めたものを状態を保ったまま維持する仕事です。名寄せはその維持の部分を、機械に寄せられる典型例だと思っています。
📗 営業の仕事をAIに任せろ — 見込み客リストから提案書まで、1人で回す営業自動化(Kindle・読み放題対象)
書籍の詳細と読者特典はこちらのサイトにまとめています。