テキストから特定のデータを抜き出す

さて。AppleSceript から Cocoa フレームワークの各機能を利用するこの企画(?)も今回で 5 回目となりました。

...なったのですが、AppleScript に関連する話題が WWDC で発表されましたね。OS X 10.10 から JavaScript でも OS X の自動化を行うことができる、と。

まぁ、長く AppleScript を触っている人にとって特別に目を引くような話題でもなんでもありません。JavaScript OSA ってものは、古くからありましたし(今はもうないのかな?)。それよりも気になるのはリリースノートを読んでいる限りでは PyObjC や RubyCocoa と同等の機能を持っているっぽいことです。

現状の AppleScript では Cocoa の機能呼び出しがスクリプトライブラリのみという制限付き。この制限が JavaScript にないのなら、JavaScript を積極的に利用する。AppleScript でも同じように機能が拡張されるといいのにな。

さらに JavaScript と AppleScript の混在ができればもっといいのにな。

Swift?

なにそれ?

おいしいの?

まぁ、ベータの Yosemite を入れればすぐに分かるんですけどね。

閑話休題。

しかし、ねえ...。ここに書いていることが Yosemite 以降、使えるのかどうか、知識が役に立つのかどうか、といったことが甚だ気になり、ブログ更新するのが億劫になるんだけど、AppleScript 自体はまだまだ現役で使えるのでがんばって更新します。

っていうか、楽になるのならなんでもウェルカムな姿勢なので、JavaScript という選択肢が増えることは大歓迎です。

おっと。また話が逸れてます。

文字列から特定のデータを抜き出す...。URL やメールアドレス、日付や電話番号などですね。この手のことがしたいなら、Automator を使えば簡単にできます。

Detect_URL_Service

このようなサービスを作っておけば、

  1. 文字列を選択
  2. コンテクストメニューでサービスを呼び出す
  3. URL がクリップボードにコピーされる

ってことができます。

こういった情報を抜き出すのに利用されているのが、NSDataDetectorNSRegularExpression のサブクラスです。

NSDataDetector なんていまさら...な感が否めませんが、使ってみます。

NSDataDetector は抜き出したいデータを複数指定することができます。抜き出せるのは、日付、住所、URL、電話番号、飛行機などの運航情報。

  • NSTextCheckingTypeDate
  • NSTextCheckingTypeAddress
  • NSTextCheckingTypeLink
  • NSTextCheckingTypePhoneNumber
  • NSTextCheckingTypeTransitInformation.

Objective-C ではビット演算子で複数を指定することになるのですが、AppleScript にはビットを操作するような演算子はありません。

こういうときどうするかというと...、単純に足してしまいます。なんだか乱暴な感じがしますが、これでいいのです。以上をふまえてライブラリを作成します。

このライブラリを次のようにして利用します。抜き出すデータの種類はリストで渡します。

一定の書式に従っていないデータは抜き出せないという制限はありますが、十分に利用できますね。

ブログの整理

ブログの見た目を変えてみたのだけど、色々とチェックしている際に気になったのが記事内にある(主に AppleScript の)ソースコード。

見にくいですよね。

もう少し見やすくならないものかと色々と探してみました。

コードの整形やカラーリングを行うには SyntaxHighlighter なるものが主流のようですね。

なんだけど、いまいちピンとこない。

他にないかなと思いつつ探していたら、Gist のコードをそのまま貼付けることができるらしい。試しに貼ってみる。

見やすいと思うのですが、どうでしょう?

導入も簡単だし、AppleScript Editor から直接 Gist に投稿、ソースコードのリンクを作成し取得するという一連の流れがすべて AppleScript で制御できる。

問題はそれなりのスクリプトならともかく、断片のようなスクリプトをどうするかって事だけど。

とりあえずは Gist との連携でいってみようか...。

画面共有を開始する一番簡単な方法

近くて遠い隣の部屋。そこに Mac はあるのに...。

そういう時の画面共有。便利ですね。

しかし、画面共有.app って /System/Library/CoreServices にあってアクセスするのが面倒だったりする。Dock にでも登録しておけばいいんでしょうけど、もう少し手軽にこいつを利用する方法はないものか、と。

