Archive for the '並列処理(Parallel Processing)' Category

07/06 指定のデジカメ画像のExif情報から指定のプロパティを取得する

指定のデジカメ画像のExif情報から指定のプロパティを取得するAppleScriptです。

デジカメ画像のExif情報を取得する場合には、Image Events経由でアクセスすることになりますが……実際に使ってみると、クラッシュしまくるImage Events。AppleScriptの実行をApple純正のアプリケーションが阻害するというあり得ない現象が頻発。

そこで、Image Eventsをアテにしないで、sipsコマンドをAppleScriptから呼び出すことにしてみました。Image Eventsはsipsを呼び出しているAS用インタフェース・アプリケーションですが、この調子ではAppleScriptによる並列処理などを考えるとsipsをダイレクトに呼び出した方がご利益が大きそうです。

指定できるプロパティ値については、Terminal上でsipsコマンドのhelpを見て確認してください。

Special property keys:
all binary data
allxml binary data

Image property keys:
dpiHeight float
dpiWidth float
pixelHeight integer (read-only)
pixelWidth integer (read-only)
typeIdentifier string (read-only)
format string jpeg | tiff | png | gif | jp2 | pict | bmp | qtif | psd | sgi | tga
formatOptions string default | [low|normal|high|best| ] | [lzw|packbits]
space string (read-only)
samplesPerPixel integer (read-only)
bitsPerSample integer (read-only)
creation string (read-only)
make string
model string
software string (read-only)
description string
copyright string
artist string
profile binary data
hasAlpha boolean (read-only)

Profile property keys:
description utf8 string
size integer (read-only)
cmm string
version string
class string (read-only)
space string (read-only)
pcs string (read-only)
creation string
platform string
quality string normal | draft | best
deviceManufacturer string
deviceModel integer
deviceAttributes0 integer
deviceAttributes1 integer
renderingIntent string perceptual | relative | saturation | absolute
creator string
copyright string
md5 string (read-only)

スクリプト名:retExifAttributeData v2
set aFile to choose file
set aRes to retExifAttributeData(aFile, “model”) of me
–> “GR Digital “

set bRes to retExifAttributeData(aFile, “formatOptions”) of me
–> “default”

–SIPSコマンドで指定のデジカメ画像のExif情報(プロパティ)を取得する
on retExifAttributeData(aFile, aParam)
  
  
set aPOSIX to quoted form of POSIX path of aFile
  
  
set aRes to do shell script “sips –getProperty “ & aParam & ” “ & aPOSIX
  
set aList to paragraphs of aRes
  
set anItem to contents of last item of aList
  
  
set colonPos to (offset of “:” in anItem) + 2
  
set eRes to text colonPos thru -1 of anItem
  
  
return eRes
  
end retExifAttributeData

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

04/25 AppleScriptによる並列処理v3a

AppleScriptによる並列処理サンプルのバージョンv3aです。v3を外部公開できるよう、実際のメイン処理部分をすべて削除して、並列処理の評価用に最小限のダミー処理を付加したものです。

本サンプルは、

(1)並列処理呼び出し部分(呼び出す側)「parallelMain_v3a」
(2)並列処理実行テンプレートAppleScript(呼び出される側。スレッド)「mainScript_v2a」

から構成されます。

(1)をAppleScriptエディタで「parallelMain_v3a.app」の名前で、アプリケーションとして保存します。「実行後、自動的に終了しない」にチェックを入れておいてください。

(2)については、通常のAppleScript書類として保存しておいてください(ここでは、「mainScript_v2a」の名前で保存されているものと仮定して説明します)。

parallelMain_v3a.appをFinder上でダブルクリックするなどして起動すると、並列実行するAppleScriptファイルを聞いてきます。「mainScript_v2a」を指定してください。

次に、処理対象フォルダを聞いてきますので、適当にファイルが入っているフォルダを指定してください。本サンプルプログラムではファイルに対して処理は行いません。ただし、処理終了マーク(ラベル)を振るため、ラベルが変更されては困るファイルが入っていないことが前提です。

最後に、結果の書き出し先ファイルを聞かれるので、適当な名前を指定しておいてください。ダミーデータ(乱数データ)がリスト形式で書かれます。

あとは、メイン側がThreadのアプレットを自動生成して起動し、処理ファイルを1つずつThread側に渡して、処理結果を受け取ります。

今後の改良の方向性は、

(a)Threadがコケたりエラー終了した場合のリカバリ
(b)テンプレートAppleScriptがいまひとつ簡潔ではないため、テンプレートのうちThreadとして振る舞うために必要な部分と本当の処理部分を分割して、並列処理を行いやすくする
(c)開発効率を優先して雑に書いた部分をもう少しなんとかする
(d)できれば、CPU負荷を計測してスレッド数を自動で増減させたい(CPUコアの個数分だけThreadを生成し、まだCPUの処理能力にアキがあれば追加するような方向で)

といったところでしょう。

スクリプト名:parallelMain_v3a
–AppleScriptによる並列実行テスト v3a(外部公開用)

–データの受け渡しを一括で行わずに、ファイルを処理するたびに受け渡すようにした

property threadMax : 4 –threadの最大数(ご自由に変更してください)

property threadNameList : {}
property threadFileList : {}
property doneList : {}
property asAppName : “thread” –thread側のアプリケーション名 (thread_1〜thread_N)  変更可
property myName : “” –メイン側の自分の名前。thread側からトークバックしてもらうのに必要

property aResList : {}

property resFileA : “”

property procFileList : {} –処理対象のパス一覧

on run
  –変数の初期化
  
set aResList to {}
  
  
set doneList to {} –これを初期化し忘れてドえらい目に逢った
  
