Archive for 1月, 2012

2012/01/30 AppleScriptObjCのメモリー浪費現象を解決?!

AppleScriptObjCで作成したプログラムが、実際に作業で使ってみたらメモリを馬鹿喰いして困る、という話を海外でよく見かけます。

自分も、1,000色ほどの色セットと任意の色の近似色を求めるAppleScriptObjCのプログラムを作成し、任意の1色の近似色検索を行う分には問題なかったものの……まとめて数十〜数百色を処理しようとしたところ、MacBook Pro搭載の8GBのメモリーをアレヨアレヨという間に喰いつぶされてしまいました。

その時には急いでいたので、根本的な解決をあきらめ……近似色検索処理だけを通常のAppleScriptのアプレットに追い出し、AppleScriptObjCのプログラムからAppleScriptアプレットをtell文で呼び出すように処理変更。

余計なプロセス間通信を行うので、余計に時間がかかるようになりましたが、メモリを喰いつぶしてスワップしまくるよりはマシという判断でした。事実、プロセス分離を行う前よりもずいぶん処理速度が落ちたのですが……背に腹はかえられません。

後日、時間に余裕があるときに調査を行ったところ……以前に掲載したCocoaベースのソートルーチンが問題の発生源であることが判明(わかってたけど)。

その場でプログラムから作成したオブジェクトについては、ループ処理などで後続の処理がすぐに発生した場合……自動的にはメモリー上から解放されないということが分りました。

ソートルーチンはサブルーチン化しておいたので、AppleScript的にはローカル変数などで処理される安全な「離れ小島」ですが、Cocoaアプリケーション的には「離れ小島ではない」という状態だと理解しました。

解決策は、作成したオブジェクトを明示的にリリース(release)すること。ほんの短い追加ではあるものの、数百件のデータを処理してもまったくメモリーを馬鹿喰いすることなく、処理を継続できました。

このあたり、Info.plistの記述を書き換えると回避できる問題なのかどうか、もうちょっと調べる必要がありますが……

スクリプト名:Cocoaで入れ子のリストを昇順ソート v2(AppleScriptObjC)
set aRecList to {{aName:“ひよこさん”, favoriteFood:“やきそば”, moneyInWallet:30}, {aName:“ぴよこ”, favoriteFood:“やきうどん”, moneyInWallet:20000}}
set sortedList to cocoaSortListAscending(aRecList, “moneyInWallet”)

–Cocoaで入れ子のリストを昇順ソート v2(AppleScriptObjC)
–連続して呼び出した場合にメモリを馬鹿喰いする現象に対処
on cocoaSortListAscending(theList, keyItem)
  
  
– make unique set
  
tell current application’s NSSet to set theSet to setWithArray_(theList)
  
  
– define sorting
  
tell current application’s NSSortDescriptor
    set theDescriptor to sortDescriptorWithKey_ascending_(keyItem, true)
  end tell
  
  
–sort
  
set sortedList to theSet’s sortedArrayUsingDescriptors_({theDescriptor})
  
  
—————————————————————————————
  
theSet’s release –** Important!! ** 重要!!
  
—————————————————————————————
  
  
return sortedList
  
end cocoaSortListAscending

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2012/01/21 iBooks AuthorはAppleScript非対応

iBook Storeで配布できるePub書類iBooks書類(Fair PLAYのDRMが施されている)を作成できるアプリケーション、iBooks AuthorがMac App Storeで無料公開されました。

author1.jpg

さっそく、iBooks AuthorのAppleScript対応度を確認すべく、AppleScriptエディタでiBooks Authorのアプリケーションアイコンをオープンして確認したところ……

author2.jpg

一応、AppleScript用語辞書が表示されるのですが、アプリケーションの機能と用語辞書の内容が対応していませんし、makeやsetという命令があったとしても、操作する対象(オブジェクト)の定義がほとんどありません。

実際に、新規書類の作成などそれらしい書き方で試してみたのですが、書類の番号のみインクリメントされるばかりで実際には表示されませんでした。

