Windows の人向けに何か簡単なソフトを作ろうとして、Ruby のあのライブラリとか使うと一発なんだけどなー、と思ったりすることがある。みんな Windows に Ruby インストールしてればいいのに!と。
Exerb を使えばスクリプトを全部まとめて .exe にすることができるのだけど、Exerb 自体がちょっと面倒。exy ファイルを作って、とか。しかも .exe にしたスクリプトが何故かエラーで動かなかったりするし。
そういうスクリプトを作った。
アイデアは Exerb と同じ。.exe を作るか作らないかの違いみたいなもん。
自分で書いた Ruby スクリプトがあるフォルダを A とし、
A/ A/your-script.rb A/some-library.rb
こんな感じのフォルダ構造になってるとする。
手元でこのスクリプトを使うときは、A に入って
> C:\Ruby186\bin\ruby your-script.rb
というふうに実行するものとする。この Ruby186 は RubyInstaller for Windows っていうのでインストールしたときのパス。iconv や zlib 等の dll が最初から入ってるのでおすすめ。
または Cygwin からだと
$ /cygdrive/c/Ruby186/bin/ruby your-script.rb
となる。ただし、Cygwin 版の Ruby は再配布には適さないので、Cygwin から使うときも mswin32 版か mingw32 版を使ったほうがいい。
次に、A の中に rbat.rb をダウンロードしてきて、
$ /cygdrive/c/Ruby186/bin/ruby -r./rbat your-script.rb
のように実行すると、スクリプトが普通に走り、A の中に ruby-dist というフォルダができ、さらに、your-script.bat というバッチファイルも作られる。
A/ A/your-script.rb A/your-script.bat A/your-library.rb A/ruby-dist/ A/ruby-dist/ruby.exe A/ruby-dist/lib/
こんな感じ。(ちょっと省略した。後述)
ruby-dist/lib の中にはこのスクリプトが読み込んだライブラリが全部含まれていて、your-script.bat を実行すると、ruby-dist/ruby のほうを使って your-script.rb を起動することになっている。
すなわち、このフォルダ全体を zip にして配布すれば、使う人は解凍して your-script.bat をダブルクリックするだけとなる。
zip にするときは rbat.rb は不要。
となっている。dll を不必要なものも含めて全部コピーしてしまうのは、$LOADED_FEATURES 変数ではどの dll がロードされているかまでは分からないため。
なので、ruby-dist フォルダの中を見て、「これは明らかに使われてないな」と思うものは削除してもいい。(または削除しつつ試しに走らせてみてもいい)
それから、Ruby と同じフォルダ内にはない(しかも普通の人はインストールしてない) dll に依存してた場合は、当然配布したときに動かない。
そこで、dll.rb というスクリプトも作った。これは、昨日書いたようにどこでも動くかどうか分からないが、開いている dll を全部リストしてくれる。
$ /cygdrive/c/Ruby186/bin/ruby -r./dll your-script.rb
というふうに。
この結果と ruby-dist フォルダの中身を見比べて、不要な dll を削除したり、必要な dll を足したりすればいい。
Win32API を使って、Ruby が開いている .dll を確認してみる。
方法はいくつかある。
まず、CreateToolhelp32Snapshot でプロセスのスナップショットを撮って、Module32First と Module32Next を使う方法。
↑のコードの主要部分を抜き出すと、こんな感じ。
require 'Win32API' # http://msdn.microsoft.com/en-us/library/ms686849(v=vs.85).aspx # load functions # DWORD WINAPI GetCurrentProcessId(void); win32_GetCurrentProcessId = Win32API.new('kernel32.dll', 'GetCurrentProcessId', 'V', 'L') # HANDLE WINAPI CreateToolhelp32Snapshot( __in DWORD dwFlags, __in DWORD th32ProcessID ); win32_CreateToolhelp32Snapshot = Win32API.new('kernel32.dll', 'CreateToolhelp32Snapshot', 'LL', 'L') # BOOL WINAPI Module32First( __in HANDLE hSnapshot, __inout LPMODULEENTRY32 lpme ); win32_Module32First = Win32API.new('kernel32.dll', 'Module32First', 'LP', 'I') # BOOL WINAPI Module32Next( __in HANDLE hSnapshot, __out LPMODULEENTRY32 lpme ); win32_Module32Next = Win32API.new('kernel32.dll', 'Module32Next', 'LP', 'I') # BOOL WINAPI CloseHandle( __in HANDLE hObject ); win32_CloseHandle = Win32API.new('kernel32.dll', 'CloseHandle', 'L', 'I') # constants _TH32CS_SNAPMODULE = 8 _INVALID_HANDLE_VALUE = -1 sizeof_MODULEENTRY32 = 548 # is this for i686 only? # start here dwPID = win32_GetCurrentProcessId.call() hModuleSnap = win32_CreateToolhelp32Snapshot.call(_TH32CS_SNAPMODULE, dwPID) if hModuleSnap == _INVALID_HANDLE_VALUE raise "Invalid handle value returned by CreateToolhelp32Snapshot" end # set dwSize first me32 = [sizeof_MODULEENTRY32].pack("N") + "\0" * (sizeof_MODULEENTRY32 - 4) # sizeof DWORD = 4 if !win32_Module32First.call(hModuleSnap, me32) win32_CloseHandle.call(hModuleSnap) raise "Error in Module32First" end while 1 #module_name = me32.slice(32, 256).sub(/\0.*/, '') executable = me32.slice(288, 260).sub(/\0.*/, '') puts executable break if 0 == win32_Module32Next.call(hModuleSnap, me32) end win32_CloseHandle.call(hModuleSnap)
LPMODULEENTRY32 というのはこういう構造体へのポインターになっている。
typedef struct tagMODULEENTRY32 { // size DWORD dwSize; // 4 DWORD th32ModuleID; // 4 DWORD th32ProcessID; // 4 DWORD GlblcntUsage; // 4 DWORD ProccntUsage; // 4 BYTE *modBaseAddr; // 1 (occupy 4 bytes due to alignment) DWORD modBaseSize; // 4 HMODULE hModule; // 4 TCHAR szModule[MAX_MODULE_NAME32 + 1];// 256 TCHAR szExePath[MAX_PATH]; // 260 } MODULEENTRY32, *PMODULEENTRY32; // 545 (total of above)
ここで気になるのが、この構造体のサイズは alignment によって変わるという点。うちではワード長が4なので MODULEENTRY32 のサイズは548に切り上げられるため、上のコードではそれが前提になっているが、もしかすると546に切り上げられたり、545のままの環境があるかもしれない。
また、うちでは sizeof(HANDLE) が4なんだけど、HANDLE の実体はポインターなので、long に変換するのは良くない気がしないでもない。(Windows 7 は 64bit OS でもポインターのサイズが 32bit なんだっけ?これは Ruby が 32bit でビルドされてるからか)
もうひとつは EnumProcessModules を使う方法。こっちもありがたくサンプルコードがあるので、写経するだけ。
#!/usr/bin/ruby require 'Win32API' # http://msdn.microsoft.com/en-us/library/ms682621(v=vs.85).aspx # functions # DWORD WINAPI GetCurrentProcessId(void); win32_GetCurrentProcessId = Win32API.new('kernel32.dll', 'GetCurrentProcessId', 'V', 'L') # HANDLE WINAPI OpenProcess( __in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in DWORD dwProcessId ); win32_OpenProcess = Win32API.new('kernel32.dll', 'OpenProcess', 'LIL', 'L') # BOOL WINAPI EnumProcessModules( __in HANDLE hProcess, __out HMODULE *lphModule, __in DWORD cb, __out LPDWORD lpcbNeeded ); win32_EnumProcessModules = Win32API.new('kernel32.dll', 'EnumProcessModules', 'LPLP', 'I') rescue Win32API.new('psapi.dll', 'EnumProcessModules', 'LPLP', 'I') # psapi.dll on XP # DWORD WINAPI GetModuleFileName( __in_opt HMODULE hModule, __out LPTSTR lpFilename, __in DWORD nSize ); win32_GetModuleFileName = Win32API.new('kernel32.dll', 'GetModuleFileName', 'LPL', 'L') # BOOL WINAPI CloseHandle( __in HANDLE hObject ); win32_CloseHandle = Win32API.new('kernel32.dll', 'CloseHandle', 'L', 'I') # constants _PROCESS_QUERY_INFORMATION = 0x0400 _PROCESS_VM_READ = 0x0010 _MAX_PATH = 260 sizeof_HANDLE = 4 # start here buflen = 1024 hMods = "\0" * sizeof_HANDLE * buflen processID = win32_GetCurrentProcessId.call() hProcess = win32_OpenProcess.call(_PROCESS_QUERY_INFORMATION | _PROCESS_VM_READ, 0, processID) raise "Error in OpenProcess" if 0 == hProcess cbNeeded = "\0" * 4 if 0 != win32_EnumProcessModules.call(hProcess, hMods, hMods.length, cbNeeded) len = cbNeeded.unpack("l!")[0] / sizeof_HANDLE hMods.unpack("l!#{len}").each {|hModule| szModName = "\0" * _MAX_PATH if 0 != win32_GetModuleFileName.call(hModule, szModName, _MAX_PATH) puts szModName.sub(/\0.*/, '') end } end win32_CloseHandle.call(hProcess)
とりあえず alignment の問題はなくなった。HANDLE と DWORD のサイズが4であるとしているところや、GetModuleFileName が LPTSTR を受け取るべきなのに勝手に LPSTR と想定しているのが問題と言えば問題。しかしうちでは動いているのが謎。Ruby のコンパイルのときに UNICODE が付いてないってことだろうか?
EnumerateLoadedModulesEx とか EnumerateLoadedModules64 というのがあるらしい。しかし、これらはコールバック関数ベースなので、たぶん Ruby で使うのは無理だと思う。
方法1と方法2の実行結果は同じ。
% ruby dlls.rb C:\cygwin\bin\ruby.exe C:\Windows\SysWOW64\ntdll.dll C:\Windows\syswow64\kernel32.dll C:\Windows\syswow64\KERNELBASE.dll C:\cygwin\bin\cygruby18.dll C:\cygwin\bin\cygcrypt-0.dll C:\cygwin\bin\cygwin1.dll C:\Windows\syswow64\ADVAPI32.DLL C:\Windows\syswow64\msvcrt.dll C:\Windows\SysWOW64\sechost.dll C:\Windows\syswow64\RPCRT4.dll C:\Windows\syswow64\SspiCli.dll C:\Windows\syswow64\CRYPTBASE.dll C:\Windows\syswow64\USER32.dll C:\Windows\syswow64\GDI32.dll C:\Windows\syswow64\LPK.dll C:\Windows\syswow64\USP10.dll C:\Windows\system32\IMM32.DLL C:\Windows\syswow64\MSCTF.dll \\?\C:\cygwin\lib\ruby\1.8\i386-cygwin\Win32API.so C:\Windows\syswow64\psapi.dll
もしスクリプトの一番上で require 'iconv' と書いたら、↓のような行が増える。
\\?\C:\cygwin\lib\ruby\1.8\i386-cygwin\iconv.so C:\cygwin\bin\cygiconv-2.dll C:\cygwin\bin\cyggcc_s-1.dll
この \\?\ というのは、Windows のパス指定のときの習慣で、MSDN に説明がある(と教えてもらった)。
とにかく、cygwin 由来の .dll や .so についてはこの接頭辞が付くことが多いので、cygwin の中間層で付け足して開いているのだと思う。
実際、mingw32 や mswin32 版の Ruby ではそういうのはつかない。
% /cygdrive/c/Ruby186/bin/ruby dlls.rb C:\Ruby186\bin\ruby.exe C:\Windows\SysWOW64\ntdll.dll C:\Windows\syswow64\kernel32.dll C:\Windows\syswow64\KERNELBASE.dll C:\Ruby186\bin\msvcrt-ruby18.dll C:\Windows\syswow64\ADVAPI32.DLL C:\Windows\syswow64\msvcrt.dll C:\Windows\SysWOW64\sechost.dll C:\Windows\syswow64\RPCRT4.dll C:\Windows\syswow64\SspiCli.dll C:\Windows\syswow64\CRYPTBASE.dll C:\Windows\syswow64\SHELL32.DLL C:\Windows\syswow64\SHLWAPI.dll C:\Windows\syswow64\GDI32.dll C:\Windows\syswow64\USER32.dll C:\Windows\syswow64\LPK.dll C:\Windows\syswow64\USP10.dll C:\Windows\syswow64\WS2_32.DLL C:\Windows\syswow64\NSI.dll C:\Windows\system32\apphelp.dll C:\Windows\AppPatch\AcLayers.DLL C:\Windows\syswow64\ole32.dll C:\Windows\syswow64\OLEAUT32.dll C:\Windows\system32\USERENV.dll C:\Windows\system32\profapi.dll C:\Windows\system32\WINSPOOL.DRV C:\Windows\system32\MPR.dll C:\Windows\system32\IMM32.DLL C:\Windows\syswow64\MSCTF.dll C:\Windows\system32\mswsock.dll C:\Ruby186\lib\ruby\1.8\i386-mingw32\iconv.so C:\Ruby186\bin\libiconv2.dll C:\Ruby186\lib\ruby\1.8\i386-mingw32\Win32API.so C:\Windows\syswow64\psapi.dll
Win32API は遊び以上の目的で使うものではないかな。
そういう需要があったので作ってみた。一応メモとして残しておく。
その README から抜粋。(hatena2epub.bat というのは hatena2epub.rb にカレントディレクトリを引数として渡すだけのバッチファイル)
Hatena2ePub
はてな記法で書かれた複数のテキストファイルをまとめて ePub にします。
使い方
- hatena2epub.bat と同じフォルダ内に TXT というフォルダを作ります。
- その中に、はてな記法のテキストを .txt という拡張子で保存します。
- hatena2epub.bat をダブルクリックして実行します。
- DOS ウィンドウが立ち上がり、成功すれば BOOK.epub というファイルが出来ます。
その他
もし hatena2epub.bat と同じフォルダ内に、以下のようなファイルがあれば、それを設定として適用します。
ファイル名 説明 デフォルト設定 STYLE.css ePub の CSS 空の CSS TITLE.txt 本のタイトル "The Book" AUTHOR.txt 本の著者 "Me" LANG.txt 本で主に使われる言語 "ja" BOOK.epub 作成時に BOOK というフォルダが出来ますが、これは消しても構いません。BOOK.epub は単に BOOK フォルダの中身を zip したものです。
次に hatena2epub.bat を実行したときには BOOK フォルダの中身は削除されます。
目次には各テキストの(拡張子部分を除いた)ファイル名、または、ファイルの1行目が見出しである場合はその見出しが使われます。
hatena2epub.rb はこうなってる。
#!/usr/bin/env ruby require 'zip/zipfilesystem' require 'hparser' require 'hparser/text' # .to_text method require 'uuid' require 'kconv' require 'erb' require 'pathname' class String def to_h # html escape self.gsub(/[<>"&]/) {|c| case c when '<' '<' when '>' '>' when '"' '"' when '&' '&' end } end def /(other) File.join(self, other) end end class Hatena2ePub TEXT_DIR = "TXT" TEXT_FILES = "*.txt" TITLE_FILE = "TITLE.txt" DEFAULT_TITLE = "The Book" AUTHOR_FILE = "AUTHOR.txt" DEFAULT_AUTHOR = "Me" LANG_FILE = "LANG.txt" DEFAULT_LANG = "ja" ZIP_FILE = "BOOK.epub" BOOK_DIR = "BOOK" CONTENTS_DIR = "contents" MIMETYPE_FILE = "mimetype" MIMETYPE_CONTENT = "application/epub+zip" CONTAINER_XML_FILE = "META-INF" / "container.xml" CONTAINER_XML_TEMPLATE = <<EOF <?xml version="1.0" encoding="UTF-8"?> <container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0"> <rootfiles> <rootfile full-path="<%= OPF_FILE.to_h %>" media-type="application/oebps-package+xml" /> </rootfiles> </container> EOF OPF_FILE = "metadata.opf" OPF_TEMPLATE = <<EOF <?xml version="1.0" encoding="UTF-8"?> <package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <dc:title><%= @title.to_h %></dc:title> <dc:creator opf:role="aut"><%= @author.to_h %></dc:creator> <dc:language><%= @language.to_h %></dc:language> <dc:identifier id="BookId"><%= @unique_id.to_h %></dc:identifier> </metadata> <manifest> <item id="ncx" href="<%= NCX_FILE.to_h %>" media-type="text/xml" /> <item id="style" href="<%= CSS_FILE.to_h %>" media-type="text/css" /> <%= opf_html_items %> </manifest> <spine toc="ncx"> <%= opf_idrefs %> </spine> </package> EOF OPF_ITEM_XHTML_TEMPLATE = <<EOF <item id="<%= id.to_h %>" href="<%= file.to_h %>" media-type="application/xhtml+xml" /> EOF OPF_IDREF_TEMPLATE = <<EOF <itemref idref="<%= id.to_h %>" /> EOF NCX_FILE = "toc.ncx" NCX_TEMPLATE = <<EOF <?xml version="1.0" encoding="UTF-8"?> <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1"> <head> <meta name="dtb:uid" content="<%= @unique_id.to_h %>"/> <meta name="dtb:depth" content="1"/> <meta name="dtb:totalPageCount" content="0"/> <meta name="dtb:maxPageNumber" content="0"/> </head> <docTitle> <text><%= @title.to_h %></text> </docTitle> <docAuthor> <text><%= @author.to_h %></text> </docAuthor> <navMap> <%= ncx_nav_points %> </navMap> </ncx> EOF NCX_NAV_POINT_TEMPLATE = <<EOF <navPoint id="<%= id.to_h %>" playOrder="1"> <navLabel> <text><%= title.to_h %></text> </navLabel> <content src="<%= file.to_h %>"/> </navPoint> EOF CSS_FILE = "STYLE.css" CSS_CONTENT = <<EOF /* CSS to be applied to the ePub book */ EOF XHTML_TEMPLATE = <<EOF <html xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink" lang="<%= @language %>" xml:lang="<%= @language %>"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title><%= title.to_h %></title> <link href="<%= CSS_FILE.to_h %>" type="text/css" charset="UTF-8" rel="stylesheet"/> </head> <body> <%= xhtml_body %> </body> </html> EOF def initialize(args) parse_opt(args) @file_list = [] @zip = nil @uuid_obj = UUID.new @unique_id = @uuid_obj.generate @hparser = HParser::Parser.new end def parse_opt(args) if args.length < 1 raise "usage: hatena2epub.rb working_dir" else @working_dir = args[0] @working_dir = @working_dir.gsub(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil? puts "working directory is #{@working_dir}" end end def start clear_existing get_config get_file_list convert_hatena make_epub puts "#{ZIP_FILE} was created." end def clear_existing if File.exist?(@working_dir / ZIP_FILE) if File.file?(@working_dir / ZIP_FILE) Pathname(@working_dir / ZIP_FILE).unlink else raise "#{@working_dir / ZIP_FILE} is not a file" end end if File.exist?(@working_dir / BOOK_DIR) if File.directory?(@working_dir / BOOK_DIR) Pathname(@working_dir / BOOK_DIR).rmtree else raise "#{@working_dir / BOOK_DIR} is not a directory" end end end def get_config @title = File.file?(@working_dir / TITLE_FILE) ? open(@working_dir / TITLE_FILE, 'r') {|f| f.gets.strip.toutf8} : DEFAULT_TITLE @author = File.file?(@working_dir / AUTHOR_FILE) ? open(@working_dir / AUTHOR_FILE, 'r') {|f| f.gets.strip.toutf8} : DEFAULT_AUTHOR @language = File.file?(@working_dir / LANG_FILE) ? open(@working_dir / LANG_FILE, 'r') {|f| f.gets.strip.toutf8} : DEFAULT_LANG end def get_file_list raise "TXT folder does not exist" unless File.directory?(@working_dir / TEXT_DIR) Dir.glob(@working_dir / TEXT_DIR / TEXT_FILES) {|file| if File.file?(file) content = open(file, 'r'){|f| f.read} next if content.empty? name = File.basename(file, ".*").toutf8 # filename without extension @file_list << { :title => name, :content => content.toutf8, :relpath => CONTENTS_DIR / name + '.html', :id => @uuid_obj.generate } end } end def convert_hatena @file_list.each {|file| lines = @hparser.parse(file[:content]) if lines[0].instance_of?(HParser::Block::Head) file[:title] = lines[0].to_text end xhtml_body = lines.map{|e| e.to_html }.join("\n") title = file[:title] file[:xhtml] = ERB.new(XHTML_TEMPLATE).result(binding) } end def make_epub @zip = Zip::ZipFile.new(@working_dir / ZIP_FILE, Zip::ZipFile::CREATE) add_mimetype add_container_xml add_opf add_ncx add_css add_html @zip.close @zip = nil end def check_zip_open if @zip.nil? raise "Zip file not open" end end def write_file_in_book_dir(relpath, content) path = Pathname.new(@working_dir / BOOK_DIR / relpath) path.dirname.mkpath puts "create file " + path.to_s open(path.to_s, "wb") {|f| f.print(content) } end def add_file_relative_to_book(relpath, content) check_zip_open write_file_in_book_dir(relpath, content) @zip.add(relpath, @working_dir / BOOK_DIR / relpath) end def add_mimetype add_file_relative_to_book(MIMETYPE_FILE, MIMETYPE_CONTENT) end def add_container_xml add_file_relative_to_book(CONTAINER_XML_FILE, ERB.new(CONTAINER_XML_TEMPLATE).result(binding)) end def add_opf add_file_relative_to_book(OPF_FILE, ERB.new(OPF_TEMPLATE).result(binding)) end def opf_html_items @file_list.map{|file| id = file[:id] file = file[:relpath] ERB.new(OPF_ITEM_XHTML_TEMPLATE).result(binding) }.join('') end def opf_idrefs @file_list.map{|file| id = file[:id] ERB.new(OPF_IDREF_TEMPLATE).result(binding) }.join('') end def add_ncx add_file_relative_to_book(NCX_FILE, ERB.new(NCX_TEMPLATE).result(binding)) end def ncx_nav_points @file_list.map{|file| id = file[:id] title = file[:title] file = file[:relpath] ERB.new(NCX_NAV_POINT_TEMPLATE).result(binding) }.join('') end def add_css css_content = CSS_CONTENT if File.exist?(@working_dir / CSS_FILE) open(@working_dir / CSS_FILE, 'r') {|f| css_content = f.read } end add_file_relative_to_book(CONTENTS_DIR / CSS_FILE, css_content) end def add_html @file_list.each{|file| add_file_relative_to_book(file[:relpath], file[:xhtml]) } end end begin Hatena2ePub.new(ARGV).start rescue => err puts err end puts "Press enter to close." $stdout.flush $stdin.gets
依存するライブラリ。
使ってみたところ、HParser の仕様やバグによりかなり制限を受ける。
例えば、引用記法の中身はすべてインラインテキストになってしまうため、実際には使えないに等しい。
あと、スーパー pre 記法の言語指定や、[ユーアールエル:title=タイトル] みたいな記法が使えなかったり。これははてな記法が進化したため。
このへんは直したい気もするが、それよりも Markdown や Textile や MediaWiki 記法などライブラリが充実しているものを ePub にするスクリプトを作ったほうが確実だし汎用性高いし良さそうな気がする。
String#/ はどっかのライブラリのパス名クラスで使われてたやつをパクってみた。こういうことが出来る Ruby はおもしろいな。
ePub 形式のメタファイルの冗長さが嫌になるレベル。同じ情報を複数ファイルに書かないといけなかったり。こんなことなら W3C Widget 形式で良かったと思う。
解説読んでもよくわからなかたので、自分で書いてみる。
ABC ABCDAB ABCDABCDABDE
という文字列 (s とする) を、
ABCDABD
という文字列 (w とする) で split する。
こうなるはず。
ABC ABCDAB ABCD ABCDABD E
s の開始位置から w.length-1 だけ右の文字を見る。s の終端に逹っしている場合は終了。w の中に含まれていない場合、s の開始位置から w.length-1 文字が w に一致することはないので、開始位置をw.length 文字シフトして 2 に戻る。w の最後の文字と一致する場合、そこから逆に見ていって、w の末尾と一致する部分を出来るだけ長く集める。w と完全に一致したら、開始位置を記憶。w の末尾と部分的に一致したら、表2のシフト数を見る。w の中に含まれるはずなので、表1で該当する文字のシフト数を見る。| 文字 | シフト |
|---|---|
| D | 0 |
| B | 1 |
| A | 2 |
| C | 4 |
これはつまり、各文字が w = 'ABCDABD' の後ろから数えて何文字目に出てくるかということ。
| N | パターン | シフト |
|---|---|---|
| 0 | '' | 1 |
| 1 | D | 3 |
| 2 | BD | 7 |
| 3 | ABD | 7 |
| 4 | DABD | 7 |
| 5 | CDABD | 7 |
| 6 | BCDABD | 7 |
N=0 が使われることはないはず。
二列目は、w = 'ABCDABD' の中で 'D' が出てくる (そしてその前に 'B' が出てきてない) のは、後ろから数えて3つめだから。
もう一つルールがあって、パターンの文字の終端の部分と w の始端が重なったところがあれば、そこをシフトとすること。
分かりにくいので ANPANMAN の例 を見たほうがいい。
# 準備 s = "ABC ABCDAB ABCDABCDABDE" w = "ABCDABD" # 表1を作る table1 = {} w.split('').reverse.each.with_index {|c, i| table1[c] ||= i } # 表2を作る table2 = [] rev = w.split('').reverse i = 0 while i < rev.length + 1 j = 0 while i + j < rev.length break if rev[i+j] != rev[j] j += 1 end if i + j < rev.length # 途中で break された → 通常ルール table2[j] ||= i else # パターンの文字の終端の部分と w の始端が重なったところがある while j < rev.length table2[j] ||= i j += 1 end end i += 1 end # 検索 positions = [] i = 0 while i < s.length - w.length j = w.length - 1 while w[j] == s[i + j] if (j == 0) positions.push(i) break end j -= 1 end i += [ table2[w.length - j - 1], table1[ s[i + j] ] || w.length ].max end # split positions.push(s.length).inject(0) {|a, b| puts s.slice!(0, b-a) puts s.slice!(0, w.length) b }
結果。
ABC ABCDAB ABCD ABCDABD E
かなり面倒だけど、実は表2は使わなくてもあまり速さは落ちないらしい。
その場合はこれだけ。
s = "ABC ABCDAB ABCDABCDABDE" w = "ABCDABD" table1 = {} w.split('').reverse.each.with_index {|c, i| table1[c] ||= i } positions = [] i = 0 while i < s.length - w.length j = w.length - 1 while w[j] == s[i + j] if (j == 0) positions.push(i) break end j -= 1 end i += table1[ s[i + j] ] || w.length end positions.push(s.length).inject(0) {|a, b| puts s.slice!(0, b-a) puts s.slice!(0, w.length) b }
skkserv のプロトコルって解説してあるページがあんまりないのね。skkserv 自体についてはけっこう見つかるのに。
探してみると、ここに載ってた。
"0"
サーバへコネクションを切断するよう要求します。
"1eee "
「見出し」 eee に対する「変換文字列」を要求します。 " " (スペース)でターミネートされていることに注意が必要です。
サーバから返される「変換文字列」は / で区切られた "1/foo/bar/baz/\n" のような形式です。
サーバから返される文字列の末尾には "\n" が必要なことに注意が必要です。
「見出し」が存在しない場合は入力の先頭の "1" を "4" に変換したものをそのまま返します。 (実はプロトコル的には 4 で始まる文字列ならば何でも良いらしいですが、一部のクライアントで問題が出るとのことです。)
"2"
サーバへ「バージョンナンバー」を要求します。
サーバから返される「バージョンナンバー」は "A.B " のような形式です。 " " (スペース)でターミネートされていることに注意が必要です。
skkserv/skkserv.c に A はメジャーバージョン B はマイナーバージョンのような記述がありますが、skkserv 自体 "A.B.C " のような形式で返していますし、他のサーバではサーバ文字列を返しているものもあるので、バージョンナンバーというよりバージョン情報と表現すべきなのかもしれません。
"3"
サーバへ「サーバのホスト名と IP アドレスのリスト」を要求します。
サーバから返される「サーバのホスト名と IP アドレスのリスト」は "hostname:addr:[addr...:] " のような形式です。 " " (スペース)でターミネートされていることに注意が必要です。
yaskkserv では未実装です。(ダミー文字列が返されます。)
"4eee "
「見出し」 eee で始まる見出しを要求します。" " (スペース)でターミネートされていることに注意が必要です。
サーバから返される「見出し」は / で区切られた "1/foo/bar/baz/\n" のような形式です。
サーバから返される文字列の末尾には "\n" が必要なことに注意が必要です。
これは新しいプロトコルで、今のところきちんとした定義は無いようです。
だそうな。
手元の AquaSKK が skkserv サーバーにもなるので、それで試してみたところ、どうやら "4" には対応していないようだった。
というわけで早速書いてみた。
#!/opt/local/bin/ruby19 # -*- coding : utf-8 -*- require "socket" require "thread" port = 1178 s = TCPSocket.open("localhost", port) s.set_encoding("EUC-JP") req = nil t = Thread.new do l = "" s.each_char do |c| l << c if req == 1 && c == "\n" puts l.encode("UTF-8") puts "\n" l = "" elsif (req == 2 || req == 3) && c == " " puts l.encode("UTF-8") puts "\n" l = "" end end end s.write("2") req = 2 sleep 0.2 s.write("3") req = 3 sleep 0.2 s.write("1a ") req = 1 sleep 0.2 s.write("1あ ") req = 1 sleep 0.2 s.write("1ほ ") req = 1 sleep 0.2 s.write("0") req = 0 t.join s.close
結果
AquaSKKServer1.0 127.0.0.1:0.0.0.0: 1/α;alpha/エー/エイ/アー;(独語)/а;cyrillic/ア/ 1/亜/吾;私/彼;=吾/阿;阿呆/婀;婀娜っぽい/痾;宿痾/唖;聾唖/亞;「亜」の旧字(人名用漢字)/椏;また/娃;美女/哇/襾/安;?/明;?/嗚;?/ 1/帆/穂/補/歩/保/火;(古訓)火群/舗;店舗/鋪;≒舗/舖;「舗」の旧字/輔;輔佐/捕;逮捕/圃;圃場/堡;橋頭堡/葆/畝;うね/浦;うら/甫;杜甫/哺;哺乳/匍;匍匐前進/葡;葡萄酒/埔/脯;ほじし/餔;(くらう)/鯆;イルカ,サバ/黼;(縫取り)/蒲;蒲公英/逋;にげる/穗;「穂」の旧字(人名用漢字)/
IO#set_encoding に気づかなくて、Encoding.default_external を弄ったりしてたのがハマったところ。
なんで 2 & 3 と 1 で区切り文字が違うのかは不思議なところだが、普通に使うぶんには 2 や 3 は使わないので each_line で OK のはず。