実は AppleScript に対応していたりするんですね。画面共有.app って。

なら、話は簡単です。

まず、共有したい Mac で画面共有を有効にします。

「システム環境設定」の「共有」にある「画面共有」にチェックを入れ、共有を開始します。

このとき、コンピューターの名前を覚えてきます。これで画面共有が利用できるようになりました。

Screen_Sharing_Preferences

次に、呼び出し側の Mac で次のような Automator サービスを作成します。

Automator_Service

スクリプトは次のようなものです。

Script Editor で開く

tell application id "com.apple.ScreenSharing"
    activate
    -- 先ほど「共有」で覚えておいた相手側 Mac の名前を指定する
    GetURL "vnc://MacBook-late-2007.local" -- 適宜、書き換えてね
end tell

実行すると画面共有.app が起動し、無事に隣の部屋にある MacBook を操作することができるようになります。

最後に Automator のサービスを保存し、「システム環境設定」の「キーボード」にある「ショートカット」でサービスにショートカットキーを割り当てます。

Keyboard_Shortcut_Preferences

ここでは F5 のショートカットキーを割り当てています。

これでいつでも簡単に画面共有を起動させることができますね。

AppleScript で JSON を利用する

AppleScript なら JSON を簡単に利用できます。

そう、Marvericks ならね。

...冗談はさておき。最近では JSON でデータの交換ってことが多くなってきて、時代から置いてきぼりな感がある AppleScript です。JSON を AppleScript で利用できるようにする方法は既に幾つかあります。

前者は OSAX として提供されており、後者は AppleScript で解決するスクリプトです。

他方、OS X と iOS には NSJSONSerialization という JSON を扱うためのクラスが少し前から追加されています。このクラスを利用すれば AppleScript のリストやレコードを JSON に、逆に JSON を AppleScript のリストやレコードに変換することができます。

まず、NSJSONSerialization を利用するライブラリを作ります。

Script Editor で開く

property NSString : class "NSString"
property NSData : class "NSData"
property NSJSONSerialization : class "NSJSONSerialization"
property NSArray : class "NSArray"
property NSDictionary : class "NSDictionary"

on serialize(theText)
    -- AppleScript 文字列を NSString に変換
    set theText to NSString's stringWithString:theText
    -- NSString を NSData に変換
    set jsonData to theText's dataUsingEncoding:(current application's NSUnicodeStringEncoding)

    -- NSData に変換した JSON を パース
    set theResult to NSJSONSerialization's JSONObjectWithData:jsonData options:(current application's NSJSONReadingAllowFragments) |error|:(missing value)

    -- NSArray か NSDictionary で結果が返ってくる
    if (theResult's isKindOfClass:(NSDictionary's |class|)) as boolean then
        -- NSDictionary なら record に型変換
        return theResult as record
    else
        return theResult as list
    end if
end serialize

on deserialize(theObject)
    -- 渡されたデータが list か record か判別
    if (class of theObject) is list then
        -- list から NSArray に変換
        set theObject to NSArray's arrayWithArray:theObject
    else
        set theObject to NSDictionary's dictionaryWithDictionary:theObject
    end if

    if (NSJSONSerialization's isValidJSONObject:theObject) then
        -- JSON に変換できるなら
        set json to NSJSONSerialization's dataWithJSONObject:theObject options:(current application's NSJSONWritingPrettyPrinted) |error|:(missing value)
        -- 変換された NSData から NSString に変換
        set theText to NSString's alloc()'s initWithData:json encoding:(current application's NSUTF8StringEncoding)

        -- NSString を text に変換
        return theText as text
    else
        return missing value
    end if
end deserialize

このスクリプトをライブラリとして保存します(ライブラリとして保存する方法はこちらを参照)。

使い方はいたって簡単。

Script Editor で開く

-- record、list まじりの list
set theList to {"a", "b", {10, 20}, {age:20}}
tell script "ASJSON" to set json to deserialize(theList)
--> 結果
"[
  \"a\",
  \"b\",
  [
    10,
    20
  ],
  {
    \"age\" : 20
  }
]"