以上の状況から総合的に判断して、残念ながらiBooks Authorの最初のバージョンはAppleScriptにはまともに対応していない状態であると判定。現状では、GUI Scriptingで強引にメニューを操作する以上のことは何もできません。GUI Scriptingで強引に操作を行って、その内容が正しく内部機能へのアクセス経由で検証できないと「結果がどうなっているか分らないけれどとりあえず操作してみた」という無責任なプログラムになってしまいます。GUI Scriptingの併用は、データへの影響を検証しながら慎重に行うべきものです。

また、一部で「JavaScriptで(内部)機能を呼び出せる」といった噂もありましたが、あくまで「HTML5.0+JavaScriptから構成されるウィジェットをePub書類iBooks書類に埋め込める」というだけのものであり、JavaScriptからiBooks Authorの内部機能を呼び出せるわけではありません。

2012/01/14 ドロップされたASをTextWranglerでdiff表示

ドロップされたAppleScriptをTextWrangler(version 3.5.3)でdiff表示するAppleScriptです。他のAppleScriptのファイルを処理してdiff表示(差分表示)を行うユーティリティ的なものです。

以前に、「ドロップされたASをdiff表示 Mac OS X 10.4対応版」というものを作ったことがありました。AppleScriptをdiff表示するのに、普通はApple純正のFileMergeを使っているのですが、そのためには文字コードなどの書き換えをする必要があり、TextWranglerを併用して……TextWrangler自体がdiff表示機能を持っていることを思い出し、TextWranglerでdiff表示させるというものでした。

すでに決着が付いたかのように思われていた、AppleScript業界の「diff問題」が再燃したのは、読者がいるんだかいないんだかさっぱり不明なこのBlogの、読者の方からの1通のメールからでした。

『いつもお世話になります。(サイトに)

希望といいますか要望だけで申し訳ないのですが、「ドロップされたASをdiff表示 Mac OS X 10.4対応版」の10.6対応版などを作ってはいただけないでしょうか?
というのも、10.6ならdiff表示v4で十分なはずなんですが、業務PCにてXcodeのインストールが許可されていないという事情がありまして、もしよろしければ、ご検討いただけますようお願いいたします。』

……「スクリプトエディタ」を操作していたところを「AppleScriptエディタ」に書き換えれば瞬殺ではないか、と思って手をつけてみたら……意外とたいへんでした(汗)

Mac OS X 10.6上でTextWranglerを使ってcompare fileコマンドでdiffを取ろうとすると……謎のエラーが出ます。

set oldPath to choose file
set newPath to choose file

set oldPath to oldPath as string
set newPath to newPath as string

tell application “TextWrangler”
  compare file oldPath against file newPath
end tell

こんな、最低限の基礎的な記述に戻してあげて(トラブル時にものすごく大事なやりかた)、テキストに書き出したAppleScriptを2つ指定してみると……あいかわらずエラーになります。

 「もう、TextWranglerじゃなくて別のツールでも使おうか……」

日も暮れて、そう考えかけたころ、「fileで指定しているのがいけないのでは?」と気付き、aliasで渡してみたら何事もなかったように表示されました。

tw_diff106.jpg

そもそも、TextWranglerのAppleScript用語辞書(アプリケーションのアイコンをAppleScriptエディタにドラッグ&ドロップすると表示)を見てみると、

twdic.jpg

などと書いてあるので、「そうかーaliasじゃダメなんだー」と受け取ったからです。これは、TextWranglerのAppleScript用語辞書が間違っています。

asdic.jpg

▲こんな風に書かなくては(AppleScriptエディタの用語辞書より「open」命令の記述)

このScriptをアプリケーション形式で保存し、出来上がったドロップレットに2つのAppleScript書類をドロップすると、TextWranglerでdiff表示を行います。

スクリプト名:asdiff
on run
  –環境確認を行うべき(書いてない)
  
  
–FileMergeの起動を最初にやっておく
  
tell application “System Events”
    set fmExists to (exists of process “TextWrangler”)
  end tell
  
  
if fmExists = false then
    tell application “TextWrangler”
      launch
    end tell
  end if
  
end run

on open fileList
  
  
tell application “Finder”
    set sortedList to sort fileList by creation date
  end tell
  
  
