日曜研究室 〜技術的な日常〜

技術的な観点から日常を綴ります

   5 月 22

PythonのちょっとマニアックかもしれないTips

バッファローのHD-PSGU2シリーズという、2.5インチHDDを内蔵したUSB外付けポータブルHDDがありまして、そのHD-PSGU2には仮想 CD機能がついてます。
仮想CD機能とはどういうものかというと、CDイメージ(isoファイル)をHD-PSGU2のHDDに保存して付属のツールで設定すると、あたかもそのCDイメージと同じ内容を持ったCDが入ったCDドライブが存在するかのごとくBIOSレベルで認識されるというものです。(説明下手や・・・)

年中PCにOSをインストールしまくったりしてる自分にとっては、OSのインストールCDを焼く必要がなく、時間の節約ができ(一般的にCD焼くよりHDDにコピーする方がはるかに早い)さらにメディアを消費しなくて済むからエコかつ財布にやさしいという素晴らしいメリットをこの仮想CD機能はもたらしてくれるのです。

ただ一つ大きな問題があって、仮想CD機能で利用するCDイメージの選択ツールがWindows専用なんです。しかも出来が悪い。
自分はMacユーザーなのでCDイメージを切り替えるためだけにWindowsを立ち上げなきゃならんという非常にめんどくさい事になってました。
(その点、本体だけでイメージの切り替えができるUMA-ISOは非常に素晴らしいのですが、HDD別売なのに割高で、基本的に楽天やAmazonのマケプレに出店してるハンファ・ジャパンのショップからしか買えないという、ハンとかファとかついてる会社には直接住所氏名メアドを渡して取引したくない自分にとっては、選択肢たえりえませんでした。)

それでMacでもなんとかならんかね?と思って調べてみたところ、HD-PSGU2のHDDの中にSYSTEM.BINという隠しファイルがあって、その中の特定の箇所にCDイメージのファイル名を書き込むと、CDイメージの指定ができるようでした。
純正イメージ切り替えツールは、それを書き換えてただけっぽいです。

一瞬Mac用のツールを作るのは楽勝かと思ったんですが、SYSTEM.BINに書き込まれているファイル名をよく見ると、8.3のDOS形式のSFN(Short File Name)なんです。
LFN(Long File Name、いわゆる普通のファイル名)からSFNを生成するアルゴリズム自体はシンプルなんですが、実際はファイルを作成したタイミングによってSFNは変わってしまうため、HD-PSGU2のデフォルトフォーマットであるFAT32を解析して保存されてるSFNを直接見ない限りは正確なところが分かりません。

例えば単にファイルを生成しただけの場合LFNとSFNの対応は以下のようになるのですが、

LFN            →    SFN
testtestA.txt    →    testte~1.txt
testtestB.txt    →    testte~2.txt
testtestC.txt    →    testte~3.txt

このあとtesttestA.txtを綺麗さっぱり削除した後に、純粋にSFN生成アルゴリズムによってtesttestC.txtのSFNを求めるとtestte~2.txtとなってズレが生じてしまう訳です。
(この後、testtestD.txtを生成するとSFNはどうなるんだろう。調べてないや)

で、Mac + Pythonという縛りで(commandでシェル実行しても構わない)SFNを取得したりFAT32を解析したりできるライブラリとかを探したんですが見つけられなかったので自分で作ってしまいました。そんでCDイメージ切り替えツールも作ってしまいました。
コマンドラインツールなので純正ツールには見劣りしますが、遥かに使い易いです。なんせ出来がいいのでw

FAT32の仕様や、解析するプログラムのソースを説明すると超長くなってしまうのでここには書きませんが、それを作る過程で個人的に勉強になったなぁと思ったPythonのTipsを書いとこうと思います。
環境はMac OS X 10.6とPython 2.6.5なので違うと使えないかも。

あるファイルがどのマウントポイントの配下にあるか調べる方法

pathにファイル名が入ってるとして、
path = os.path.abspath(os.path.join(path, os.pardir))
でどんどん上の階層をたどりながら、
os.path.ismount(path)
で調べるだけで分かります。
最初はpathはファイル名じゃなくてディレクトリ名で始めないとマズくね?と思ったりもしますがその辺はabspath先生がよしなにやってくれるようです。

マウントポイントからブロックデバイスのパスを取得する方法

commandsモジュールを使ってオプションなしのmountコマンドを実行( commands.getstatusoutput(‘mount’) )し、その実行結果から調べたいマウントポイントを含む行を抽出すればその行にブロックデバイス名が含まれてるので正規表現などでうまい具合に抽出すればOK。

ブロックデバイスのパスを受け取ってマウント・アンマウントする方法