-- record 
set theRecord to {a:100, b:200, c:{10, 20}}
tell script "ASJSON" to set json to deserialize(theRecord)
--> 結果  
"{
  \"a\" : 100,
  \"b\" : 200,
  \"c\" : [
    10,
    20
 ]
}"

--> JSON を変換してみる
"{
  \"a\" : 100,
  \"b\" : 200,
  \"c\" : [
    10,
    20
  ],
  \"d\" : null,
  \"e\" : false,  
  \"f\" : 0.13
}"

set json to result
tell script "ASJSON" to set json to serialize(json)
--> 実数の値が...
--> {d:missing value, b:200, e:false, c:{10, 20}, a:100, f:0.129999995232}

実数の取り扱いに難があるものの、きれいに変換できます。実際の利用となると、エラーの処理(パースできなかったとき等)が必要になるものの、これで Web API を利用した AppleScript がより身近になりますね。

Welcome, Otto's Remote!!

Otto って Automator のあのロボット君の名前ですよね?

Automator アイコン

iOS デバイスから Mac のスクリプトを実行させる...。こういうアプリケーションがなかったため、一時期自分で作ってやろうかと本気で考えておりました。まあ、三日であきらめましたが。

そういうアプリケーションが存在するということを AS Hole さんの記事で知りました。

Otto's RemoteOtto's Antenna の導入や使い方、現状での問題点などはこの記事を参照していただくと一目瞭然。ここではこの記事で触れられていないことを取り上げます。

Otto's Remotex-callback-url に対応しているんですよね。これは地味にうれしい。現状では Automator とシェルには引数を(文字列として)渡すことができます。が、AppleScript には引数を渡すことができません。

しかし、x-callback-url を使うと AppleScript にも引数を渡すことができます。

以下のようなスクリプトを Mac 上に用意し、Otto's Antenna が利用するフォルダ(~/Library/Application Scripts/com.amolloy.ottosantenna/)に保存します。

Script Editor で開く

on run {arg}
    tell me
        activate
        display dialog arg
    end tell
end run

iOS デバイス上から以下の URL を開きます。

otto://x-callback-url/action?computer=[Mac の名前]&action=[スクリプト名]&argument1=[引数1]

computer と action、argument1 に渡す値は適宜変更してください。この URL を開くと Otto's Remote が起動し、指定したスクリプトに指定した引数を渡して実行されます。引数は複数でも大丈夫です。複数の場合は、

otto://x-callback-url/action?computer=[Mac の名前]&action=[スクリプト名]&argument1=[引数1]&argument2=[引数2]...

となります。引数は URL エンコードしておく必要があります。

AppleScript といっても現在ではシェルスクリプトとしても利用できるので、このようなことができます。x-callback-url を利用しなければいけないという制限はあるものの、逆に言えば利用範囲は広がります。

x-callback-url というのは iOS デバイス上でのアプリ間連携に関するプロトコルですので、なんらかのアプリで利用したデータを Otto's Remoteに渡し、Mac 上の AppleScript で処理ということができるようになります。

ちょっとした例として、現在地を Mac 上のファイルに追記して行くスクリプトを。

iOS 上で簡単に現在地を取得できるようなアプリがあればいいのですが、ここでは Pythonista というアプリを利用します。

このアプリは iOS 上で Python が実行できるアプリで、特筆すべきは iOS 上のいろんな機能(アドレス帳、通知センター、GPS、クリップボード、写真等々...)にアクセスすることができることです。

URL スキームも持っているので作成したスクリプトをいろんなところから実行することができます。ちょっと準備が手間ですが、やってみましょう。

まず、Mac 上に次のようなスクリプトを用意します。

Script Editor で開く

property filename : "GPS_LOG.txt"
property directory : path to desktop folder as text

on run {lat, long}
    set theDate to (current date) as text

    set theText to theDate & "," & (lat as text) & "," & (long as text)

    try
        set fh to open for access file (directory & filename) with write permission
        if (get eof fh) is 0 then
            write theText to fh as «class utf8»
        else
            set eof fh to (get eof fh) + 1
            set theText to (string id 10) & theText
            write theText to fh as «class utf8» starting at eof
        end if
        close access fh
    on error
        close access fh
    end try

    display notification "GPS を追記しました" with title "GPS"
    delay 1