set sortedList to reverse of sortedList
  
  
set oldPath to writeASSourceToTempFolder((item 1 of sortedList) as alias)
  
set newPath to writeASSourceToTempFolder((item 2 of sortedList) as alias)
  
  
–do shell script “/usr/bin/opendiff ” & oldPath’s POSIX path’s quoted form & ” ” & newPath’s POSIX path’s quoted form & ” > /dev/null 2>&1 &”
  
  
tell application “TextWrangler”
    compare oldPath against newPath –Mac OS X 10.6用にaliasで渡すように書き換えた
  end tell
  
  
end open

–AppleScriptのソースを取得してファイルに書き出し
on writeASSourceToTempFolder(aScript)
  –ASのソースを取得
  
set scriptSource to getContentsOfScript(aScript) of me
  
if scriptSource = false then
    display dialog “指定のASのオープン時にエラーが発生”
    
return false –エラー
  end if
  
  
tell application “Finder”
    set origName to name of file aScript
  end tell
  
set origName to makestr_alphabetNumeric_only(origName) of me
  
  
  
–tmpにASのソースを一時ファイルとして保存
  
set tmpPath to (path to temporary items from system domain) as string
  
set aFN to do shell script “/bin/date +%Y%m%d_%H%M%S”
  
set tmpPathFull to tmpPath & origName & “_” & aFN & “.txt”
  
set fRes to write_to_file_UTF8(scriptSource, tmpPathFull, false) of me
  
if fRes = true then
    return tmpPathFull as alias –書き出したテキストのフルパスを返す(10.6用に変更)
  else
    return false –エラー
  end if
end writeASSourceToTempFolder

–指定したAppleScriptのソースを取得する(Mac OS X 10.4用)
on getContentsOfScript(aScript)
  tell application “AppleScript Editor”
    try
      with timeout of 600 seconds –アプリケーションの起動に時間がかかるケースもあるので180秒から延長
        set aScript to open aScript
      end timeout
    on error
      return false
    end try
    
    
tell window 1
      set aName to name
    end tell
    
    
tell document aName
      set aCon to contents
      
close without saving
    end tell
  end tell
  
  
return aCon
end getContentsOfScript

–ファイルへのUTF8での書き込み
on write_to_file_UTF8(this_data, target_file, append_data)
  try
    set the target_file to the target_file as text
    
set the open_target_file to ?
      open for access file target_file with write permission
    if append_data is false then ?
      set eof of the open_target_file to 0
    write this_data to the open_target_file as «class utf8» starting at eof
    
close access the open_target_file
    
return true
  on error
    try
      close access file target_file
    end try
    
return false
  end try
end write_to_file_UTF8

–アルファベットと数字のみの文字列にして返す
on makestr_alphabetNumeric_only(aKeyword)
  set aKeyword to aKeyword as string
  
set aKeyword to aKeyword as Unicode text
  
  
set cList to characters of aKeyword
  
set newName to “”
  
  
repeat with i in cList
    set aRes to checkAN(i) of me
    
if aRes = true then
      set newName to newName & (i as string)
    end if
  end repeat
  
  
return newName
end makestr_alphabetNumeric_only