set threadNameList to {} –これを初期化し忘れてドえらい目に逢った
  
  
–メイン側プログラムの名前を取得(コールバックのために必須)
  
set mePath to path to me
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
  
  
–読み込むAppleScriptを選択
  
set aFile to choose file with prompt “並列実行するAppleScriptを選択してください”
  
try
    set aSubObj to load script aFile
  on error
    display dialog “AppleScript以外のファイルが指定されました。” buttons {“OK”} default button 1 with icon 2 with title “ERROR”
    
return
  end try
  
  
–指定フォルダ内のファイルを、Thread数で分割する
  
set aFolder to choose folder with prompt “処理対象フォルダを選択してください”
  
set procFileList to getFilePathList(aFolder) of me –数百〜数千ぐらいのファイル数を考慮。数万ファイル以上になる場合には修正を要する(本当)
  
  
–結果を書き出すファイルを指定
  
set resFileA to choose file name with prompt “処理結果を書き出すファイル(1)を指定してください”
  
  
  
–thread(アプレット)の書き出し先
  
set dFol to (path to desktop from user domain) as string
  
set threadNameList to {}
  
  
  
–threadsを書き出す
  
repeat with i from 1 to threadMax
    
    
set threadName to asAppName & “_” & i as string
    
    
set asPath to dFol & threadName
    
set asRes to makeASexecutable(aSubObj, asPath) of me
    
    
if asRes is not equal to true then –何らかの理由でApplet書き出しに失敗した場合
      display dialog asRes
      
return
    else
      set aThreadFilePath to (asPath & “.app”)
      
tell application “Finder”
        open file aThreadFilePath –threadを起動
      end tell
      
set the end of threadFileList to (aThreadFilePath as alias)
    end if
    
    
–threadに処理依頼を行う
    
set fItem to {contents of first item of procFileList} –要素は1つだけ。ただし、複数要素を渡す可能性もあるのでリスト形式で渡しておく
    
set procFileList to rest of procFileList
    
set aRec to {mainName:myName, paramList:fItem} –threadに渡すパラメータを組み立てる。myNameを渡しているのは、名前を指定してメイン側に結果を返してもらうため
    
–1ファイルのみ渡すようにした
    
    
    
ignoring application responses –ものすごく大事
      tell application threadName
        open aRec –これでthread側は処理開始
      end tell
    end ignoring
    
    
set the end of threadNameList to threadName
    
  end repeat
  
end run

–thread側からメッセージを受信する
on open aData
  set threadName to subName of aData
  
set aRes to resultList of aData
  
  
set aResList to aResList & aRes
  
  
if procFileList is not equal to {} then
    
    
–threadに処理依頼を行う
    
set fItem to {contents of first item of procFileList} –要素は1つだけ。ただし、複数要素を渡す可能性もあるのでリスト形式で渡しておく
    
set procFileList to rest of procFileList
    
set aRec to {mainName:myName, paramList:fItem} –threadに渡すパラメータを組み立てる。myNameを渡しているのは、メイン側へのトークバックのため
    
    
ignoring application responses –ものすごく大事
      tell application threadName
        open aRec –これでthread側は処理開始
      end tell
    end ignoring
    
    
  else
    set the end of doneList to threadName
    
    
tell application threadName
      quit
    end tell
  end if
  
  
  
–本来ならthreadの終了確認を個数を数えるという野蛮な方式で行うべきではない
  
if length of doneList = length of threadNameList then
    –threadから受け取ったデータのファイル書き出しなどを行う(1)
    
write_to_file_AsList(aResList, resFileA) of me
    
    
    
–thread実行ファイルの削除
    
try
      repeat with i in threadFileList
        set j to quoted form of POSIX path of i
        
do shell script “/bin/rm -rf “ & j
      end repeat
    end try
    
    
tell me to quit
  end if
end open

–ファイルの追記ルーチン「write_to_file_AsList」  (リストとして書き込み)
–データ、対象ファイル
on write_to_file_AsList(this_data, target_file)
  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
    
set eof of the open_target_file to 0
    
write this_data to the open_target_file starting at eof as list
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file_AsList

–AppleScriptのソースを取得して返す
on getASSource(macPath)
  set aName to (info for macPath size 0)’s name
  
set tmpPath to ((path to temporary items from system domain) as text) & (do shell script “/usr/bin/uuidgen”) & “.txt”
  
  
set scptText to do shell script “/usr/bin/osadecompile “ & quoted form of POSIX path of macPath
  
  
return scptText
end getASSource

–指定のテキストをAppleScriptとしてコンパイルし、実行可能なアプレットとして出力する
on makeASexecutable(aText, outPathAlias)
  
  
–出力先のaliasからファイル名のみ撮り出す
  
set aStr to outPathAlias as string
  
set fnRes to getLastLayerOfDirData(aStr) of me
  
  
–指定のファル名に拡張子が適切に付いていない場合には補う
  
if fnRes does not end with “.app” then
    set fnRes to fnRes & “.app”
  end if
  
  
–テンポラリフォルダ内のテキストファイルに書き出す
  
set aFileName to (do shell script “uuidgen”) & “.applescript”
  
set tmpAppName to (do shell script “uuidgen”) & “.app”
  
  
set aTempFol to path to temporary items folder from system domain
  
set aTempFolPosix to (POSIX path of aTempFol) & aFileName
  
  
set aTempPathText to (aTempFol as string) & aFileName
  
–write_to_file(aText, aTempPathText, false) of me
  
  
store script aText in file aTempPathText replacing yes
  
  
–ファイルキャッシュをHDDにシンクロさせる
  
–do shell script “sync”–> 実行しなくても大丈夫だった
  
  
–osacompileコマンドでアプレットを生成
  
