Excel の罫線で木構造を描いてあるドキュメントを眺めて毒づきつつ、ふと 2次元空間に対する正規表現を思いついた。
正規表現での単一文字のマッチは、「現在の位置」にある文字があることを確認した後、現在位置を「右に」一文字ずらすという動作である。
そして、単一文字のマッチ以外に「現在の位置」をずらす機能はない。
とすると、そこを右だけじゃなくて上下左右にずらせるようにするだけで、2次元に拡張できるのではないか?
そうするための拡張方法はとりあえずふたつ考えられる。
とりあえず前者で実装してみよう。
% cat 2d.rb def match(pat, strary, start=[0,0], dir=:right) map = {} strary.each_with_index {|str, y| str.each_char.with_index {|ch, x| map[[x,y]] = ch } } try(pat, map, [start], dir) {|path2, dir2| return path2 } nil end def try(pat, map, path, dir, &b) case pat[0] when :lit; _, ch = pat; try_lit(ch, map, path, dir, &b) when :cat; _, *rs = pat; try_cat(rs, map, path, dir, &b) when :alt; _, *rs = pat; try_alt(rs, map, path, dir, &b) when :rep; _, *rs = pat; try_rep(rs, map, path, dir, &b) when :dir; _, d = pat; try_dir(d, map, path, dir, &b) else raise "unexpected: #{pat.inspect}" end end def try_dir(d, map, path, dir) yield path, d end def try_lit(ch, map, path, dir) if map[path.last] && map[path.last] == ch x, y = path.last case dir when :right then x += 1 when :left then x -= 1 when :up then y -= 1 when :down then y += 1 end yield [*path, [x,y]], dir end end # r1 r2 ... def try_cat(rs, map, path, dir, &block) if rs.empty? yield path, dir else r, *rest = rs try(r, map, path, dir) {|path2, dir2| try_cat(rest, map, path2, dir2, &block) } end end # r1 | r2 | ... def try_alt(rs, map, path, dir, &block) rs.each {|r| try(r, map, path, dir, &block) } end # (r1 r2 ...)* def try_rep(rs, map, path, dir, &block) try_cat(rs, map, path, dir) {|path2, dir2| if !path[0...-1].include?(path.last) # xxx: cannot cross. try_rep(rs, map, path2, dir2, &block) end } yield path, dir end strary = [ "************", "* * * * *", "* *** *", "* * **** *", "* * *G *", "************", ] pat = [:cat, [:rep, [:alt, [:dir, :right], [:dir, :left], [:dir, :up], [:dir, :down]], [:lit, " "]], [:lit, "G"]] require 'pp' pp match(pat, strary, [1,1]) % ruby 2d.rb [[1, 1], [1, 2], [1, 3], [2, 3], [3, 3], [3, 4], [4, 4], [5, 4], [5, 3], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [10, 2], [10, 3], [10, 4], [9, 4], [8, 4], [7, 4], [6, 4]]
とりあえず、迷路を解けている。(初期位置の左上 (1,1) から G と描いてある (7,4) を突き抜けた (6,4) までの経路が得られている。突き抜けているのは G の文字にマッチした後、位置をひとつ進めるから)
[:cat, [:rep, [:alt, [:dir, :right], [:dir, :left], [:dir, :up], [:dir, :down]], [:lit, " "]], [:lit, "G"]]
というパターンは、4方向のどれか方向を設定して、一文字空白を進み、それを 0回以上繰り返して、G にたどりつく、というものである。
しかし、実装して気がついたのだが、問題は繰り返しの無限再帰防止検査である
1次元正規表現では以下のように、繰り返しのそれぞれでまったく進まなかったケースはそれ以降調べないというものであった。
def try_rep(r, ary, pos, &block) try(r, ary, pos) {|pos2| if pos < pos2 try_rep(r, ary, pos2, &block) end } yield pos end
2次元正規表現では、(まだまじめに考えていないが) とりあえず以下のように今までに通った位置だったらそれ以降調べない、としてみた。
def try_rep(rs, map, path, dir, &block) try_cat(rs, map, path, dir) {|path2, dir2| if !path[0...-1].include?(path.last) # xxx: cannot cross. try_rep(rs, map, path2, dir2, &block) end } yield path, dir end
どうやるのが適切だろうか。
まじめにやるなら、その繰り返しの各サイクルで同じ場所になってはならない、というのがいいか。
def try_rep(rs, map, path, dir, visited={}, &block) return if visited[path.last] visited2 = visited.dup visited2[path.last] = true try_cat(rs, map, path, dir) {|path2, dir2| if visited2[path.last] try_rep(rs, map, path2, dir2, visited2, &block) end } yield path, dir end
Two-dimensional languages, Dora Giammarresi, Antonio Restivo
をざっと眺めてみる。
上下左右に動けるオートマトンは Four-way automaton といって、ずいぶんと前からあるようだ。
そして、あまりきれいな性質ではない模様。
しかし、きれいな範囲では用途にあわない感じである。
とりあえず長方形にマッチしたいのだが、幅と高さが任意なので、この時点で有限状態では済まない (と思う)。
そうなるときれいでない拡張をいれないと実用にならないので、むしろ仕掛けがわかりやすいほうがいいか。
ちょっと試して、エンジンが方向を持つというのは (とりあえず) やめることにした。90度回転したものにマッチするのが難しくなるが、とりあえず今は気にしない。
方向を状態として持たないので、移動の時に方向を直接指定する。:e, :w, :s, :n で東西南北にひとつ移動するようにしてみた。(いつかエンジンが方向を再度保持するようにしたときのために、左右は温存しておこう)
長方形のマッチの方法は、繰り返しの回数を記録できるようにして、その回数を後の繰り返しで参照できるようにした。
で、たとえば以下の 2次元構造 (nethack の 地図) から、長方形の部分を検出できる。
% cat maze2.rb require 'table' strary = <<End.lines.map {|l| l.chomp } ------------- |...........| ------- |...........| |.....|# #..<.........| ############......|# #-----.------- ------- # |.....|# #### ######### |.....-##### ----.--# # # |....>| # ## ## # # ### |......########################`# # # |.....| # #################%################# ## ------- ### # ###### # -.----|------ # ------.------# # ###############-...........| ### |...........|# `# ----------# |...(.@.....| # |...........|### .........-# |...........| ##............| # |........| ------------- ------------- ###|........| #-........| ---------- End hdoor = [:grid, [/\A[-|]\z/, [:origin, [:alt, ".", "|"]], /\A[-|]\z/]] vdoor = [:grid, [/\A[-|]\z/], [[:origin, [:alt, ".", "-"]]], [/\A[-|]\z/]] pat = [:cat, [:save_pos, :top_left], [:repeat, :width, 2, nil, true, [:alt, "-", hdoor], :e], :w, :s, [:repeat, :height, 1, nil, true, [:alt, "|", vdoor], :s], [:save_pos, :bottom_right], [:repn, :width, [:alt, "-", hdoor], :w], :e, :n, [:repn, :height, [:alt, "|", vdoor], :n]] require 'pp' aa = Table::Pathfinder.strary_to_aa(strary) Table::Pathfinder.each_match(pat, aa) {|spos, epos, cap| p cap x1, y1 = cap[:top_left] x2, y2 = cap[:bottom_right] y1.upto(y2) {|y| puts aa[y][x1..x2].join('') } } % ruby -Ilib maze2.rb {:top_left=>[54, 0], :width=>13, :height=>3, :bottom_right=>[66, 4]} ------------- |...........| |...........| ..<.........| -----.------- {:top_left=>[29, 2], :width=>7, :height=>3, :bottom_right=>[35, 6]} ------- |.....| ......| |.....| ----.-- {:top_left=>[9, 5], :width=>7, :height=>4, :bottom_right=>[15, 10]} ------- |.....- |....>| |...... |.....| ------- {:top_left=>[64, 10], :width=>13, :height=>3, :bottom_right=>[76, 14]} -.----|------ -...........| |...(.@.....| |...........| ------------- {:top_left=>[28, 11], :width=>13, :height=>3, :bottom_right=>[40, 15]} ------.------ |...........| |...........| ............| ------------- {:top_left=>[46, 12], :width=>10, :height=>4, :bottom_right=>[55, 17]} ---------- .........- |........| |........| -........| ----------
地図があれば経路を出してみたい、というわけで上り階段 "<" から下り階段 ">" への経路を出してみよう。
やってみるとうまくいかない。終了しない。
繰り返しが DFS (深さ優先探索) に対応するわけだが、それだけならまだしも、サイクルの検査しかしていないので、以前他の経路で到達した場所に到達しても、枝刈りがされないのである。
やっぱ BFS (幅優先探索) が必要だよね、ということで :bfs を導入する。
% cat maze3.rb require 'table' strary = <<End.lines.map {|l| l.chomp } ------------- |...........| ------- |...........| |.....|# #..<.........| ############......|# #-----.------- ------- # |.....|# #### ######### |.....-##### ----.--# # # |....>| # ## ## # # ### |......########################`# # # |.....| # #################%################# ## ------- ### # ###### # -.----|------ # ------.------# # ###############-...........| ### |...........|# `# ----------# |...(.@.....| # |...........|### .........-# |...........| ##............| # |........| ------------- ------------- ###|........| #-........| ---------- End pat = [:cat, ">", [:bfs, [:pos], [:push_pos, :path], [:alt, ".", ">", "#", "%"], [:alt, :e, :w, :s, :n]], [:push_pos, :path], "<"] require 'pp' aa = Table::Pathfinder.strary_to_aa(strary) Table::Pathfinder.each_match(pat, aa) {|spos, epos, cap| aa2 = aa.map {|a| a.dup } cap[:path].each {|x,y| aa2[y][x] = '*' } puts aa2.map {|a| a.join } } % ruby -Ilib maze3.rb ------------- |...........| ------- |...........| |.....|# ****.........| ############......|# *-----.------- ------- # |.....|# **** ######### |.....-##### ----.--# * # |....*| # ## ## # * ### |....**************************`# * # |.....| # ########******************######### ## ------- ### # ###### # -.----|------ # ------.------# # ###############-...........| ### |...........|# `# ----------# |...(.@.....| # |...........|### .........-# |...........| ##............| # |........| ------------- ------------- ###|........| #-........| ----------
このパターンだと、ドアを通れないのがナニではある。
ちなみに、実装は table というライブラリに入っている。
なお、Excel (でなくてもいいが表計算ツール) で、情報を取り出すのに罫線を利用する必要がある形式は、検索してもなかなか公開された実例が見つからない。
そういうコレクションとかないのだろうか。
探索の状態を表現するのに、persistent な辞書が欲しい。
BFS がなければ、ephemeral だけど hash を使って、バックトラックする時に元に戻すという手があったが、BFS があると無理である。ある時点で queue に入っている状態すべてを保持しないといけない。
また、状態には頻繁に変化する部分とあまり変化しない部分がある。たとえば現在位置は頻繁に変化するが、長方形の辺の長さの記録などは一回決まったら最後まで変化しない。
splay tree が向いていそうな気はする。
これが噂の Excel 方眼紙というやつか。
こだわりEXCEL(エクセル)テンプレート: 履歴書(配布用)
(そういう用途を想定しているわけではないようだが) もし、このテンプレートに Excel で記入されたものがたくさん送られてきて、それを機械処理したくなったと仮定すると、どうするかな。
csv に変換する時に、merged cell の情報をもっと明確に生成できた方がいいかもしれない。(今の sample/excel2csv と sample/poi-xls2csv.rb は merged area の左上の cell に text を入れるのと、すべての cell に同じ text を入れるふたつの指定しかできない)
地震である。停電である。
停電している最中はバッテリがなんとも重要ですな。
そうそう、地震以降 cvs.m17n.org は動いていません。復旧は少なくとも週明け以降になります。
Excel 方眼紙な履歴書を眺めて、データを取り出す方法を考える。
長方形を積み木のように組み合わせているから、2次元正規表現が向いているかもしれないな。
つまり、シンボル・縦連結・横連結・縦の繰り返し・横の繰り返し・集合和(・集合積・補集合) でシンボルが長方形に並んだものの集合(picture language)を表現する。
バックトラッキングエンジンを直接制御するのに比べて、どういう順序で探索するか記述しないぶんコンパクトな記述になるはずで、また、(同じことだが) 探索の順番が異なっても同じ記述で済むケースが出てくる。たとえば、長方形を左上から調べていくのと、右上から調べていくのは、バックトラッキングエンジンを直接制御するなら異なる記述になるが、2次元正規表現なら同じ記述で済むかもしれない。
素朴な2次元正規表現エンジンを書いてみる。
% cat 2d.rb def conv_to_aa(strary) aa = [] strary.each_with_index {|str, y| aa[y] = [] str.each_char.with_index {|ch, x| aa[y][x] = ch } } aa end def match(pat, aa, start=nil) if start try(pat, aa, start) {|pos2| return start, pos2 } else aa.each_with_index {|a, y| a.each_index {|x| start = [x,y] try(pat, aa, start) {|pos2| return start, pos2 } } } end nil end def each_match(pat, aa, start=nil) if start try(pat, aa, start) {|pos2| yield start, pos2 } else aa.each_with_index {|a, y| a.each_index {|x| start = [x,y] try(pat, aa, start) {|pos2| yield start, pos2 } } } end nil end def try(pat, aa, pos, &b) case pat when String try_lit(pat, aa, pos, &b) when Array *pat, opts = pat if Hash === pat.last case pat[0] when :lit; _, val = pat; try_lit(val, aa, pos, &b) when :alt; _, *rs = pat; try_alt(rs, aa, pos, &b) when :hcat; _, *rs = pat; try_hcat(rs, aa, pos, opts, &b) when :vcat; _, *rs = pat; try_vcat(rs, aa, pos, opts, &b) when :hrep; _, *rs = pat; try_hrep(rs, aa, pos, opts, &b) when :vrep; _, *rs = pat; try_vrep(rs, aa, pos, opts, &b) else raise "unexpected: #{pat.inspect}" end else raise "unexpected: #{pat.inspect}" end end def try_lit(val, aa, pos) x, y = pos if 0 <= y && y < aa.length && 0 <= x && x < aa[y].length && aa[y][x] == val yield [pos[0]+1, pos[1]+1] end end # r1 | r2 | ... def try_alt(rs, aa, pos, &block) rs.map {|r| lambda { pos_try(r, aa, pos, &block) } } end def try_hcat(rs, aa, pos, opts=nil, &block) opts ||= {} opt_height = opts[:height] if rs.empty? yield [pos[0], pos[1]+(opt_height||0)] else r, *rest = rs try(r, aa, pos) {|pos2| if pos == pos2 try_hcat(rest, aa, pos, {:height=>opt_height}, &block) else height = pos2[1]-pos[1] if !opt_height || opt_height == height pos3 = [pos2[0], pos[1]] try_hcat(rest, aa, pos3, {:height=>height}, &block) end end } end end def try_vcat(rs, aa, pos, opts=nil, &block) opts ||= {} opt_width = opts[:width] if rs.empty? yield [pos[0]+(opt_width||0), pos[1]] else r, *rest = rs try(r, aa, pos) {|pos2| if pos == pos2 try_vcat(rest, aa, pos, {:width=>opt_width}, &block) else width = pos2[0]-pos[0] if !opt_width || opt_width == width pos3 = [pos[0], pos2[1]] try_vcat(rest, aa, pos3, {:width=>width}, &block) end end } end end def try_hrep(rs, aa, pos, opts=nil, &block) opts ||= {} opt_height = opts[:height] opt_min = opts[:min] opt_max = opts[:max] opt_greedy = opts.fetch(:greedy, true) opt_min = nil if opt_min == 0 if !opt_greedy && !opt_min yield [pos[0], pos[1]+(opt_height||0)] end if !opt_max || 0 < opt_max try_hcat(rs, aa, pos, {:height=>opt_height}) {|pos2| next if pos == pos2 height = pos2[1]-pos[1] if !opt_height || opt_height == height pos3 = [pos2[0], pos[1]] opts2 = { :height => height, :min => opt_min && (opt_min-1), :max => opt_max && (opt_max-1), :greedy => opt_greedy } try_hrep(rs, aa, pos3, opts2, &block) end } end if opt_greedy && !opt_min yield [pos[0], pos[1]+(opt_height||0)] end end def try_vrep(rs, aa, pos, opts=nil, &block) opts ||= {} opt_width = opts[:width] opt_min = opts[:min] opt_max = opts[:max] opt_greedy = opts.fetch(:greedy, true) opt_min = nil if opt_min == 0 if !opt_greedy && !opt_min yield [pos[0]+(opt_width||0), pos[1]] end if !opt_max || 0 < opt_max try_vcat(rs, aa, pos, {:width=>opt_width}) {|pos2| next if pos == pos2 width = pos2[0]-pos[0] if !opt_width || opt_width == width pos3 = [pos[0], pos2[1]] opts2 = { :width => width, :min => opt_min && (opt_min-1), :max => opt_max && (opt_max-1) } try_vrep(rs, aa, pos3, opts2, &block) end } end if opt_greedy && !opt_min yield [pos[0]+(opt_width||0), pos[1]] end end strary = <<End.lines.map {|l| l.chomp } ------------- |...........| ------- |...........| |.....| |...........| |.....| ------------- ------- |.....| |.....| ------- |.....| |.....| |.....| ------- ------------- ------------- |...........| |...........| ---------- |...........| |...........| |........| |...........| |...........| |........| ------------- ------------- |........| |........| ---------- End pat = [:vcat, [:hrep, '-', :min=>1], [:vrep, [:hcat, "|", [:hrep, "."], "|"], :min=>1], [:hrep, '-', :min=>1]] require 'pp' aa = conv_to_aa(strary) each_match(pat, aa) {|pos1, pos2| p [pos1, pos2] pos1[1].upto(pos2[1]-1) {|y| pos1[0].upto(pos2[0]-1) {|x| print aa[y][x] } puts } } % ruby 2d.rb [[54, 0], [67, 5]] ------------- |...........| |...........| |...........| ------------- [[29, 2], [36, 7]] ------- |.....| |.....| |.....| ------- [[9, 5], [16, 11]] ------- |.....| |.....| |.....| |.....| ------- [[64, 10], [77, 15]] ------------- |...........| |...........| |...........| ------------- [[28, 11], [41, 16]] ------------- |...........| |...........| |...........| ------------- [[46, 12], [56, 18]] ---------- |........| |........| |........| |........| ----------
左上の位置を指定すると、右下の位置を (可能性の数だけ) yield するという形なので、思った以上に 4-way automaton と融合させやすそうだ。
ただ、単一文字にマッチしたとき、2次元正規表現だと現在位置が右下にすすむけれど、自前で位置を制御する時には動かない方がわかりやすい。とりあえずそこに機能の衝突がある。
本が (主に文庫本が) 際限なく増えていく (そして引越のときに厄介なことになる) 生活に決別するため、ScanSnap S1500 を買う。まぁ、読んだ端から捨てればいいのだが、優柔不断なのでそれができないのである。
さて、ScanSnap S1500 は Ubuntu ではすぐに使えるという話で、実際 Ubuntu Natty (development branch) の scanimage で scan できる。(もっと前の Ubuntu から動いていたという話だが)
xsane は最初動いていたのだが途中からなんか動かなくなってしまった。なにかをやってしまったのだろうが、よくわからない。scanimage は依然として動くのでとくに問題はないけれど。
とりあえず (あまりおもしろくなかった) 文庫本を1冊カッターでばらして、scan してみる。両面でぜんぶ scan するのもたいした手間ではない。一冊全部シートフィーダに入れられるといいのだが、残念ながらかなり薄い本でないと無理だろう。
さて、scan した結果をどうするか。scanimage はデフォルトで pnm (ppm, pgm, pbm) のファイルを生成するのだが、これをどうやって保存・利用するか。
tar.gz (や tar.xz) というのも考えられるが、読むのに適した感じにはならないだろう。ランダムアクセスできないし。
zip はランダムアクセスできるのでマシかもしれない。
世の中では PDF が多いらしい。viewer が揃っているという点の他に、OCR 結果を格納できるという点でも PDF 形式は魅力である。実際的には、自由で実用的な日本語 OCR は現在のところ難しそうだが。それはそれとして、PDF にしてしまうと画像ファイル用のツールがだいたい使えなくなるのはよろしくない。
とりあえず apt-cache search pdf として、いろいろと調べて見たところ、sam2p と poppler-utils というものを見つけた。
sam2p は (1枚の) 画像ファイルを (1ページの) PDF に変換できる。(比較してみたのだが、ImageMagick や GraphicsMagick で変換するよりも小さくなるようだ。)
poppler-utils の pdfimages は PDF に含まれた画像をファイルとして抽出できる。
そして、どちらも劣化しない。
実際、以下のようにすると同じ ppm ファイルが再生されることがわかる。(厳密に言えば ppm でも、同じ画像が異なるバイト列になることもあるが、ここでは同じバイト列になっている)
% sam2p -j:quiet -pdf:2 a.ppm b.pdf % pdfimages b.pdf c % cmp a.ppm c-000.ppm
というわけで、pdftk で PDF を連結できることを考えると、sam2p+pdftk と pdfimages で PDF を ppm のアーカイブのようなものとして扱えることになる。
とりあえず、300dpi・カラーで scan して、PDF でとっておくか。
さて、それはそれとして、読むためには、表紙・口絵はカラー、挿絵はグレースケール、本文は白黒にしたい。とくに本文を白黒にするのは、裏写りの除去とファイルサイズの点から魅力的だが、どうやってページを区別したものか。また、余白を削るのも必要だろう。そうすると最終的には対話的な部分が出てくるだろうから、なにか GUI が必要で、local な web server を動かして browser からやるのが楽かな。
scanimage には pnm じゃなくて tiff を生成させることにしよう。
tiff には dpi が入っているので、pdf の生成のときに実際の大きさを引き継げる。
が... sam2p の -m:dpi:res はなんか変である。解像度を大きくすると画像も大きくなってしまうのだが、それは逆だろう。
Debian BTS 619824: sam2p -m:dpi:res generates bigger image for bigger resolution
あと、pbm を feh でうまく見れないことに気がついた。
png でも解像度を含められるようだ。とすると tiff のかわりに png にする手もあるか。
png だとブラウザで直接扱える点が良い。
ただ、dot/inch じゃなくて dot/meter なのはいいのかわるいのか。
11811 dot/meter とかいってもわかんないよねぇ。ひとが見るものではないのかもしれないが。
[latest]