on checkAN(aKeyword)
  set anList to {“a”, “b”, “c”, “d”, “e”, “f”, “g”, “h”, “i”, “j”, “k”, “l”, “m”, “n”, “o”, “p”, “q”, “r”, “s”, “t”, “u”, “v”, “w”, “x”, “y”, “z”, “-”, “+”, “.”, “_”, “=”, “(”, “)”, “#”, “$”, “%”, “&”, “~”, “^”, “0″, “1″, “2″, “3″, “4″, “5″, “6″, “7″, “8″, “9″}
  
  
set aKeyword to aKeyword as Unicode text
  
set aKeyword to aKeyword as string
  
set kList to characters of aKeyword
  
repeat with i in kList
    ignoring case
      if i is not in anList then
        return false
      end if
    end ignoring
  end repeat
  
return true
end checkAN

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2012/01/09 指定Finder Windowのツールバー&サイドバーの表示、非表示をコントロール

指定Finder Windowのツールバーおよびサイドバーの表示/非表示状態を制御するAppleScriptです。

FinderのAppleScript用語辞書をオープンすると、サイドバーを表す要素が見つかりません。そこで、サイドバーの表示/非表示の制御はできないもの……と、思ってしまわれがちですが、サイドバーはウィンドウ上部のツールバーと表示/非表示制御が一括して行われているため、ツールバーの制御を行うと、一緒に表示状態をコントロールできます。

finwin1.jpg

finwin2.jpg

裏を返せば、サイドバー/ツールバーともに単独で状態を制御できないということになりますが、それはOSの(ウィンドウシステムの)仕様なので、言っても仕方のないところ。

AppleScriptで各種アプリケーションをコントロールするには、こうしたOSやアプリケーションの挙動に対する「暗黙のお約束」的な仕様を意識する必要があり、その典型的な例として挙げてみました(このサブルーチン自体には…………あまり利用価値がないような、、、サブルーチン化しなくても、もっと簡潔に書けるわけで)。

表示中のFinderのすべてのWindowに対して制御を行う場合には、tell every window……と書いたほうが効率的で、すべてのFinder Windowをリストに入れてループ処理……というのはAppleScript「らしい」やりかたではありません。ねんのため。

スクリプト名:指定Finder Windowのツールバー&サイドバーの表示、非表示をコントロール
tell application “Finder”
  set targWin to window 1 –最前面のWindowを指定。任意のWindow指定も可能
end tell

showHideToolbarAndSidebar(targWin, false) of me –非表示にする
–showHideToolbarAndSidebar(targWin, true) of me –表示にする

–指定Finder Windowのツールバー&サイドバーの表示、非表示をコントロール
on showHideToolbarAndSidebar(targetWindow, showF)
  try
    tell application “Finder”
      tell targetWindow
        set toolbar visible to showF
      end tell
    end tell
  end try
end showHideToolbarAndSidebar

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2012/01/05 文字入力モードを制御

GUI Scripting経由でSystemUIServerを制御して、IMの文字入力モードを変更するAppleScriptです。

実行には、GUI Scriptingがオンになっている必要があります。また、現時点ではMac OS X 10.6.8と10.7.2で確認してある状態です(10.5は微妙。10.4ではダメだと思います)。

→ 後日確認したところ、Mac OS X 10.5.8と10.4.11では動きませんでした。予想どおり。

文字入力モードをAppleScriptからコントロールするのは、普通に考えれば無理そうです。

真っ先に思いつくのが、AppleScriptObjCによるGUIつきアプリケーションを作成して、そのアプリケーションのWindow上にNSTextFieldを作成し、入力文字種類を制御するやりかたです。これなら、文字入力を制御できているといえなくもありません。

サードパーティのIMまで目を向けると、ジャストシステムのATOKはATOKダイレクトAPIなるAPIをユーザーに公開しており、コントロールできなくもなさそうな雰囲気はしているのですが、メーカーが想定している使い方しかさせてもらえなさそうな雰囲気も漂っています。

AppleScriptObjC経由で、ことえりのステータスを変更ないしは固定するようなプログラムを呼び出す方法も考えられなくもないですが……すぐには情報が見つかりませんでした。

そこで、きわめてAppleScript的に、GUI Scripting経由でコントロールしてみようということになりました。

画面上部のメニュー右側に表示されるMenu Extraは、SystemUIServerというプログラムが管轄しています(Mac OS X 10.6/10.7)。

menu1.jpg

このため、SystemUIServerのメニューの各アイテムにAppleScriptからアクセスすると、最低限、どのような内容を表示しているか取得できそうです。ユーザーによってMenu Extraの内容や並び順はカスタマイズされまくっているので、Menu Extra同士の識別ができなくてはなりません。

descriptionという属性を調べることで、どのプログラムが表示しているものか識別できそうです。Apple純正のMenu Extraしか値を取得できていない状態ですが、今回の目的のためにはこれで十分です。

並び順からいって、「text input」というのがIMのMenu Extraを表しているようです。

スクリプト名:System UI Serverから何がmenu extraを表示しているかを取得
activate application “SystemUIServer”
tell application “System Events”
  tell process “SystemUIServer”
    tell menu bar 1
      set aList to description of every menu bar item
    end tell
  end tell
end tell
–> {”time machine”, “bluetooth”, “iChat”, “displays”, “AirMac Menu Extra”, “システムサウンド音量”, “AppleScript”, “バッテリーメニュー。 完全充電まで 5 時間 39 分 .”, “clock”, “text input”, “user”}

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

次に、どのプログラムがどのような情報を表示しているのか「value」属性を調べてみました。「英字」「ひらがな」「カタカナ」といった情報が取得できます。けっこういい感じです。

スクリプト名:System UI Serverから各menu extraが何を表示しているかを取得
activate application “SystemUIServer”
tell application “System Events”
  tell process “SystemUIServer”
    tell menu bar 1
      set aList to value of every menu bar item
    end tell
  end tell
end tell
–> {missing value, missing value, missing value, missing value, “Extreme net の 4 本のうち 4 本の信号”, missing value, missing value, missing value, “1月4日(水) 23:49″, “英字”, missing value}

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

ちなみに、このvalueをAppleScriptから変更できないか試してみたのですが、valueはあくまで現状を反映させたものであり、書き換えてみても何も起きませんでした。

そこで、これまたGUI Scripting的なアプローチで、この「text input」のmenu extraをクリックして、表示されたメニューから指定のアイテムをクリックするという操作を行ってみました。

スクリプト名:System UI Serverを操作してIMの文字入力状態を変更する

setInputState(“英字”) of me
–setInputState(”ひらがな”) of me

–IMの入力状態を設定する
on setInputState(aState)
  activate application “SystemUIServer”
  
tell application “System Events”
    tell process “SystemUIServer”
      tell menu bar 1
        set aList to every menu bar item whose description is “text input”
        
set anItem to first item of aList
        
set curVal to value of anItem
        
        
if aState = curVal then return –変更の必要がなければリターン
        
        
tell anItem
          click
          
tell menu 1
            set mList to every menu item whose title is aState
            
set m1Item to first item of mList
            
tell m1Item
              click
            end tell
          end tell
        end tell
        
      end tell
    end tell
  end tell
  
end setInputState

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

人間、やればできるもんです。できないかと思っていたのですが、やってみたら案外手軽にできてしまいました。

ただし、複数のIMがインストールされている環境では、それぞれを識別するのはちょっと難しそうで……

menu2.jpg

もうちょっと調べてみないとなんともいえないところでしょうか。

2012/01/03 作成日時からラジオ録音番組名を設定

録音したRadikoの音声ファイルを、それぞれの番組の条件リストをもとにリネームするAppleScriptです。

r0013976.JPG

自作のRadiko録音アプリ「Radirec」で録音しためたファイルに対し、条件リストをもとにファイル作成日時をてがかりに番組判定を行います。

条件リストはこんな感じに……番組名、放送曜日、開始時刻、終了時刻のセットです。

property progList : {{“日曜天国”, Sunday, “10:00″, “11:55″}, {“深夜の馬鹿力”, Tuesday, “01:00″, “03:00″}, {“ジブリ汗まみれ”, Sunday, “23:00″, “23:30″}}

rad1.jpg

もともと、録音アプリケーション(Radirec)のほうでこうした処理をしておけばよかったのですが、最低限の機能だけで安定して動作すればいいやぐらいで作ったので……あとで苦労しているわけで。

年末年始は寝て過ごしていたので、ちょっとプログラミングのカンが鈍ってしまい、ちょっとしたリハビリがわりにつくってみたというところでしょうか。

a reference toによるアクセス高速化ではなく、別Scriptのpropertyに対してアクセスするタイプの高速化を行っています。あんまり効き目のなさそうなところにも使っていますが、AppleScriptObjCでの高速化のための練習、みたいな感じでしょうか。

これまでのサブルーチンはAppleScript Studioに入れられることを前提に作ってきましたが、これからはAppleScriptObjCを前提に作る必要があります。

rad2.jpg

録り貯めたファイルのうち、ほとんどは処理できたものの、一部ファイルの判定は行えなかったので、まだ納得できていないレベルです。

スクリプト名:作成日時からラジオ録音番組名を設定
–スピードアップ記述の練習用に(意味もないのに)プロパティを別Scriptオブジェクトに分けてみた
script speedUp
  –ファイル一覧のハンドリング用
  
property fList : {}
  
  
–区分けする番組リスト(番組名、放送曜日(開始時刻のみ判定)、開始時刻、終了時刻
  
property progList : {{“日曜天国”, Sunday, “10:00″, “11:55″}, {“深夜の馬鹿力”, Tuesday, “01:00″, “03:00″}, {“ジブリ汗まみれ”, Sunday, “23:00″, “23:30″}}
  
  
–処理対象ファイルの拡張子リスト
  
property extList : {“mov”, “mp3″, “m4a”}
  
end script

–録音日時が日付をまたがないことが処理条件
–もうちょっと機能アップしないと不十分?

set falseList to {} –処理できなかったファイルの一覧が入る

set tFol to choose folder with prompt “処理対象フォルダを指定してください”

tell application “Finder”
  –指定フォルダ内のファイル一覧を取得(指定フォルダ以下のすべてのフォルダ内を走査)
  
try
    set (fList of speedUp) to entire contents of tFol as alias list
  on error
    display dialog “Error” buttons {“OK”} default button 1 with icon 2
    
return
  end try
end tell

–メインループ

repeat with i in (fList of speedUp)
  –set j to contents of i
  
set aInfo to info for i
  
  
–ファイル拡張子で処理対象を判別
  
tell application “Finder”
    set aExt to name extension of aInfo
    
–log aExt
    
if aExt is in (extList of speedUp) then
      
      
–作成日時と作成曜日を取り出す
      
set sDate to creation date of aInfo
      
–log sDate
      
      
set yNum to year of sDate
      
set dayNum to weekday of sDate as number
      
set monthNum to month of sDate as number
      
set dateNum to day of sDate
      
      
–番組リストとの照合
      
set hitF to false
      
repeat with ii in progList of speedUp
        set {pName, sWeekday, sTime, eTime} to ii
        
        
if (sWeekday as number) = dayNum then
          –開始時刻のdate objectを求める
          
set s1Time to sDate
          
set {h1Num, m1Num} to words of sTime
          
set h1Num to h1Num as number
          
set m1Num to m1Num as number
          
set hours of s1Time to h1Num
          
set minutes of s1Time to m1Num
          
set s1Time to s1Time - (10 * 60) –開始時刻を10分前倒しで判定
          
          
–終了時刻のdate objectを求める
          
set e1Time to sDate
          
set {h2Num, m2Num} to words of eTime
          
set h2Num to h2Num as number
          
set m2Num to m2Num as number
          
set hours of e1Time to h2Num
          
set minutes of e1Time to m2Num
          
set e1Time to e1Time + (10 * 60) –終了時刻を10分後ろ倒しで判定
          
          
          
if s1Time sDate and sDate e1Time then
            –ファイル名指定用の数値データを桁数指定しつつ文字列化
            
set yStr to retZeroPaddingText(yNum, 4) of me
            
set mStr to retZeroPaddingText(monthNum, 2) of me
            
set dStr to retZeroPaddingText(dateNum, 2) of me
            
set targStr to pName & ” “ & yStr & ” “ & mStr & “月” & dStr & “日.” & aExt –ここを変更するとファイル名のフォーマットが変わる
            
            
set name of i to targStr –ファイルの名称変更
            
            
set hitF to true
            
            
exit repeat
            
          end if
        end if
      end repeat
      
      
if hitF = false then
        set the end of falseList to (contents of i)
      end if
      
    else
      set the end of falseList to (contents of i)
    end if
  end tell
end repeat

return falseList

–数値にゼロパディングしたテキストを返す
on retZeroPaddingText(aNum, aLen)
  set tText to (“0000000000″ & aNum as text)
  
set tCount to length of tText
  
set resText to text (tCount - aLen + 1) thru tCount of tText
  
return resText
end retZeroPaddingText

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に