【実録】非エンジニアの私が、コードを1行も書かずに22万件の住所→郵便番号を自動取得した話

自動化(Excel・スプレッドシート)

「住所から郵便番号、自動で取れますよ」――軽い一言から始まった

クラウドワークス※(※企業や個人が、仕事を発注・受注できるサイト)で、私はこんな案件を見つけました。

「住所データから郵便番号を調べて、スプレッドシートに入力してほしい。約280件です。」

よくある「データ入力」のお仕事です。

でも、経理として日々スプレッドシートと格闘している私の頭に、ある考えがよぎりました。

「これ、手入力じゃなくて……自動で取れるんじゃないか?」

そこで私は、応募メッセージにこう書き添えてみたんです。

「郵便局が郵便番号と住所のデータを公開しているので、ネット検索ではなく、そのデータを使って自動で取得できます。今後、同じような作業が出たときの自動化もご相談に乗れます」と。

結論から言うと、280件の入力作業そのものは、納期の都合で他の方に決まりました。

ところが――。

その一言がきっかけで、クライアントから別の相談が舞い込んだんです。

「データ入力ではなく、自動化の方をお願いできませんか? まずは試しに、この3万件のファイルでできるか見てもらえますか?」

280件のはずが、いきなり3万件。

正直、ビビりました。

でも、ここで引き下がる私ではありません(いや、本当はちょっと引きました)。

「やってみます」と答えて、私の”AI×自動化”への挑戦が始まりました。


この案件、何がそんなに大変なのか

やること自体は、シンプルに見えます。

「住所」を見て、対応する「郵便番号」を返す。それだけ。

実は日本郵便は、全国の住所と郵便番号の対応表を「KEN_ALL.csv」※(※郵便番号データの公式ファイル。誰でも無料でダウンロードできます)という形で公開しています。

だから理屈の上では、

手元の住所と、この対応表を照らし合わせれば、郵便番号が分かる

はずなんです。

ところが、ここに**「日本語の住所」という、とんでもない伏兵**が潜んでいました(これは後ほど、たっぷりお話しします)。

ちなみに今回扱ったのは、厚生労働省が公開している介護サービスの情報――いわゆるオープンデータ※(※国や自治体が、誰でも使えるように公開しているデータ)でした。

個人情報ではないので、安心して取り組めたのは救いでした。


なぜ私は「Claude」ではなく「Gemini」を選んだのか

このブログを読んでくださっている方はご存じの通り、私のメインAIはClaudeです。

それなのに、今回の自動化では、最初からGeminiを相棒に選びました。

理由は、過去の苦い経験です。

以前、GAS※(※Google Apps Script。スプレッドシートなどを自動で動かすためのプログラム)をスマホでも実行できるようにしたくて、Claudeに相談したことがありました。

ところが、Claudeの提案はGASの仕様に合っておらず、どうやっても動かない。

試しに同じ質問をGeminiに投げてみたら――一発で正解でした。

この一件以来、私は、

「GAS絡みの相談は、Geminiが強い」

と、用途によってAIを使い分けるようにしています。

「推しのAIだから、何でもそれでいく」のではなく、得意分野で選ぶ。

このあたりの”AIとの付き合い方”は、別記事に詳しく書いています。

5児の父・経理マンの私が、AIで絶対にやらないこと3選


白状します。私は「コードを書く」ことを、していません

ここが、今日いちばん伝えたいことかもしれません。

「GASでプログラムを組んだ」と言うと、何やらすごいことをやったように聞こえます。

でも、正直に白状します。

私は、コードを1行も自分では書いていません。

私がやったのは、たったこれだけ。

① Geminiに「こういう処理がしたい」と、日本語で伝える ② Geminiが書いたコードを、スプレッドシートに貼り付けて実行する ③ 出てきた結果を見て、「ここがおかしい」とGeminiに伝える ④ ①〜③を、納得いくまで繰り返す

そう、私の役割は「プログラマー」ではなく、**「テスト係 兼 ダメ出し係」**だったんです。