これもcommandsを使えば簡単。
deviceにデバイスのパスが入ってるとして、
commands.getstatusoutput(‘diskutil mount ‘ + device)
もしくは
commands.getstatusoutput(‘diskutil umount ‘ + device)
とするだけです。オプションをつけてファイルシステムを指定することも当然できます。
上のもそうですが実際にcommands使うときはリターンコードはきちんとチェックすべき。
またマウント系の操作はsudoでスクリプトを実行するかSUIDを付加しないと多分そんな権限はないよ的なエラーが出るはず。

なんか、前振りが長いわりに本題がしょぼくてすいませんw


   5 月 22

xlrd, xlwtでExcelファイルをテンプレートとして使う

PythonでExcelファイル(BIFF形式、拡張子でいえばxls)を扱う場合には、

辺りを使うのが一般的かと思います。他の選択肢は知りません。
今回は開発環境がMac OS X 10.6、デプロイ環境がCentOS 5.4だったので当然Win32OLEの選択肢は除外しました。
pyExceleratorは、罫線やフォントやセルのスタイルなどのデータを細かく指定して書き出せるようですが、読み込み機能が凄くシンプルで、セルの値とその座標のみ抽出できる(罫線やフォントやセルのスタイルなどは読み込めない)ようで、「装飾を施して体裁を整えたExcelファイルを読み込む」→「必要な部分だけ上書きして保存」といういわゆるテンプレート方式のExcelファイルの利用ができません。(まぁ、かなりのコード数を利用側で費やせば不可能ではないと思いますが、それってPythonでExcelファイルを扱うモジュールを自作するようなもので・・・)

xlrd, xlwt, xlutils(多分それぞれExcel Read, Excel Write, Excel Utilitiesの略かな?)も、モジュールが別になってる事から分かる通り、読み込みと書き込みが別になってるのですが、xlutilsを使うことである程度連携させることができます。
具体的には、


from xlrd import open_workbook
from xlutils.copy import copy

rb = open_workbook('test.xls', formatting_info=True)
wb = copy(rb)
wb.save('test2.xls')

とすれば、test.xlsの体裁を保ったままtest2.xlsに保存する事ができます。
ミソはformatting_info=Trueで、これを指定するとxlrdがセルのスタイル情報も読み込んでくれて、xlutils.copyがちゃんとxlwtにコピーしてくれます。(ただ、ページ設定等はコピーされないようです。)

で、wb.saveの前にwb(xlwt.Workbookクラスのインスタンス)のget_sheetメソッドを使ってxlwt.Worksheetクラスのインスタンスを取得し、そのwriteメソッドなどを使って必要なデータを上書きすれば目的達成!となるはずですが、実際にやってみると上書きしたセルのスタイル情報がクリアされてしまうという問題が起きます。

そこで、Worksheetクラスのwriteメソッドの引数定義を見ると


def write(self, r, c, label='', style=Style.default_style):

となっていて、styleを指定しない場合にはデフォルト値(スタイルなしだと思われる)で上書きされるようになってます。
styleにxlwt.Styleクラスのインスタンスを渡せば良さそうですが、xlrdからスタイルを読み込んでこのメソッドのstyleに渡してもクラスが違うとかでエラーになります。
xlwtとxlrdのファイル構成を見ると分かるのですが、かなりクラスの構成が違うようで、xlwtのstyleとxlrtのstyleはダックタイピングには対応してないようです。

じゃあ、xlrdじゃなくてxlwt自体から元のstyleを取り出してwriteメソッドの引数に渡してやればいいんじゃないか?xlutils.copyでスタイル情報もコピーされてるんでしょ?と思いますが、ソースを見るとxlwt.Styleクラスのインスタンスは即座にインデックス(単なる数値)に変換されてそのインデックスが保持されるようになっていて、xlwt.Styleクラスのインスタンスはどこにも保持されてないように見えました。
さらにインデックスからxlwt.Styleに逆変換するのも難しい感じでした。(BIFF形式に詳しければ不可能ではないと思います)

まぁ、正直言うとxlrdのスタイル情報から属性を全部読み取ってxlwt.Styleクラスのインスタンスを生成してwriteメソッドに渡すコードを書けばいいだけですが、スタイルの属性も多岐にわたっててなんかめんどくさそうだったのと、writeメソッドの仕様的にstyleを指定しなかったら元のスタイルを保持するほうが美しくね?と思ったのでその方向で行くことにしました。

