rubyでバイナリファイルを読んでみる [プログラミング]
■はじめに
何か新しく言語を勉強したいと思い、オブジェクト指向もいっしょに勉強できそうな ruby を選んでみました。
さっと入門サイトで勉強してみたのですが、文法をいくら教えてもらったところで実践しないと身につかないものです。
題材は何がいいか悩んだのですが、仕事でも関係しているGDSIIフォーマットというバイナリファイルのパーサを作ってみることにしました。
明らかにスクリプト言語には不向きと思われるのですが、ざっと調べるとバイナリファイルの取扱もできそうですし、GDSIIは十数種の可変長レコードの塊で、各レコードが階層構造を構成したりしているので、それらをオブジェクトとして扱えたりするのでは、という期待もありました。
■GDSIIフォーマット
本当にrubyでGDSIIを扱いたいのなら、すでにそういうライブラリが存在するようです。
今回は勉強が目的なので、スクラッチで組み上げます。
GDSIIフォーマットについては、こちらのサイトがまとまってて助かりました。
■パーサ
いちおうGDSIIファイルを読んで、各レコードの情報を取り出せるようになりました。パースするところまでで、その先の各レコードの階層構造を構築するところはできていません。
なにしろruby初心者なので、「rubyらしく」実装できていないと思います。各レコードをクラスにしてはみましたが、オブジェクト指向をほとんど理解していないので、どこまで有効な設計になっているか、はなはだ疑問です。このあたりは一人でやっていても駄目ですね。
#!/usr/bin/env ruby
#
# stream.rb -- GDSII Data Parser
# "Stream Format" is output format of GDSII data.
#
# Todo:
# - do like ruby.
# - probably bug about upper/lower nibble
#
# Reference:
# GDSII Format: http://www7b.biglobe.ne.jp/~garaku/HSGDS/GDSII_p01.html
#
# History:
# 0.2 change method to class of each record, add Colrow
# 0.1 implement some records will be used usually
#
# Author:
# M.Hori
####################
# Constant
####################
# Variables
MY_VERSION = '0.2'
USAGE_MSG = "Usage: " << $PROGRAM_NAME << " [options] GDSII_File"
####################
# Module
####################
module Util
# Data Converter
def int2(input)
input.unpack("n")[0]
end
def int4(input)
input.reverse.unpack("l*")[0]
end
def real4(input)
end
def real8(input)
# S=sign, E=index, M=mantissa
# SEEEEEEE MMMMMMMM MMMMMMMM ...(total 7 byte)... MMMMMMMM
# real8 = (-1)^sign * (0.mantissa)_2 * 16^(index-64)
sign = (input & 0x8000000000000000)>>63
index = (input & 0x7F00000000000000)>>56
mantissa = (input & 0x00FFFFFFFFFFFFFF)/(2.0**56)
((-1)**sign)*(mantissa*16**(index-64))
end
def ascii(input)
input.unpack("A*")
end
# long print
def lpr(*var)
printf(*var) if $long
end
module_function :int2, :int4, :real4, :real8, :ascii, :lpr
end
####################
# クラス
####################
class Record
# 2byte ==> unpack("n") # convert to big endian (byte order)
# 1byte ==> unpack("c")
# +----------------------+
# | size | 2 byte
# +-----------+----------+ --+
# |record_type|_data_type| 1 byte + 1 byte |
# +-----------+----------+ +-- others
# | data | (size-4) byte |
# | : | |
# +----------------------+ --+
# readable instance variables
attr_reader :record_type, :_data_type, :data
# Array
@@record_type_array = [
"HEADER", "BGNLIB", "LIBNAME", "UNITS", "ENDLIB",
"BGNSTR", "STRNAME", "ENDSTR", "BOUNDARY", "PATH",
"SREF", "AREF", "TEXT", "LAYER", "DATATYPE",
"WIDTH", "XY", "ENDEL", "SNAME", "COLROW",
"TEXTNODE", "NODE", "TEXTTYPE", "PRESENTATION","SPACING",
"STRING", "STRANS", "MAG", "ANGLE", "UNITEGER",
"USTRING", "REFLIBS", "FONTS", "PATHTYPE", "GENERATIONS",
"ATTRTABLE","STYPTABLE","STRTYPE", "ELFLAGS", "ELKEY",
"LINKTYPE", "LINKKEYS", "NODETYPE", "PROPATTR", "PROPVALUE",
"BOX", "BOXTYPE", "PLEX", "BGNEXTN", "ENDEXTN",
"TAPENUM", "TAPECODE", "STRCLASS", "RESERVED", "FORMAT",
"MASK", "ENDMASKS", "LIBDIRSIZE","SRFNAME", "LIBSECUR",
]
@@data_type_array = [
"NO_DATA", "BIT_ARRAY", "INT_2", "INT_4", "REAL_4",
"REAL_8", "STRING",
]
# Initializer
def initialize(size, others)
@size = size
@record_type, @_data_type = others[0,2].unpack("cc")
@data = others[2,size-4] # size=2, record=1, data=1, total=4byte
end
# to_s
def to_s
return @@record_type_array[@record_type],
@@data_type_array[@_data_type]
end
# no_operation
def no_operation
Util.lpr(" --------------------")
end
# Parser
def parse_data
case record_type
when 0 # HEADER
Util.lpr(" GDSII_Version=%s", Header.new(@data).gds_version)
when 1 # BGNLIB
_m, _a = Bgnlib.new(@data).date
Util.lpr(" Mod=%s Acc=%s", _m, _a)
when 2 # LIBNAME
Util.lpr(" LibraryName=%s", Libname.new(@data).name)
when 3 # UNITS
#units()
_uu, _um = Units.new(@data).units
Util.lpr(" dbUnitByUserUnit=%.1e dbUnitByMeter=%.1e", _uu, _um)
when 4 # ENDLIB
no_operation()
when 5 # BGNSTR
$log["structures"] += 1
_m, _a = Bgnstr.new(@data).date
Util.lpr(" Mod=%s Acc=%s", _m, _a)
when 6 # STRNAME
Util.lpr(" StructureName=%s", Strname.new(@data).name)
when 7 # ENDSTR
no_operation()
when 8 # BOUNDARY
$log["boundaries"] += 1
no_operation()
when 9 # PATH
$log["paths"] += 1
no_operation()
when 10 # SREF
$log["srefs"] += 1
no_operation()
when 11 # AREF
$log["arefs"] += 1
no_operation()
when 12 # TEXT
$log["texts"] += 1
no_operation()
when 13 # LAYER
Util.lpr(" LayerNumber=%d", Layer.new(@data).number)
when 14 # DATATYPE
Util.lpr(" DataType=%d", Datatype.new(@data).number)
when 15 # WIDTH
Util.lpr(" Width=%.1f", Width.new(@data).width)
when 16 # XY
Util.lpr(" %s", Xy.new(@data).xy_list)
when 17 # ENDEL
no_operation()
when 18 # SNAME
Util.lpr(" StructureName=%s", Sname.new(@data).name)
when 19 # COLROW
_col, _row = Colrow.new(@data).colrow
Util.lpr(" Col=%d, Row=%d", _col, _row)
when 22 # TEXTTYPE
Util.lpr(" Text_Type=%d", Texttype.new(@data).number)
when 23 # PRESENTATION
_obj = Presentation.new(@data)
Util.lpr(" Font#=%d Origin=%s", _obj.font, _obj.origin)
when 25 # STRING
Util.lpr(" String=%s", Strings.new(@data).name)
when 26 # STRANS
_obj = Strans.new(@data)
Util.lpr(" Mirror=%s Mag=%d Angle=%d",
_obj.mirror_flag, _obj.mag_flag, _obj.angle_flag)
when 27 # MAG
Util.lpr(" Mag=%.1e", Mag.new(@data).value)
when 28 # ANGLE
Util.lpr(" Angle=%.1f", Angle.new(@data).value)
when 33 # PATHTYPE
Util.lpr(" PathType=%d", Pathtype.new(@data).number)
when 48 # BGNEXTN
Util.lpr(" ProjectionSize=%.1f", Bgnextn.new(@data).width)
when 49 # ENDEXTN
Util.lpr(" ProjectionSize=%.1f", Endextn.new(@data).width)
end
end
end
# HEADER
class Header
def initialize(data)
@data = data
end
def gds_version
case @data.unpack("n")[0]
when 0 then @ver = "v3.0"
when 3 then @ver = "v3.0"
when 4 then @ver = "v4.0"
when 5 then @ver = "v5.0"
when 600 then @ver = "v6.0"
else @ver = "unknown"
end
@ver
end
end
# BGNLIB
class Bgnlib
def initialize(data)
@data = data
end
def date
my,mm,md,mh,mn,ms,ay,am,ad,ah,an,as = @data.unpack("n12")
@last_modify =
"%4d/%02d/%02d,%02d:%02d:%02d"%([1900+my,mm,md,mh,mn,ms])
@last_access =
"%4d/%02d/%02d,%02d:%02d:%02d"%([1900+ay,am,ad,ah,an,as])
return @last_modify, @last_access
end
end
# LIBNAME
class Libname
def initialize(data)
@data = data
end
def name
Util.ascii(@data)
end
end
# UNITS
class Units
def initialize(data)
@data = data
end
def units
# unpack("H16") H: Hex(upper nibble first)
@unit_by_uu = Util.real8(@data[0, 8].unpack("H16").join.hex)
@unit_by_meter = Util.real8(@data[8, 8].unpack("H16").join.hex)
return @unit_by_uu, @unit_by_meter
end
end
# BGNSTR
class Bgnstr < Bgnlib
end
# STRNAME
class Strname < Libname
end
# LAYER
class Layer
def initialize(data)
@data = data
end
def number
Util.int2(@data)
end
end
# DATATYPE
class Datatype < Layer
end
# WIDTH
class Width
def initialize(data)
@data = data
end
def width
Util.int4(@data)
end
end
# XY
class Xy
def initialize(data)
@data = data
end
def xy_list
# unpack("l*") : long(32bit signed int)
# reverse : convert endian
# map(&:to_i) : convert array to int
xys = @data.reverse.unpack("l*").map(&:to_i)
@xy_list = ""
xys.each_slice(2) do |x, y|
@xy_list <<= "(" << x.to_s << "," << y.to_s << ")"
# '<<' beter more than '+' for cat strings
end
@xy_list
end
end
# SNAME
class Sname < Libname
end
# COLROW
class Colrow
def initialize(data)
@data = data
end
def colrow
@data.unpack("n2")
end
end
# TEXTTYPE
class Texttype < Layer
end
# PRESENTATION
class Presentation
def initialize(data)
@data = data
end
def font
@data.unpack("c2")[0].to_i
end
def origin
text_origin_array = [
"upperLeft", "upperCenter", "upperRight", nil,
"centerLeft", "centerCenter", "centerRight", nil,
"lowerLeft", "lowerCenter", "lowerRight", nil
]
text_origin_array[@data.unpack("c2")[1].to_i]
end
end
# STRING
class Strings < Libname
end
# STRANS
class Strans
def initialize(data)
@data = data
end
def mirror_flag
(@data.unpack("c2")[0] & 0x80)>>7
end
def mag_flag
(@data.unpack("c2")[1] & 0x04)>>2
end
def angle_flag
(@data.unpack("c2")[1] & 0x02)>>1
end
end
# MAG
class Mag
def initialize(data)
@data = data
end
def value
Util.real8(@data[0, 8].unpack("H16").join.hex)
end
end
# ANGLE
class Angle < Mag
end
# PATHTYPE
class Pathtype < Layer
# 0=no projection,1=half round projection,
# 2=half width projection, 4=width is BGNEXTN,ENDEXTN
end
# BGNEXTN
class Bgnextn < Width
end
# ENDEXTN
class Endextn < Width
end
####################
# Main
####################
# Variables
$log = {
"records" => 0,
"structures" => 0,
"boundaries" => 0,
"paths" => 0,
"srefs" => 0,
"arefs" => 0,
"texts" => 0,
}
# Method
def usage()
STDERR.printf("%s\n", USAGE_MSG)
end
def cprint(*var)
console = open('/dev/tty', "w") do |con|
con.printf(*var)
end
end
# Parse Options
require 'optparse'
OptionParser.new do |opt|
opt.banner = USAGE_MSG
opt.version = MY_VERSION
opt.on('-l', 'print long format information') { |v| $long = v }
begin
opt.parse!(ARGV)
rescue OptionParser::InvalidOption => e
puts e.message
usage
exit
end
end
# Open File then Read and Parse
INPUT_FILE = ARGV[0]
if !INPUT_FILE then usage; exit end
begin
# not neccesary file.close
File.open(INPUT_FILE, "rb") do |file|
record = {}
c = 0
$bgnstr = 0; $boundary = 0; $path =0;
$sref = 0; $aref = 0; $text = 0
# Display Title
Util.lpr("%4s %3s %-12s %-9s %s\n",
"#", "byte", "RecType", "DataType",
"Contents (XY,WIDTH is dbUnit)")
# Read File
while !file.eof
# Get Record Size
size = file.read(2).unpack("n")[0] # to big endian
if size > 0
data = file.read(size-2) # get remain record
record[c] = Record.new(size, data)
_record_type, _data_type = record[c].to_s
Util.lpr("%4d: %3d %-12s %-9s",
c, size, _record_type, _data_type)
record[c].parse_data()
Util.lpr("\n")
else
break
end
c += 1
cprint("\r%d records loaded.", c) if !$long
end
cprint("%s: %d records, %d structures, %d boundaries, %d paths, %d srefs, %d arefs, %d texts\n",
INPUT_FILE, c, $log["structures"], $log["boundaries"], $log["paths"], $log["srefs"], $log["arefs"], $log["texts"])
end
rescue Errno::ENOENT
STDERR.printf("\n%s not found.\n", INPUT_FILE)
rescue Errno::EACCES
STDERR.printf("\ncannot open %s.\n", INPUT_FILE)
rescue => e
STDERR.printf("\nexception: class=#{e.class}, msg=#{e.message}\n")
end
#EOF
■余談
実行してみると、やっぱり遅い。読んでパース、というのを繰り返しているからかもしれません。一気に読んでからパースすれば少しは早くなるかも。でもまあ、速さを求めるなら最初からCで作りますよね。
その昔、まだCGIでもてはやされる前のperl4を勉強したのですが、独特の文法と変数の頭につくいろいろな記号に馴染めないうちに、perl5でがらっと変わってしまい、perlとは決別しました。それ以来、shとawkとCで乗り切ってきました。今回、pythonを横目で見つつrubyに挑戦してみました。
コメント 0