正直な気持ちを言えば――「めんどくさい」

何度も結果を確認しては、ダメ出しして、を延々と繰り返すわけですから。

でも、それ以上に強く思ったのは、**「思っていたより、はるかに楽だ」**ということ。

だって、一番難しい「コードを書く」部分は、全部Geminiがやってくれるんですから。

3年前、コーディングに挫折して逃げ出した私が、です。


最大の山場「表記ゆれ地獄」

とはいえ、すべてが順風満帆だったわけではありません。

最大の敵は、**日本語の住所の”表記ゆれ”**でした。

例えば、同じ場所を指しているのに、書き方がこんなに違うんです。

・「14条」と「十四条」(アラビア数字と漢数字) ・「茅ヶ崎」と「茅ケ崎」(大きい”ヶ”と、小さい”ケ”) ・「大字○○」の”大字”が、ついていたり、いなかったり

人間なら「どっちも同じでしょ」と分かります。

でもプログラムは、1文字でも違うと「別の住所」と判断して、郵便番号を見つけられない。

そこで私は、Geminiに「アラビア数字を漢数字に変換して」「”大字”は無視して」と、ひとつずつ”ルール”を追加してもらいました。

中には、見た目はまったく同じなのに、文字コード※(※コンピュータ内部での、文字の管理番号)が違う”そっくりさん漢字”まで現れて、これには本当に手を焼きました。

いちばん「うなった」バグの話

この表記ゆれ対応で、私がもっとも勉強になった出来事があります。

「Aという表記ゆれ」を直してもらい、次に「Bという表記ゆれ」を見つけて修正を依頼した、そのとき――。

Bは直ったのに、さっき直したはずのAが、元に戻っていたんです。

「ちゃんと直したのに、なんで?」

頭を抱えました。

原因を探ってわかったのは、こういうことでした。

Geminiは、新しい修正を頼むたびに、プログラム全体をゼロから書き直していた

その結果、前回の修正が、こっそり消えてしまっていたんです。

解決策は、拍子抜けするほどシンプルでした。

お願いの仕方を、こう変えただけ。

「これまでの修正はすべて保持したまま、新しい修正を追加してください」

この一文を添えた瞬間、ピタッと問題が止まりました。

AIは、何も言わないと”さっきの自分”を忘れてしまうことがある。

これは、AIと一緒に仕事をするうえで、絶対に覚えておきたい教訓でした。


実際に動いたコードも、置いておきます

「で、結局どんなコードなの?」という方のために――。

包み隠さず、が私のブログの方針です。

Geminiが記述したコードを「折りたたみ」で置いておきます。

「コードはちょっと……」という方は、読み飛ばして大丈夫です。