set sText to “cd “ & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -x -o “ & fnRes & ” “ & aFileName
  
–set sText to “cd ” & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -o ” & fnRes & ” ” & aFileName
  
try
    –コンパイル時にアプリケーションの起動を待ったりする可能性があるので、待ち時間を1時間に設定
    
with timeout of 3600 seconds
      do shell script sText
    end timeout
  on error erMes
    return erMes
  end try
  
  
  
–テンポラリに書き込んだTextのAppleScriptを消す
  
try
    do shell script “rm -f “ & quoted form of aTempFolPosix
  end try
  
  
  
–生成したアプレットを指定のフォルダに移動する
  
set origAppPath to POSIX path of ((aTempFol as string) & fnRes)
  
set destPath to POSIX path of getOnlyFolder(aStr) of me & fnRes
  
  
try
    do shell script “mv -f “ & quoted form of origAppPath & ” “ & quoted form of destPath
  on error erMes
    return erMes
  end try
  
  
return true
  
end makeASexecutable

–テキストで与えられたパスデータから、フォルダのみを返す
on getOnlyFolder(a)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to {“:”}
  
set aList to text items of a
  
set bList to items 1 thru -2 of aList
  
set aText to bList as string
  
set AppleScript’s text item delimiters to curDelim
  
set aText to aText & “:”
  
return aText
end getOnlyFolder

–テキストで与えられたパスデータから、最終階層のテキストのみを返す
on getLastLayerOfDirData(aData)
  set aColon to “:” as Unicode text
  
copy aData to bData
  
if bData ends with “:” then
    set bData to text 1 thru -2 of bData
  end if
  
  
set aLen to length of bData
  
set rData to reverse of (characters of bData)
  
set rData to rData as string
  
set dPos to offset of aColon in rData
  
set resData to text (aLen - dPos + 2) thru -1 of bData
  
return resData
  
end getLastLayerOfDirData

