より効率的なリスト・レコードのJSON変換を目指す

AppleScriptのオブジェクトをソースコード化するcoding()ハンドラの作り方を発見してから、すっかりその魅力にハマってしまった。最近の頭の中は、寝ても覚めてもAppleScript脳になってしまっている。そもそもの始まりは、AppleScriptのレコード情報をJavaScriptの世界に渡そうとしたことが事の始まりだった。
そのためにはまず、

  1. レコードをAppleScriptのソーステキストに変換して、
  2. そのソーステキストをJSONに変換して、

do JavaScriptで実行するのだ。

1の変換を担うのが、coding()ハンドラ。(この発見がすべての始まり)
2の変換を担うのが、json_from()ハンドラ。

バージョン1

  • AppleScriptのレコードとJSONは似ているので、二つの変換をするだけでJSONになる。
    • リストの{}を[]に変換する。
    • |キー|のように囲われていたら、「|」を取り除く。
      • AppleScriptでは、予約語や日本語をキーとする場合は、|日本語|のように囲う必要があるのだ。
  • 最初に作ったコードは以下のようなものだった。


{{a:1}, {b:2}} json_from1(result) --結果:"[{a:1}, {b:2}]"

on json_from1(list_recode) coding(list_recode) _parse_list_record1(result's items, 1) replace(result, "|", "") end json_from1
--json_fromから呼び出され、リストの{}[]に変換する
on _parse_list_record1(str_list, i) set output to {} repeat
set v to str_list's item i
if v = "{" then
set box to _parse_list_record1(str_list, i + 1) set output's end to box
set i to i + (box's length) else if v = "}" then
set obj to run script "{" & output & "}"
if obj's class = list then
return "[" & output & "]"
else
return "{" & output & "}"
end if
else
set output's end to v
set i to i + 1
end if
if i > str_list's length then exit repeat
end repeat
output
end _parse_list_record1

on coding(obj) try
if obj's class = text then
"\"" & obj & "\""
else if obj's class = list or obj's class = record then
obj as number
else
obj as text
end if
on error msg
try
--match_str("/\\{.*\\}/=~" & quoted form of msg) --依存するので以下のコードに回避した
"require \"jcode\";$KCODE=\"u\";/\\{.*\\}/=~" & quoted form of msg & ";puts($&);"
do shell script "ruby -e " & quoted form of result
on error
--do shell script262,144バイト制限エラーでも、日本語環境なら救われる(それ以外はNG
--http://developer.apple.com/jp/technotes/tn2002/tn2065.html
msg's items 1 thru -24 as text
end try
end try
end coding

on replace(sourceText, text_list1, text_list2) join(split(sourceText, text_list1), text_list2) end replace

on split(sourceText, delimiter) if sourceText = "" then return {} set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set theList to text items of sourceText
set AppleScript's text item delimiters to oldDelimiters
return theList
end split

on join(sourceList, delimiter) set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to delimiter
set theText to sourceList as text
set AppleScript's text item delimiters to oldDelimiters
return theText
end join

  • これを実行すると、サンプルのような結果になる。問題ない。
  • ところが、より実践的な環境でいざ使ってみると、問題発生!

遅い!のだ。


{https___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_do:{{selected:false, value:"1234", type:"text", |name|:"okyakusamaBangou1", |index|:"3"}, {checked:false, value:"5678", type:"text", |name|:"okyakusamaBangou2", |index|:"4"}, {checked:false, value:"12345", type:"text", |name|:"okyakusamaBangou3", |index|:"5"}}}
display dialog (json_from(result))

  • 具体的には、auto_loginスクリプトで使った。
  • auto_loginの持つログイン情報は上記のような形式だ。
  • 上記はその一部で、実際にはその100倍以上の情報量を一気に扱う。
  • パフォーマンスを計測してみた。


{https___direct_jp_bank_japanpost_jp_tp1web_U010101SCK_do:{{selected:false, value:"1234", type:"text", |name|:"okyakusamaBangou1", |index|:"3"}, {checked:false, value:"5678", type:"text", |name|:"okyakusamaBangou2", |index|:"4"}, {checked:false, value:"12345", type:"text", |name|:"okyakusamaBangou3", |index|:"5"}}}
set a_record to result

set t to current date
repeat 10 times
set res to json_from1(a_record)
end repeat
(current date) - t
display dialog result --43

43秒かかった。

      • 計測には実際使っているauto_loginのログイン情報を利用した。
      • 以下コード中のレコードは、そのほんの一部である。
      • なので以下コードのまま計測すると一瞬で終了する。
  • つまり、毎回JSON変換する度に4.3秒変換処理中になり、反応が止まることになる。

結構イラつく。

  • AmazonではWebページの買物レスポンスが0.1秒遅くなると、1%売上げが落ちるそうである。
  • よって、4.3秒待たされると、43%も売上げが落ちてしまうのである。とんでもない遅さである。
  • AmazonAppleScriptを採用して、coding()およびjson_from()ハンドラを使う事はまずないけれど、

このまま放置するのは心が痛む。

バージョン2

  • そこで、別のアルゴリズムjson_from()を実装してみる。
    • 再帰呼び出しはやめた。スタックへのpshu、pullで括弧の対応探る。
    • 一文字ずつ処理するのもやめた。最初に括弧とその他の要素にリスト分割してから、処理する。


on json_from2(list_recode) coding(list_recode) replace(result, "{", "__DLMT__{__DLMT__") replace(result, "}", "__DLMT__}__DLMT__") replace(result, "__DLMT____DLMT__", "__DLMT__") split(result, "__DLMT__") _parse_list_record2(result) replace(result, "|", "") end json_from2
--json_fromから呼び出され、リストの{}[]に変換する
on _parse_list_record2(elm_list) set stack to {} repeat with e in elm_list
set e to e as text
if e = "}" then
set inner to {} repeat
set pull to stack's item 1
set stack to stack's rest
if pull = "{" then
set obj to run script "{" & inner & "}"
if obj's class = list then
set stack's beginning to "[" & inner & "]"
else
set stack's beginning to "{" & inner & "}"
end if
exit repeat
else
set inner's beginning to pull
end if
end repeat
else
set stack's beginning to e
end if
end repeat
stack as text
end _parse_list_record2

  • 計測してみた。

すると、2秒!

  • あまりに速過ぎるので、ループを100回に変更してみると、

24秒だった。

  • つまり、1回当り0.24秒の処理時間。15倍以上の改善だ。

スクリプトメニュー問題

  • これならAmazonにも採用してもらえるかも(あり得ん)、と気を良くしていると、またもや問題発生。
  • AppleScriptエディタを開いて実行している時は何の問題もなかったのだが、
  • スクリプトメニューから実行すると、json_from()が実行できないのである。

謎である。

  • コードを追って行くと、何回目かのrun scriptの部分で、AppleScript Runner.appが突然落ちる。
  • 落ちた瞬間のsystem.logには、以下のように記録されていた。
/System/Library/CoreServices/AppleScript Runner.app/Contents/MacOS/AppleScript Runner[2794]: CPSGetFrontProcess(): This call is deprecated and should not be called anymore
  • しかし、この時はそれ以上の原因までは追求できなかった。暫し、この問題は放置することにした。
  • 一つ言える事は、AppleScriptエディタで実行した時はAppleScriptエディタ.appがAppleScriptコードを実行するが、
  • スクリプトメニューから実行した時は、/System/Library/CoreServices/AppleScript Runner.appがコードを実行する。
  • 実行環境は全く同じではない。AppleScript Runner.appではダメな理由がどこかにあるはず。

バージョン3

  • スクリプトメニュー問題は未だ解決できないが、その過程で別の発想が浮かんだ。
  • 問題は、run scriptを実行して、リストかレコードかを判定している部分である。
  • 試しにその判定をやめて実行してみると、さらに高速化されたのである。
    • 但し、常にリストと判定するようにしたので、JSON変換はできていない。

run scriptは、コストのかかる処理なのだ。

  • もし、run script以外の方法でリストかレコードかを判定できれば、さらに高速化するかもしれない。
  • そう考えてやってみたのが、以下のコード。


on json_from3(list_recode) coding(list_recode) replace(result, "{", "__DLMT__{__DLMT__") replace(result, "}", "__DLMT__}__DLMT__") replace(result, "__DLMT____DLMT__", "__DLMT__") split(result, "__DLMT__") _parse_list_record3(result) replace(result, "|", "") end json_from
--json_fromから呼び出され、リストの{}[]に変換する
on _parse_list_record3(elm_list) set num to 0
set stack to {} repeat with e in elm_list
set e to e as text
if e = "}" then
set inner to {} repeat
set pull to stack's item 1
set stack to stack's rest
if pull = "{" then
if _is_list_code(inner) then
set stack's beginning to "[" & inner & "]"
else
set stack's beginning to "{" & inner & "}"
end if
exit repeat
else
set inner's beginning to pull
end if
end repeat
else
set stack's beginning to e
end if
end repeat
stack as text
end _parse_list_record
--_parse_list_recordから呼び出され、リストかどうか判定する
on _is_list_code(inner) if inner's item 1's item 1 is in "{[" then
true
else
set kv_list to split(inner as text, ":") if kv_list's number = 1 then
true
else
set qt_list to split(kv_list's item 1, "\"") if qt_list's number = 1 then
false
else
true
end if
end if
end if
end _is_list_code

  • パフォーマンスを計測すると、100回ループさせて5秒。

さらに高速化された!

  • 1回当りの処理時間、0.05秒。劇的な改善である。
  • Amazonが使っても売上げの減少ほとんどなし。(笑)

別解

  • 以上は、リスト・レコードが複合的に組み合わされたオブジェクトのJSON変換を、一般化して追求したものである。
    • 一般化とは、どのようにリスト・レコードが組み合わされていても変換できるような方法という意味。
  • ところが、特定の形式のリスト・レコードのみに対応するだけで良いのなら、次元の違う高速な処理方法がある。
  • 例えば、auto_loginスクリプトのログイン情報に限って言えば、以下の方法でJSONに変換できるのだ。


on json_from4(list_recode)
coding(list_recode)
replace(result, "{{", "[{")
replace(result, "}}", "}]")
replace(result, "|", "")
end json_from4

  • コードはたった、これだけである。
  • リスト・レコードの構造が決まっているのなら、規則的に括弧が重なる部分が現れるので、それを変換するだけで十分なのだ。
  • パフォーマンスを計測すると、3秒。
  • AppleScriptにおいては、置き換え処理はほとんど時間がかからないくらい高速なので、ほとんどはcoding()ハンドラの処理時間。

スクリプトメニュー対策

  • もはや、run scriptは使わずにリスト判定しているので、この対策は不要になってしまったが、
  • AppleScript Runner.appに、何が原因で問題が起こっていたのか、ようやく分かった。

それは、selected: だった!

  • レコードのキーのであるselected:を|selected|:に置き換えると、スクリプトメニューからも正常に実行できるようになった。


replace(result, "selected:", "|selected|:") --selectedキー = AppleScript Runner NG word

謎である...。