速水拓真 営業×AI 実践シリーズ
Zenn

営業リストの名寄せを自動化する — 会社名の表記ゆれを正規化して重複を消す

初出: Zenn

リストは、増えるほど汚れる

見込み客リストを集め始めた頃は、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社あっても不思議ではありません。だから削除はせず、印を付けて人間に渡す設計にしています。名寄せの自動化とは「消す作業の自動化」ではなく「判断が必要な行を集める作業の自動化」——ここを取り違えると、消してはいけない行を消します。

運用に乗せるときの順序

いきなり全件を処理しないでください。次の順で進めると、事故なく入ります。

  1. コピーしたシートで試す。 本番シートでいきなり走らせない。
  2. addNormKeyColumn() を実行して、正規化キーが意図どおりか先頭20行を目視する。ここで正規化のルール漏れが見つかります。
  3. markDuplicates() を実行し、赤くなった行を確認する。「重複」の印が付いても、消すかどうかは別の判断です。
  4. listSimilarPairs() で前方一致の候補を出し、別シートで1件ずつ判断する。
  5. 問題がなければ本番シートに同じコードを適用する。

正規化のルールは、運用しながら育てるものです。最初から完璧を狙わず、目視で見つかったゆれを法人格のリストや記号のリストに1行足していく方が、結果的に速く安定します。

一度作れば、以後は追加時に効く

この仕組みの価値は、初回の一掃よりも以後の追加時に自動で効く点にあります。

新しくリストに会社を足したとき、正規化キーが既存行と一致すれば、その場で重複の印が付きます。「これ、前にアプローチした会社では」と気づくのが、メールを送る前になるわけです。

見込み客リストは、集める仕組みを作った時点で「集める仕事」は終わります。そのあとに残るのは、集めたものを状態を保ったまま維持する仕事です。名寄せはその維持の部分を、機械に寄せられる典型例だと思っています。

📗 営業の仕事をAIに任せろ — 見込み客リストから提案書まで、1人で回す営業自動化(Kindle・読み放題対象)

書籍の詳細と読者特典はこちらのサイトにまとめています。

営業×AI実践シリーズ 全3作

Kindle Unlimited 読み放題対象 — 追加料金なしで読めます

← 記事一覧へ

← お礼メールと提案書を同時に下書きする — AIへ…GAS×AI — スプレッドシートに届く請求書を… →