end run

これを GPS という名前で保存しておきます。

次に iOS デバイス上の Pythonista で次のようなスクリプトを作成します。

import location
import time
import webbrowser

url = 'otto://x-callback-url/action?computer=[YOUR MAC NAME]&action=GPS&argument1='

location.start_updates()
time.sleep(2)
current = location.get_location()
location.stop_updates()

url = url + str(current['latitude']) + '&argument2=' + str(current['longitude'])

webbrowser.open(url)

URL の Mac の名前は適宜変更してください。

最後に Launch Center Pro のようなランチャーアプリで URL スキームを登録。

pythonista://GPS?action=run

実行すると Pythonista が起動し、スクリプトが実行され、Otto's Remote が起動し、現在地を Mac 上のスクリプトに送信し、ファイルに経度と緯度を追記します。

単純に位置情報が欲しいだけなら他にも方法があるでしょうけど...。

AppleScript の実行結果を iOS デバイス上で受け取ることはできませんが、これはどうとでもなる問題でしょう。結果をメモ.app を使って iCloud で同期とか、Dropbox を利用する、メールや SMS でもいいでしょう。

このように iOS デバイス上のアプリを組み合わせて Mac 上の AppleScript を動かす...。なんだか、妄想が広がりますね。

Mavericks の Finder タグをスクリプトから操作

Mavericks の新機能の一つに『タグ』があります。

ファイルやフォルダに付加することができ、OS 全体で利用できます。タグの使い方は「[K]【マニュアル】ファイル管理が超捗る!Appleの新OS『OS X Mavericks』のFinderタグ機能の使い方 - Knowledge Colors」が分かりやすかったです。

Finder にあったラベルが拡張されたようですが、AppleScript からは取得も設定もできないようです。

Finder のラベルは AppleScript で操作できるんですが...。

ちょっと調べたところ、NSURL の NSURLTagNamesKey で設定されているタグは取得できるんですね。なら、ASOC ライブラリで作ってしまいましょう。

新しく AppleScript/Objective-C ライブラリスクリプトを作成し、以下のように記述。

Script Editor で開く

property NSURL : class "NSURL"

on tags(thisFile)
    (*
        タグをリストで取得する
    *)
    set fileURL to NSURL's fileURLWithPath:thisFile
    set dict to fileURL's resourceValuesForKeys:{current application's NSURLTagNamesKey} |error|:(missing value)
    set tagList to dict's objectForKey:"NSURLTagNamesKey"
    if tagList is missing value then return missing value
    return (tagList as list)
end tags

次のようにして呼び出します。

Script Editor で開く

set theFile to choose file

tell script "Tags"
    tags(POSIX path of theFile)
end tell

タグがない場合は missing value が返ります。

では、設定はどうでしょうか。

タグの設定は NSURL の setResourceValue:forKey:error: メソッドで NSURLTagNamesKey を指定して設定できます。

Script Editor で開く

on setTags(tagList, thisFile)
    (*
        現在付けられているタグを上書き
    *)
    set fileURL to NSURL's fileURLWithPath:thisFile
    fileURL's setResourceValue:tagList forKey:(current application's NSURLTagNamesKey) |error|:(missing value)
end setTags

使ってみます。

Script Editor で開く

set theFile to choose file

tell script "Tags"
    setTags({"オレンジ", "レッド"}, POSIX path of theFile)
end tell

これもできました...が、既に設定しているタグを上書きするようです。設定しているタグをそのままに、新しく追加するには次のようにします。

Script Editor で開く

on addTags(tagList, thisFile)
    (*
        現在付けられているタグに追加
    *)
    set currentTags to tags(thisFile)
    if currentTags is missing value then set currentTags to {}
    repeat with i from 1 to count tagList
        set tag to item i of tagList
        if currentTags does not contain tag then
            set currentTags to currentTags & tag
        end if
    end repeat

    setTags(currentTags, thisFile)
end addTags