–ファイルの追記ルーチン「write_to_file」
–追記データ、追記対象ファイル、boolean(trueで追記)
on write_to_file(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 starting at eof
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file

–文字置換ルーチン
on repChar(origText, targStr, repStr)
  set {txdl, AppleScript’s text item delimiters} to {AppleScript’s text item delimiters, targStr}
  
set temp to text items of origText
  
set AppleScript’s text item delimiters to repStr
  
set res to temp as text
  
set AppleScript’s text item delimiters to txdl
  
return res
end repChar

–指定フォルダ中のファイルのパスを返す
on getFilePathList(aFolder)
  
  
set fileList to {}
  
  
tell application “Finder”
    tell folder aFolder
      try
        –フォルダ中のファイルをalias listで取得
        
–あまりファイル数が多すぎると、Finderが音を上げる可能性があるので注意
        
with timeout of 3600 seconds
          set fileList to every file as alias list –whose name ends with “.dcm.txt”
        end timeout
        
      on error
        –ファイルが指定フォルダ中に1個だけだった場合
        
set fList to every file
        
set fileList to {}
        
repeat with i in fList
          set the end of fileList to i as alias
        end repeat
      end try
    end tell
  end tell
  
  
return fileList
  
end getFilePathList

–値のインクリメント
on incVal(aVar, aMax)
  if aVar < aMax then
    set aVar to aVar + 1
  else
    set aVar to 1
  end if
  
return aVar
end incVal

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

スクリプト名:mainScript_v2a
property mePath : “”
property myName : “”

on run
  set mePath to path to me
  
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
end run

–メイン側からのパラメータ受信用
on open aParamRec
  
  
set mainProgram to mainName of aParamRec
  
set pList to (paramList of aParamRec)
  
  
ignoring application responses
    tell application “Finder”
      set label index of mePath to 2 –視覚的に処理がはじまったことを表現(赤)
    end tell
  end ignoring
  
  
set aRes to mainProgram(pList) of mainScript of me
  
  
–メイン側に処理結果を通知
  
ignoring application responses –ここが超重要
    tell application mainProgram
      open {subName:myName, resultList:aRes}
    end tell
  end ignoring
  
  
ignoring application responses
    tell application “Finder”
      set label index of mePath to 7 –視覚的に処理が終わったことを表現(グレー)
    end tell
  end ignoring
end open

–ここに処理本体を記述
script mainScript
  
  
on mainProgram(fList)
    –ここに並列処理するプログラムを記述する
    
    
set fileSpecList to {} –IDリスト(IDのみ)
    
set aSpecList to {} –ID詳細情報のリスト
    
    
–指定フォルダ内をループ処理
    
repeat with i in fList
      
      
delay (random number from 1 to 10) –ダミー処理
      
      
ignoring application responses
        tell application “Finder”
          set label index of i to 4 –青
        end tell
      end ignoring
    end repeat
    
    
return (random number from 1 to 10) –ダミー
    
  end mainProgram
  
end script

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

04/22 AppleScriptによる並列処理 v3〜実行ムービー

AppleScriptを並列処理するAppleScriptの改良型、v3の実行中のムービーです。

先週末には出来上がっていたのですが、挙動がおかしい箇所があったので地道にデバッグしていました。

並列処理デモv3(5.7MB)

04/16 AppleScriptによる並列処理 v2〜実際に使ってみての評価

AppleScriptによる並列処理 v2を実際に使ってみての評価です。

■前提となる話

たまたま、大量かつそこそこの容量のテキストファイルの処理をする必要があったので、マシンのCPU性能を生かすために並列処理を行わせてみた。

通常のデスクトップ用アプリケーションを複数同時起動できるわけではないし、複数のAppleScriptアプレットから同時に1つのアプリケーションにアクセスした場合にきちんと排他処理してくれたりするかどうかは不明(昔は、FileMaker Proがbegin transaction〜end transactionの構文をサポートしていたが、他で見かけたことがない)。そんなに高度なことを期待してもダメ。

InDesignとかIllustratorの処理を1台で並列実行とかいう話は無理。ただし、もしもMac OS X10.7になって複数ユーザーが同時ログインして別々のGUI環境を同時に実行できるとかいう環境になってくると、1つのユーザー環境から他のユーザー環境上のアプリケーションをコントロールできる可能性は……ないこともない。

AppleScriptのプログラムを動的に実行バイナリに変換して、同じ仕事を手分けして処理するという程度の話がこの話のゴール地点。テキストファイルの大量処理という話ならけっこう使える。

ひととおり使えるレベルのプログラムに育ったら、1台で複数の処理をやるのと同様に、複数のマシンで分散処理を行うプログラムを作ってみるとよさそう。安くてそこそこの性能のマシンを複数台集めるとか、オフィスや学校にあるMacを夜間に動かして仕事を分散処理させるのはけっこうよさそう。

■評価

処理対象のファイル一覧をスレッド数で分割して、まとめて渡すというやり方はよくない。処理対象のファイルにはサイズにバラツキがあるので、それぞれのスレッドの処理時間がバラバラ。「数」で割って仕事を割り振るのは、無駄が多いことが分かった。

p1.jpg

たとえば、4スレッドで並列処理を行っても、割り当て分を終わらせたスレッドは早々にプログラムを終了してしまう。処理が早く終わったスレッドは、処理が終わっていないスレッドの未処理分を処理すべきなのに、それができていない。最後のほうでCPUの利用効率が落ちてしまう。

また、途中でトラブルに遭遇して停止した場合、1スレッド文の処理がいきなりすべて無駄になってしまうのは時間の無駄すぎます。

そのため、1ファイルずつスレッド側に渡して処理するように変更すべきだと思われました。……というか、もう出来上がってテスト中です。

p2.jpg

04/13 AppleScriptによる並列処理 v2

以前に掲載したAppleScriptで並列処理を行うプログラムの改良版です。

1ファイルあたり数千〜数十万行のテキストファイルを数百個処理する必要に迫られ、さまざまな高速化手法を投入してみたものの、通常のAppleScriptでは与えられたファイルを順次実行するだけ。昨今の複数CPUコアが常識化したMacでは、1つのCPUコアが忙しいだけで、他のコアはほとんど遊んでいる状態です。

メニーコアの、無駄に遊んでいるCPUのパワーを絞り出すため、ぴよまるソフトウェアではAppleScriptの並列実行に取り組んできました。

para2.jpg

以前に掲載したもの(v1)は、並列処理を行ううえで必要な技術のテストを行うことが目的だったので、実用性はほとんどありませんでした。

今回のv2では、並列処理実行数を柔軟に指定でき、実際に実戦でテストを行って実用性を確認できています。純粋に4スレッド指定した状態で、1スレッド状態の4分の1の時間で処理を終了しています。1時間以上かかる処理が15分程度で終了する、という感じです。

para1.jpg

v1では、メイン側とサブ側の間でファイルI/Oを用いてイベントのやりとりを行っていましたが、その後に助言をいただいたりテストを行う中で、「openハンドラ経由でデータをやりとりする場合に、ファイルでやりとりする必要はない」ことが分かってきました(これは知らなかった!)。

コマンド内容を一度ファイルに書き出さず、相手側のアプレットのopenハンドラにlistやrecordを直接渡せるため、信頼性や処理速度の向上が期待されます。

また、v1では(ファイルI/Oの信頼性計測のために)イベントを頻繁にやり取りしていましたが、実際にはそんなに頻繁にイベントのやりとりを行うわけではありません。「一度パラメータを与えれば終了時までイベントのやりとりは行わない」ケースが多いことが予想されます。v2では最初と最後だけイベントをやり取りするようにしてテストを実施しました。

p3.jpg

まずは、こちらのAppleScriptを実行形式(実行後終了しない)で保存してください。AppleScriptを呼び出すためのアプレットです。

スクリプト名:parallelMain_v2_exe
–ワンショットタイプのAppleScriptによる並列実行テスト v2

property threadMax : 4 –threadの最大数(環境ごとに静的に変えるか、何らかの動的な判断を行うか?)
property threadNameList : {}
property threadFileList : {}
property doneList : {}
property asAppName : “thread” –thread側のアプリケーション名 (thread_1〜thread_N)  変更可

property aResList : {}
property resFile : “”

on run
  
  
–メイン側プログラムの名前を取得(コールバックのために必須)
  
set mePath to path to me
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
  
  
–読み込むAppleScriptを選択
  
set aFile to choose file with prompt “並列実行するAppleScriptを選択してください”
  
set aSubObj to load script aFile
  
  
–指定フォルダ内のファイルを、Thread数で分割する
  
set aFolder to choose folder with prompt “処理対象フォルダを選択してください”
  
set splitList to splitFilePathToThreadNum(aFolder, threadMax) of me
  
  
–結果を書き出すファイルを指定
  
set resFile to choose file name with prompt “処理結果を書き出すファイルを指定してください”
  
  
  
–thread(アプレット)の書き出し先
  
set dFol to (path to desktop from user domain) as string
  
set threadNameList to {}
  
  
  
–Sub Threadsを書き出す
  
repeat with i from 1 to threadMax
    
    
set threadName to asAppName & “_” & i as string
    
    
set asPath to dFol & threadName
    
set asRes to makeASexecutable(aSubObj, asPath) of me
    
    
if asRes is not equal to true then –何らかの理由でApplet書き出しに失敗した場合
      display dialog asRes
      
return
    else
      set aThreadFilePath to (asPath & “.app”)
      
tell application “Finder”
        open file aThreadFilePath –Sub Threadを起動
      end tell
      
set the end of threadFileList to (aThreadFilePath as alias)
    end if
    
    
–Sub Threadに処理依頼を行う
    
set aRec to {mainName:myName, paramList:contents of item i of splitList} –Threadに渡すパラメータを組み立てる。myNameを渡しているのは、メイン側へのトークバックのため
    
    
    
ignoring application responses –ものすごく大事
      tell application threadName
        open aRec –これでThread側は処理開始
      end tell
    end ignoring
    
    
    
set the end of threadNameList to threadName
    
    
(*
    –処理のピークをずらすために、乱数秒だけウェイトを入れる
    set randNum to (random number from 0 to 5)
    delay randNum
    *)

  end repeat
  
end run

–thread側からメッセージを受信する
on open aData
  set threadName to subName of aData
  
set aRes to resultList of aData
  
  
set the end of doneList to threadName
  
  
set aResList to aResList & aRes
  
  
–本来ならthreadの終了確認を個数を数えるという野蛮な方式で行うべきではない
  
if length of doneList = length of threadNameList then
    –threadから受け取ったデータのファイル書き出しなどを行う
    
write_to_file_AsList(aResList, resFile) of me
    

    
    
–thread実行ファイルの削除
    
tell application “Finder”
      delete threadFileList
    end tell
    
    
tell me to quit
  end if
end open

–ファイルの追記ルーチン「write_to_file_AsList」  (リストとして書き込み)
–データ、対象ファイル
on write_to_file_AsList(this_data, target_file)
  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
    
set eof of the open_target_file to 0
    
write this_data to the open_target_file starting at eof as list
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file_AsList

–AppleScriptのソースを取得して返す
on getASSource(macPath)
  set aName to (info for macPath size 0)’s name
  
set tmpPath to ((path to temporary items from system domain) as text) & (do shell script “/usr/bin/uuidgen”) & “.txt”
  
  
set scptText to do shell script “/usr/bin/osadecompile “ & quoted form of POSIX path of macPath
  
  
return scptText
end getASSource

–指定のテキストをAppleScriptとしてコンパイルし、実行可能なアプレットとして出力する
on makeASexecutable(aText, outPathAlias)
  
  
–出力先のaliasからファイル名のみ撮り出す
  
set aStr to outPathAlias as string
  
set fnRes to getLastLayerOfDirData(aStr) of me
  
  
–指定のファル名に拡張子が適切に付いていない場合には補う
  
if fnRes does not end with “.app” then
    set fnRes to fnRes & “.app”
  end if
  
  
–テンポラリフォルダ内のテキストファイルに書き出す
  
set aFileName to (do shell script “uuidgen”) & “.applescript”
  
set tmpAppName to (do shell script “uuidgen”) & “.app”
  
  
set aTempFol to path to temporary items folder from system domain
  
set aTempFolPosix to (POSIX path of aTempFol) & aFileName
  
  
set aTempPathText to (aTempFol as string) & aFileName
  
–write_to_file(aText, aTempPathText, false) of me
  
  
store script aText in file aTempPathText replacing yes
  
  
–ファイルキャッシュをHDDにシンクロさせる
  
–do shell script “sync”–> 実行しなくても大丈夫だった
  
  
–osacompileコマンドでアプレットを生成
  
–set sText to “cd ” & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -x -o ” & fnRes & ” ” & aFileName
  
set sText to “cd “ & (quoted form of POSIX path of aTempFol) & ” && osacompile -s -o “ & fnRes & ” “ & aFileName
  
try
    –コンパイル時にアプリケーションの起動を待ったりする可能性があるので、待ち時間を1時間に設定
    
with timeout of 3600 seconds
      do shell script sText
    end timeout
  on error erMes
    return erMes
  end try
  
  
  
–テンポラリに書き込んだTextのAppleScriptを消す
  
try
    do shell script “rm -f “ & quoted form of aTempFolPosix
  end try
  
  
  
–生成したアプレットを指定のフォルダに移動する
  
set origAppPath to POSIX path of ((aTempFol as string) & fnRes)
  
set destPath to POSIX path of getOnlyFolder(aStr) of me & fnRes
  
  
try
    do shell script “mv -f “ & quoted form of origAppPath & ” “ & quoted form of destPath
  on error erMes
    return erMes
  end try
  
  
return true
  
end makeASexecutable

–テキストで与えられたパスデータから、フォルダのみを返す
on getOnlyFolder(a)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to {“:”}
  
set aList to text items of a
  
set bList to items 1 thru -2 of aList
  
set aText to bList as string
  
set AppleScript’s text item delimiters to curDelim
  
set aText to aText & “:”
  
return aText
end getOnlyFolder

–テキストで与えられたパスデータから、最終階層のテキストのみを返す
on getLastLayerOfDirData(aData)
  set aColon to “:” as Unicode text
  
copy aData to bData
  
if bData ends with “:” then
    set bData to text 1 thru -2 of bData
  end if
  
  
set aLen to length of bData
  
set rData to reverse of (characters of bData)
  
set rData to rData as string
  
set dPos to offset of aColon in rData
  
set resData to text (aLen - dPos + 2) thru -1 of bData
  
return resData
  
end getLastLayerOfDirData

–ファイルの追記ルーチン「write_to_file」
–追記データ、追記対象ファイル、boolean(trueで追記)
on write_to_file(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 starting at eof
    
close access the open_target_file
    
return true
  on error error_message
    try
      close access file target_file
    end try
    
return error_message
  end try
end write_to_file

–文字置換ルーチン
on repChar(origText, targStr, repStr)
  set {txdl, AppleScript’s text item delimiters} to {AppleScript’s text item delimiters, targStr}
  
set temp to text items of origText
  
set AppleScript’s text item delimiters to repStr
  
set res to temp as text
  
set AppleScript’s text item delimiters to txdl
  
return res
end repChar

–指定フォルダ中のファイルのパスをthread数に分ける
on splitFilePathToThreadNum(aFolder, threadMax)
  
  
set fileList to {}
  
  
repeat threadMax times
    set the end of fileList to {}
  end repeat
  
  
set aFolderStr to aFolder as string
  
  
tell application “Finder”
    tell folder aFolder
      set nameList to name of every file –whose name ends with “.dcm.txt”
    end tell
  end tell
  
  
set subCount to 1
  
set aLen to length of nameList
  
  
repeat with i in nameList
    set j to contents of i
    
set the end of item subCount of fileList to (aFolderStr & j)
    
set subCount to incVal(subCount, threadMax) of me
  end repeat
  
  
return fileList
  
end splitFilePathToThreadNum

–値のインクリメント
on incVal(aVar, aMax)
  if aVar < aMax then
    set aVar to aVar + 1
  else
    set aVar to 1
  end if
  
return aVar
end incVal

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

次に、並列実行されるAppleScriptです。こちらは、普通のAppleScriptとして保存し、アプレット実行時に「並列実行するAppleScriptを選択してください」とダイアログが出るので、その際に指定してください。

この試作品v2では、多数のファイルの処理を行うケースを前提にしており、処理対象ファイルをスレッド数で分割してそれぞれのスレッドに渡すようになっています。

4コアのCore i7の環境で、通常タイプのAppleScriptにくらべて処理時間は大幅に(CPUパワーの許す範囲でスレッド数を増やして)短縮できました。

並列処理で大量のデータを処理する際には、メモリー使用量にも気をつけ、アクティビティ・モニタで監視しながら実行するのがよいでしょう。

para3.jpg

▲AppleScript実行中に大量のデータを抱えすぎてえらい状態になっている例

今回は、何度もテスト実行を行いながらスレッド数の最適化を行いましたが、CPUのコア数や世代(Core Duo、Core 2 Duo、Core iなどなど)を取得してスレッド数の自動判定や増減をコントロールできるとよさそうだと思われました。

また、並列処理を行う対象のAppleScriptは、若干書き換える必要がありますが……書き換えを最低限で済ませられるような仕組みも作るとよさそうです(choose fileとかchoose folderのイベントハンドラをAppleScript自身で横取りするとできそう)。

スクリプト名:testScript
property mePath : “”
property myName : “”

on run
  set mePath to path to me
  
  
tell application “Finder”
    set myName to displayed name of mePath
  end tell
end run

–メイン側からのパラメータ受信用
on open aParamRec
  set mainProgram to mainName of aParamRec
  
set pList to paramList of aParamRec
  
  
set aRes to mainProgram(pList) of mainScript of me
  
  
ignoring application responses –ここが超重要
    tell application mainProgram
      open {subName:myName, resultList:aRes}
    end tell
  end ignoring
  
  
  
tell application “Finder”
    set label index of mePath to 6 –なんとなく、視覚的に処理が終わったことを表現してみた
  end tell
  
  
tell me to quit
  
end open

script mainScript
  on mainProgram(pList)
    –ここに並列処理するプログラムを記述する
    
    
–ここからダミー処理(無意味)
    
set randNum to (random number from 5 to 10)
    
delay randNum
    
–ダミー処理ここまで
    
    
return pList
    
  end mainProgram
end script

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

09/19 openハンドラでファイルパス以外を渡す

読者の方からメールでお寄せいただいたプログラムの中に入っていて、「え? なにこれ?!」と驚いてしまったのが、このopenハンドラでファイルパス以外のものを渡すというやり方。

AppleScript Studioのアプリケーションに外部のAppleScriptから命令を渡すような場合に、コマンド入りのファイルを作成して、openハンドラ経由でそのファイルをAppleScript Studioアプリ側にオープンさせ、ファイルを経由してコントロール。

AppleScript Studioアプリケーションの内部ハンドラを外部からコントロールすることは事実上無理で、さまざまな迂回のための手段が考え出されてきましたが、ファイル経由でコマンド送信するのはなかなかいいやり方でした。……そう、処理スピードを考えなければ。

ファイルI/Oが毎回発生するので、スピードはいまひとつです。ただ、信頼性という意味では並列処理で連続して発生させても問題がなかったので、大丈夫だと考えています。

そのうえ、openハンドラをファイルI/Oを経由せずに呼び出せるのであれば、それに越したことはありません。

そんなところに飛び込んできた、openハンドラに渡せるのがファイルパスだけではないという情報。

さっそく、ためしてみました。

スクリプト名:openハンドラでファイルパス以外を渡す
set aRes to open {1, 2, 3}

on open {aFlag, bFlag, cFlag}
  display dialog aFlag as string
  
display dialog bFlag as string
  
display dialog cFlag as string
  
return true
end open

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

たしかに、openハンドラにファイルパス以外のものを渡しても大丈夫でした。ただ、openハンドラの受信パラメータに複数指定してしまうと、肝腎のファイルを(ドラッグ&ドロップで)渡したときにパラメータエラーになってしまいます。

drop1.jpg

そんなわけで、受信パラメータは1つに変更。その内容を判断して、処理するようにいろいろ試してみました。

スクリプト名:openハンドラでファイルパス以外を渡す 2
–recordをopenハンドラに渡すテスト
set aRec to {piyoID:10, piyoName:“piyopiyo”, piyoData:“ひよこさんだよ”}
set aRes to open aRec

–file aliasを単体でopenハンドラに渡すテスト
set aFile to choose file
set aRes to (open aFile)

–alias listをopenハンドラに渡すテスト
set aFol to choose folder
tell application “Finder”
  tell folder aFol
    set filesList to every file as alias list
  end tell
end tell
set aRes to open filesList

–レコード、リストに入ったaliasなどを受け付けるためのopenハンドラ
on open anObject
  set aClass to class of anObject as string
  
  
if aClass = “record” then
    set aID to piyoID of anObject
    
set aName to piyoName of anObject
    
set aData to piyoData of anObject
    
set aRes to ((aID as string) & return & aName & return & aData)
    
  else if aClass = “list” then
    –listが渡された場合
    
set bClass to (class of (first item of anObject)) as string
    
if bClass = “alias” then
      –alias listが渡されたものとして判定
      
set aRes to length of anObject –テストで長さを数えて返す。実際にはループでファイルを処理するような処理を行う
    end if
    
  else if aClass = “alias” then
    –エイリアス(ファイルパス)が単体で渡された場合。実際には、渡されたaliasのファイルをオープンしたりチェックしたりする
    
set aRes to anObject as string
    
  end if
  
  
return aRes
end open

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

04/27 AppleScriptによる並列処理テスト

AppleScriptで並列処理を行うサンプルです。メイン(GUI)側はAppleScript Studio(Xcode 3.1.4 on Mac OS X 10.5)、サブ側は通常のAppleScriptで記述しています。

子プログラムからメインプログラム(画面つきのプログラム)に向けて、ファイルシステム経由でイベントを6プロセス並列で送信。子プログラム側では1〜1000まで順次カウントアップし、すべての子プログラムがカウントアップを終了したら、その時間を表示するようになっています。

yyyeyoyycyayee2010-04-27-10458e.jpeg

→ テストプログラムをダウンロードする

使用方法は……アーカイブを展開したら、「docOpen1」アプリケーションを起動して、「START」ボタンを押すだけです。

newimage.jpg

結果は、手元のマシンでは……

  CoreDuo 2.0GHzのMacBook Pro:95〜100秒
  Core2Duo 2.4GHzのMacBook Pro:90〜95秒
  Core i7 2.66GHzのMacBook Pro:55〜60秒

といったところです(Mac OS X 10.6.3上で計測)。Core i7のMacBook Proでは6プロセス並行して走らせてもまだCPUに余力があり、HyperThreadingで仮想的に4コアのCPUとして振る舞えることが、こうしたタイプのプログラムの実行に有利に働いているようです。Core 2 Duo 2.4GHzの環境ではCPUの全能力を必要としていたAppleScriptのプログラムが、余裕で動いてしまう様にはほれぼれします>Core i7

yyyeyoyycyayee2010-04-27-10504e.jpeg
▲MacBook Pro Core i7 2.66GHzで本サンプルが稼働中の負荷

本サンプルの本当の目的は、並列処理時の子プログラムからメインプログラムへの状況・結果通知のテストでした。それぞれの子プログラムが並列でてんでバラバラにメッセージを投げた場合でも、イベントを取りこぼさずに正しく処理できることを確認できました。

パラメータをファイルに書き出して、そのファイルをメインプログラムでオープンさせるようにイベントを投げるというシンプルな処理ですが、並列処理時や長期間に渡る処理を行わせた場合の時間計測など、さまざまな用途が考えられます。実際に、ぜんぜん速度を要求されない「アドレスブック.app(のプラグインAppleScript)からAppleScript Studioアプリへのデータ転送」に、このファイルシステム経由のイベント送信を使って実装したことがあります。

本当は、こんなに短い間隔で連続的に子プログラム→メインプログラムの方向でイベントを送るようなシーンは考えていませんでした。かなりまとまった処理を子プログラムに行わせて、子プログラム側の処理終了後にその実行結果なりエラー内容なりを親プログラム側に伝えられればよい、というスタンスです。

さらに……本来であれば子プログラムの生成については、ダイナミックにその数を増減させられるのですが、GUIのプログラムに経過を表示する関係で、本サンプルでは6プロセス固定で行っています。

とても高価なため自分の手元にはないのですが、もっとCPUコア数の多いMacProなどで処理を行った場合の速度などが分かると有意義でしょう。速いマシンでの実行結果(秒数)については、コメント欄にて教えていただけると助かります。

08/29 Snow LeopardのAppleScriptに関するリリースノート(2)

Snow LeopardのAppleScriptについてのリリースノートのうち、変更点に関するものが別途アップされていました(AppleScript Users MLより)。

Mac Dev Center: AppleScript Release Notes: 10.6 Changes

(more…)

03/30 AppleScriptによる並列処理実験用アプレット

AppleScriptによる並列処理実験に用いたアプレットです。

とくに、手の込んだところはひとつもないのですが、このAppleScriptを保存する際に、「アプリケーションバンドル」形式で保存し、さらにオプションで「実行後、自動的に終了しない」チェックボックスをオンにしておく必要があります。

sub.zip

スクリプト名:sub
on run
  identifyMe()
end run

on identifyMe()
  activate
  
  
tell application System Events
    set pName to name of every process whose frontmost is true and visible of it is true
  end tell
  
set aName to contents of first item of pName
  
  
display dialog aName buttons {”OK“} giving up after 1
end identifyMe

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

03/30 アプレットをコピーしてリネーム v2

AppleScriptによる並列処理実験Scriptのv2です。v1では、複数のアプレットがDockに表示され……16プロセス起動すると同じアイコンが16個並んでしまいましたが、このv2ではInfo.plistのLSUIElementエントリを作成して、Dockに表示しないようにします。

あとは、アプレットを複製する場所を~/Library/Caches/TemporaryItemsあたりにでもして、終了後の実行ファイルの削除あたりを行うようにライブラリを整理すればいい感じでしょうか。

スクリプト名:アプレットをコピーしてリネーム v2
property maxNum : 10

set aFile to choose file
選択したファイルがバンドル形式のアプレットかどうかをチェック
set apRes to chkAppBundle(aFile) of me
if apRes = false then return

set aFile to aFile as alias

repeat with i from 1 to maxNum
  set aName to (”sub & i as string) & .app
  
  
ファイルをコピーしてリネーム
  
tell application Finder
    set dRes to (duplicate aFile)
    
set dRes to dRes as alias
    
set name of dRes to aName
  end tell
  
  
各アプレットのCFBundleNameを書き換えてLaunch
  
set newApp to renameAppBundle(dRes, aName) of me
  
tell application Finder
    open newApp
  end tell
end repeat

指定アプレットのInfo.plistを書き換える
on renameAppBundle(aFile, aName)
  アプリケーションバンドル内のInfo.plistへのフルパスを組み立てる
  
set aFile to aFile as alias
  
  
set aPosixFile to POSIX path of aFile
  
set pListpath to aPosixFile & Contents/Info.plist
  
  
CFBundleNameを書き換える
  
tell application System Events
    set plistRec to (value of property list file pListpath)
    
set |CFBundleName| of plistRec to aName
    
set plistRec to plistRec & {|LSUIElement|:1} Dockに表示させない
    
set value of property list file pListpath to plistRec
  end tell
  
  
return aFile
end renameAppBundle

指定ファイルがバンドル形式のアプレットかどうかチェック
on chkAppBundle(a)
  set aInfo to info for a
  
set fType to file type of aInfo “APPL”
  
set fCreator to file creator of aInfo “aplt”
  
set typeIdentifier to type identifier of aInfo “com.apple.application-bundle”
  
  
log {fType, fCreator, typeIdentifier}
  
  
if {fType, fCreator, typeIdentifier} = {”APPL“, aplt“, com.apple.application-bundle“} then
    return true
  else
    return false
  end if
end chkAppBundle

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

03/30 アプレットをコピーしてリネーム v1

AppleScriptによる並列処理の実験Scriptです。あらかじめ、他にバンドル形式のアプレットをAppleScriptで作成しておき……そのアプレットをコピーしつつバンドル内のInfo.plistの中のエントリを書き換えて別名のアプレットに仕立て上げ、別々のプロセスとして扱えるようにします。

最近のMacは最低でもデュアルコア、最高で16コアというマルチコア化まっしぐらの状況なので、時間のかかる処理については並列処理を考えたほうが得策です。AppleScript自体に並列処理のための機能は……複数のアプレット間でプロセス間通信を行ったりプロパティを参照し合ったりというぐらい。いまひとつマルチコア時代に適した便利な機能を持っていませんが、なければ作るまでのこと。

そこで、アプレットのファイルをコピーして必要なプロセス分用意し、さらにそのバンドル内のInfo.plistを書き換えて、別プロセスとして扱えるように細工します。

これで、プロセス名(アプリケーション名)を個別に指定してハンドラを呼び出したり、プロパティの参照/書き換えを行ったりできるので、1つの時間のかかる処理(大量のJPEG画像ファイルの破損チェックなど)を複数プロセスの生成・起動により時間短縮できるものと思われます。

複数のアプレットから特定のアプリケーション(QuickTime Playerなど)に同時アクセスするのは得策ではないため、それらのリクエストを受け付けるキューイング用のアプレットを別途用意するなどの対策は必要になってくることでしょう。

私の手元にはDual Coreのマシンしかないので、どこからか実験用に16コアのMac Proでも貸していただければ、さまざまな検証も行えるのですが(^ー^;。

スクリプト名:アプレットをコピーしてリネーム v1
property maxNum : 10

set aFile to choose file
選択したファイルがバンドル形式のアプレットかどうかをチェック
set apRes to chkAppBundle(aFile) of me
if apRes = false then return

set aFile to aFile as alias

repeat with i from 1 to maxNum
  set aName to (”sub & i as string) & .app
  
  
ファイルをコピーしてリネーム
  
tell application Finder
    set dRes to (duplicate aFile)
    
set dRes to dRes as alias
    
set name of dRes to aName
  end tell
  
  
各アプレットのCFBundleNameを書き換えてLaunch
  
set newApp to renameAppBundle(dRes, aName) of me
  
tell application Finder
    open newApp
  end tell
end repeat

(*
delay 1

repeat with i from 1 to maxNum
  set aName to (”sub” & i as string) & “.app”
  tell application aName
    identifyMe()
  end tell
  delay 1
end repeat
*)

指定アプレットのInfo.plistを書き換える
on renameAppBundle(aFile, aName)
  アプリケーションバンドル内のInfo.plistへのフルパスを組み立てる
  
set aFile to aFile as alias
  
  
set aPosixFile to POSIX path of aFile
  
set pListpath to aPosixFile & Contents/Info.plist
  
  
CFBundleNameを書き換える
  
tell application System Events
    set plistRec to (value of property list file pListpath)
    
set |CFBundleName| of plistRec to aName
    
set value of property list file pListpath to plistRec
  end tell
  
  
return aFile
end renameAppBundle

指定ファイルがバンドル形式のアプレットかどうかチェック
on chkAppBundle(a)
  set aInfo to info for a
  
set fType to file type of aInfo “APPL”
  
set fCreator to file creator of aInfo “aplt”
  
set typeIdentifier to type identifier of aInfo “com.apple.application-bundle”
  
  
log {fType, fCreator, typeIdentifier}
  
  
if {fType, fCreator, typeIdentifier} = {”APPL“, aplt“, com.apple.application-bundle“} then
    return true
  else
    return false
  end if
end chkAppBundle

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