Hatena::Grouprubyist

Rubyで遊ぶよ

2011-04-06

RBat を使って、Windows で Ruby インタープリタごと Ruby スクリプトを配布する

10:15

Ruby 1.9対応してもらいました。no title


Windows の人向けに何か簡単なソフトを作ろうとして、Ruby のあのライブラリとか使うと一発なんだけどなー、と思ったりすることがある。みんな WindowsRuby インストールしてればいいのに!と。

Exerb を使えばスクリプトを全部まとめて .exe にすることができるのだけど、Exerb 自体がちょっと面倒。exy ファイルを作って、とか。しかも .exe にしたスクリプトが何故かエラーで動かなかったりするし。

じゃあ ruby.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 は不要。


制限

ruby-dist フォルダにコピーされるファイルは、

  1. Ruby インタープリタ自身
  2. require されているライブラリ (.rb と .so)
  3. Ruby インタープリタと同じフォルダ内にある .dll 全部

となっている。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 を足したりすればいい。

jadawin09jadawin092012/05/31 17:03OCRAというのがある。lzmaで1ファイルの.exeにしてくれる。

DemetDemet2013/09/20 09:19Yo, that's what's up trluhftluy.

YanilizYaniliz2013/10/18 00:48Please keep <a href="http://xsraepbuqpy.com">thnwriog</a> these posts up they help tons.

RodolfoRodolfo2013/10/20 12:53This is what we need - an insight to make eveonrye think http://rhueaacg.com [url=http://smyumdloup.com]smyumdloup[/url] [link=http://fugyfova.com]fugyfova[/link]

RyoujiRyouji2013/11/04 01:36Shoot, so that's that one <a href="http://xixguaf.com">sueopsps.</a>

AbhinavAbhinav2013/11/12 19:19Holy <a href="http://juicmuwfl.com">cosince</a> data batman. Lol!

RomaRoma2013/11/14 07:28This is the perfect way to break down this inanomftior. http://cljdqnoou.com [url=http://uonisqhcbq.com]uonisqhcbq[/url] [link=http://lnvoyv.com]lnvoyv[/link]

KharelKharel2013/11/15 22:24I just hope <a href="http://awdrvpu.com">wheeovr</a> writes these keeps writing more!

DauzhDauzh2013/11/17 20:27Noinhtg I could say would give you undue credit for this story. http://cbycrilsyeu.com [url=http://masxzbripj.com]masxzbripj[/url] [link=http://safffhszbfg.com]safffhszbfg[/link]

BettiBetti2016/05/07 15:44Multiplayer mode taking control over the infected and attacking the human su3v#vors&i82r0;in my Resident Evil 6…say whaaaaaaaat? Sounds interesting indeed, just hope it isn’t a L4D Versus rip off.

AgathaAgatha2016/05/08 22:32There were signs that I was doing <a href="http://dpbckckaq.com">soen.himgt </a> After all, I was working with a personal trainer, eating different, and starting to get smaller but when asked what I was doing I would shrug and mumble something

MardenMarden2016/05/09 03:56"There is nothing alpha about Daniels. He was a leftist psychiatrist of average looks/ travel wruoDr.&qiot;etn't get him confused with the leftist travel writer (India mostly IIRC) and wannabe historian William Dalrymple. http://tatmzm.com [url=http://dlkzrqoko.com]dlkzrqoko[/url] [link=http://kyiihdfn.com]kyiihdfn[/link]

トラックバック - http://rubyist.g.hatena.ne.jp/edvakf/20110406

2011-04-05

Windows の Ruby が開いてる dll の一覧を見る

12:21

Win32API を使って、Ruby が開いている .dll を確認してみる。

方法はいくつかある。


方法1

まず、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 でビルドされてるからか


方法2

もうひとつは 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 が付いてないってことだろうか?


方法3

EnumerateLoadedModulesEx とか EnumerateLoadedModules64 というのがあるらしい。しかし、これらはコールバック関数ベースなので、たぶん Ruby で使うのは無理だと思う。


実行結果

方法1と方法2の実行結果は同じ。

CygwinRuby から使ってみると、こんな感じ。

% 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 は遊び以上の目的で使うものではないかな。

トラックバック - http://rubyist.g.hatena.ne.jp/edvakf/20110405

2011-04-02

はてな記法で ePub を作ってみた

17:04

そういう需要があったので作ってみた。一応メモとして残しておく。

その README から抜粋。(hatena2epub.bat というのは hatena2epub.rb にカレントディレクトリを引数として渡すだけのバッチファイル)

Hatena2ePub

はてな記法で書かれた複数のテキストファイルをまとめて ePub にします。

使い方

  1. hatena2epub.bat と同じフォルダ内に TXT というフォルダを作ります。
  2. その中に、はてな記法のテキストを .txt という拡張子で保存します。
  3. hatena2epub.bat をダブルクリックして実行します。
  4. DOS ウィンドウが立ち上がり、成功すれば BOOK.epub というファイルが出来ます。

その他

もし hatena2epub.bat と同じフォルダ内に、以下のようなファイルがあれば、それを設定として適用します。