現在のタグに含まれないタグだけを新しいタグのリストから追加しています。これで追加も可能になりました。

ASOC を使ったライブラリですのでちょっと手間がかかりますが、これでタグを自由自在。ま、そのうち AppleScript のみで利用できるようになるのでしょうが、それまでの代替です。

AppleScript + 顔認識

Core Image のフィルタを利用すれば、画像に最近流行のトイカメラのようなエフェクトを簡単に施せるようになります。簡単な利用方法は「AppleScript + Core Image」で触れました。今度は Core Image の顔検出を利用してみましょう。

「AppleScript + Core Image」で利用した CoreImage.scptd ライブラリを拡張します。

このファイルを ~/Library/Script Libraries に保存しておいてください。

顔の検出は CIDetector で行います。 CoreImage.scptd の property に CIDetector を追加します。

property CIDetector : class "CIDetector"

顔の検出の手順は

  1. 検出器を作成/設定
  2. 検出器に画像を渡す

検出器は検出器の種類といくつかのオプションを指定して作成します。検出器の種類は現在のところ「人の顔」だけです。オプションには検出器の精度があります。精度は CIDetectorAccuracyLow と CIDetectorAccuracyHigh がありますが、前者は速度重視、後者は速度を犠牲にして正確さ重視になっています。

iOS ではどうか分からないのですが、どちらでも違いがあまり分かりませんでした...。

では作ってみましょう。

Script Editor で開く

