松屋ジェネレータジェネレータによる、よりカオスなメニューの生成
この記事は(略
松屋の思い出
学生時代、東小金井駅前の松屋ができる前から、武蔵小金井の松屋まで歩いて行ってはよく食べていたものでした。食券制に慣れてしまいうっかり宝華で無銭飲食(勘定忘れ)をやりかけたことが一度あります。ですが国分寺ではスタ丼(「すた丼」ではない)からんぷ亭に行くことがもっぱらでした。らんぷ亭のとうふ麺はいつか復活してほしいものです。
松屋ジェネレータジェネレータ
作ったのは、松屋ジェネレータtoshi-a.hatenablog.comのようなものですが、松屋のウェブページからデータを取ってきて、ジェネレータのためのデータも自動生成させています。ソースコードは以下。
部分ごとに解説します。
open('index.html'){|file| file.each_line {|line| if (line =~ %r!\A\<noscript\>\n\z!)..(line =~ %r!\A\</noscript\>\n\z!) then if m = %r!\A\<li\>\<a href\=\"\/menu\/([^/]+)\/!.match(line) then dirnames << m[1] end end } }
index.html から各メニューへのリンクの情報を取り出しています。最初は REXML で解析しようとしたのですがvalidでないということでエラーになるので諦めました。
begin Dir.mkdir 'menu' rescue Errno::EEXIST STDERR.write "directory already exist: ignored\n" end path_base = Pathname('menu') dirnames.each {|dirname| sub_path = path_base + dirname begin sub_path.mkdir rescue Errno::EEXIST STDERR.write "directory already exist: ignored\n" end }
ダウンロード先にするためのディレクトリを作っています。
dirnames.each {|dirname| ofile_path = path_base + dirname + 'index.html' url_str = "http://www.matsuyafoods.co.jp/" + ofile_path.to_s.gsub(Pathname::SEPARATOR_PAT){'/'} pid = spawn({}, ['/usr/bin/fetch', 'fetch'], '-o', ofile_path.to_s, url_str) Process.waitpid pid sleep(10+rand(10)) }
各サブメニューのHTMLを fetch で取ってきます。
menus = [] Dir.glob('menu/*/index.html').each {|path| open(path){|file| file.each_line {|line| if m = %r|\A\<h4\>(.+)\<\/h4\>\<\!\-\- [1-9][0-9]* \-\-\>\n\z|.match(line) then menus << m[1] end } } }
やはりad hocな感じでメニュー文字列を取り出します。一番長いのは「肉カレーうどん(プレミアム牛めし使用)ミニプレミアム牛めしセット」でしたが、そんなメニューが実在するのですね。
bigram = {} menus.each {|menu_str| last_char = :START menu_str.each_char {|c| bigram[last_char] ||= {} bigram[last_char][c] ||= 0 bigram[last_char][c] += 1 last_char = c } bigram[last_char] ||= {} bigram[last_char][:FIN] ||= 0 bigram[last_char][:FIN] += 1 }
全てのメニュー(文字列)について、「ある文字の次に、別のある文字が現れるのはいくつか」をカウントします。順方向のバイグラムという奴です。
本格的な自然言語処理では、ここで事前処理を頑張るわけですが、ここでは手抜きをして、生データのまま、次の生成に使ってしまいます。
rand_gen = Random.new loop { current_char = :START loop { next_hash = bigram[current_char] total = next_hash.values.inject :+ r = rand_gen.rand total keys = next_hash.keys.sort_by! {|c| if c == :FIN then Float::INFINITY else c.ord end } keys.each {|k| r -= next_hash[k] if r < 0 then current_char = k break end } break if current_char == :FIN print current_char } puts }
分析の逆で、「この文字の次に現れる文字はこれとこれとこれで、確率は...」というデータにもとづいてメニュー文字列を生成しています。実際に生成させてみると、たとえば次のような感じになります。
オリジナルカレミニ牛めしポンソージエッグW定食 おろし きつねうどんミアム牛めし 肉カルビ丼 大根お子 きつねうどん 肉使用) プレミアム牛皿<選べる小鉢> 豚汁変更 ブラス オリルチキム牛肉カレーグセット 焼鮭 お新香 とろろし 納豆(プレミニラウング定食 プレートマト とろたま オリジルチキンバーうどん きつねうどん(関西風だし)<選べる小鉢> ビ焼肉カレーグセット 生野菜100000000%使用) プレミナ豚テト みそ汁 肉使用) ビビ焼鮭 ミナ豚テト券 ミニ牛めし 焼肉定食 プラ焼定食 オリジナルチセット 大根お子 肉使用)ミアムカレンソーうどん(関西風だし 冷やっこ<選べる小鉢> ネギ付) 焼きつねうどん(プレーうどん(プレミアム牛めしセット券 とろたまト ビビビ焼肉使用)<選べる小鉢> ソーセッグW定食 ビビ焼肉カレーうどん(関西風だし) アム牛めし)ミニプレーグセー 定食 牛めしプレミアムチキムチキン酢牛めしセーグ定食 プレー
カッコの特別扱いとかをしていないので、対応していないカッコがちょっと不気味ですね。あと100000000%とかいう変なパーセントが出ています(逆に "生野菜10%使用" とかいうと残り90%はなんなんだ、という感じですね)。