function matchZipCodes() {

  var ss = SpreadsheetApp.getActiveSpreadsheet();

  var targetSheet = ss.getSheetByName(“施設一覧”);

  var masterSheet = ss.getSheetByName(“マスタ”);

  if (targetSheet === null || masterSheet === null) {

    SpreadsheetApp.getUi().alert(“エラー:シート名が正しくありません。「施設一覧」と「マスタ」というシートが存在するか確認してください。”);

    return;

  }

  // — アラビア数字を漢数字に変換(100以上は無視) —

  function toKanji(str) {

    var num = parseInt(str, 10);

    if (isNaN(num)) return str;

    if (num >= 100) return str;

    var kanji = [“”, “一”, “二”, “三”, “四”, “五”, “六”, “七”, “八”, “九”];

    if (num === 10) return “十”;

    if (num < 10) return kanji[num];

    var tens = Math.floor(num / 10);

    var ones = num % 10;

    var res = “”;

    if (tens === 1) res += “十”;

    else if (tens > 1) res += kanji[tens] + “十”;

    res += kanji[ones];

    return res;

  }

  // — 究極の正規化:入力ミスや異体字をすべて吸収 —

  function normalizeText(addr) {

    if (!addr) return “”;

    addr = String(addr).normalize(“NFKC”);

    // 先頭・末尾の不要な記号、先頭の謎の数字を削除

    addr = addr.replace(/^[-ー-_]+/g, “”);

    addr = addr.replace(/[-ー-_]+$/g, “”);

    addr = addr.replace(/^\d+(都道府県|北海道|(青森|岩手|宮城|秋田|山形|福島|茨城|栃木|群馬|埼玉|千葉|東京|神奈川|新潟|富山|石川|福井|山梨|長野|岐阜|静岡|愛知|三重|滋賀|京都|大阪|兵庫|奈良|和歌山|鳥取|島根|岡山|広島|山口|徳島|香川|愛媛|高知|福岡|佐賀|長崎|熊本|大分|宮崎|鹿児島|沖縄)県)/, “$1”);

    // 【修正】異体字やよくある漢字の間違いを統一(蘂、檜を追加)

    var weirdChars = {

      “⽟”:”玉”, “⼤”:”大”, “⾼”:”高”, “⿐”:”鼻”, “⽬”:”目”,

      “⼭”:”山”, “田”:”田”, “⽔”:”水”, “⽊”:”木”, “⾦”:”金”,

      “⼟”:”土”, “⽉”:”月”, “⽇”:”日”, “⽯”:”石”, “⽴”:”立”,

      “⼝”:”口”, “⼿”:”手”, “⼼”:”心”, “⾞”:”車”, “⾨”:”門”, “⻑”:”長”,

      “冨”:”富”, “礪”:”砺”, “諌”:”諫”, “﨑”:”崎”, “烏”:”鳥”, “靜”:”静”,

      “蘂”:”蕊”, “檜”:”桧” // ← 新規追加

    };

    addr = addr.replace(/[⽟⼤⾼⿐⽬⼭⽥⽔⽊⾦⼟⽉⽇⽯⽴⼝⼿⼼⾞⾨⻑冨礪諌﨑烏靜蘂檜]/g, function(m) {

      return weirdChars[m] || m;

    });

    addr = addr.replace(/甚平衛/g, “甚兵衛”);

    addr = addr.replace(/群馬県足利市/g, “栃木県足利市”);

    addr = addr.replace(/[\s ]+/g, “”);

    addr = addr.replace(/^知県/, “愛知県”);

    addr = addr.replace(/県[a-zA-Z]/g, “県”);

    // 【修正】表記揺れの統一(「が」を追加、「条通」を「条」に)

    addr = addr.replace(/の/g, “ノ”);

    addr = addr.replace(/[ヶヵが]/g, function(m) { return m === “ヵ” ? “カ” : “ケ”; }); // ← 「が」も「ケ」に強制統一

    addr = addr.replace(/条通/g, “条”); // ← 「条通」の「通」を無視する

    addr = addr.replace(/大字/g, “”).replace(/字/g, “”);

    addr = addr.replace(/第(\d+)/g, function(match, p1) {

      return “第” + toKanji(p1);

    });

    addr = addr.replace(/([0-9]+)(-|ー|‐|-)/, function(match, p1) {

      return toKanji(p1) + “丁目”;

    });

    addr = addr.replace(/(\d+)(条|丁目|線|番町|割)/g, function(match, p1, p2) {

      return toKanji(p1) + p2;

    });

    return addr;

  }

  // — マスタデータのカッコ書き(丁目)を自動展開 —

  function expandTowns(town) {

    var match = town.match(/^(.*?)((.*?))$/);

    if (!match) return [town];

    var base = match[1];

    var content = match[2];

    var results = [base];

    if (content.match(/次|一円|その他|地階/)) return results;

    content = String(content).normalize(“NFKC”).replace(/丁目/g, “”);

    var parts = content.split(/、|,/);

    for (var i = 0; i < parts.length; i++) {

      var part = parts[i];

      if (part.indexOf(“〜”) !== -1 || part.indexOf(“-“) !== -1 || part.indexOf(“~”) !== -1) {

        var range = part.split(/〜|-|~/);

        if (range.length === 2) {

          var start = parseInt(range[0], 10);

          var end = parseInt(range[1], 10);

          if (!isNaN(start) && !isNaN(end) && start <= end) {

            for (var j = start; j <= end; j++) results.push(base + toKanji(j.toString()) + “丁目”);

          }

        }

      } else {

        var num = parseInt(part, 10);

        if (!isNaN(num)) results.push(base + toKanji(num.toString()) + “丁目”);

      }

    }

    return results;

  }

  // 1. 辞書の作成

  var masterData = masterSheet.getDataRange().getValues();

  var zipMap = {};

  for (var i = 0; i < masterData.length; i++) {

    var zip = String(masterData[i][2]);

    var pref = String(masterData[i][6]);

    var city = String(masterData[i][7]);

    var rawTown = String(masterData[i][8]);

    var town = rawTown;

    if (town === “以下に掲載がない場合”) town = “”;

    var expandedTowns = expandTowns(town);

    var cityPrefixes = [pref + city, city];

    if (city.indexOf(“郡”) !== -1) {

      var cityNoGun = city.split(“郡”)[1];

      cityPrefixes.push(pref + cityNoGun, cityNoGun);

    }

    if (city.indexOf(“市”) !== -1 && city.indexOf(“区”) !== -1) {

      var cityNoCity = city.split(“市”)[1];

      cityPrefixes.push(pref + cityNoCity, cityNoCity);

    }

    var mapVal = [zip, pref, city, rawTown];

    for (var t = 0; t < expandedTowns.length; t++) {

      var eTown = expandedTowns[t];

      for (var p = 0; p < cityPrefixes.length; p++) {

        var key = normalizeText(cityPrefixes[p] + eTown);

        if (key && !zipMap[key]) zipMap[key] = mapVal;

        if (eTown.slice(-1) === “町”) {

          var keyMachi = normalizeText(cityPrefixes[p] + eTown.slice(0, -1));

          if (keyMachi && !zipMap[keyMachi]) zipMap[keyMachi] = mapVal;

        }

      }

      var keyTownOnly = normalizeText(eTown);

      if (keyTownOnly && !zipMap[keyTownOnly]) zipMap[keyTownOnly] = mapVal;

      if (eTown.slice(-1) === “町”) {

        var keyTownMachiOnly = normalizeText(eTown.slice(0, -1));

        if (keyTownMachiOnly && !zipMap[keyTownMachiOnly]) zipMap[keyTownMachiOnly] = mapVal;

      }

    }

    var cityKey1 = normalizeText(pref + city);

    var cityKey2 = normalizeText(city);

    var cityKey3 = “”;

    if (city.indexOf(“区”) !== -1) {

      var parts = city.split(“市”);

      if(parts.length > 1) cityKey3 = normalizeText(parts[1]);

    } else if (city.indexOf(“郡”) !== -1) {

      var parts = city.split(“郡”);

      if(parts.length > 1) cityKey3 = normalizeText(parts[1]);

    }

    if (!zipMap[cityKey1]) zipMap[cityKey1] = mapVal;

    if (!zipMap[cityKey2]) zipMap[cityKey2] = mapVal;

    if (cityKey3 && !zipMap[cityKey3]) zipMap[cityKey3] = mapVal;

  }

  // 2. 検索と書き出し

  var lastRow = targetSheet.getLastRow();

  if (lastRow < 2) return;

  var targetData = targetSheet.getRange(2, 4, lastRow – 1, 6).getValues();

  var outputValues = [];

  for (var j = 0; j < targetData.length; j++) {

    var rawPref = String(targetData[j][0] || “”);

    var rawCity = String(targetData[j][1] || “”);

    var rawAddress = String(targetData[j][5] || “”);

    if (rawAddress === “”) {

      outputValues.push([“”, “未合致: (元データが空欄)”, “”, “”, “”]);

      continue;

    }

    var normalizedAddress = normalizeText(rawAddress);

    var combinedAddress = normalizeText(rawPref + rawCity + rawAddress);

    var foundZip = “”;

    var usedAddress = “”;

    var outPref = “”;

    var outCity = “”;

    var outTown = “”;

    function searchZip(searchTarget) {

      for (var len = searchTarget.length; len > 0; len–) {

        var searchStr = searchTarget.substring(0, len);

        if (zipMap[searchStr]) {

          var val = zipMap[searchStr];

          var cleanZip = val[0].replace(/-/g, “”);

          while (cleanZip.length < 7) {

            cleanZip = “0” + cleanZip;

          }

          var formattedZip = cleanZip.substring(0, 3) + “-” + cleanZip.substring(3);

          return { zip: formattedZip, pref: val[1], city: val[2], town: val[3], matched: searchStr };

        }

      }

      return null;

    }

    var result = searchZip(normalizedAddress);

    if (result) {

      foundZip = result.zip;

      usedAddress = result.matched;

      outPref = result.pref;

      outCity = result.city;

      outTown = result.town;

    } else {

      var combinedResult = searchZip(combinedAddress);

      if (combinedResult) {

        foundZip = combinedResult.zip;

        usedAddress = combinedResult.matched;

        outPref = combinedResult.pref;

        outCity = combinedResult.city;

        outTown = combinedResult.town;

      }

    }

    if (foundZip === “”) {

      usedAddress = “未合致: ” + normalizedAddress;

    }

    outputValues.push([foundZip, usedAddress, outPref, outCity, outTown]);

  }

  targetSheet.getRange(2, 15, outputValues.length, 5).setValues(outputValues);

  SpreadsheetApp.getUi().alert(“処理が完了しました!O列からの出力を確認してください。”);

}