on faceDetect(imageRef)
    -- 検出器のオプションを NSDictonary で作成
    set opts to {CIDetectorAccuracy:(current application's CIDetectorAccuracyHigh)}
    -- 検出器をオプションとタイプを指定して作成
    set detector to CIDetector's detectorOfType:(current application's CIDetectorTypeFace) context:(missing value) options:opts

    -- 顔の検出を行う際のオプションを NSDictonary で作成
    set opts to {CIDetectorImageOrientation:(imageRef's |properties|()'s valueForKey:"Orientation")}

    -- 顔検出を実行
    set faceList to detector's featuresInImage:imageRef options:opts

    -- 検出された顔の位置とサイズをログに出力
    repeat with i from 1 to count faceList
        set face to item i of faceList
        log ((i as text) & ": 検出")
        log (face's |bounds|())
    end repeat

    return faceList
end faceDetect

検出器を作成し、そのまま顔検出を行っています。検出は CIDetector の featuresInImage:options: メソッドで行います。このときに指定するオプションは画像の向き、まばたきの検出、笑顔の検出が指定できます。大事なのは画像の向きでこれが正しくないと検出が正確に行えません。

まばたきと笑顔は OS X Mavericks 以降でしか利用できません。

AppleScript としての注意点ですが、本来ならオプションは NSDictionary で作成します。が、ここでは AppleScript の record で代用しています。互換性がある...というわけではないのですが、代替として利用できます。

これは NSArray と AppleScript の list でも同様で、上記でも NSArray として返ってくる検出の結果を AppleScript 的に繰り返しで処理しています。

ここまでで顔検出が行えますのでいったんテストしてみましょう。テストで利用するのは以下の画像です(photo by pakutaso.com )。

元の画像

ちょっと時期外れのサンタさんです。

Script Editor で開く

on run
    set inputFile to choose file of type {"public.jpeg"}

    tell script "CoreImage"
        set imageRef to openImageFile(POSIX path of inputFile)
        set faceList to faceDetect(imageRef)
    end tell
end run

顔が写っている画像を選択して実行してみてください。顔が検出されれば、AppleScript Editor のログに結果が表示されます。

検出結果

うまく検出できているでしょうか?

顔の範囲が分かったら顔だけを切り抜いたり、様々なフィルタをかけることができます。

試しに Core Image Programming Guide にある Anonymous Faces Filter Recipe を AppleScript に移植してみましょう。このサンプルは検出された顔に対してモザイクをかけるものです。引き続き、先ほどのサンタさんの画像を使います。

やることは以下の通り。

  1. オリジナルの画像をモザイクにする
  2. 検出した顔の部分以外を黒く塗りつぶしたマスク画像を作る
  3. オリジナルの画像、モザイクをかけた画像、マスク画像を重ね合わせる

まず、新しく利用するクラスを追加します。

property CIColor : class "CIColor"
property CIVector : class "CIVector"

そして、以下のハンドラを追加。

Script Editor で開く

on max(a, b)
    -- 大きい方の値を返す
    if a > b then return a
    return b
end max

on min(a, b)
    -- 小さい方の値を返す
    if a < b then return a
    return b
end min

on faceMask(imageRef)
    -- 顔の部分のマスクを作成
    set faceList to faceDetect(imageRef)
    if (count faceList) is 0 then return missing value

    -- 顔の範囲を緑の円で、それ以外を黒で塗りつぶす   
    set maskImage to missing value
    repeat with face in faceList
        set centerX to (face's |bounds|()'s origin's x) + ((face's |bounds|()'s |size|'s width) / 2.0)
        set centerY to (face's |bounds|()'s origin's y) + ((face's |bounds|()'s |size|'s height) / 2.0)
        set radius to (min(face's |bounds|()'s |size|'s width, face's |bounds|()'s |size|'s height)) / 1.5

        set faceCenter to current application's NSMakePoint(centerX, centerY)

        set radialGradient to filterWithName("CIRadialGradient")
        (radialGradient's setValue:radius forKey:"inputRadius0")
        (radialGradient's setValue:(radius + 1.0) forKey:"inputRadius1")
        (radialGradient's setValue:(CIColor's colorWithRed:0.0 green:1.0 blue:0.0 alpha:1.0) forKey:"inputColor0")
        (radialGradient's setValue:(CIColor's colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0) forKey:"inputColor1")
        (radialGradient's setValue:(CIVector's vectorWithCGPoint:faceCenter) forKey:"inputCenter")

        set circleImage to (radialGradient's valueForKey:"outputImage")

        if (missing value is maskImage) then
            copy circleImage to maskImage
        else
            -- 複数の顔が検出されたら作成したマスク画像を重ね合わせる
            set filter to filterWithName("CILightenBlendMode")
            (filter's setValue:circleImage forKey:"inputImage")
            (filter's setValue:maskImage forKey:"inputBackgroundImage")
            set maskImage to (filter's valueForKey:"outputImage")
        end if
    end repeat

    return maskImage
end faceMask

on anonymousFacesFilter(imageFile)
    (* 顔にモザイクをかけた画像を作成する *)
    set imageRef to my openImageFile(imageFile)

    set imageRect to imageRef's extent()
    set {w, h} to {imageRect's |size|'s width, imageRect's |size|'s height}

    -- モザイク画像を作る
    set pixellate to filterWithName("CIPixellate")
    pixellate's setValue:imageRef forKey:"inputImage"
    pixellate's setValue:((max(w, h)) / 60) forKey:"inputScale"

    -- 顔だけを除外するマスク画像を作成する
    set maskImage to faceMask(imageRef)

    -- CIBlendWithMask フィルタでモザイクとオリジナル、マスクを重ねる
    set blend to filterWithName("CIBlendWithMask")
    blend's setValue:(pixellate's valueForKey:"outputImage") forKey:"inputImage"
    blend's setValue:imageRef forKey:"inputBackgroundImage"
    blend's setValue:maskImage forKey:"inputMaskImage"

    return blend's valueForKey:"outputImage"
end anonymousFacesFilter

ライブラリを保存したら、次のように利用します。

Script Editor で開く

on run
    set inputFile to choose file of type {"public.jpeg"}

    tell script "CoreImage"
        set imageRef to anonymousFacesFilter(POSIX path of inputFile)
        set outputFile to POSIX path of (choose file name default name "Untitled.jpg")
        saveAsJPEG(imageRef, outputFile)
    end tell
end run

結果は以下のように顔の部分だけモザイクになっています。

スクリプト実行後の画像

CIFilter には他にも色々なフィルタが用意されています。また、CIImage には property メソッドがあり、画像のメタデータにアクセスすることも可能です。GPS を読み取って Map アプリでその場所を開くなんてこともできます。こういったことは Image Events 単体ではできないことなので重宝するのではないかと。