「住所から郵便番号、自動で取れますよ」――軽い一言から始まった
クラウドワークス※(※企業や個人が、仕事を発注・受注できるサイト)で、私はこんな案件を見つけました。
「住所データから郵便番号を調べて、スプレッドシートに入力してほしい。約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との付き合い方”は、別記事に詳しく書いています。
白状します。私は「コードを書く」ことを、していません
ここが、今日いちばん伝えたいことかもしれません。
「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が作ったものを、鵜呑みにしないこと。
これだけは、忘れないでくださいね。
▶ あわせて読みたい