ひとつだけ、お願いがあります。

このコードをコピーして使う場合も、出てきた結果は必ず自分の目で検算してください。

AIの作るものは、決して完璧ではありません(これも、痛いほど経験しました)。


そして、3万件は22万件になった

最初にクライアントから渡されたのは、テスト用の「3万件のファイル」でした。

これが問題なく動いたので、残りのファイルもまとめて処理することに。

実は今回の厚労省のオープンデータ、20個近いファイルに分かれていたんです。

そこで、それらをGASで1つのスプレッドシートに統合。

最終的に、扱うデータは合計22万件にまで膨らみました。

ここまで来ると、もう手作業では、何日かけても終わりません。

「AIに任せて正解だった」と、しみじみ実感した瞬間でした。


でも、この話には”続き”があります

ここまで読んで、こう思った方もいるかもしれません。

「非エンジニアなのに、22万件の自動化を完成させたなんて、すごいじゃないか」と。

……ありがとうございます。素直に嬉しいです。

でも、最後に正直に言わせてください。

この案件、私は1円も受け取れませんでした。

動くものは、ちゃんと完成した。

クライアントにも「OKです」と言ってもらえた。

なのに、報酬は1円も入ってこなかったんです。

「作れること」と「稼げること」は、まったくの別物だった――。

その痛恨の顛末は、別記事に包み隠さず書きました。

同じ失敗をする人が、一人でも減るように。

【失敗談】22万件のデータ自動化を完成させたのに、1円も稼げなかった話


まとめ:非エンジニアでも、AIでここまでできる

今回お伝えしたかったことは、シンプルです。

コードが書けなくても、AIと”対話”できれば、業務は自動化できる。

私がやったのは、日本語でお願いして、結果を確認して、ダメ出しを繰り返しただけ。

特別なスキルは、何ひとつ要りませんでした。

もしあなたが「プログラミングなんて、自分には無理」と思い込んでいるなら、ぜひ一度、AIに”お願い”してみてください。

「あ、自分にもできるかも」

そう思える瞬間が、きっとあるはずです。

ただし――最後にひとつだけ。

AIが作ったものを、鵜呑みにしないこと

これだけは、忘れないでくださいね。


▶ あわせて読みたい

タイトルとURLをコピーしました