ソースを読むとxlwt.Worksheet.writeメソッドは最終的にxlwt.Row.insert_cellメソッドを実行するようになっています。
そう、メソッド名から分かるとおり上書きだろうが常にcell(xlwt.Cellクラスのインスタンス)を追加するようになっています。
セルの値は単純な数値以外のデータ(文字列など)もスタイルと同じようにインデックスに変換されて保持されていて、この
insert_cellメソッド内で、元のセルの値のインデックスを削除して新しいセルを追加するという処理が行われています。
ここで、元のセルのスタイルのインデックスを新しいセルに設定してやればうまく行きそうだという事が分かります。

実際にはxlwt.Row.insert_cellを以下のように書き換えました。(xlwt 0.7.2  Row.py)


def insert_cell(self, col_index, cell_obj):
if col_index in self.__cells:
if not self.__parent._cell_overwrite_ok:
msg = 'Attempt to overwrite cell: sheetname=%r rowx=%d colx=%d' \
% (self.__parent.name, self.__idx, col_index)
raise Exception(msg)
prev_cell_obj = self.__cells[col_index]
cell_obj.xf_idx = prev_cell_obj.xf_idx
sst_idx = getattr(prev_cell_obj, 'sst_idx', None)
if sst_idx is not None:
self.__parent_wb.del_str(sst_idx)
self.__cells[col_index] = cell_obj

”cell_obj.xf_idx = prev_cell_obj.xf_idx”の行を追加しただけです。
cellのxf_idxにスタイルのインデックスが入ってるのでそれを元のセルから新しいセルにコピーしただけですね。
本当はsst_idx(単純な数値以外のデータのインデックス)と同じようにgetattrとか使った方がいいのかもしれませんが、xlwtの〜Cellクラスにはすべてxf_idxが定義されてるようでしたので。

site_packagesに入ってるソースを直接書き換えたり、インストール前に書き換えてからsetup.py installしてもいいのですが、ライブラリを直接変更するのもあれだし、このままだと逆にxlwt.Worksheet.writeメソッドに独自に指定したstyleが無視されてしまうし、他に不具合があるかもしれないので、実際は継承するなりして修正点を反映するほうがよろしいかと思います。
今回はxlwtのクラス全体的にわたる別の機能追加も行いたかったので、継承はせずにxlwtの各クラスにメソッドを追加するモジュールを書きました。

あとは、ページ設定などが反映されない問題は、wsにxlwt.Worksheetのインスタンスが入ってるとして


ws.top_margin = 3.0 / 2.54    # 1インチは2.54cm
ws.left_margin = 1.5 / 2.54    # 1インチは2.54cm
ws.right_margin = 1.5 / 2.54    # 1インチは2.54cm
ws.header_str = ''
ws.footer_str = ''
ws.fit_num_pages = 1

なんて感じで、余白とヘッダ・フッタ、収めたいページ数なんかを指定するとだいだいの要望は満たせるかと。
用紙のサイズなんかも他のプロパティで設定できるようです。(デフォルトはA4サイズっぽい)


   5 月 22

Twitterもいいけど

いやぁなんかブログ書くの久しぶりですねぇ。
Twitterを本格的に使い始めてからブログの更新が滞ってました。
ブログは元々、書きたい事が少しづつ溜まっていって我慢できなくなったら書くという感じでやっていたのですが、Twitterでこまめに書きたい気持ちをガス抜きできてたのでなかなか溜まらず、ブログを書く気が起きませんでした。

いつもよく物事を筋道だてて考えてるような人は、Twitterとブログの使い分けができるんでしょうね。
僕はいつもほとんど何も考えてない(というか言語で思考してない?)ので、「腹減った〜」とか「どこどこ行った〜」とか「なになにが旨かった〜」とかTwitterに書けるだけでだいたい満足してました。

しかしさすがの僕でも?仕事柄技術的なネタは少しづつ溜まっていきますし、それをTwitterにも書いてはきたのですが、140文字の制限が煩わしくなってきました。
技術的なネタを有用たらしめるためのひとつの属性として正確性が挙げられると思います。
例えばapache一つをとっても、正確に指定しようと思ったら、「何々のOSの標準のパッケージのヤツで、バージョンは〇〇、httpd.confで△△の設定を□□施したやつで〜」とか書きたくなるんですが、さらに「で、そのapacheがどうしたの?」って所まで書くことを考えると140文字なんて全然足りない。
うまい具合に省略したりとか分割したりとかしてなんとかやってましたが、そこに労力を掛けるのが馬鹿らしくなってきたのと、諦めて書かなかった事がかなり増えてきてガス抜きができない状況になってしまいました。

Twitterがどうこう言うつもりは全くなくて、僕の使い方がおかしいだけだと思いますw

ということで、ブログを書く頻度を少し上げようかなと思ってます。
意思が弱いので、実際に頻度が上がるかどうかは神のみぞ知るところですw