プログラミング雑記帳
ここでは、プログラミングに関するちょっとした覚え書きをメモとして残します。
このWebページのボリュームが大きくなったので、 新しい「プログラミング雑記帳」 を設けました(2014.10.05)。
- □ kramdown ver 1.3.1 によるpdfファイルの生成
- □ ruby 2.0.0のfiddleとWin32API
- □ MS-Windowsでのクリップボード操作
- □ ADOによるExcel2007ファイルへのデータ書き込み(rubyスクリプト)
- □ ADOでExcel2007のファイルを読み書きする(rubyスクリプト)
- □ Hashを構造体のようにして扱う
- □ Excelでクロス集計表をカイ二乗(χ2)検定・xmlss生成版
- □ Excelでクロス集計表をカイ二乗(χ2)検定
- □ tv_yahoo2.rbの修正版
- □ MS-Windowsにおけるドライブ情報の取得、システムエラー出力の抑制
- □ htmlの数値文字参照(&#……;)とhpricot
- □ htmlのtable(セル結合あり)をrubyの配列に変換するメソッド・その2
- □ htmlのtable(セル結合あり)をrubyの配列に変換するメソッド
- □ MS-AccessのQuery Form Reportなどの中身をテキストファイルに出力
- □ input.xmlと実質的に同じoutput.xmlを作るruby scriptの自動生成
------------
□ kramdown ver 1.3.1 によるpdfファイルの生成
**rubyライブラリのkramdown&prawnにより、LaTeXなしでmarkdownをpdfに変換する方法について、以下の記事に改良を加えたものをkpdfについてに掲載しました(2015.01.28)。よかったら覗いてみて下さい。
最終更新日: 2014/02/04
kramdownは、markdownの原稿をhtmlやLaTeXの文書に変換する機能を持つruby用のライブラリです。
その kramdown の ver 1.3.1 が2014/01/10にリリースされました。
このバージョンでは、markdownの原稿をpdfファイルとして書き出すことができるようになっています。
いったんLaTeX文書に変換した上で、それを別のコマンドでpdfに変換する、といったことではなく、直接pdfファイルを書き出すことができます。
ただし、prawnというruby用のライブラリを利用するので、そのprawnがインストールされている必要があります。
といっても、kramdown も prawn も、gemで簡単にインストールできます。
gem install kramdown [enter] gem install prawn [enter]
上のように実行すると、それぞれ最新版がインストールされます。
ところで、本題のpdfファイルの生成ですが、markdownの原稿が変数 mkd_str に代入されている場合、次のようにします。
pdf_str = Kramdown::Document.new(mkd_str).to_pdf File.open("result.pdf", "wb") {|ff| ff.write pdf_str}
上に出てくる「to_pdf」が変換の要点です。
参考まで、実際に動かすことのできるrubyスクリプトを掲げてみます。
−−−−−−−− # encoding: utf-8 require "kramdown" mkd_str = <<EOS # test for PDF\n kramdown ver 1.3.1 was released on 10, January in 2014.\n This release brings a pure Ruby PDF converter for kramdown based on the Prawn library. EOS pdf_str = Kramdown::Document.new(mkd_str).to_pdf File.open("result.pdf", "wb") {|ff| ff.write pdf_str} −−−−−−−−
さて、問題はここからです。上のスクリプトで、markdownの原稿中にマルチバイト文字の日本語を含めると、生成されたpdfに日本語がうまく盛り込まれません。
そこでフォントを指定しようということになりますが、私のみたところでは、フォントを指定するための十分な仕組みが用意されていないように思います。
kramdown の pdf.rb をみると、次の4つのメソッドでフォントの指定が行われています。メソッド名と、その中で指定されているフォント名を列記します。
- root_options(): Times-Roman
- header_options(): Helvetica
- codeblock_options(): Courier
- codespan_options(): Courier
4つのメソッドのいずれも、戻り値は Hash です。関連のオプション群の値を保持したHashを返します。
メソッド名から推測するに、root_options() で指定されているフォントが、全体を通して用いられる基本のフォントでしょうし、header_options() で指定されているのは、見出しに適用するフォントだろうと思います。codeblock_options(), codespan_options() は、文書中に貼り付けられているプログラムソースなどに適用されるものだと思います。
で、日本語を扱えるようにする方法ですが、粗っぽいやり方ながら、上の4つのメソッドを再定義するという方法が考えられます。
日本語のフォントは、例えば ipaexm.ttf のような、拡張子がttfのファイル名で指定します。
日本語を扱えるLaTeXをインストールしているのであれば、おそらくipaフォントが既にどこかにあると思いますが、それを使わない場合は、次のサイトからダウンロードできます。
IPAexfont00201.zipをダウンロードした場合でいうと、まず、それを適当なディレクトリで解凍します。私の場合、MS-Windows環境下で「C:/usr/font/」の下に解凍しました。
すると、ipaexm.ttf(明朝体)、ipaexg.ttf(ゴシック体)の2つのフォントファイルと関連ファイルが出てきます。このttfファイルのフルパス名が必要になります。
ちなみに、後述のサンプルスクリプト kpdf.rb では、「C:/usr/font/IPAexfont00201/ipaexm.ttf」などのような記述をしています。
kramdown の pdf.rb に話を戻します。root_options() の中には
{:font=>'Times-Roman', :size=>12, ……}
という記述がありますが、「Times-Roman」のところを「ipaexm.ttf」(ほんとはフルパス名で書く)のようにします。
サンプルスクリプト kpdf.rb は、4つのメソッドを再定義しているので長くなってしまいます。なので、ここには貼り付けません。 kpdf.rb としてアップロードしておきますので、よかったら覗いてみて下さい。文字コードは utf-8 です。
kpdf.rbを実際に動かす時は、その中に出てくる変数 dir に適切な値を代入して下さい。ttfファイルの所在ディレクトリを代入します。
また、$kfont.root, $kfont.header などに、実際に利用できるフォントファイル(*.ttf)の名前を代入して下さい。
このサンプルでは、kramdownのオリジナルのPdfクラスを上書き変更するのでなく、Pdf2という派生クラスを別に設けて、それを呼び出しています。
「to_pdf」ではなく「to_pdf2」とすれば、派生クラスであるPdf2による変換が行われます。
Pdf2クラスは、基本的にPdfクラスと同じですが、フォント設定に関わる4つのメソッドだけが違います。
もっとスマートな方法があるような気はしますが、とりあえず思いついたものをメモしました。
この項 おわり。
------------
□ ruby 2.0.0のfiddleとWin32API
最終更新日: 2013/09/08
MS-Windowsのuser32.dllなどを利用する際、これまでは Win32API.new('user32', ……) を使ってました。
user32.dllの中で定義されている関数を利用する場合、例えば、MessageBoxを使う時だと次のようにしてました。
Mbox = Win32API.new('user32', 'MessageBox', %w(i p p i), 'i') Mbox.call(0, "OKですか?", "確認", 33)
MessageBox関数は、引数としてint型,ポインタ2つ,int型の4つをわたします。戻り値はint型です。
そのことが Win32API.new(……) で記述されています。
さて、ruby 2.0.0 になってから、
*.dll, *.so などを利用する際、Win32API とか dl というライブラリが非推賞になってしまいました。
それらライブラリが使えないわけではありませんが、使った時に「DL is deprecated, please use Fiddle」という警告メッセージが出ます。
スクリプトは正常に実行されますが、こういうメッセージが出ると、なんとなく正しく実行されなかったのでは?という不安な感じが残ります。
そこで、fiddleを使ってスクリプトを書き直すということになりますが、具体的には次のようになります。MessageBoxの場合を記します。
# (coding: Windows-31J) require "fiddle/import" module W extend Fiddle::Importer dlload "user32.dll" extern "int MessageBox(int, char*, char*, int)" end W.MessageBox(0, "OKですか?", "確認", 33)
MessageBox() の第2引数と第3引数は、ポインタ(char*)を引きわたすことになっていますが、上のように、rubyの文字列をそのまま書いても大丈夫のようです。
でも、それだと不安だという人は、ちゃんとポインタをわたすのがいいかもしれません。
ポインタは、「require "fiddle"」を書いた上で、「Fiddle::Pointer[str]」のように書くみたいです。
参考まで、Fiddle::Pointerを用いた例も示してみます。
# (coding: Windows-31J) require "fiddle" require "fiddle/import" module W extend Fiddle::Importer dlload "user32.dll" extern "int MessageBox(int, char*, char*, int)" end W.MessageBox(0, Fiddle::Pointer["OKですか?"], Fiddle::Pointer["確認"], 33)
というようなことを踏まえて、MS-Windowsのクリップボードを操作するモジュル Rclip を作り直しました。
rclip.rbとして私のサイトにアップロードしました。よかったらダウンロードして下さい。
ruby 1.8.x, 1.9.x, 2.0 のいずれでも警告メッセージなしで動作するはずです。
エンコードとして utf-8 を書いてありますが、必要に応じて Windows-31J などに変更して下さい。
rclip.rbの最後の方に、=begin, =end で囲む形で利用サンプルを示してありますが、削除してかまいません。
この項 おわり。
------------
□ MS-Windowsでのクリップボード操作
最終更新日: 2013/04/23
rubyから統計解析ソフトRを使うためのライブラリrrxを作りましたが、Rがクリップボードの入出力をサポートしているので、rrxでもサポートしたいと考えました。
なるべくインストール作業なしで使えるものにしたかったので、rubyの標準添付ライブラリで実現しようと試みました。で、Windows環境限定ではありますが、なんとか作ることができました。
クリップボード操作のためのモジュールをrrxwin.rbに Rclip という名前で組み込んであります。
その仕様についてはrrxwin02を参照して下さい。
ruby ver 1.8系であれば、
http://tailcodenote.shisyou.com/code/ruby/006.html
で紹介されている方法でクリップボードを操作することができます。
難しかったのは ruby ver 1.9系で同じことを行うノウハウです。
標準添付ライブラリ Win32API の仕様が違っているため、同じやり方を適用できません。
第1点目: クリップボードに記録されているデータを取得する際に、そのデータがあるメモリ領域のポインタを得て、それが指し示す領域を得るという形を採ります。
ruby 1.8系だと、ポインタ ptr がメモリ領域のデータをダイレクトにあらわすようです。
簡略化してしまうと、「data = ptr」でいいわけです。
しかし、ruby 1.9系では違います。ある意味、プログラミング的には妥当な形に是正したといえますが、少々ややこしいです。
「data = DL::CPtr[ptr].to_s」とします。
この場合、予め「require "dl"」をやっておく必要があります。
第2点目: クリップボードにデータを書き出す場合、やはりクリップボード用のメモリ領域のポインタを取得して、その領域にrubyの文字列をコピーしてやります。
ruby 1.8系だと、kernel32.dllのlstrcpyを用いて、簡単に文字列をコピーできます。
lstrcpyは次のように定義されます。
lstrcpy = Win32API.new('kernel32', 'lstrcpyA', ['P', 'P'], 'P')
ところが、ruby 1.9系でこのlstrcpyを使おうとしても、なぜか うまくいきません。いろいろ試みてみましたが駄目でした。
結局、msvcrt.dllのmemcpyを使うことにしました。その定義は次のとおりです。
memcpy = Win32API.new('msvcrt', 'memcpy', ['I', 'P', 'I'], 'I')
ruby 1.9系で kernel32.dllのlstrcpy を使う方法はあるのだろうと思いますが、どうしたらいいんでしょうか。
この項 おわり。
------------
□ ADOによるExcel2007ファイルへのデータ書き込み(rubyスクリプト)
最終更新日: 2011/08/24
前項:8月18日の「ADOでExcel2007のファイルを読み書きする(rubyスクリプト)」で、『test.xlsxの中に「名前付き範囲」の定義があれば Sheet1 を使い、ない時に限って [Sheet1$] を使うようにする、そうすればトラブルなく test.xlsx を読み書きできるようです。』と書きました。参考まで、これに関連する一つのサンプルスクリプト write_xlsx.rb を掲げておきます。
MS-Windowsのコマンドラインで、「ruby write_xlsx.rb ↓」と入力すれば動きます。
使っているrubyライブラリは win32ole のみです。
Excel2007以降がインストールされていないと、スクリプトが正しく動作しないので注意して下さい。
カレントフォルダに test01.xlsx, test02.xlsx が存在しない状態で実行して下さい。
このスクリプトの処理の流れは次のとおり。
- Excelを起動して test01.xlsx を作成。ワークシート名は sheet_by_excel。これには「名前付き範囲」の定義がない。
- ADOを利用して test02.xlsx を作成。ワークシート名は sheet_by_ado。これには「名前付き範囲」の定義があり、その名前はワークシート名と同じ。
- ADO利用により test01.xlsx, test02.xlsx のそれぞれにデータを書き込む。insert命令によるレコード挿入、RecordsetのAddNewによるレコード挿入、この2種類の方式を含む。
このスクリプトの要点は、「名前付き範囲」の定義がない時は、ワークシート名として [Sheet1$] を使い、定義がある時は Sheet1 を使うという点です。
「名前付き範囲」の定義があるかどうかの確認は、test.xlsx のテーブル名一覧を取得してみれば分かります。一覧の中に Sheet1 と Sheet1$ の両方があれば、「名前付き範囲」の定義があることになります。一方、Sheet1$ しかなければ、ワークシートはあるものの、「名前付き範囲」の定義がないことになります。
テーブル名一覧の取得は、ADOX により行います。一覧を得るためのメソッドを次に掲げます。
def table_names(cn) res = [] cat = WIN32OLE.new("ADOX.Catalog") cat.ActiveConnection = cn cat.Tables.each {|table| res << table.Name } cat = nil return res end
上のメソッドの引数 cn は、ADODB.Connection オブジェクトです。ADOの接続用文字列が conn_str であるとき、次のようにして生成・オープンされるものです。
cn = WIN32OLE.new("ADODB.Connection") cn.Open conn_str
以下、ちょっと長めですが、write_xlsx.rb を掲げておきます。
−−−− ここから #! ruby -Ks require "win32ole" # フルパス名に変換 def full_pathname(filename) fso = WIN32OLE.new("Scripting.FileSystemObject") return fso.GetAbsolutePathName(filename) end # データベースのテーブル名一覧を得る def table_names(cn) res = [] cat = WIN32OLE.new("ADOX.Catalog") cat.ActiveConnection = cn cat.Tables.each {|table| res << table.Name } cat = nil return res end # Excelを動かして test01.xlsx を作成 filename = full_pathname("test01.xlsx") sheet_name = 'sheet_by_excel' xl = WIN32OLE.new("Excel.Application") xl.Visible = true wb = test(?e, filename) ? xl.Workbooks.Open(filename) : xl.Workbooks.Add() ss = wb.Worksheets.Item("Sheet1") # Sheet1を処理対象に ss.Name = sheet_name [%w(氏名 身長 誕生日 資格の有無), ['佐藤', 167.4, '1992/3/4', false], ['鈴木', 180.6, '1994/1/9', false]].each_with_index do |row, i| r = i + 1 ss.Range(ss.Cells.Item(r,1), ss.Cells.Item(r,row.size)).Value = [row] end xl.DisplayAlerts = false # 確認ダイアログを表示しないための表示停止 wb.SaveAs filename, 51 xl.Workbooks.Close xl.Quit # Excelの終了 # ADO利用により test02.xlsx を作成 filename = full_pathname("test02.xlsx") sheet_name = 'sheet_by_ado' conn_str = "Provider=Microsoft.ACE.OLEDB.12.0;" + "Data Source=#{filename};" + "Extended Properties=\"Excel 12.0 XML;HDR=Yes;\"" cn = WIN32OLE.new("ADODB.Connection") cn.Open conn_str # テーブル(ワークシート)の作成 sql = "create table #{sheet_name} " + "(氏名 varchar, 身長 float, 誕生日 date, 資格の有無 bit);" cn.Execute(sql) sql = "insert into #{sheet_name} values('高橋', 172.3, #1994/10/15#, 1);" cn.Execute(sql) sql = "insert into #{sheet_name} values('津島', 180.6, #1994/1/9#, 0);" cn.Execute(sql) cn.Close cn = nil # ADO利用により test01.xlsx, test02.xlsx にデータを書き込む puts 'Excelファイルにデータ書き込み' file_list = ['test01.xlsx', 'test02.xlsx'] sheet_list = ['sheet_by_excel', 'sheet_by_ado'] while filename = file_list.shift do puts "file name: #{filename}" conn_str = "Provider=Microsoft.ACE.OLEDB.12.0;" + "Data Source=#{full_pathname(filename)};" + "Extended Properties=\"Excel 12.0 XML;HDR=Yes;\"" cn = WIN32OLE.new("ADODB.Connection") cn.Open conn_str # 用いるべきテーブル名を検証(Sheet1 | [Sheet1$]) sheet_name = sheet_list.shift list = table_names(cn) tblname = list.include?(sheet_name) ? sheet_name : "[#{sheet_name}$]" puts "table name: #{tblname}" # insert命令によるレコード挿入 sql = "insert into #{tblname} values('吉田', 163.6, #1985/12/3#, 0);" cn.Execute(sql) # RecordsetのAddNewによるレコード挿入 sql = "select * from #{tblname};" rs = WIN32OLE.new("ADODB.Recordset") rs.Open sql,cn,3,3 # 読み書き両用・自由移動形式 rs.MoveFirst field_names = [] rs.Fields.each do |fld| field_names << fld.Name end datas = [%w(渡辺 173.4 1962/8/7 0), %w(安部 169.8 1983/10/15 1)] for rec in datas rs.AddNew rec.each_index {|i| rs.Fields.Item(field_names[i]).Value = rec[i] } rs.Update end rs.Close cn.Close end −−−− ここまで
[補足] うまく実行できないsql命令
サンプルスクリプト内では、sql命令として create table および insert を実行しています。その他、私が試したところでは update も問題なく実行できました。
しかし、「select * into new_sheet from …… where ……」のようにして、条件に合うレコードを抽出してその結果を新しいワークシートに書き出すのは、うまく実行できませんでした。新しいシート new_sheet が設けられはするものの、レコードが書き出されません。のみならず、既存のワークシートの中身が奇妙に書き換えられてしまいます。この問題を回避する方法は分かりません。
このトラブルは、Excel2003用の test.xls を処理する時は起きませんが、Excel2007用の test.xlsx を扱う時に発生します。そういう仕様なのか、何か回避策があるのか、よく分かりません。
いずれにしても、Excelファイルをsql命令で扱う時は、レコード削除のdeleteが実行できない、仮想テーブルを使えない、等々いろいろ制限があるので、sqlを本格的に使いたいのであれば、Accessなどのデータベースファイルで処理しておいて、最終的に欲しい結果をExcelファイルで得る、そんなふうにするのが無難な感じがします。
この項 おわり。
------------
□ ADOでExcel2007のファイルを読み書きする(rubyスクリプト)
最終更新日: 2011/08/18
MS-Windows の ADO(ActiveX Data Objects)という仕組みを利用して、Excel2007のファイルを読み書きすることについて、少々確認してみたので記します。rubyスクリプトによる読み書きです。
ここでは test.xlsx を新たに作成し、データを書き込みます。また、その test.xlsx のデータを読み込んで、標準出力にタブ区切りテキストの形で出力します。
こうした処理を行う上で、私がつまずいたのは次の2点です。
(1) ADO接続用文字列「Provider=Microsoft.ACE.OLEDB.12.0;……」の記述
この文字列中に「Excel 12.0」というのが出てきます。Excel2003のファイル test.xls を処理するのであれば「Excel 8.0」とすべきものです。
しかし、試してみると、ruby実行時にエラーは出ないものの、作成された test.xlsx をExcelで開こうとすると、エラーメッセージが出て正常に開くことができません。
結果的に、「Excel 12.0」ではなく「Excel 12.0 XML」とすれば、問題なく開けるtest.xlsxを作成できました。少なくとも、私の MS-Windows VISTA, Office2007 環境ではそうでした。
(2) ワークシート名の記述
sql命令によってExcelファイルを操作する場合、「create table」の時を除いて、テーブル名(即ちワークシート名)を記述する時は、[Sheet1$] のように、ドル記号を末尾に付けた上で [……] で囲みます。insert, select などの命令を書く場合は、そうするものだと思っていました。
しかし、その流儀でやると、なぜか test.xlsx の中のデータが全て文字列型になります。データ型を日付型(date)にしていたはずなのに、文字列型になってしまいます。bit(YesNo)型にしたはずのものも文字列型になり、True, False ではなく、文字としての 1, 0 になります。ちなみに、Bit型というのは、ExcelでいうとBoolean型です。
この原因が分からないまま放置していましたが、ワークシート名を [Sheet1$] ではなく普通に Sheet1 にしてみたところ、意図したとおりのデータ型になることが分かりました。いったいどういうことなのか首をかしげたくなりますが、ともあれ私の環境ではトラブル回避できます。
なお、ADOでExcelファイルを作成してレコード挿入すると、Sheet1などのワークシート名が「名前付き範囲」の名前にもなります。例えば、test.xlsxに4列・5行のデータを書き込んだ場合、その4列・5行が「名前付き範囲」として定義され、その名前がSheet1などのワークシートと同じ名前になります。
そのため、sqlのinsertやselect命令でテーブル名を記述する際、 [Sheet1$] のように、明示的にワークシート全体を指し示す記述方法を採らなくても、Sheet1 だけで大丈夫なわけです。
いくつかのスクリプトを書いて試してみたところ、test.xlsxの中に「名前付き範囲」の定義があれば Sheet1 を使い、ない時に限って [Sheet1$] を使うようにする、そうすればトラブルなく test.xlsx を読み書きできるようです。
ということで(?)、下にrubyスクリプトのサンプルを載せておきます。ライブラリとしては win32ole を使っているだけです。スクリプトの大まかな流れは次のとおり。
- create table で test.xlsx 及びその中のワークシートを作成。
氏名(varchar)、身長(float)、誕生日(date)、資格の有無(bit)の4つのフィールドからなるテーブルを作成。
- sqlのinsert命令により2つのレコードを書き込む。
- RecordsetのAddNewにより2つのレコードを書き込む。
- test.xlsxの中身を読む。select命令の実行結果をRecordsetとして取得。
- 読み込んだ結果をタブ区切りテキストの形で標準出力に出力。
このサンプルスクリプトをダウンロードしたい方は rw_xlsx.rb をどうぞ。
私のところでは ruby ver 1.8.7 および 1.9.1 の両方で動作しました。
−−−− ここから #! ruby -Ks # usage: ruby rw_xlsx.rb [enter] require "win32ole" filename = "test.xlsx" sheet_name = "test_sheet" fso = WIN32OLE.new('Scripting.FileSystemObject') conn_str = "Provider=Microsoft.ACE.OLEDB.12.0;" + "Data Source=#{fso.GetAbsolutePathName(filename)};" + "Extended Properties=\"Excel 12.0 XML;HDR=Yes;\"" cn = WIN32OLE.new("ADODB.Connection") cn.Open conn_str # テーブル(ワークシート)の作成 sql = "create table #{sheet_name} " + "(氏名 varchar, 身長 float, 誕生日 date, 資格の有無 bit);" cn.Execute(sql) # sqlのinsert命令によるレコード挿入 sql = "insert into #{sheet_name} (氏名, 身長, 誕生日, 資格の有無) " + "values('高橋', 172.3, #1994/10/15#, 1);" cn.Execute(sql) # フィールド名列挙を省略したinsert命令 sql = "insert into #{sheet_name} values('鈴木', 180.6, #1994/1/9#, 0);" cn.Execute(sql) # RecordsetのAddNewによるレコード挿入 field_names = %w(氏名 身長 誕生日 資格の有無) datas = [%w(佐藤 167.4 1992/3/4 0), %w(若林 165.1 1984/11/13 1)] sql = "select * from #{sheet_name};" rs = WIN32OLE.new("ADODB.Recordset") rs.Open sql,cn,3,3 # 読み書き両用・自由移動形式 for rec in datas rs.AddNew rec.each_index {|i| rs.Fields.Item(field_names[i]).Value = rec[i] } rs.Update end rs.Close # レコードの読み込み fld_names = [] # フィールド名記録用変数 sql = "select * from #{sheet_name};" rs.Open sql,cn,0,1 # 読取専用・前から後ろに逐次の形式 rs.MoveFirst rs.Fields.each do |fld| fld_names << fld.Name end tab_text = "" rs.MoveFirst until rs.EOF rec = [] rs.Fields.each do |fld| val = fld.Value if val.class == Array # バイナリ型 val = val.pack("C*") end rec << val end tab_text += rec.join("\t") + "\n" rs.MoveNext end rs.Close puts fld_names.join("\t") print tab_text cn.Close −−−− ここまで
この項 おわり。
------------
□ Hashを構造体のようにして扱う
最終更新日: 2011/06/01
rubyにおけるHashは、文字列をキーにすることができる配列(連想配列)です。
dog['name'] = "John" dog['age'] = 4 dog['body']['tail'] = 1 dog['body']['paw'] = 4
上のような記述ができます。
しかし、これを次のように書くことができれば、より便利です。
dog.name = "John" dog.age = 4 dog.body.tail = 1 dog.body.paw = 4
このようにピリオド記号でつなげて書く方法は、構造体の記述方法です。
このような記述がしたければ、素直に構造体を使うべきかもしれませんが、普通の構造体だと、構造体定義の時に予めキーになるものを決めておかなければなりません。Hashに比べると柔軟性に欠けます。
柔軟に利用できる構造体として、rubyのライブラリにostructがあります。これを使えば、キーを予め決めておく必要はありません。
ただ、Hashとしても使いつつ、記述方法の便利さを取り入れたい、ということになると、method_missingメソッドの定義を含むHashクラスの再設定になるでしょうか。
次の2つのwebサイトを参考にして、構造体的に扱えるHashの定義を考えてみました。
以下に、私なりに考えた module, class を記します。 hash_struct.rb と同じ内容です。
−−−− ここから #! ruby -Ks # Hashを構造体のように使えるようにするためのモジュール module HashStruct NoError = true def method_missing(sym, *arg) name = sym.to_s case arg.size when 0 arg = nil when 1 arg = arg[0] end if name =~ /^(.+)=$/ key = $1 res = arg if arg self[key] = arg end elsif self.member?(name) res = self[name] else if NoError self[name] = {}.structize res = self[name] else STDERR.printf("method missing [%s]\n", name) res = nil end end return res end end class Hash def structize unless @structize_switch extend HashStruct @structize_switch = true end return self end end ## test dog = {}.structize dog.name = "John" dog.age = 4 dog.body.tail = 1 dog.body.paw = 4 p dog −−−− ここまで
module HashStruct と class Hash を取り込むことによって、構造体的な記述でHashを扱うことができるようになります。
ただし、総てのHashオブジェクトがそうなるわけではありません。Hashに追加されている structizeメソッドを呼び出さない限り、従来のHashと同じ記述方法しか採れません。
つまり、従来型のままにしておくか、あるいは構造体的にするかをオブジェクトごとに選択できます。
Hashオブジェクトに追加した structizeメソッドは、そのオブジェクトが既に構造体化されている時は何もしません。まだ構造体化されていない場合に限って働きます。
それから、定数 NoError を false にすると、未設定のものの値が nil になります(サンプルでは true にしてあります)。
例えば、dog.bodyに値をセットしないうちに dog.body.tail を呼び出すと、当然ながらエラーが発生します。
Hash形式で書くと、dog['body'] に値をセットしていない(つまりnilになっている)状態で dog['body']['tail'] を利用しようとしたわけですから、エラーが起きます。既存のルールに従うなら自然な挙動といえます。
しかし、それだと不便なこともあるので、NoErrorをtrueにすれば、いきなり dog.body.tail を扱えるようになります。
この場合、dog.body には structize されたHashオブジェクトが自動的にセットされ、その上で dog.body.tail が呼び出されます。
実は、拙作 exlap_cライブラリ(Excel用のxmlスプレッドシート作成用ライブラリ)において、各々のセルをHashオブジェクトとして処理していますが、それを構造体的に記述できるようにすれば便利かなと思い、今回 提示したモジュールを組み込もうかと考えています。
その場合、いきなり cell.Font.Size のような書き方ができないと不便なので、上記のような NoError を設けてみました。
この項 おわり。
------------
□ Excelでクロス集計表をカイ二乗(χ2)検定・xmlss生成版
最終更新日: 2011/05/21
カイ二乗検定結果をExcel用のファイルとして書き出すためのrubyスクリプトについて、2011/05/15にWebサイトに載せました(当サイトの一つ前の項に掲げてあります)。
今回は、その時のスクリプトを改定し、Excel用のxlsファイルでなく、xmlssファイルを書き出すスクリプトを作ったので記します。
xmlss(xmlスプレッドシート)は、test.xml のように拡張子が ".xml" のファイルで、Excelで扱うことができます。ただし、Excel2002以降でないと扱えません。
関連ファイル一式は、 chitest2.zip に含まれています。
今回のスクリプト chitest2.rb の特徴は次の2点です。
- スクリプト実行時、Excelを動かすわけではないので処理が速い。そして、MS-Windows以外のOSでもスクリプトを実行できる。
- xmlssの他にテキストファイルも出力するが、その中にχ2値、自由度、P値、期待値、残差、調整残差の数値を一通り書き出している。なので、Excelを用いなくてもχ2検定結果を確認できる。
その他、機能的には前回の chitest.rb と同じです。
それから、スクリプト中の chi_test() というメソッドに渡す引数は、前回のものだと、検定の材料となる表の領域(Range)を1つ渡すだけでした。
今回の chitest2.rb では、領域をRangeとして表現できないので、ワークシートオブジェクトに加え、始点(左上端のy,x座標)、終点(右下端のy,x座標)の計5つの引数を渡します。
[補足1]
χ2検定の結果であるP値を算出するためのrubyメソッド qchi() は、同梱のqchi.rbに書かれています。
これに記述されているメソッドは、 確率分布ライブラリ にあるC言語プログラムをruby用に書き換えたものです。なるべく原本に忠実に書き換えたつもりです。
(5月19日に掲載したスクリプトでは、P値を自前で算出していませんでしたが、今回、算出するように改めました。)
[補足2]
今回のスクリプトでは、拙作 exlap_c というライブラリを用いています。これはxmlss生成用のライブラリで、今回のパッケージに同梱されている exlap_s.rb がそのライブラリファイルです。
chitest2.rbを書くに当たって、exlap_cライブラリに使いにくい点があったので、若干修正しました。ライブラリの新版については rubyでExcel用xmlssファイルを作成するためのライブラリ exlap_c を参照して下さい。
この項 おわり。
------------
□ Excelでクロス集計表をカイ二乗(χ2)検定
最終更新日: 2011/05/15
アンケート結果をクロス集計表にしたとき、そこからどんな傾向を読み取れるのか分かりにくいケースがあります。
例えば、2008年の国民性に関する意識動向調査で、男女別に生活満足度を尋ねた結果は次のようになっています。
満足 | やや満足 | やや不満 | 不満 | 不明 | 合計 | |
男 | 193 | 403 | 151 | 36 | 5 | 788 |
女 | 299 | 440 | 114 | 32 | 3 | 888 |
合計 | 492 | 843 | 265 | 68 | 8 | 1676 |
上の表を見ると、「やや満足」が男女とも多いことは分かりますが、男女別で満足度に違いがあるのかなど、よく分かりません。
そこで、実測値でなくパーセンテージで見ると次のとおり。
満足 | やや満足 | やや不満 | 不満 | 不明 | 合計 | |
男 | 24.5 | 51.1 | 19.2 | 4.6 | 0.6 | 100.0 |
女 | 33.7 | 49.5 | 12.8 | 3.6 | 0.3 | 100.0 |
合計 | 29.4 | 50.3 | 15.8 | 4.1 | 0.5 | 100.0 |
実測値に比べると、少し分かりやすくなりました。
「満足」の割合は、男より女が多い傾向。
一方、「やや不満」は、女より男が多い傾向が窺えます。
ただ、そうした傾向が「誤差の範囲」なのか、「意味のある差(有意差)」なのかは、やはり統計検定を行ってみないと分かりません。
こうしたクロス集計表の検定によく用いる方法がカイ二乗(χ2)検定です。
仮に男女の間で満足度に違いがないとしたら、論理的にどんな値になるか(期待値)を算出し、それと実測値との差の大きさを手がかりにして検定します。
例えば、男の「満足」の期待値は、231.32です(実測値は 193)。実測値が期待値よりも小さいので、男女で差がないと仮定した時に比べると、実際の値が小さいと分かります。つまり男の満足度は小さい。
期待値算出の考え方は次のとおり。
回答者総計が1,676人、うち男が788人です。男が占める割合は 788/1,676=47%となります。回答者総数の男女比が 47:53 となるわけですが、男女間に違いがないとすれば、どの回答もこの男女比に従うはずです。
一方、「満足」と回答した人の合計は 492人ですから、これに男が占める割合47%を掛け合わせると、男女間に違いがないと仮定した時の男の「満足」の値(期待値)が出ます。これを式で書くと下のようになります。
「男の総数」÷「回答者総数」×「満足と回答した人の総数」
これを他のセルにも適用できるように抽象化して書くなら次のとおり。
「注目セルの行の合計」 × 「注目セルの列の合計」 ÷ 「総計」
上のような手順で各セルの期待値を算出すると、Excelにおいてχ2検定を行うことができます。
Excelでは、実測値が書かれた領域と期待値が書かれた領域が分かっていれば、CHITEST()関数でχ2検定の結果を得ることができます。
例えば、実測値が A1:C3、期待値が A11:C13 に書かれていたとすれば、
=CHITEST(A1:C3,A11:C13)
によってχ2検定の結果(P値)を得ることができます。
関数 CHITEST() で得られたP値が 0.05未満であれば、「性別によって満足度が違うといったことはない。」(男女とも満足度は同じ)という仮説が棄却されます。つまり「性別によって満足度に違いがある」という結論になります。
ただし、この結論が誤っている確率が5%程度あります。P値が 0.01未満であれば、結論が誤っている確率は1%くらい。
なお、前掲の表のP値は、約6.4e-005という、ごく小さな値でした。e-005は10万分の1です。
さて、男女間で満足度に違いがあることは検定できましたが、どの辺に違いがあるのかがまだ分かりません。
パーセンテージの表を見れば、それなりに傾向を読み取れますが、例えば、「やや満足」の男女間での差が有意なのか誤差の範囲なのか、判断できません。
P値が0.05未満で、全体的に「違いがある」と検定された時に、更にセルごとにどうなのかを見るためには、各セルの調整残差を求めます。
残差(調整残差でなく残差)は、実測値から期待値を差し引いた値です。その大小でそれなりの傾向を読み取ることができます。
ただし、残差の大きさは、調査の規模や項目の数によって様々な値になります。なので、それが大きいからといって有意に大きいと判断することはできません。そこで、残差を所定の手続きで基準化して、調整残差を求めます。
調整残差は、次の式で求めることができます。
(実測値−期待値)÷SQRT(期待値×(1−行計÷総計)×(1−列計÷総計))
SQRT(x) は、xの平方根(ルート)を求める関数です。
「行計」と「列計」は、該当セルが属する行の合計、列の合計のことです。
こうして求めた調整残差が1.96より大きければ(または -1.96より小さければ)、そのセルの値が有意に大きい(または小さい)と結論できます。ただし、その結論が誤っている確率が5%程度あります。
調整残差が2.58より大きければ(または -2.58より小さければ)、結論が誤っている確率は1%程度になります。
この調整残差を求める関数は、残念ながらExcelに用意されていないと思います。
ということで、前ふりが長くなりましたが、Excelを用いてχ2検定を行うためのrubyスクリプトを書いてみたので紹介します。
chitest.zip にスクリプトとcsvファイルが入っています。
chitest.rbがχ2検定を行うサンプルです。
ruby.exe chitest.rb ↓
と実行すれば、chitest.xlsが作成されます。
カレントフォルダにあるcsvファイルを読み込んで、それぞれについてχ2検定を行い、その結果をchitest.xlsのワークシートに書き出します。1つの検定結果を1つのワークシートに書き出します。
ワークシートに書き出されるのは、次のものです。
- (1) 検定の材料となるオリジナルの集計表
- (2) オリジナルの集計表に縦・横両方の合計欄を付加した表
- (3) χ2値、自由度、P値
- (4) 期待値の表
- (5) 残差の表
- (6) 調整残差の表
- (7) χ2値の表
最後のχ2値の表は、各セルについて、「残差×残差÷期待値」を算出した表です。こうして求めた各セルの値を合計したものが(3)のχ2値です。
chitest.rbは、以上のものをExcelのワークシートに書き出すほか、同じものをテキストファイルとしても書き出すようにしています。data01.csvの検定結果がdata01.txt、data02.csvの結果がdata02.txtに出力されます。
chitest.rbの中で chi_test() というメソッドが定義されています。このメソッドは、χ2検定を行って、上記 (2)〜(7)をワークシートに書き出すメソッドです。
このメソッドに渡す引数は、基本的に1つです。材料となる集計表の書かれている領域(Range)を渡します。
chi_test(ss.Range("A1:C3"))
などのように呼び出します。そうすると、前述の(2)〜(7)が書き出されます。
(1)の材料となる集計表は、chi_test() を呼び出す前に、予めワークシートに書いておく必要があります。
なお、テキストファイルの出力は、chi_test()メソッドでは行われません。chitest.rbで記述しているように、別途そのための記述が必要です。
引数のRangeには、合計欄を含めないようにします。横方向の合計欄も、縦方向の合計欄も含めないようにします。
もし材料となる集計表に合計欄があったとしても、それを外してRangeを指定すればOKです。
どうしても合計欄を含めた形でRangeを指定したい時は、第2引数として true を与えます。
chi_test(ss.Range("A1:D4"), true)
のように呼び出します。こうすると、オリジナルの集計表の合計欄を用いてχ2検定を行います。この場合、(2)の「オリジナルの集計表に縦・横両方の合計欄を付加した表」は作成されません。
実は、(2)〜(7)を書き出す際、セルに数値や文字列をセットしていません。"=B8" とか "=SUM(B8:F8)" などのように、セル参照や計算式をセットしています。
例外は「自由度」で、この欄には数値をセットします。
このようにしておくと、ExcelをGUI操作している最中、オリジナルの集計表の数値を変更した時に、それに応じて (2)〜(7)が総てリアルタイムで変化します。
オリジナルの集計表の行数や列数を変更すると正しい結果が得られませんが、行数と列数が同じであるかぎりは、中身を書き換えた時に、それに応じた各種の結果が得られます。もちろん、数値だけでなく見出しを変更してもOKです。
[補足]
- 生活の満足度について「不明」という項目を上げましたが、これは、元々の表の「その他」と「わからない」を合わせたものです。この2つの実測値がごく少数だったため、2つを合わせました。
- χ2検定を行う場合、セルの値が5未満のものがある時は補正した方がいいということになっていますが、chi_test()は、そうした補正には対応していません。
- chi_test()は、期待値が予め与えられる適合度検定には対応していません。
[余談]
本格的な統計検定を行うのであれば、Rというソフトがお勧めです。Windows版や各種unix系OSで動く版などがあります。
私自身は、7〜8年くらい前、仕事でRを多用してました。その時は FreeBSD 上で perl + R でいろいろ処理してました。
しかし、その後、Rを使う機会がないまま過ぎました。で、最近ちょっと統計的な処理を行う必要があって思い出そうとしたのですが、みごとなくらい忘却してます(あせ)。それに、昔さんざん使っていたはずのスクリプト類も、どこにいったかみつかりません。
そこで、本格的に統計処理するわけではないのでExcelで試してみようかと思い、作ってみたのが chitest.rb です。
でも、やっぱりRをもう一度やり直した方がいいかも。と思ったりしてます。グラフ化して視覚的に把握することができない視覚障害者の私にとっては、検定結果がとても参考になります。もちろん頼り過ぎるのは禁物ですが。
この項 おわり。
------------
□ tv_yahoo2.rbの修正版
最終更新日: 2011/04/15
Yahooのテレビ番組情報サイトの変更があり、以前 掲げた tv_yahoo2.rb が正しく働かなくなっていました。
そこで、修正したものを tv_yahoo3.rb としてアップしました。
メソッド tv_yahoo(……) における地域の指定は、東京、埼玉、千葉のように都道府県名で行いますが、北海道については次の7種があります。
北海道(札幌)、北海道(函館)、北海道(旭川)、北海道(帯広)、北海道(釧路)、北海道(北見)、北海道(室蘭)
(2011/04/09にアップロードした tv_yahoo3.rb の修正が不十分だったため、04/15に再アップしました。)
この項 おわり。
------------
□ MS-Windowsにおけるドライブ情報の取得、システムエラー出力の抑制
最終更新日: 2011/02/23
MS-Windows上で「SDカードが装着されているかどうかを確認し、装着されていれば……」というrubyプログラムを作ろうとして少々調べました。
で、次のrubyスクリプトで、ドライブに関する各種の情報(装着の有無だけでなくボリュームラベルとか使用可能容量等)を確認できることが分かりました。
−−−− ここから require 'win32ole' drive_types = %w(Unknown Removable HDD Network CD-ROM RAM) file_system = WIN32OLE.new("Scripting.FileSystemObject") drives = file_system.Drives drives.each do |drive| puts "Drive letter: #{drive.DriveLetter}" puts "Is ready: #{drive.IsReady}" puts "Drive type: #{drive_types[drive.DriveType]}" unless drive.IsReady printf("\n") next end puts "File system: #{drive.FileSystem}" puts "Share name: #{drive.ShareName}" puts "Total size: #{drive.TotalSize}" puts "Available space: #{drive.AvailableSpace}" puts "Volume name: #{drive.VolumeName}" puts "Serial number: #{drive.SerialNumber}" puts "Root folder: #{drive.RootFolder.Path}" puts "Path: #{drive.Path}" printf("\n") end −−−− ここまで
上に出てくる drive.IsReady が true であれば、該当のドライブがアクセス可能(装着されている)という意味になります。
false なら装着されていないことを意味します。
ドライブが装着されていない状態でも、「ドライブを挿入して下さい」とか「ドライブにディスクがありません」のようなシステムエラーメッセージが出ることはありません。プログラムが中断してユーザーのキー入力待ちになったりしないので助かります。
それから、ドライブ未装着の時などに発生するシステムエラーのメッセージが出ないようにする方法としては、KERNEL32 の SetErrorMode を用いるものがあります。例えば、次のようなスクリプトです。
−−−− ここから require "Win32API" set_error_mode = Win32API.new('KERNEL32', 'SetErrorMode', %w(I), 'L') before_mode = set_error_mode.Call(0) # 従来のモード値を記録 set_error_mode.Call(1) # システムエラーメッセージ出力抑制 ………… (何か処理を行う) set_error_mode.Call(before_mode) # 元のモード値に戻す −−−− ここまで
set_error_mode.Call() の引数として与えることのできる数値には、次のものがあるようです。
- 0 現在のモード値を取得
- 1 システムエラーを抑制 SEM_FAILCRITICALERRORS
- 2 メモリ整列違反を自動的に修復 SEM_NOGPFAULTERRORBOX
- 0x8000 ファイル未検出でもメッセージ出力しない SEM_NOOPENFILEERRORBOX
この項 おわり。
------------
□ htmlの数値文字参照(&#……;)とhpricot
最終更新日: 2010/03/06
rubyのhtmlパーサhpricot(ver 0.8.2)を使って、web pageから必要な箇所を切り取り、テキストに変換するというのをやっています。
いろいろ試していると、ごく稀に部分的ながら文字化けすることがあります。例えば、ニュース記事の一部
~「モバゲータウンの海外展開とオープン化を支える社内制度」公開~<br />
というのが文字化けしました。「&#65374;」は「〜」になるべきところ、なぜか化けます。
しかし、hpricotが数値文字参照(&#……;)に対応していないわけではありません。ちょっとしたテスト用のweb pageを自作して試してみると、ちゃんと「&#65374;」が「〜」に変換されます。
どんな場合に化けるのかを正確に指摘することはできませんが、改行関連タグの前後に&#……;が現れると、乱れるような気がします。
それと、16進数で書かれた数値文字参照は、hpricotが変換せずそのままにしておくことが分かりました。「&#x3044;」のようなケースです。
そこで、文字化けと16進数の数値文字参照に対応するためのメソッドを書いてみました。
そのメソッドに触れる前に、数値文字参照の部分を文字に変換する一般的なメソッドを掲げてみます。ライブラリのnkfを用います。
下のスクリプト ncr_test.rb は、変換メソッドの ncr_convert と、それをテストする main の部分から構成されています。
−−−− ここから #! ruby -Ks # ncr_test.rb (coding: Shift-JIS) 2010/03/06 require 'nkf' # html中の数値文字参照(&#……;)を文字に変換 # 出力文字コードは $BASE_C (j, e, s, w など)に従う。 def ncr_convert(str) unless str =~ /\&\#.+;/ return str end res = str.gsub(/(\&\#\d+;|\&\#x[0-9a-f]+;)+/i) {|part_str| part_str.gsub!(/\&\#x([0-9a-f]+);/i) {|ch| n = $1.hex ch = "&##{n.to_s};" } part_str.gsub!(/\&\#(\d+);/) {|ch| n = $1.to_i ch = [n].pack("n") } part_str = NKF.nkf("-m0 -W16 -#{$BASE_C}", part_str) } return res end ## main $BASE_C = $KCODE[0,1].downcase $BASE_C = 'w' if $BASE_C == 'u' # 全角文字列・10進数タイプ str = 'オープンソース' + 'ソフトウェア-ruby' puts ncr_convert(str) # 全角文字列・16進数タイプ str = 'あいうえお' puts ncr_convert(str) # 半角文字列 str = '<>&' puts ncr_convert(str) str = '~「モバゲータウンの海外展開とオープン化を支える社内制度」公開~' puts ncr_convert(str) −−−− ここまで
上では、16進数記述の数値文字参照をいったん10進数記述に変換し、その上で10進数記述の箇所を文字に変換しています。
数値部分を文字に変換するところは、
ch = [n].pack("n")
としていますが、これは下の3行で置き換えることができます。
h = Integer(n / 256) l = n % 256 ch = h.chr + l.chr
これをビット演算で行うなら、次のように書けます。
h = n >> 8 l = n & 0xff ch = h.chr + l.chr
数値から変換された文字は、一般にUTF-16として扱っていいだろうと思います。そこで、nkfライブラリでそれを目的の文字コード(グローバル変数$BASE_Cで指定)に変換しています。
さて、これをhpricotによるweb page解析に適用する場合、気をつけなければいけないのは、半角文字の扱いです。
数値文字参照部分を変換した結果が半角の '<', '>', '&' などのhtmlタグ文字になる場合、タイミングがわるいとトラブルの原因になります。
そこで、htmlドキュメントをhpricotに引き渡す前に、全角文字だけを予め変換することを考えました。半角文字については、hpricotにお任せで問題になることはないと思います。
全角文字は、数値でみた時に 256 (0x100) 以上になります。なので、前述の ncr_convert メソッドを全角のみ対応にするのは容易です。一応、そのメソッドを下に掲げておきます。
−−−− ここから def ncr_convert(str) unless str =~ /\&\#.+;/ return str end res = str.gsub(/(\&\#\d+;|\&\#x[0-9a-f]+;)+/i) {|part_str| part_str.gsub!(/\&\#x([0-9a-f]+);/i) {|ch| n = $1.hex ch = "&##{n.to_s};" } part_str.gsub!(/\&\#(\d+);/) {|ch| n = $1.to_i if n >= 0x100 ch = [n].pack("n") end } part_str = NKF.nkf("-m0 -W16 -#{$BASE_C}", part_str) } return res end −−−− ここまで
htmlドキュメント(String)を取得した直後に、上のメソッドをそのドキュメントに適用し、それからhpricotに引き渡してやればOKだと思います。
なお、hpricotでは空白に置換される「&nbsp;」が化けてしまうことがあるので、同じタイミングでこれも変換した方がいいかもしれません。
この項 おわり。
------------
□ htmlのtable(セル結合あり)をrubyの配列に変換するメソッド・その2
最終更新日: 2010/02/25
htmlの中のtable(セル結合あり)をrubyの配列に変換するメソッドについて前述しました(このサイトでは下の方に掲載されています。)。
これに関連して、rubyのメーリングリスト
[ruby-list:46867] Re: html→csv変換|tableのセル結合に対応したものは?
において、よりすっきりと整理された table_to_array2 を提示していただきました。
また、セルの結合箇所の埋め草を "" に設定してしまう仕様の危うさを指摘していただきました。元々 "" であるセルと埋め草との区別がつかない危うさです。
そこで、埋め草を nil にするよう改め、かつ、table中のtdのみ抽出していた(thを取り出せなかった)点を修正して、新メソッド table_to_array2 を書き直しました。
そして、その新メソッドをYahooのテレビ・ラジオ番組表取得スクリプト tv_yahoo2.rb に組み込みました。
利用される場合は、新メソッド table_to_array2 の方を参考にして下さい。
なお、セル結合のない table に対して table_to_array2 メソッドを用いても、もちろん問題ありません。
単純なtableを扱う例として、読売オンラインの市況サイトから、日経平均やTOPIXの情報を得てcsvに書き出し、合わせて、リンクされている記事も書き出すスクリプト yomiuri_kabu.rb を載せておきます。
ruby yomiuri_kabu.rb ↓
と実行すれば、カレントディレクトリに yomiuri_kabu.csv と yomiuri_kabu.txt が書き出されます。
(tv_yahoo2.rb や yomiuri_kabu.rb を実行するためには、予めrubyのライブラリ hpricot のインストールが必要です。)
この項 おわり。
------------
□ htmlのtable(セル結合あり)をrubyの配列に変換するメソッド
〜 テレビ・ラジオ番組表の取得への応用 〜
最終更新日: 2010/02/19
htmlの中のtableがセル結合を含む場合、それを単純にテキスト変換すると、セルの位置がごちゃごちゃになってしまいます。ブラウザで見る時には把握できるはずのセルの縦と横の関係が、分からなくなってしまいます。
これを何とかしたいと思い、rubyでtable_to_arrayメソッドを作りました。htmlのtable部分について、(セル結合があっても)セルの位置関係を崩さずに配列に変換するものです。
方法は単純で、tableに含まれる行数(trの個数)を記録できるだけの配列をまず用意し、あとはセル(td)を1つづつ読み込んで記録していきます。
その時に、tdタグの属性値 colspan と rowspan の数に応じて、セル結合箇所(データが入らないセル)に、空文字列 "" をセットしていきます。そして、データを記録する時は "" を避けて、該当の行の nil のところに記録します。
これを単純に繰り返していけば、結果的に、セルの縦・横の関係を維持した形の配列が作られます。
table_str = '<table> …… </table>' ary = table_to_array(table_str)
のようにすると、2次元配列 ary を得ます。配列の各要素は、htmlタグを除去したテキストです。
テキストでなく、タグ付きの「<td> …… </td>」を各要素に記録したい時は
ary = table_to_array(table_str, :HTML)
とします。
htmlパーサにはhpricotを使っています。
このメソッドだけを掲げても仕方ないので、応用例として、Yahooのテレビ・ラジオ番組覧(web page)の情報を取得して、箇条書きふうのテキストファイルとして書き出すスクリプト tv_yahoo.rb を載せておきます。
ruby tv_yahoo.rb ↓
と実行すれば、カレントディレクトリに am.txt, bs1.txt, bs2.txt, fm.txt, tv.txt の5つのファイルが書き出されます。
サンプルは東京地区の番組を得るようになっていますが、スクリプトの最後の方にある
ary = tv_yahoo('東京', cat, 12, 1)
の「東京」を変更すれば、他の地区の番組を得ることができます。
tv_yahoo.rb自体は EUC-JP で書かれていますが、出力される am.txt などは Shift-JIS です。スクリプト内の $OUT_C の値を変更すれば出力文字コードを替えることができます。
スクリプト本体の文字コードをEUC-JP以外に変更する時は、第1行目の -Ke の e を適当に替えて下さい。
それから、スクリプト内には、おまけで、MS-Windows上でhtmlドキュメントを取得するための class Mshttp を入れてあります。Msxml2.xmlhttp を利用した単純・素朴なものです。
mshttp = Mshttp.new(:WIN32) html_doc = mshttp.web_open('http://www.hoge.com/q.cgi?a=1&b=2')
などのようにすると、htmlドキュメントをバイナリで得られます(改行は "\r\n" になる。zip圧縮ファイルなども取得可能)。
*.pacによる設定の場合を含め proxyがどうなっているかを気にせずに使えます。InternetExploreが動く環境であれば、このclassも動くと思います。
なお、:WIN32 を指定せずに単に Mshttp.new と初期化した場合は、open-uri の openメソッドでwebを取得するようになります。
その場合、win32oleを用いないので linux などでも動くと思います。ただし、proxyへの対応は、(必要なら)別途 行う必要が出てきます。
tv_yahoo.rbは、Windows専用でなく汎用の方になっているので、Windows以外の環境でも動作すると思います。
Windows専用にしたい場合は、Mshttp.new を Mshttp.new(:WIN32) に書き換えて下さい。1ヶ所だけ書き換えればOKです。
その他、詳細は tv_yahoo.rb のコメントを参照して下さい。
P.S.
地上波テレビ放送の番組情報は、「テレビ王国」のサイトが充実しているようです。なので、そのサイトから情報を取得するためのスクリプト tv_so_net.rb も載せておきます。
ruby tv_so_net.rb ↓
と実行すれば、カレントディレクトリに tv_so_net.txt が書き出されます。
こちらは table の処理は関係ありません。
この項 おわり。
------------
□ MS-AccessのQuery Form Reportなどの中身をテキストファイルに出力
最終更新日: 2010/02/03
win32oleを活用して、MS-Accessの Query Form Report Macro Module の中身をテキストファイルに書き出す rubyスクリプト acdb_text.rb を掲げます。このスクリプトは、MS-Accessがインストールされている環境で動かします。
ruby acdb_text.rb C:\database\Northwind.mdb ↓
のように実行すると、C:\database の下に Form とか Report などのサブフォルダができ、その下にテキストファイルが書き出されます。
テキストファイルの名前は、例えば「運送会社.txt」 「四半期売上高サブフォーム.txt」などです。「.txt」より前は、そのFormやReportの名前です。
なお、スクリプト中に出てくる SaveAsText の代わりに LoadFromText を用いると、テキストファイルをロードして Form や Report を設定できるようです。
SaveAsText と LoadFromText の引数は次のとおりです(Formの場合で記述)。両者とも同じ引数です。
SaveAsText(AcForm, 'フォーム名', 'C:\database\form01.txt') LoadFromText(AcForm, 'フォーム名', 'C:\database\form01.txt')
上では「AcForm」という定数を示していますが、Formオブジェクトが変数 obj にセットされている場合、obj.Type に AcForm と同じ値がセットされているようです。下のスクリプトでは定数でなく obj.Type を使っています。
−−−− スクリプトここから #! ruby -Ks # MS-Accessを操作して Query Form Report Macro Module の # 各々の中身をテキストファイルに書き出す require 'win32ole' # 指定のファイル名をフルパスで返す def getAbsolutePath fname fso = WIN32OLE.new('Scripting.FileSystemObject') return fso.GetAbsolutePathName(fname) end # 各オブジェクトのコンテナとメソッドの組合せの定義 enum_specs = [ {:Type => "Table", :Container => :CurrentData, :Method => :AllTables}, {:Type => "Query", :Container => :CurrentData, :Method => :AllQueries}, {:Type => "Form", :Container => :CurrentProject, :Method => :AllForms}, {:Type => "Report", :Container => :CurrentProject, :Method => :AllReports}, {:Type => "Macro", :Container => :CurrentProject, :Method => :AllMacros}, {:Type => "Module", :Container => :CurrentProject, :Method => :AllModules}] ## main acdb = WIN32OLE.new("Access.Application") for filename in ARGV filename = getAbsolutePath(filename) next unless test(?e, filename) target_dir = File.dirname(filename) acdb.OpenCurrentDatabase(filename) enum_specs.each {|spec| type_name = spec[:Type] next if type_name == "Table" dir_name = "#{target_dir}\\#{type_name}" acdb.send(spec[:Container]).send(spec[:Method]).each {|obj| unless test(?d, dir_name) Dir.mkdir(dir_name) end outfile = "#{dir_name}\\#{obj.Name}.txt" acdb.SaveAsText(obj.Type, obj.Name, outfile) } } end acdb.Quit −−−− ここまで
上のスクリプトでは「next if type_name == "Table"」と記述することで、Tableオブジェクトを対象外にしています。このスクリプトでTableを処理しようとするとエラーになります。
このTable除外の箇所を、例えば
next unless type_name == "Query"
とすれば、Queryだけがテキストファイルに書き出されることになります。
参考サイト:Access/オブジェクトを列挙するスクリプト(Ruby編) - SnakaWiki 2nd Edition
この項 おわり。
------------
□ input.xmlと実質的に同じoutput.xmlを作るruby scriptの自動生成
最終更新日: 2010/01/31
WIN32OLEを使わずに、Excel用のxml-ssドキュメントを書き出すスクリプトをrubyで作れないかと試みています。
その過程の副産物として、input.xmlと実質的に概ね同じoutput.xmlを書き出すrubyスクリプト生成プログラム mkrexml.rb を作ってみました。
例えば、input.xml が与えられた場合、
$ ruby mkrexml.rb input.xml ↓
と実行すると、カレントディレクトリに input_xml.rb が書き出されます。そして、次に
$ ruby input_xml.rb > output.xml ↓
とすれば、output.xml が生成されるわけですが、これが実質的に input.xml と概ね同じはず?というものです。
input.xmlと似たようなxml文書をrubyで作ろうとするとき、自動生成された input_xml.rb の中身が参考材料になるかなと思い、こんなのを作ってみたものです。
△ 補足
- input.xmlのencodingが何であれ、output.xmlは utf-8 になります。
- mkrexml.rbには、引数(ARGV)を複数与えることができます。
△ 課題
例えば、下のようにテキスト部分が子要素で分割されている場合、
<p>string01<b>bolder</b>string02</p>
output.xml では string01 だけが書き出されます。string02 の方は切り捨てられてしまいます。
この課題を簡単に解決するやり方は、あるでしょうか?
この項 おわり。
------------
Keyword(s):[ruby] [win32] [ms-access] [rexml] [hpricot] [番組表] [数値文字参照]
References: