2024/10/04 追加 Lua 2 -- Metatable
今年の冬休みは Lua で WebDAV のスクリプトを書く事にした。現在は Yuno さんによる Perl によるスクリプトの改造版を使っているものの、実は僕は Perl は吐き気がするほと嫌いだ。あのグロテスクな言語はとてもプログラムに使えたものではない。そこで Lua で書き直すことにしたのだ。
Perl を Lua で置き換えると大きなメリットがある。それらを挙げると:
つまりこれだけのメリットが期待できれば、面倒だけど書き換えたくなるのだ。
なぜ Python ではなく Lua を?
Python は太り過ぎているが Lua は小さい。それに Lua は僕にとって初めての言語だから...
Lua は実行速度において評判の良いプログラミング言語である。言語仕様は欲張ってなく、処理系も小さい。
何故 Lua を? Lua は僕にとって今回が初めての言語である。ついでに Lua の感触を掴もうと言う訳だ。あまり慣れていないプログラミング言語を使って何かをやると行き詰まる事が多い。必要な機能が無い事に気付くのだ。Lua は大丈夫が? 「大丈夫だ」と Lua に関しては自信を持って言える。C とのインターフェースが極めて良いからだ。「不足するものがあれば C を使え」、これが Lua の設計者の思想である。実際 Lua は OS インターフェースを殆どサポートしていない。結局自分で組み込まなくてはならないが、難しくはない。Python の C インターフェースに比べると遥かに良くできている。なんと言っても C インターフェース関数をも含めて Lua の仕様としてドキュメント化されているのが嬉しい。
Lua は初心者向けの言語か? No!
初歩的なプログラムであればどんな言語を使っても大差はない。しかしちょっと高度なプログラム、例えばシステムプログラム、になると Lua の場合には不足するものがすぐに現れる。この場合には Lua では C のライブラリを組み込む必要があるのだ。従って Lua は C のエキスパートが使う言語だと言える。
+' ではなく、'..' である。なぜ '+' にはしなかったかと言えば、Lua では数字と、その文字列表現との区別が曖昧だからだ。"1" + "2"
12", Lua なら "3" である。Lua では明確に数字に変換したい時には tonumber() なる関数が準備されているが、しかし何となく状況に応じてうまく数字に変換されている。しかし僕は厳格な Python 流が好きだ。
Lua の方針: 関数は、処理結果と、ステータスを返すべきである。
nil と Python の None を比較する。変数へのこれらの値の代入x = nil # Lua
y = None # Python
y = None
None が定義されているのである。nil はもっと意味が強い。x = nil
del x # 名前の削除
Lua では未定義変数は nil の値をとる。他方 Python では未定義変数を参照するとエラーになる。
Lua のはルーズなやり方である。表面上は Lua の方が便利ではあるが、変数のスペルミスなどの実行時のエラーは Python の方が早く取れる。例えば代入文
x = foo
string.sub(s,2,5)
s[1:5]
2 ≤ i ≤ 51 ≤ i < 5
部分文字列はプログラミング時に頻繁に現れるので、Lua の書き方はうんざりする*。
s:sub(2,5) の書き方が可能である。Lua 5.1 から可能になったらしい。(2012/05/01 追記)
Lua も Python 流を採用した場合に言語設計の他の部分と矛盾するかと言えば、そうでもないらしい。それどころか文字列 s に対して
print(s[2])
Lua を設計した TeCGraf は意外と頑固なんだと思う。「 [ ] の記法は table に対して適用されるのであって、文字列は table ではない」と誘惑を振り切ったんだろうね。君ならどうする?
R"...."
Lua では何故か '|' がサポートされていない。これをサポートした場合には何かネガティブな問題をもたらすので止めたのであろう。どうしても '|' が必要であれば Lua は採用できないことになる。もっとも WebDAV サーバー程度のプログラム(相当複雑な部類に属する)では '|' は必要はなかった。
モジュールの作成は次の例で分かる。
ファイル: foo.lua
print("module foo is being loaded")
module("foo",package.seeall)
function f()
print("OK")
end
print("module foo is loaded")
ここでは関数 f() をモジュール名 foo でモジュール化している。最初の print はモジュールの外での実行、残りの print はモジュールの中での実行である。
モジュール foo を呼び出すには:
ファイル: a.lua
#!/usr/local/bin/lua
print(package.path)
require("foo")
foo.f()
ここで package.path はモジュールが置かれているパスであり、default は以下の実行結果で判明するが、自分で設定してもよい。
実行結果は
-bash$ a.lua ./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua module foo is being loaded module foo is loaded OK
さて分からないのは module の第二引数(オプション)の package.seeall である。これによって module foo の中でグローバル変数が使えるようになる。逆にこれが無ければ、モジュールの中では事実上何も出来ない。そして module の第二引数で可能なのは(現在のところ)事実上 package.seeall だけのようである。(こんな事分かんないよねー!)
何故第二引数の default を package.seeall にしなかったのか?
module の第二引数がどのように使われるのかを見てみよう。
print("module foo is being loaded")
function g()
print("g:OK")
package.seeall(foo)
end
module("foo",g)
function f()
print("f:OK")
end
print("module foo is loaded")
a.lua は同じである。実行結果は
-bash$ a.lua ./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua module foo is being loaded g:OK module foo is loaded f:OK
module の第二引数に package.seeall 以外の関数を与えた場合に、どのように役に立つのだろうか ?package.seeall 以外の関数がサポートされるのかも知れない。
なおモジュールパスの ? は shell の * と似ている。例えば lib?.lua と書けば、ファイル libfoo.lua にジュール foo を作る事になろう。僕なら ? ではなく、率直に * の仕様にするのに...
関数 module に関して疑問を述べたが、久しぶりに http://www.lua.org/ にアクセスして見ると Lua 5.2 がリリースされていた。マニュアルを読むと、関数 module は廃止すると言う。(実際には互換性のために残されているが、使わない方が良い)
理由は、関数 module が無くても問題はないと言う事である。この件に関しては、「モジュールと 関数 require」で取り上げる。
Lua では長いコメントは
--[[
...
...
--]]
</pre>
のように書く。"{...}" の部分は何でもよい。この中に "{--]]}" が含まれるなら
<pre class="code">
--[=[
...
...
--]=]
と書けばよい。"=" の個数は実はいくつでもよい。
僕はこのような Lua の仕様に最初は違和感を覚えたが、実に良く出来ている事に気付いた。(気付いたのは今日ではなくもっと以前である。今頃とは言わないで...)
だってえ...
-- [[ ... ... --]]のように "
-" と "[" の間に空白を一個入れるだけでコメントが外れるからだ。
最初に Lua に接した時から僕は Lua の local 修飾子に違和感を覚えた。そして今もなお "?" だ。
Lua はなぜ Javascript や他の言語のように var による変数宣言を採用しなかったのだろうか?
言葉の響きから var は変数宣言であるが、local は一般的な修飾子である。修飾子としての local が意味を持ちそうなものに function への local 修飾がある。
local function f()
print("f")
end
f()
これはエラーになる*。最後の f() はグローバルな名前の呼び出しだからである。うーん、local は C の static とも違うんだね...
修飾子 local は module の中では意味を持っている。
module("foo",package.seeall)
local function f()
print("f")
end
function g()
print("g")
end
g()
f()
これを foo.lua に保存して-bash$ lua
Lua 5.1.2 Copyright (C) 1994-2007 Lua.org, PUC-Rio
> require("foo")
g
f
> foo.g()
g
> foo.f()
stdin:1: attempt to call field 'f' (a nil value)
stack traceback:
stdin:1: in main chunk
[C]: ?
>
つまり local の修飾子は module を使った時に合理的な(つまり local の響き通りの)振る舞いをしている。
関数の中での関数の定義はどうか?
function f()
function g()
print("g")
end
print("f")
end
g()
f()
これを実行するとlua: bar.lua:7: attempt to call global 'g' (a nil value) stack traceback: bar.lua:7: in main chunk [C]: ?これは当然。関数
f() の中で定義された g() は外からは見えないのだからと思ったら間違いである。function f()
function g()
print("g")
end
print("f")
end
f()
g()
を実行するとエラーにならない。出力はf gエラーにならなかったのは、
f() が実行されることによって g がグローバルな名前空間(Lua では _G)に持ち込まれるからだ。
そこで今度は
function f()
local function g()
print("g")
end
print("f")
end
f()
g()
を実行してみる。するとf lua: bar.lua:8: attempt to call global 'g' (a nil value) stack traceback: bar.lua:8: in main chunk [C]: ?つまり
local の役割はグローバルな名前空間に名前が入るのをブロックしているのだ。
しかし、何と言うシンプルな割り切り方だ! このシンプルさによって、Lua の実装は簡単になったであろう。その代償は、直感の犠牲である。宣言の構造が名前空間を決めるのではなく、実行の流れが名前空間を決めているのであるから直ぐには分からない...
import は Python でモジュールを使う時の宣言子である。モジュール foo を使う時には
import foo
require() がある。但し 宣言子ではなく、関数である。 require("foo")
from foo import *
foo から全ての名前を取り込める。また * の部分にfrom foo import bar,baz
Lua の場合には、もしも欲しいなら、自分で作らなくてはならない。次のコードが参考になるだろう。
function import(t,...)
local u = {...}
if u[1] == "*" then
for k,v in pairs(t) do
_G[k] = v
end
else
for k,v in ipairs(u) do
_G[v] = t[v]
end
end
end
これは次のように実行する。
t = require("foo")
import(t,"*")
モジュール foo の中の、bar, baz を取り込みたいなら
t = require("foo")
import(t,"bar","baz")
とやればよい。require() と import() の二つを使うのが嫌なら、import() を一つに纏めた仕様にすることもできるだろう。
しかし、僕は import() は要らないと思う。代わりに
require("foo")
bar = foo.bar
baz = foo.baz
とやればよいだけだから。(名前を変えて取り込む事もできる。Python でも同じだけどもね...)
module の廃止が予告されている。この関数は、モジュールの中で定義されている関数が、モジュールを呼び出した際に、そのまま名前空間に入って行くのを防いでいる。例えば
function foo()
print("This is foo")
end
function bar(s)
print(s)
end
require("hoge")
foo()
bar("This is bar")
は、次の出力を出す。
This is foo This is bar
つまり、require("hoge") は Python の
from hoge import *
hoge の全ての名前を取り込む。これは、名前の衝突を引き起こす可能性があり、多くのモジュールを持ち出すと、そのリスクが増大する。module 関数は、仕様に変な部分があるが、その問題に対する1つの回答だったのである。
では、どうしたら良いか? Lua 5.2 のマニュアルには、Lua の一般的な方法の中で問題を解決できるのだと言う。(しかし具体的な解決策は示されていない)
次のようなのはどうか?
local function foo()
print("This is foo")
end
local function bar(s)
print(s)
end
hoge={}
hoge.foo=foo
hoge.bar=bar
このように書けば、グローバルな名前空間に取り込まれるのは hoge だけである。
require("hoge")
hoge.foo()
hoge.bar("This is bar")
結果は
This is foo This is bar
となる。つまり Python の
import hoge
import hoge as fuga
hoge だって衝突するかも知れないから、代替え手段が欲しいのである。
理想的なのは、require が、モジュールの中で定義されている名前テーブルを返してくれることである。どうすれば良いか? 筆者は今日になって、ようやく方法が存在する事が分かった。モジュールの最後の return で解決するのだ。
local function foo()
print("This is foo")
end
local function bar(s)
print(s)
end
local t={}
t.foo=foo
t.bar=bar
return t
fuga = require("hoge")
fuga.foo()
fuga.bar("This is bar")
のように実行するのである。
require("hoge") を2度実行しても、二重にはロード(実行)されない。Lua にはモジュールの二重実行を防ぐための仕組みがある。hoge が汎用的なモジュールであれば、複数のモジュールから hoge が呼び出されるであろう。hoge がデータを持つ場合には、そのデータも共有される。それが問題をもたらすようであれば、closure を使って、データを独立させる必要がある。
-- Lua Tools
-- coded by Kenar (Kenji Arisawa)
-- email: arisawa@aichi-u.ac.jp
local function printf(fmt,...)
io.write(string.format(fmt,...))
end
local function dumpstr(s)
if type(s)=="string" then
local repl={["\n"]="\\n",["\t"]="\\t"}
return s:gsub("(%c)",repl)
end
return s
end
local spectype={}
spectype["string"]=true
spectype["number"]=true
spectype["boolean"]=true
local function inspecttab(t,n,done)
if n==nil then
done={}
n=""
end
if t==nil then
return
end
done[t]=true
for k,v in pairs(t) do
if type(v) == "table" then
if done[v] then
printf("%s%s\t%s # done\n",n,k,v)
else
printf("%s%s\t%s\n",n,k,v)
inspecttab(v,n.."\t",done)
end
elseif spectype[type(v)] then
printf("%s%s\t%s: %s\n",n,k,type(v),dumpstr(v))
else
printf("%s%s\t%s\n",n,k,v)
end
end
end
local t={
printf=printf,
inspecttab=inspecttab
}
return t
実行例を示す。手軽に利用できる一番大きなテーブルは _G である。_G のテーブルは、参照関係が循環しているので、注意してコードを書かないと、無限ループにハマる。図4のプログラムでは、一度書き出したテーブルは、再度書かないことで、この無限ループを避けている。「# done」と表示されているテーブルは、既出のテーブルなのである。
ソートした方が良かったかも... (iPad だと全部が見えない...)
Lua は XML の処理に使いやすい言語だと思う。Lua で扱うデータ構造は十分に強力だし、正規表現による文字列の操作も扱いやすい。Python の場合には正規表現を使う前にはコンパイルしなくてはならないので面倒であるが、Lua はその必要はないのはありがたい。(2009/04/04)
http://lua-users.org/wiki/LuaXml に載っていた。
function parseargs(s)
local arg = {}
string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a)
arg[w] = a
end)
return arg
end
function collect(s)
local stack = {}
local top = {}
table.insert(stack, top)
local ni,c,label,xarg, empty
local i, j = 1, 1
while true do
ni,j,c,label,xarg, empty = string.find(s, "<(%/?)(%w+)(.-)(%/?)>", i)
if not ni then break end
local text = string.sub(s, i, ni-1)
if not string.find(text, "^%s*$") then
table.insert(top, text)
end
if empty == "/" then -- empty element tag
table.insert(top, {label=label, xarg=parseargs(xarg), empty=1})
elseif c == "" then -- start tag
top = {label=label, xarg=parseargs(xarg)}
table.insert(stack, top) -- new level
else -- end tag
local toclose = table.remove(stack) -- remove top
top = stack[#stack]
if #stack < 1 then
error("nothing to close with "..label)
end
if toclose.label ~= label then
error("trying to close "..toclose.label.." with "..label)
end
table.insert(top, toclose)
end
i = j+1
end
local text = string.sub(s, i)
if not string.find(text, "^%s*$") then
table.insert(stack[#stack], text)
end
if #stack > 1 then
error("unclosed "..stack[stack.n].label)
end
return stack[1]
end
次にこのコードの実行例を載せる。content = [[ <?xml version="1.0" encoding="utf-8"?> <D:lockinfo xmlns:D="DAV:"> <D:lockscope><D:exclusive/></D:lockscope> <D:locktype><D:write/></D:locktype> <D:owner> <D:href>http://www.apple.com/webdav_fs/</D:href> </D:owner> </D:lockinfo> ]] s = collect(content) print(s[1]) print(s[2].label, s[2].xarg) t = s[2].xarg for k,v in pairs(t) do print(k,v) endこの結果を次に示すが明らかにおかしい。
<?xml version="1.0" encoding="utf-8"?> D table: 0x6F488 D DAV:どうやら問題のプログラムコードは名前ラベルが存在しない事を前提にしているのだ。それにしても何で "label" を意識したコードが含まれているのだ?
XML のタグは
<NAME ATTRS> <NAME ATTRS/> </NAME>
NAME はD:lockinfo
ATTRS はxmlns:D="DAV:"
NAME のシンタックスは単純である。NAME の中には空白は許されない。NAME の中に /<> は許されない。
また ATTRS は空白で区切られた ATTR のリストである。ここでのシンタックス規則は単に
ATTRS の中に /<> は許されない。
次のプログラムの関数名は
xml.collect
." を含む名前付けを可能にするためには xml = {}
function xml.collect(s)
local ns
local stack = {}
local top = {}
local ni,c,name,attrs, empty
local i,j,decl =string.find(s, "<%?xml *(.-)%?>")
table.insert(stack, top)
j = j + 1
i = j
while true do
-- tags have the form: "<NAME ATTRS/?>", "<NAME/?>", "</NAME>",
ni,j,c,name,attrs, empty = string.find(s, "<(/?)([^%s/<>]+) *(.-)(/?)>", i)
-- print("DBG0: ",ni,j,c,"/",name,attrs,"/",empty)
if not ni then break end
local text = string.sub(s, i, ni-1)
if not string.find(text, "^%s*$") then
table.insert(top, text)
end
if empty == "/" then -- empty element tag
local e
e = {name=name, attrs=attrs}
table.insert(top,e)
elseif c == "" then -- start tag
top = {name=name, attrs=attrs}
table.insert(stack, top) -- new level
else -- end tag
local toclose = table.remove(stack) -- remove top
top = stack[#stack]
if #stack < 1 then
error("nothing to close with "..name)
end
if toclose.name ~= name then
error("trying to close "..toclose.name.." with "..name)
end
table.insert(top, toclose)
end
i = j+1
end
local text = string.sub(s, i)
if not string.find(text, "^%s*$") then
table.insert(stack[#stack], text)
end
if #stack > 1 then
error("unclosed "..stack[stack.n].name)
end
return stack[1]
end
このコードに現れる正規表現であるがstring.find(s, "<(/?)([^%s/<>]+) *(.-)(/?)>", i)
(.-)
([^/<>]*)
次のプログラムは xml.collect が集めた結果を分析するのに役に立つ。
function xml.printstack(t, Ntab)
-- this function shows the structure of stack returned from xml.collect()
-- please call me by: t = xml.collect(content); xml.printstack(t)
if t == nil then
return
end
if Ntab == nil then
Ntab = 0
end
if type(t) == "string" then
print(Ntab,'T', t) -- text outside of tags
return
end
Ntab = Ntab + 1
-- Ntab: tag level (namespace level)
-- #t: number of element, starting tag (if #t>0) or empty tag (if #t == 0)
-- t.name: tag name; t.attrs: tag attributes
print(Ntab,#t,t.name, t.attrs)
for i=1, #t do
xml.printstack(t[i], Ntab)
end
print(Ntab,"/",t.name)
Ntab = Ntab - 1
end
実行例はcontent = [[ <?xml version="1.0" encoding="utf-8"?> <D:lockinfo xmlns:D="DAV:"> <D:lockscope><D:exclusive/></D:lockscope> <D:locktype><D:write/></D:locktype> <D:owner> <D:href>http://www.apple.com/webdav_fs/</D:href> </D:owner> </D:lockinfo> ]] s = xml.collect(content) print(s[1].name) print(s[1][1].name) print(s[1][1][1].name) xml.printstack(s[1])この結果は
term% lua libxml.lua D:lockinfo D:lockscope D:exclusive 1 3 D:lockinfo xmlns:D="DAV:" 2 1 D:lockscope 3 0 D:exclusive 3 / D:exclusive 2 / D:lockscope 2 1 D:locktype 3 0 D:write 3 / D:write 2 / D:locktype 2 1 D:owner 3 1 D:href 3 T http://www.apple.com/webdav_fs/ 3 / D:href 2 / D:owner 1 / D:lockinfo term%読者は
xml.printstack のコードを利用して、xml.collect の結果を必要に応じて様々に加工する関数を書く事が事ができる。
collect()を使ってみると、分かり難さを感じる。諸悪の根源は collect() が情報を十分に組織化していないことにある。例えば node を collect() から得られたノードの一つとする。そのときに node[1] が表しているものは、type(node[1]) が string ならそのノードのテキストデータ、table なら子供のノードである。このような非一貫性が collect() の使い心地を酷く損なっている。次に示す mktree() はこの問題を解決している。(2009/04/04)
さて僕は最近、現実に現れるある複雑な XML データ(XBRL のデータ)を処理していて、強力なツールを必要とした。そのために開発したのが次の mktree() である。この関数は気に入っているので紹介しておく。
XML のノードは、名前(tag name)と属性(tag attributes)とノードに属するテキストデータを持っている。さらに一般的に言えば、親と子供たちと繋がっている。
<definition>The <term>computer</term> is blah blah ... </definition>
ノードを N とすると、
N.name # node name N.attrs # node attributes N.attr # elements of the attributes N.data # value of the node N.parent # parent node N.no # array index of this node referenced by the parent N[1] # node of the child 1 N[2] # node of the child 2 ... N[n] # node of the child n
n は #N に等しい。また<comment id="X0123" name="alice" age="18">Hello everyone, my name is alice.</comment>
N.name は "comment" であり、N.attrs はid="X0123" name="alice" age="18"
N.data はHello everyone, my name is alice.
data があれば子供は無い。子供があれば、data は無い。i 番目の子供のノードを表現するなら N[i] ではなくN.child[i]
N に対して形式的に N[1],N[2],... を使えるけれども、それらは辞書の要素であって、リストではない。あとのデータ処理を考えると好ましい考え方ではない。(実行速度の問題、加工に伴うよけいな複雑さ、子供の個数の管理など) Lua の table は Python で言う辞書とリストを兼ねているのである。
一般的に言えば N.attrs は要素を持っており、それらを簡単に参照したいとするニーズは高い。そこで N.attr を導入した。このケースでは
N.attr["id"] # "X0123" N.attr["name"] # "alice" N.attr["age"] # "18"
N.no は親ノードから見た、そのノードの配列番号である。従って
N.parent[N.no]
N.parent[N.no + 1]
さらに DOM 的な機能を実現しやすい情報を追加する。すなわち、ノードの名前(tag name) と、属性に現れる id の一覧のデータベースを返す。つまり XML 仕様の文字列 xmldata をインプットとして
node,tags,ids = mktree(xmldata)
node の他に、タグ名の一覧 tags と id の一覧 ids を返す。一般的に言えば、同じ名前のタグ名は複数存在するので tags は Lua の配列であり、for i,v in ipairs(tags["comment"]) do print(v.attr.name,v.attr.age) # alice 18 -- etc print(v.data) # Hello everyone, my name is alice. --- etc end
id の場合には、一意に決まるはずだから繰り返し文を使わなくてもv = ids["X0123"] print(v.attr.name,v.attr.age) # alice 18 print(v.data) # Hello everyone, my name is alice
match = string.match
find = string.find
local function deco(a)
-- decompose attrs
local t = {}
local s,b,c,d
local q = "'\""
if a == nil then
return nil
end
s = split(a)
for i=1,#s do
a,b,c,d = match(s[i],"([^=]+)=(["..q.."])([^"..q.."]*)(["..q.."])")
if a == nil or b ~= d then
print("## invalid xml attr format: ",s[i])
else
t[a] = c
end
end
return t
end
local function putdb(e,names,ids)
if names[e.name] == nil then
names[e.name] = {}
end
table.insert(names[e.name], e)
if e.attr and e.attr.id then
if ids[e.attr.id] then
error("same id: "..e.attr.id)
end
ids[e.attr.id] = e
end
end
function mktree(s)
-- modified collect()
-- mktree() has a DOM like ability
-- a powerful tool to evaluate xml data
local ns
local stack = {}
local top = {}
local ni,c,name,attrs, empty
local i,j,decl =find(s, "<%?xml *(.-)%?>")
local ids = {} -- id table
local names = {} -- tag name table
if j == nil then
i = 1
else
i = j + 1
end
while true do
-- tags have the form: "<NAME ATTRS/?>", "<NAME/?>", "</NAME>",
-- we must skip comment! we cant't use "repeat ... until" here
ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
while name == "!--" do
i = j+1
ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
end
-- here attrs == "" if no attr but we make it nil
if attrs == "" then
attrs = nil
end
if not ni then
break
end
local text = sub(s, i, ni-1)
if not find(text, "^%s*$") then
top.data = text
end
if empty == "/" then -- empty element tag
local e,attr
attr = deco(attrs)
e = {name=name, attrs=attrs, attr=attr, parent=stack[#stack], no=#top+1}
table.insert(top,e)
putdb(e,names,ids)
elseif c == "" then -- start tag
local e,attr
attr = deco(attrs)
e = {name=name, attrs=attrs, attr=attr, parent=stack[#stack], no=#top+1}
table.insert(stack, e) -- new level
putdb(e,names,ids)
top = e
else -- end tag
close = table.remove(stack) -- remove top
if close.name ~= name then
error("inconsitent open and close tag: "..close.name.." with "..name)
end
if #stack < 1 then
break
end
top = stack[#stack] -- stack[0] == nil
table.insert(top, close)
end
i = j+1
end
if #stack > 1 then
error("unclosed "..stack[#stack].name)
end
return close,names,ids
end
ここに現れる split() は次のように定義されている。
gsub = string.gsub
find = string.find
function split(s,sep)
local i,j,k
local t={}
if sep == nil then
gsub(s,"([^%s]+)",function(a) table.insert(t,a) end)
return t
end
i = 1
while true do
j,k = find(s,sep,i,true)
if j == nil then
table.insert(t,sub(s,i))
break
end
table.insert(t,sub(s,i,j-1))
i = k + 1
end
return t
end
以上のコードは自由に利用しても構わないが、出典だけは明示して欲しい。
You can use the codes above freely under the Lua license 5.0 and later versions (http://www.lua.org/license.html)
mkidf
現実に使われる XML のデータは複雑である。例えばビジネスに使われる会計データの仕様(XBRL)が最近から運用されだしている。公開されている企業別のデータを見てみると、実に複雑で大きくて、一つの企業の処理に関係する XML で書かれたファイルは数十個に及ぶ。その中の一つの冒頭部部だけを示すと
<?xml version="1.0" encoding="utf-8"?>
<!-- Edited with XiRUTE .NET Library 01-04 -->
<xbrli:xbrl xmlns:iso4217="http://www.xbrl.org/2003/iso4217" xmlns:jpfr-q3r-E02149-000="http://info.edinet-fsa.go.jp/jp/fr/gaap/E02149-000/q3r/2008-12-31/01/2009-02-12" xmlns:jpfr-t-cte="http://info.edinet-fsa.go.jp/jp/fr/gaap/t/cte/2008-02-01" xmlns:link="http://www.xbrl.org/2003/linkbase" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpfr-di="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/di/2008-02-01" xmlns:xbrli="http://www.xbrl.org/2003/instance">
<link:schemaRef xlink:type="simple" xlink:href="jpfr-q3r-E02149-000-2008-12-31-01-2009-02-12.xsd" />
<link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumber" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumber" />
<link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeYTD" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfIncomeYTD" />
<link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeQuarter" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/
ConsolidatedQuarterlyStatementsOfIncomeQuarter" />
<link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumberPeriodEnd" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumberPeriodEnd" />
<link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyBalanceSheets" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyBalanceSheets" />
<link:roleRef xlink:type="simple" xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfCashFlowsIndirect" roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfCashFlowsIndirect" />
<xbrli:context id="CurrentQuarterConsolidatedInstant">
<xbrli:entity>
<xbrli:identifier scheme="http://info.edinet-fsa.go.jp">E02149-000</xbrli:identifier>
</xbrli:entity>
<xbrli:period>
<xbrli:instant>2008-12-31</xbrli:instant>
</xbrli:period>
</xbrli:context>
....
....
のようなものである。 実際のこのデータ a.xbrl
このようなデータを眺めていると XML データというのは人間にとって、そのデータ構造が分かり難いものだと思う。このケースでは特に属性部の記述量が多くて、見難くしている。もっと分かりやすい表現法がないととても全体像が捉えられないであろう。そこで次のような形式で表示するツールを作った。
# file: jp/fr/gaap/E02149-000/q3r/2008-12-31/01/2009-02-12/jpfr-q3r-E02149-000-2008-12-31-01-2009-02-12.xbrl # format: idf-1.0 # Edited with XiRUTE .NET Library 01-04 xbrli:xbrl - xmlns:iso4217="http://www.xbrl.org/2003/iso4217" - xmlns:jpfr-q3r-E02149-000="http://info.edinet-fsa.go.jp/jp/fr/gaap/E02149-000/q3r/2008-12-31/01/2009-02-12" - xmlns:jpfr-t-cte="http://info.edinet-fsa.go.jp/jp/fr/gaap/t/cte/2008-02-01" - xmlns:link="http://www.xbrl.org/2003/linkbase" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:jpfr-di="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/di/2008-02-01" - xmlns:xbrli="http://www.xbrl.org/2003/instance" link:schemaRef - xlink:type="simple" - xlink:href="jpfr-q3r-E02149-000-2008-12-31-01-2009-02-12.xsd" link:roleRef - xlink:type="simple" - xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumber" - roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumber" link:roleRef - xlink:type="simple" - xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeYTD" - roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfIncomeYTD" link:roleRef - xlink:type="simple" - xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfIncomeQuarter" - roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfIncomeQuarter" link:roleRef - xlink:type="simple" - xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleNotesNumberPeriodEnd" - roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/NotesNumberPeriodEnd" link:roleRef - xlink:type="simple" - xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyBalanceSheets" - roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyBalanceSheets" link:roleRef - xlink:type="simple" - xlink:href="http://info.edinet-fsa.go.jp/jp/fr/gaap/o/rt/2008-02-01/jpfr-rt-2008-02-01.xsd#RoleConsolidatedQuarterlyStatementsOfCashFlowsIndirect" - roleURI="http://info.edinet-fsa.go.jp/jp/fr/gaap/role/ConsolidatedQuarterlyStatementsOfCashFlowsIndirect" xbrli:context - id="CurrentQuarterConsolidatedInstant" xbrli:entity xbrli:identifier - scheme="http://info.edinet-fsa.go.jp" * E02149-000 xbrli:period xbrli:instant * 2008-12-31 .... ....
ここに
# で始まる行はコメントである。XML のコメントの他に、あらたにビューアのコメントが追加されている。- で始まる行は属性である。* で始まる行はタグに属するデータである。a.xbrl をこの形式(僕は IDF と読んでいる)に変換したファイルも参考のために見えるようにしておく。a.idf
次に示すのは関数 mkidf() と mkxml() である。関数 mkidf() は XML ファイルを読み取り、IDF 形式で出力する。関数 mkxml() はその逆を行う。mkxml() ではインデントが指定できるので、この二つの関数の組み合わせで、XML ファイルをインデントを指定して整形できる。なお mkidf() のインデントは TAB 固定である。インデントが命であるから、空白とタブの混合はサポートしない。(Python はプログラムのブロックをインデントで表しているが、そのようなやりかたは、データ表現に対しては重大な問題を引き起こすだろう)
local function mksp(n)
s = ""
for i=1,n do
s = s .. "\t"
end
return s
end
function mkidf(file)
-- file: xml file
-- print in indented data format(idf)
-- idf is useful to view logical structure of xml file
local ni,c,name,attrs, empty
local stack = {}
local s,t
local f
print("# format: idf-1.0")
f = io.open(file)
if f == nil then
print("no such file: "..file)
return
end
s = f:read("*a")
f:close()
local i,j,decl =find(s, "<%?xml *(.-)%?>")
if j == nil then
i = 1
else
i = j + 1
end
while true do
-- tags have the form: "<NAME ATTRS/?>", "<NAME/?>", "</NAME>",
ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
while name == "!--" do
-- <!-- comments -->
print(mksp(#stack).."# "..sub(s,ni + 5,j - 4))
i = j+1
ni,j,c,name,attrs, empty = find(s, "<(/?)([^%s/>]+) *(.-)(/?)>", i)
end
-- here attrs == "" if no attr but we make it nil
if attrs == "" then
attrs = nil
end
if not ni then
break
end
local text = sub(s, i, ni-1)
if not find(text, "^%s*$") then
print(mksp(#stack - 1).."* "..text)
end
if empty == "/" or c == "" then -- empty element tag or starting tag
print(mksp(#stack)..name)
if attrs then
t = split(attrs)
for i=1,#t do
print(mksp(#stack).."- "..t[i])
end
end
if empty ~= "/" then -- start tag
table.insert(stack,name)
end
else -- end tag
local toclose = table.remove(stack) -- remove top
if toclose ~= name then
error("trying to close "..toclose.." with "..name)
end
end
i = j+1
end
end
function mkxml(file,indent)
-- make xml file from idf file
-- reverse of mkidf()
local function sp(n)
local s = ""
for i=1,n do
s = s .. " "
end
return s
end
local s,t,i,stack,t0,t00
if indent == nil then
indent = 0
end
stack = {}
io.write('<?xml version="1.0" encoding="utf-8"?>\n')
i = -1
for line in io.lines(file) do
s = match(line,"^(\t*)")
t = sub(line,#s + 1)
if sub(t,1,1) == " " then
print("\n### use tabs for indent")
os.exit()
end
t0 = sub(t,1,2)
if t0 == "- " then -- an attribute/value pair
io.write(" "..sub(t,3))
elseif t0 == "* " then -- a data value
if t00 and t0 ~= t00 then
io.write(">")
end
io.write(sub(t,3))
elseif sub(t,1,2) == "# " then -- comments
io.write(sp(#stack * indent).."<!-- "..sub(t,3).." -->\n")
elseif match(t,"^%w") then -- a name
-- we have 9 cases:
-- t00 = "- "; t00 = "* "; others
-- #s > i; #s == i; #s < i
if #s > i then -- new level, t must be a name
-- note that t00 ~= "* " here
if i ~= -1 then
io.write(">\n")
end
table.insert(stack,name)
if #s ~= #stack then
print("# invalid indent")
os.exit()
end
io.write(sp(#s * indent).."<"..t)
else
if t00 == "* " then
io.write("</"..name..">\n")
else
io.write("/>\n")
end
if #s < i then -- back to low level until #s == i
repeat
name = table.remove(stack)
io.write(sp(#stack * indent).."</"..name..">\n")
until #s == #stack
end
io.write(sp(#s * indent).."<"..t)
end
name = t
i = #s
else
print("# invalid format")
os.exit()
end
t00 = t0
end
if t00 == "* " then
io.write("</"..name..">\n")
else
io.write("/>\n")
end
while #stack > 0 do
name = table.remove(stack)
io.write(sp(#stack * indent).."</"..name..">\n")
end
end
関数 mkidf は mktree() を使えばもっと簡単に書けるのだが、その場合にはテータを全てメモリーに読み取る事になるので、実行効率の良い mkidf() を新たに書いた。
エンコードやデコードを行っていないので注意する。
以上のコードは自由に利用しても構わないが、出典だけは明示して欲しい。
You can use the codes above freely under the Lua license 5.0 and later versions (http://www.lua.org/license.html)
性格の違うものを一緒に議論するのは混乱の元である。XML に関してはこの点はどうか? XML は XBRL のようにコンピュータによるデータ処理のためのものなのか? それとも HTML のように人に読ませるためのドキュメント処理のためのものなのか? あるいはその両方を目指しているのか?
この二つは、共通部分もあるが、基本的に正反対の方向である。
前者の場合には、処理の誤りは致命的である。さらに悪い事にその処理の誤りが見え難い。データ形式については組織化できるし、組織化しなくてはならない。
他方後者の場合には自由が求められる。処理の誤りは直ちに人目に曝され、致命的な結果をもたらす事はない。
一例を挙げよう。前者の場合には計算処理に使われるデータは
<foo>.....</foo>
....." の中には他のタグを入れるべきではない*。しかし後者の場合にそのような制約を課していたら文書を作れない。
mktree() も mkidf() も mkxml() もこの規則が守られていることを前提にしている。
XML の仕様書は膨大であり、読む気にもなれない。膨大になって行く原因は、スーパーセットを目指しているからだ。しかしスーパーセット主義は失敗する事を歴史が教えている。
ムーアの法則が示しているように、コンピュータの処理能力は指数関数的に発展している。プログラムの複雑さはどうか? Python や Lua を使っていると、随分楽になったなと思う。新しい言語の出現がプログラムの複雑さを時々大きく緩和してくれるのだ。しかし規格書の複雑さはどうか? XML の仕様書を見ると、プログラマが読まなくてはならない文書が指数関数的に増大し複雑になっていると感じる。やがては人間の能力の限界を越えるであろう。何かがまずいのである。
HTTP のように TCP レベルのプログラムを書くプログラマは、その下位のレベルのプロトコル(IP や イーサネットレベル)を詳しく知る必要は無い。HTML レベルの文書を作る場合にも、同様に HTTP レベルの詳しい知識は要らない。そのような階層化された構造を持っているからこそ、発展可能なのである。XML に関しては無理なのか?