セーラームーンと明治チョコのコラボ My Map を ChatGPT とコラボして作った
作ったもの
セーラームーンと明治チョコのコラボチョコレートが販売されるローソンの店舗一覧から住所を取ってきて、 Google Map のカスタム My Map を作った。
コラボ概要は下記。
株式会社 明治×『美少女戦士セーラームーン』タイアップ第2弾 ワタシを輝かせるプリズムなひと粒
絵柄めっちゃかわいい・・・
プチバズった
BUZZ ちゃったか。
なんで作った?
公式が提供したローソン店舗一覧は下記の通り、なぜか PDF で見づらかった。
この PDF を見たとき、乾いた笑いが出た。
作り方
ChatGPT 様々。
PDF から CSV を作る
最初はコピペして Google Spreadsheet にペーストしてみたが、元ネタの PDF が変なデータの持ち方をしているらしく、きれいな表としてペーストができなかった。
そこで ChatGPT に聞いてみたところ、 pdfplumber
を使って PDF から CSV として取り出す Python のサンプルコードを書いてくれた。
import pdfplumber
import pandas as pd
pdf_path = "./lawson.pdf" # PDFファイル名を指定
csv_path = "output.csv"
# PDFを開く
with pdfplumber.open(pdf_path) as pdf:
tables = []
for page in pdf.pages:
# ページ内のテーブルを取得
table = page.extract_table()
if table:
tables.extend(table)
# データを整形
df = pd.DataFrame(tables)
# 1行目をカラム名にする(カラム名が含まれていない場合は手動で設定)
df.columns = ["都道府県", "店舗名",
"美少女戦士セーラームーンアーモンドチョコレート",
"美少女戦士セーラームーンピスタチオチョコレート",
"美少女戦士セーラームーンマカダミアチョコレート",
"美少女戦士セーラームーンヘーゼルナッツチョコレートミルク",
"美少女戦士セーラームーンアーモンドチョコレート香るカカオ"]
# `●` の有無を判定し、値が空の場合は `""` にする
df = df.fillna("")
# CSVとして保存
df.to_csv(csv_path, index=False, encoding="utf-8-sig")
print(f"CSVを {csv_path} に保存しました。")
columns だけ直したけど、コピペですぐに動いた。すごすぎる。
店舗名から住所情報をスクレイピングする
店舗名一覧が先の Python で作った CSV から取得できたので住所情報をスクレイピングでローソン公式サイトから持ってくることにした。
これも ChatGPT に聞いてみたら、すぐに使える Puppeteer
のサンプルコードを作ってくれた。
const puppeteer = require('puppeteer');
const fs = require('fs');
const keywords = [
"札幌中島パーク",
"南8条西六丁目",
"札幌川下",
// 略
"アラハビーチ前",
"てだこ浦西駅前",
"宜野湾大謝名一丁目"
]; // 検索文字列の配列
const baseURL = "https://www.e-map.ne.jp/p/lawson/";
const inputSelector = "#keyword";
const buttonSelector = "body > div > div.box-shadow > div.top-page-search.search-top-page > form > button";
const resultSelector = "body > div > div.bg-details > div > div.box-shadow > a > dl > dd > ul:nth-child(1) > li";
(async () => {
const browser = await puppeteer.launch({ headless: true }); // 開発中は headless: false で動きを見れる
const page = await browser.newPage();
let results = [];
for (const keyword of keywords) {
console.log(`Searching for: ${keyword}`);
await page.goto(baseURL, { waitUntil: 'domcontentloaded' });
// キーワードを入力
await page.type(inputSelector, keyword);
// 検索ボタンをクリック
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.click(buttonSelector)
]);
try {
// 指定の要素を取得
const text = await page.$eval(resultSelector, el => el.textContent.trim());
results.push(`${keyword},${text}`);
} catch (error) {
console.error(`Error fetching data for "${keyword}",`, error);
results.push(`${keyword},No Data Found`);
}
// **2秒待機**
console.log("Waiting for 2 seconds...");
await new Promise(resolve => setTimeout(resolve, 2000));
}
await browser.close();
// 結果をファイルに書き出し
fs.writeFileSync('result.txt', results.join("\n"), 'utf-8');
console.log("結果を result.txt に保存しました!");
})();
これもすぐに動いた。 Puppeteer は書いたことあるので同じものは作れるけど、 ChatGPT のほうがアウトプット早いのでビビった。これからエンジニアは書けるだけじゃなくて、要件を言語化する能力がより重要視されそうやね。
処理の流れとしては、だいたい下記。
- 店舗名配列を全件ループで下記処理をする
- 店舗名をローソンのフリーワード検索フォームに入力して検索する
- 検索結果のページから住所情報が書いてある要素のテキストを結果配列に保持する
- 2 秒待つ(サイトへの負荷を考慮)
- 全件ループが終わったら txt ファイルに書き出し
これをプロンプトに起こした。
対象店舗が 2000 件以上あったので、だいたいサイト表示 1 秒と仮定して、待ち 2 秒追加で発生するので 2 時間くらいゴニョゴニョしてました(パソコンが)。
ハマったこと -> 解決
検索結果ページの URL のエンコードがわからなかった。
わからなかったのですが、 kanarus さんからコメントいただいた通り、 EUC-JP のエンコードでした。 ChatGPT が Shift-JIS じゃないか?って言われてそうなんだで浅く考えててハマりました・・・アウトプットをレビューする能力は引き続き必要ですね。
試したソースコード。
const iconv = require('iconv-lite');
function eucJPEncode(text) {
// EUC-JP にエンコード
const buffer = iconv.encode(text, 'EUC-JP');
// 各バイトを "%XX" 形式に変換
return [...buffer].map(byte => `%${byte.toString(16).toUpperCase()}`).join('');
}
const testString = "札幌中島パーク";
console.log(eucJPEncode(testString)); // → %BA%CC%CB%DA%C3%E6%C5%E7%A5%D1%A1%BC%A5%AF
console.log を URL のキーワードに付与したら開けました。これができるとローソンウェブサイトへのアクセスが一つ減るので最初からあるのが理想的でしたね。
なお、 ChatGPT に聞いたら一言も言ってなかったのに実は EUC-JP じゃないかと思ってたんだよね〜みたいなことを言われて、イラッとしましたw
お前・・・
以下、古い記述。
コラボ商品取り扱いしている 札幌中島パーク
の検索結果ページの URL が下記です。
keyword の後ろの %BB%A5%CB%DA%C3%E6%C5%E7%A5%D1%A1%BC%A5%AF%A1%A1
が 札幌中島パーク
をエンコードした値っぽいのはわかったのですが、どうエンコードしてもこの結果にならず。試したのは下記。
- JavaScript 標準の encodeURI/encodeURIComponent
- querystring モジュールの escape
- iconv-lite モジュールの iconv.encode(text, 'Shift_JIS')
- encoding-japanese モジュールの Encoding.convert(text, { to: 'SJIS', type: 'array' })
全部うまく行かなかった。これどういうエンコードなんですかね?識者の方教えてください。
結果の txt から CSV にする
これは Google Spreadsheet にそのままコピペして、作成したシートを CSV としてダウンロードして終わり。後から考えたら、最初から CSV 形式で保存したらよかったな。
CSV を My Map に読み込ませる。
Google Map のサイトに行って作った CSV を My Map に読み込ませました。
最初エラーが出たので調べたところ、 CSV インポートできるのは 1 ファイルに付き 2000 行までらしく、 2 分割してます。コラボマップが 2 つのレイヤーに分かれてるのは Google Map の制限によるものです。
手作業したこと
下記作業は自動化できず手作業で 100 弱 Map にピンを手打ちする作業が発生した。これはなかなか面倒な作業だった。
ローソン公式の検索でも出てこない店舗名がある
足柄サービスエリア上り店と岡店、博多店の 3 店舗は検索できなかった。
足柄サービスエリア上り店と岡店は手作業で検索してもそもそも出てこず、 Google Map で検索した住所を Map にピン打ちしました。
博多店は 該当する店舗名が多いため入力内容を変えて再度検索してください。
と表示され、検索結果が出てきませんでした。これはローソンの店舗検索結果としてどうなんだと思いましたが、同様に Google Map で検索した住所を Map にピン打ちしてます。
真っ当な住所なのに Google Map に取り込みが失敗する
確か 90 件くらいこのエラーが出て弾かれた。(作業メモもう消しちゃった)
たとえば、 南8条西六丁目
の住所は北海道札幌市中央区南8条西6‐289で、普通に Google Map で検索すると出てくるのになぜか失敗する。
数としては多くなかったのでこれは気合のコピペして Map にピン打ちする手作業でしのぎました。スクレイピングとか Browser Use とかで自動化できそうだったけど、件数少ないのでコピペの方が早いと判断。
ローソン検索結果では住所として取り扱われているが、 Google Map では住所として取り扱われない住所がある
いや、なんで?地図システムによって住所の定義が違うんだろうか。
久居戸木
の住所は三重県津市戸木町西羽野5571‐1で、これは Google Map で検索しても出てこず。これは自動化が思いつかなかったので、ローソン検索結果の地図と Google Map を突き合わせてローソンを探して、 Map にピン打ちしました。これが一番めんどうだった。
感想
無事買えた。絵柄が良すぎる
第二弾は無事買えて本当によかった。
第一弾は瞬殺でいろいろ巡ったのに一つしか見つからず・・・まあその一つが最推しのマーズとヴィーナスのパッケージだったのでよかったんだけど。
公開してポストしてちょっとしたらプチバズった
何店舗かピックして PDF に店あるよな・・・とか、住所合ってるよな・・・とか、シークレットウィンドウで開いて誰でも見れるよな・・・とか確認して問題なさそうだと判断した。念の為、 ChatGPT に免責事項のテキスト書いてもらってそれも説明に貼った。
最初は言うてあんま伸びないな〜と思って寝て普通に X 見たらすごい数のエンゲージになってびっくりした。
種類ごとにフィルターする機能を作りたかった
店ごとに販売するチョコレートの種類が違う(= 絵柄が違うのでファンには死活問題)ので、種類ごとにフィルターできる簡易なサイトにしようかと思ったけど、間に合わなかった。 Map だけでも役に立つだろうと思って、諦めて早々にポストしてよかった。
宣伝
セーラームーンを読んでください。
Discussion
これ、EUC-JP だと思われます…
ありがとうございます! EUC-JP でエンコードしたらできました!