読者です 読者をやめる 読者になる 読者になる

松屋ジェネレータジェネレータによる、よりカオスなメニューの生成

この記事は(略

松屋の思い出

学生時代、東小金井駅前の松屋ができる前から、武蔵小金井松屋まで歩いて行ってはよく食べていたものでした。食券制に慣れてしまいうっかり宝華で無銭飲食(勘定忘れ)をやりかけたことが一度あります。ですが国分寺ではスタ丼(「すた丼」ではない)からんぷ亭に行くことがもっぱらでした。らんぷ亭のとうふ麺はいつか復活してほしいものです。

松屋ジェネレータジェネレータ

作ったのは、松屋ジェネレータ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%はなんなんだ、という感じですね)。