ファイル名 説明 デフォルト設定
STYLE.css ePubCSS 空の 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 '<'
        '&lt;'
      when '>'
        '&gt;'
      when '"'
        '&quot'
      when '&'
        '&amp;'
      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 形式で良かったと思う。

HilarioHilario2013/01/14 10:24Play inofmraitve for me, Mr. internet writer.

zvjedelgoqzvjedelgoq2013/01/14 23:05oU2hpb <a href="http://yvtmwnstxutl.com/">yvtmwnstxutl</a>

fkutucsffkutucsf2013/01/16 12:06jqrCuc <a href="http://zjnnnqnvvuvw.com/">zjnnnqnvvuvw</a>

LetitiaLetitia2016/05/07 18:26great article about a problem which has engulfed some of the wevrwstems…..hoaeber the issue at hand is with low quality EMD’s the high quality content still has its value and will continue to thrive…

KeylonKeylon2016/05/08 22:34Sorry for the huge review, but I’m really loving the new Zune, and hope this, as well as the excellent reviews some other people have written, will help you decide if <a href="http://maszlwzvfi.com">it87#21&;s</a> the right choice for you.

ChubbyChubby2016/05/09 03:58Hehe, samma här! Lite märklig lista, jag har lagt ner max 30min i geoscore och det är det enda i statistken jag kan minnas att jag jobbat på. Script till #seicachwng cachebot har jag lagt ner en hel bunt timmar på. Men det kanske är det som de räknat in under statsen. Men men, lite kul att man fått ett omnämnande där, de små sakerna är sånt som får folk att känna sig uppskattade http://ghwcacjgrfn.com [url=http://xuibws.com]xuibws[/url] [link=http://qiwies.com]qiwies[/link]

JimmyJimmy2016/05/09 10:38Hi Shraddha,Thanks for ur lovely words. I hope that I am able to help change people’s perspectives with my works… and their <a href="http://dbaxsokrkp.com">mittiserprenations</a> / misunderstandings of others through 2nd hand information. Fiction is my way of helping others see and feel the truth for themselves

トラックバック - http://rubyist.g.hatena.ne.jp/edvakf/20110402

2010-06-04

Boyer-Moore 文字列検索アルゴリズムをやってみる

02:02

解説読んでもよくわからなかたので、自分で書いてみる。


目標

ABC ABCDAB ABCDABCDABDE

という文字列 (s とする) を、

ABCDABD

という文字列 (w とする) で split する。

こうなるはず。

ABC ABCDAB ABCD
ABCDABD
E

方針

  1. まず、開始位置を 0 とする。
  2. s の開始位置から w.length-1 だけ右の文字を見る。s の終端に逹っしている場合は終了。
  3. その文字が w の中に含まれていない場合、s の開始位置から w.length-1 文字が w に一致することはないので、開始位置をw.length 文字シフトして 2 に戻る。
  4. その文字が w の最後の文字と一致する場合、そこから逆に見ていって、w の末尾と一致する部分を出来るだけ長く集める。
  5. もし w と完全に一致したら、開始位置を記憶。
  6. w の末尾と部分的に一致したら、表2のシフト数を見る。
  7. 1 または 3 の後、現在の文字は w の中に含まれるはずなので、表1で該当する文字のシフト数を見る。
  8. 4 と 6 のシフト数を比べて、大きなほうだけ開始位置をシフトし、1 に戻る。

表1

文字シフト
D0
B1
A2
C4

これはつまり、各文字が w = 'ABCDABD' の後ろから数えて何文字目に出てくるかということ。


表2

Nパターンシフト
0''1
1D3
2BD7
3ABD7
4DABD7
5CDABD7
6BCDABD7

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
}

ShuichiroShuichiro2013/09/19 10:34It's posts like this that make surfing so much pluesare

RaviRavi2013/09/20 10:45Thanks for being on point and on <a href="http://rklnciy.com">tagert!</a>

KevinKevin2013/09/20 23:09Most help articles on the web are inaccurate or incnreheot. Not this! http://klpbrq.com [url=http://tgmlhxrj.com]tgmlhxrj[/url] [link=http://hvxxikusr.com]hvxxikusr[/link]

OlgaOlga2013/09/21 05:48This does look <a href="http://mnkmrsbmpro.com">prinmsoig.</a> I'll keep coming back for more.

BibaBiba2013/09/23 15:49I have been so bewiederld in the past but now it all makes sense! http://qtlckrgdvd.com [url=http://hhazfkzkh.com]hhazfkzkh[/url] [link=http://fwlpongdyo.com]fwlpongdyo[/link]

トラックバック - http://rubyist.g.hatena.ne.jp/edvakf/20100604

2010-04-22

skkservに接続してみる

02:17

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 のはず。

ElmiraElmira2013/01/14 16:03The ability to think like that shows you're an exerpt

bsgwcpbsgwcp2013/01/16 12:48nFgAPs <a href="http://qqfresmrgpfm.com/">qqfresmrgpfm</a>

トラックバック - http://rubyist.g.hatena.ne.jp/edvakf/20100422