AppleScript とデザインパターン (3)

前回、Observer パターンをアプリケーションオブジェクトを使って試してみました。

なんで、初回が Observer パターンなのか。『PHP Hacks』で最初に取り上げられていたからというだけで、他に理由はありません。しかし...iPod ゲームはいけませんね。駄目です。とてもではないですが、暇つぶしで終わらせることなんてできません。全部買ってしまいそうな勢いです。

それはさておき...。前回は極力難しいことを考えずに Observer パターンを適用しました。デザインパターンの本や Web サイトを見ていると UML 図やパターンの概念的なコードがあります。スクリプトオブジェクトで概念的なコードを書いて一応 AppleScript でもデザインパターンを適用できるよ、という感じで進めていこうと思っていたのですが、概念的なコードってあくまで理解するためのものなのでどういうふうに使えばいいかがよく分からない。で、前回のようなスクリプトを書いたのですが...UML 図はともかく、一応動作を理解できるようにデザインパターンの概念的なスクリプトも書いておきます。

監視されるオブジェクトは、誰が監視しているかを知っています。変更があったときに通知を送るためにです。監視するオブジェクトを追加、削除できるような機能を持っています。監視オブジェクトを複数保持することができるようになっていて、それらに一括で通知を送る機能も持っています。

監視している方も監視対象を知っています。監視する方では必ず update() ハンドラを実装していました。監視されているオブジェクトは監視者の update() ハンドラを呼び出す、という取り決めの上に関係が成り立っているからです。

これらをスクリプトオブジェクトで表現すると以下のようになります。

Script Editor で開く

-- 監視オブジェクト
on Observer()
    script Observer
        -- 通知を受け取るハンドラ
        on update()
            log "update"
        end update
    end script
end Observer

-- 監視対象オブジェクト
on Observable()
    script Observable
        -- 監視オブジェクトのリスト
        property observers : {}

        -- 監視オブジェクトを加える
        on addObserver(observerObject)
            set end of observers to observerObject
        end addObserver

        -- 監視オブジェクトを削除する
        on deleteObserver(observerObject)
            repeat with thisObject in observers
                if (contents of thisObject) is observerObject then
                    set contents of thisObject to missing value
                    exit repeat
                end if
            end repeat

            set observers to scripts of observers
        end deleteObserver

        -- 通知を送る
        on notifyObservers()
            repeat with thisObject in observers
                update() of thisObject
            end repeat
        end notifyObservers
    end script
end Observable

監視する方、される方、どちらも最低限の機能だけを実装しています。Observer パターンは、これらのオブジェクトを拡張することで実現します。

Observer オブジェクトは、update() ハンドラを持ってます。これは実装側で実際の処理を記述します(インターフェースのつもり)。Observable オブジェクトは、監視者を追加、削除するハンドラと通知を送るハンドラを持っています。こちらも実際の処理はこのオブジェクトを継承したオブジェクトで実装し、自身が持つべきデータ(属性)とそれらのデータを取得、設定するハンドラを追加します。

蛇足なのですが、deleteObserver() ハンドラの最後の部分で

set observers to scripts of observers

としています。これは、スクリプトオブジェクトの利用を考えているのでこのようにしていますが、監視者が他のオブジェクト(アプリケーション等)の場合は、このままでは動かないので適切に変更する必要があります(他にも問題はあるのですが)。

実際に利用するには以下のように監視する方、される方、どちらも目的に合った実装を行います。

Script Editor で開く

-- 監視者オブジェクトの実装
on ConcreteObserver1()
    script ConcreteObserver1
        property state : missing value
        property observableObject : missing value

        on setObject(theObject)
            set observableObject of me to theObject
        end setObject

        on getObject()
            return observableObject of me
        end getObject

        on update()
            set state of me to observableObject's getState()
            log "Observer 1 update."
        end update
    end script
end ConcreteObserver1

on ConcreteObserver2()
    script ConcreteObserver2
        property state : missing value
        property observableObject : missing value

        on setObject(theObject)
            set observableObject of me to theObject
        end setObject

        on getObject()
            return observableObject of me
        end getObject

        on update()
            set state of me to observableObject's getState()
            log "Observer 2 update."
        end update
    end script
end ConcreteObserver2

-- 監視対象オブジェクトの実装
on ConcreteObservable()
    script ConcreteObservable
        property parent : Observable()
        property state : missing value

        on getState()
            return state of me
        end getState

        on setState(theState)
            set state of me to theState
        end setState
    end script
end ConcreteObservable

-- クライアント
on run
    -- 監視対象の作成
    set subjectObject to ConcreteObservable()
    -- 監視者の作成
    set observerObject1 to ConcreteObserver1()
    set observerObject2 to ConcreteObserver2()
    -- 監視対象を監視者に登録
    tell observerObject1 to setObject(subjectObject)
    tell observerObject2 to setObject(subjectObject)

    tell subjectObject
        -- 監視者を登録
        addObserver(observerObject1)
        addObserver(observerObject2)
        -- 状態の変化
        setState(true)
        -- 通知を送る
        notifyObservers()
        -- 監視者を削除
        deleteObserver(observerObject1)
        log "Delete Observer 1"
        -- 状態の変化
        setState(true)
        -- 通知を送る
        notifyObservers()
    end tell
end run

-- 結果
(*Observer 1 update.*)
(*Observer 2 update.*)
(*Delete Observer 1*)
(*Observer 2 update.*)

Script Editor のイベントログで確認してみてください。監視オブジェクトは、update() ハンドラを実装しています。ここで処理を行ってもいいですし、他のスクリプトオブジェクトやアプリケーションに処理を渡してもいいです。ここでは監視対象を属性で保持していますが、これは update() ハンドラの引数で受け取るようにしても構いません(その方が監視オブジェクトはそのままで、他でも再利用しやすくなるかもしれません)。update() ハンドラさえあれば実装はどのようなものでも構いません。

監視対象は自分の属性を操作するハンドラを持っています。監視する方はこのハンドラを使って状態を調べます。監視オブジェクトの追加や削除、通知を送るハンドラは親で定義されているので、監視対象が実装する必要があるのは自身の属性とハンドラです。

基本的にこのような形になり、目的や状況に応じて Observer パターンの考え方を適用しつつスクリプトを記述します(デザインパターンはプログラム設計の指針のようなものなので必ずしもこう書かなくてはならないというものではありません)。

使い道はいろいろあると思いますが...AppleScript は、一度に一つの動作しか実行できないので通知を送り、監視オブジェクトが処理を終了するまで監視対象の処理が待ち状態になります。AppleScript Studio でなら、ボタンを押した、ビューが更新されたなどのイベントがあったときに他のオブジェクトに通知を送るような設計が可能です。が、先にも書いたように通知先で処理が滞るようなら Observer パターンの適用は考えた方がいいと思います。

AppleScript Studio ではなく、前回のようなアプリケーション形式のスクリプトなら ignoring application responses を使って処理を待たずに先に進むことができます。いろいろなことを考えるとデザインパターンは、アプリケーション形式のスクリプトをオブジェクトに見立てて(見立ててって AppleScript ではオブジェクトなのですが)それらの関係にデザインパターンを用いる方が使い勝手はいいかもしれません。

前回の iTunes の曲が変わったときになんらかの動作を行うというスクリプトを以上を踏まえて変更すると以下のようになります。

Script Editor で開く

-- 監視オブジェクトのリスト
property observers : {}
-- iTunes の現在の曲
property curTrack : missing value

-- 監視オブジェクトを加える
on addObserver(observerObject)
    set end of observers to observerObject
end addObserver

-- 監視オブジェクトを削除する
on deleteObserver(observerObject)
    repeat with thisObject in observers
        if (contents of thisObject) is observerObject then
            set contents of thisObject to missing value
            exit repeat
        end if
    end repeat

    set observers to strings of observers
end deleteObserver

-- 通知を送る
on notifyObservers(observableObject)
    repeat with thisObject in observers
        ignoring application responses
            tell application thisObject to update(observableObject)
        end ignoring
    end repeat
end notifyObservers

-- 属性の取得
on getState()
    return curTrack of me
end getState

-- 属性の設定
on setState(thisTrack)
    set curTrack of me to thisTrack
end setState

on idle
    tell application "System Events"
        set bool to (name of processes whose visible is true) contains "iTunes"
    end tell

    if bool then
        tell application "iTunes"
            if player state is playing then
                set tmp to current track
            else
                set tmp to curTrack
            end if
        end tell

        if tmp is not curTrack then
            my setState(tmp) -- 状態の変更
            my notifyObservers(me) -- 通知を送る
        end if
    end if

    return 1
end idle

これを iTunesChecker という名前にして終了しないアプリケーションで保存します。監視者を追加するハンドラと通知を送るハンドラは変更しています。この部分は後で説明します。起動しても、まだ監視者がいないので曲が変わってもなんの反応もしません。

次に監視オブジェクトを作ります。以下は、Monzai を使った曲名の読み上げスクリプトです。

Script Editor で開く

on update(theObject)
    set theTrack to getState() of theObject
    if theTrack is missing value then return

    tell application "iTunes"
        set theArtist to artist of theTrack
        set theTitle to name of theTrack

        set msg to (theArtist & "の" & theTitle & "です") as Unicode text
    end tell

    tell application "monzai"
        speak controller 1 moji msg
    end tell
end update

update() ハンドラだけを持っています。これをアプリケーション形式で保存します。もう一つ、ダイアログで曲名を表示する監視オブジェクトを作ります。

Script Editor で開く

on update(theObject)
    set theTrack to getState() of theObject
    if theTrack is missing value then return

    tell application "iTunes"
        set theArtist to artist of theTrack
        set theTitle to name of theTrack

        set msg to (quote & theTitle & quote & return & return & theArtist) as Unicode text
    end tell

    tell application (path to frontmost application as Unicode text)
        activate
        display dialog msg buttons {"OK"} default button 1 giving up after 2 with icon 1 with title "Track Info"
    end tell
end update

こちらもアプリケーションで保存します。

最後にクライアントとなるべきスクリプトを作成します。監視対象と監視オブジェクトの操作はここで行います。

Script Editor で開く

on run
    tell application (path to frontmost application as Unicode text)
        activate
        display dialog "Add or Remove?" buttons {"Cancel", "Remove", "Add"} default button 3
        set theButton to button returned of result
    end tell

    set theApp to choose file of type {"APPL"} without invisibles
    set theInfo to info for theApp
    if (file creator of theInfo) is "aplt" then
        if theButton is "Add" then
            tell application "iTunesChecker" to addObserver(theApp as string)
        else
            tell application "iTunesChecker" to deleteObserver(theApp as string)
        end if
    end if
end run

実行し、先ほど保存した Monzai での読み上げアプリケーションか、ダイアログを表示するスクリプトを選択します。追加するとそのアプリケーションに通知が送られます。ここでは、監視オブジェクトのファイルパスを監視対象に渡しています。パスではなく、アプリケーションを渡せばいいのでは?と思います。先に『監視者を追加するハンドラと通知を送るハンドラは変更しています。この部分は後で説明します』と書きましたが、アプリケーションやスクリプトオブジェクトを監視対象に登録するのは問題があるのです。

なにが問題かといいますと...例えば、アプリケーションを登録するようにしておきます。

addObserver(application "Log")

アプリケーションを登録してから iTunesChecker を終了します。終了後もスクリプトオブジェクトの属性は保持されます。次回起動時も登録されたままなので面倒がありませんが、削除ができないのです。

deleteObserver(application "Log")

これが実行できない。なぜかというと、最初に登録したアプリケーションオブジェクトと削除しようとしているアプリケーションオブジェクトが別のものと判断されるからです。削除するには同じオブジェクトでないといけないのですが、iTunesChecker に渡して終了した時点で iTunesChecker で別のオブジェクトとして保存されるのです(同じスクリプトオブジェクト、同じアプリケーションオブジェクトと判定できるルーティンを組めばいいのですが、そうなると余分な処理に時間がかかる)。

AppleScript のスクリプトオブジェクトは、スクリプトの中に一つしか存在できません。copy 命令や load script 命令で同じスクリプトオブジェクトを複製、生成したとしてもオリジナルとは異なるオブジェクトになります。オブジェクトの独自性が保証されているのですが、同じものを渡すことができないような状況では困ることがあります。

だから、アプリケーションのパスを渡しているのです。これなら、同じ文字列なら同じ文字列と判定されるので。が、そのためにアプリケーションの場所を移動できないという制限が加わります(alias でも構わないのですが)。

あちらを立てればこちらが立たず...AppleScript ではこれが限界かな...?

デザインパターンを用いると必然的にコード量が増えるのですが、ここまでする必要があるのか?という疑問があると思います。もちろん、作るスクリプトによりけりなのですが、上記のスクリプトを一つにまとめてみることを考えるとメリットとデメリットが分かると思います。

iTunes で曲が変わったときに曲名を使う人の好みによりダイアログか声で知らせる。...面倒そうに思えます。

Observer パターンを使った方は、機能を追加したいときにその部分だけを作ればいいようになっています。iTunesChecker もクライアントも修正する必要がありません。update() ハンドラを持ったスクリプトアプリケーションを作るだけです。いい面、悪い面ありますが、こういう AppleScript もいいかもしれません。

AppleScript とデザインパターン (2)

「なんで iTunes で曲が変わったときに教えてくれないんだ?」

「iPod がマウントされたときに自動的にテキストを転送したいんだけど...」

「外付けの HDD をマウントしたときに勝手にバックアップしてくれよ」

「HTML を編集・保存したときにブラウザで勝手に再読み込みして欲しいんだけど」

「新着メールが来たら教えて欲しい」

...人間というのは結構わがままな生き物ですね。iTunes といえば、新しい iTunes 7。track クラスに動画関連の属性が追加されていますね。それ以外で大きな変更は見当たらず...つまり、今までのスクリプトはほとんどそのままで使えるということです。多分。

いや、iTunes 7 のことではなくて。先に上げたような要望を満たすには、idle ハンドラを使えばいいでしょう。例えば、iTunes で次の曲になったときに動作するようなスクリプトなら、以下のような感じ。

Script Editor で開く

property currentTrack : ""

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        tell application (path to frontmost application as Unicode text)
            activate
            display dialog tmp buttons {"OK"} default button 1 giving up after 2 with icon 1
        end tell

        set currentTrack to tmp
    end if

    return 1
end idle

これで曲が変わったときだけ教えてれますね。Dock に表示されて鬱陶しいというなら、スクリプトをアプリケーションバンドル形式で保存してプロパティリストをごにょごにょ...とすればいいでしょう。

AppleScript には「あるオブジェクトの状態が更新されたらそれを通知する」という機能がありません。まぁ、スクリプト言語なんだからそこまで必要ありませんが。あることが起きたかどうかを監視したいというとき、上記のようなスクリプトを作り、idle ハンドラで目的を達成するというのが基本的な方法ではないでしょうか。

と、ここまでくればなんのデザインパターンを取り上げようとしているのか分かるのではないでしょうか。あるオブジェクトの状態を監視し、更新されたことを通知してもらう Observer パターンです。

ほとんどの場合、上記のようなスクリプトで問題はなく、わざわざデザインパターンを用いることもないのですが...「AppleScript でこういうこともできるんだ」というぐらいに捉えていただければさいわいです。話のネタってやつです。

Observer パターンを簡単に説明。監視するオブジェクトと監視されるオブジェクトがあります。先のスクリプトを監視側と監視される側に分けてみましょう。監視されるオブジェクトは iTunes で曲が変わったかどうかを調べ、それを自分の属性(currentTrack 属性)として持ちます。

監視するオブジェクトは、監視対象のオブジェクトの状態を監視しています。ここでは、iTunes の曲が変わり、属性が更新されたかどうかです。

曲が変わり、currentTrack 属性が更新された時、監視されているオブジェクトは監視しているオブジェクトに更新があったことだけを伝えます。監視しているオブジェクトは通知を受け取るとなんらかの処理(ここではダイアログを表示しています)を実行します。

監視されているオブジェクトはデータを持っています。監視するオブジェクトはなんらかの処理を行います。Observer パターンでは、データと処理が分離され、お互いのことを詳しく知る必要がありません。このため、処理の追加と既存のコードの修正が容易になります。

先のスクリプトを監視と監視される側に分離してみます。まず、監視するスクリプト。

Script Editor で開く

on update(theObject)
    set theDate to getState() of theObject
    tell application (path to frontmost application as Unicode text)
        activate
        display dialog theDate buttons {"OK"} default button 1 giving up after 2 with icon 1
    end tell
end update

このスクリプトを Observer という名前でアプリケーション形式で保存します。update() ハンドラが監視しているオブジェクトの通知を受け取るハンドラで、監視しているオブジェクトを引数にとります。そして、監視しているオブジェクトの getState() ハンドラを使ってオブジェクトの状態を取り出します。

監視されるオブジェクトは、以下のようになります。

Script Editor で開く

property currentTrack : ""
property observerObject : application "Observer"

on getState()
    return currentTrack of me
end getState

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        set currentTrack to tmp
        ignoring application responses
            tell observerObject to update(a reference to me)
        end ignoring
    end if

    return 1
end idle

このスクリプトを Observable という名前で「実行後、自動的に終了しない」にチェックを入れ、アプリケーション形式で保存します。監視されるオブジェクトでは、監視しているオブジェクトに通知を送るために監視するオブジェクトを属性として持っています。また、監視しているオブジェクトが状態を取得できるように getState() ハンドラを定義しています。

このスクリプトを起動します。iTunes の曲が変わると Observer として保存したスクリプトアプリケーションに自分の参照を引数にして通知を送ります。Observer は通知を受け取ると Observable の状態を取得し、ダイアログを表示します。

...おお。頭の中だけで考えていたことだけど、ちゃんと動きますね。

このようにすることでなにが嬉しいか。そこが問題です。監視される方のオブジェクトは複数から監視されることがあります。今のサンプルでは監視側は一つです。複数の監視オブジェクトを保持するように変更しましょう。まず、監視するオブジェクトを作ります。

Script Editor で開く

on update(theObject)
    beep
end update

なんのことはなく、ビープするだけです。これを Beeper としてアプリケーション形式で保存します。観察されるオブジェクトは以下のように変更します。

Script Editor で開く

property currentTrack : ""
property observerObject : {application "Observer", application "Beeper"}

on getState()
    return currentTrack of me
end getState

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        set currentTrack to tmp
        repeat with thisObserver in observerObject
            ignoring application responses
                tell thisObserver to update(a reference to me)
            end ignoring
        end repeat
    end if

    return 1
end idle

複数の監視オブジェクトを持つように属性を変更し、繰り返しで通知を送っているだけです。監視側の処理を待たずに先に進むようにしているのでそれほ処理に手間取ることはないと思います。監視側も監視しているオブジェクトのことを気にすることなく勝手に動作しているので監視されているオブジェクトの idle ハンドラに影響が及ぶことはありません。これは、監視するオブジェクトを増やしているのですが、同時に機能を追加していることにもなります。

iTunes の状態を監視しているオブジェクトが監視されているオブジェクトで、そいつを監視するオブジェクトがいるというなんとも奇妙な関係ですが、iTunes で再生している曲が変わったときに教えてくれるようにハンドラを追加することができないので仕方がないです。本当は、iTunes にハンドラを追加する方がスマートな感じなのですが。しかも、観察されているオブジェクトが常時起動していて監視しているオブジェクトが変更時に呼び出されるだけですし。

今のままではダイアログが表示されるのでキーを打っているときに鬱陶しいのですが、Monzai や say コマンドを利用して発話させるようにすればもう少し使い勝手もあがるでしょう。

Script Editor で開く

on update(theObject)
    set theDate to getState() of theObject
    tell application "monzai"
        speak controller 1 moji theDate
    end tell
end update

さて。Observer は以上のような感じなのですが...思ったより長くなったので続きは、また今度。

AppleScript とデザインパターン (1)

PHP Hacks』という書籍があります。個人的にはとても楽しめた書籍でした。ソースコードは PHP 特有の分かりにくさがありますが。

PHP 特有の分かりにくさというのは、HTML の中の PHP(PHP が分かりにくいのではありません)。 という行が頻出するけど、これはだいぶ前にあった if の閉じ括弧なのか、if よりさらに前にあった for の閉じ括弧なのか...とかなり悩む。実際に書いてみても分からない。シンタックスカラーリング機能のあるエディタで書いてみてもこういうコードをまともに色付けしてくれなかったりする。

PHP と HTML は分けて欲しかった...。そう思うけど、そうするとページ数が足りなくなるか。

しかし、面白かった。iPhoto のライブラリにアクセスとか PSP の活用とか Jabberlivedoor Weather Hacks との組み合わせとか。ぱっと見て気を引く内容のものが多かった。Mac OS X になってなにが良かったかというと、PHP でも Perl でも C 言語でも Windows を対象に書かれている書籍でも十分役に立つということでしょうか(Windows の機能に依存しているものは駄目ですが)。『PHP Hacks』自体が Linux、Mac OS X、Windows を対象にしているし。

そして、この書籍の中に PHP でデザインパターンを使ってみよう、という章がありました。『オブジェクト指向における再利用のためのデザインパターン』です。23 種類全てのデザインパターンを試しているわけではないのですが、なかなか興味深かったです。

こういう刺激を受ける書籍を読んでいると、やりたくなるんですよね。AppleScript ではどうだろうか?と。

そんなわけで、AppleScript + デザインパターンにハマっていました。「だから、更新がなかったのか」と思ったあなた。鋭いです。

AppleScript でデザインパターン...。無理があると言われればそうなのですが、『オブジェクト指向における再利用のためのデザインパターン』がオブジェクト指向で汎用的に利用できるプログラム設計の方法論といった感じのもの(つまり、オブジェクト指向ならプログラミング言語を問わない)なので AppleScript でもスクリプトオブジェクトを使って恩恵を蒙ることはできます。Template Method や Factory、Strategy などは、使っていてとても気持ちいいです。特に同じようなことを異なるアプリケーションで実現することが多い(もしくは、同じようなコードを使い回すことが多い)AppleScript ではデザインパターンを使うメリットはあるのではないか?とも思いました。

ま、しかし。こういうことは実際に試してみないことにはなにも分かりません。ということで、唐突ですが、AppleScript でデザインパターン始めます。

スクリプト内のパスワード

数日前に livedoor Reader未読件数取得と自動ログインのスクリプトを紹介しました。そのスクリプトについて Travellers Tale の Hiro さんから掲示板の方に情報をいただきました。以下に引用させていただきます。

「(前略)...property でパスワード設定するのが嫌な場合に do shell script で security コマンドを叩いてやる...(後略)」

この引用における Hiro さんの情報の要点は、

  1. security というコマンドがある。
  2. property でパスワードを設定することの危うさ

です。

security というコマンド、教えていただくまで知りませんでした。Apple の Developer サイトにあるマニュアルで調べてみると、Mac OS X 10.3 の頃からあったようです。ざっと見てみると、キーチェーンと Security.framework に関わる操作が行えるようです。

Security.framework の方は AppleScript から利用する方法は用意されていませんが、キーチェーンの方は Keychain Scripting.app から利用することができます。しかし、以前にも書いたと思うのですが、Keychain Scripting.app はバグがあって容易に利用できない(バグが修正されているかどうかは未確認)。security コマンドは、代替として利用できますね。

そして、スクリプトの中にパスワードをそのまま書き込むことについて。個人的にはそのリスクについて分かっているつもりなのですが、なにしろ設定が簡単という利便性に負けて、よくこのような方法を用いています。

このようにスクリプト中にパスワードを記述することの問題点は、

  1. パスワードが平文
  2. スクリプトを開くと一目瞭然
  3. load script、store script で書き換え可能
  4. コマンドラインの string コマンドでごにょごにょ...

と、まぁ、いろいろあります。個人で使うだけなのだからそれほど神経質になることもない、と言えるかもしれませんが、近頃多発している情報流出は決して他人事ではないですし、Mac ユーザーだから大丈夫と軽々に言えるものでもないので、神経質になるぐらいがちょうどいいのかもしれません。

AppleScript でどこまで神経質になるかケースバイケースですが、スクリプトの中にパスワードを埋め込まない、というようにスクリプトを改変してみましょう。

ところで。最近 Web アプリケーションのセキュリティや脆弱性について調査しています。セキュリティ対策を行うには、どのような攻撃方法があるかを知る必要があります。敵を知り、己を知らば...ですね。知っている攻撃方法もあれば、初めて知るようなものもある。しかし、どの攻撃方法も知恵を絞っているのが分かる。それだけに目的はどうあれ面白い。実際に試してみたくなる(おい)。

Web アプリケーションのサンプルを掲載しているサイトの全てがセキュリティ対策を行っているとも思えない。簡単なサンプルなら、なおさら手を抜いている可能性がある。そういったサイトで手当たり次第試してみる、というのも一興...無論、冗談ですが。

こういうことって書かない方がいいのかな?先導、誘導、煽っているみたいに読める?

自分自身への警告、そういうサイト(があればそういうサイト)への警鐘といったつもりなのですが。わざわざこのような一文を入れなければいけないのが Web で情報を発信する際の面倒臭さと思わないでもない。

閑話休題。

まず、スクリプトを変更する前に行うことがあります。/Applications/Utilities/Keychain Access.app で、インターネットパスワードを作成します。もちろん、この作成も security コマンドや Keychain Scripting.app で行えるのですが、割愛。

Keychain Access.app では、パスワードなどをキーチェーンで管理します。おそらく「ログイン」と「システム」というキーチェーンが既にあると思います。「ログイン」キーチェーンではログインしているユーザーが利用しているパスワードなどを管理しています。「システム」キーチェーンでは管理者や OS が利用するパスワード項目が管理されています。

今から作るインターネットパスワードは、「ログイン」キーチェーンの方に作ります。「ログイン」キーチェーンはログインした時点でロックが解除されています。「ログイン」キーチェーンを選択し、「ファイル」メニューの「新規パスワード項目...」をクリック。パスワードの情報を入力するシートが表示されます。

シートの「キーチェーン項目名:」に URL(この場合は「http://reader.livedoor.com/reader/」)を入力します。その下の「アカウント名:」に livedoor Reader にログインするときの ID を。「パスワード:」の部分に livedoor Reader にログインするときのパスワードを入力し、間違いがないかを確認し「追加」ボタンをクリック。これで livedoor Reader のインターネットパスワードが作成できました。「ログイン」キーチェーンの中に「reader.livedoor.com」という名前のインターネットパスワードが追加されています。

Terminal で security コマンドを使って確認してみましょう。Terminal を起動して次のコマンドを入力します。

security find-internet-password -g -r http -s reader.livedoor.com

-r オプションは、プロトコルの指定です。この場合は、http になります。-s オプションがサーバーの指定になります。今回は、「reader.livedoor.com」です。

-g オプションは、パスワードを出力するかどうかの指定です。このオプションをつけて実行すると、「キーチェーンへのアクセス確認」というダイアログが表示されると思います。これで正常です。拒否すると処理自体が拒否されます。「常に許可」を選択すると Keychain Access.app で作ったインターネットパスワードの「アクセス制御」に security コマンドが追加されます。以降、このダイアログは表示されなくなります。これは、Keychain Access.app で作成したインターネットパスワードをダブルクリックすると表示されるウィンドウで編集できます。もし、「常に許可」をやめたいのであれば、ここで security コマンドを外してください。

先のコマンドを実行するとインターネットパスワードの情報が表示され、最後にパスワードも出力されます。Terminal でなら、この出力を加工すればいいでしょう。が、AppleScript の do shell script 命令で先のコマンドを実行してもパスワードは出力されません。

これは、パスワードが標準エラー出力に出力されているからです。ですので AppleScript からパスワードを取得するなら security コマンドの標準出力と標準エラー出力をリダイレクトして出力するようにします。以下のような感じです。

do shell script "security find-internet-password -g -r http -s reader.livedoor.com 2>&1"

これでパスワードとともに livedoor Reader のログインに関する情報も取得できました。ここで利用した以外にも security コマンドにはいろいろと機能があるので、一度マニュアルを見ておくといいと思います。

出力された結果を見ると分かるように、なかなかごちゃごちゃしています。この結果からパスワードとログイン ID を取り出します。

Script Editor で開く

on run
    set theResult to do shell script "security find-internet-password -g -r http -s reader.livedoor.com 2>&1"

    repeat with thisLine in paragraphs of theResult
        if thisLine contains "password" then
            set livedoorPassword to my getValue(contents of thisLine, ":")
        else if thisLine contains "acct" then
            set livedoorID to my getValue(contents of thisLine, "=")
        end if
    end repeat

    {livedoorID, livedoorPassword}
end run

on getValue(str, offsetStr)
    set n to offset of offsetStr in str
    set str to text (n + 1) thru -1 of str
    set AppleScript's text item delimiters to ASCII character 34
    set theList to text items of str
    set AppleScript's text item delimiters to {""}
    return (theList as Unicode text)
end getValue

これで取り出せますので、後は以前のログインスクリプトと組み合わせて完成。

Script Editor で開く

property readerURL : "http://reader.livedoor.com/reader/"
property notifyAPI : "http://rpc.reader.livedoor.com/notify?user="

on run
    set {livedoorID, livedoorPassword} to my userInfo()
    set itemNum to my unreadedCount(livedoorID)

    try
        tell application (path to frontmost application as Unicode text)
            display dialog "Unreaded Items : " & itemNum with icon 1 buttons {"Cancel", "Login"} default button 2
            my login(livedoorID, livedoorPassword)
        end tell
    on error
        return
    end try
end run

on unreadedCount(userID)
    set theResult to do shell script "curl " & (notifyAPI & userID)
    set the AppleScript's text item delimiters to "|"
    set theResult to every text item of theResult
    set the AppleScript's text item delimiters to {""}
    return (item 2 of theResult)
end unreadedCount

on login(userID, userPassword)
    tell application "Safari"
        activate
        make new document with properties {URL:readerURL}
        set bool to my pageLoaded(10)
        if bool then
            do JavaScript ("document.forms[0].livedoor_id.value = \"" & userID & "\"") in document 1
            do JavaScript ("document.forms[0].password.value = \"" & userPassword & "\"") in document 1
            do JavaScript "document.forms[0].submit()" in document 1
        else
            display dialog "Connection failure." with icon 0 buttons {"OK"} default button 1
        end if
    end tell
end login

on pageLoaded(timeoutValue)
    set num to (time of (current date)) + timeoutValue
    delay 1
    repeat
        tell application "Safari"
            if (do JavaScript "document.readyState" in document 1) is "complete" then
                return true
            end if
        end tell
        if (time of (current date)) is greater than num then exit repeat
    end repeat
    return false
end pageLoaded

on userInfo()
    set theResult to do shell script "security find-internet-password -g -r http -s reader.livedoor.com 2>&1"

    repeat with thisLine in paragraphs of theResult
        if thisLine contains "password" then
            set userPassword to my getValue(contents of thisLine, ":")
        else if thisLine contains "acct" then
            set userID to my getValue(contents of thisLine, "=")
        end if
    end repeat

    return {userID, userPassword}
end userInfo

on getValue(str, offsetStr)
    set n to offset of offsetStr in str
    set str to text (n + 1) thru -1 of str
    set AppleScript's text item delimiters to ASCII character 34
    set theList to text items of str
    set AppleScript's text item delimiters to {""}
    set value to theList as Unicode text
    if value contains space then
        set AppleScript's text item delimiters to space
        set theList to text items of value
        set AppleScript's text item delimiters to {""}
        set value to theList as Unicode text
    end if
    return value
end getValue

いろいろな部分で手を抜いていますが...。getValue() ハンドラを少し変更しています。スペースが含まれていると無条件で排除します。なので ID とパスワードにスペースが含まれていると困ることになるかも(そんな人いないですよね!?)。

どうせなら

すいません。更新できていませんね。

さて、最近は RSS リーダーに livedoor Reader を利用しています。Safari もいいのですが、未読数が表示されてしまうとなんとなく精神的によろしくなくて...。

livedoor Reader は、ブラウザで操作するいわゆる Web アプリケーションです。未読の件数を表示する Dashboard のウィジェットも配布されていたりして、Mac OS のことも考慮に入れられておりなかなかいい感じです。

で、今まではこのウィジェットを使って未読を確認し、ウィジェットから livedoor Reader のログインページに移動したりしていたのですが、どうせなら、ログインもついでに行ってくれるといいなと思ったのです。

こういうときこそ AppleScript。未読数を表示、(そのままログインしたいなら)livedoor Reader のページに移動し、ログインを行うスクリプトを作ってみました。

Script Editor で開く

property readerURL : "http://reader.livedoor.com/reader/"
property notifyAPI : "http://rpc.reader.livedoor.com/notify?user="
property livedoorID : "your livedoor ID is here."
property livedoorPassword : "your livedoor login password is here."

on run
    set itemNum to my unreadedCount()

    try
        tell application (path to frontmost application as Unicode text)
            display dialog "Unreaded Items : " & itemNum with icon 1 buttons {"Cancel", "Login"} default button 2
            my login()
        end tell
    on error
        return
    end try
end run

on unreadedCount()
    set theResult to do shell script "curl " & (notifyAPI & livedoorID)
    set the AppleScript's text item delimiters to "|"
    set theResult to every text item of theResult
    set the AppleScript's text item delimiters to {""}
    return (item 2 of theResult)
end unreadedCount

on login()
    tell application "Safari"
        activate
        make new document with properties {URL:readerURL}
        set bool to my pageLoaded(10)
        if bool then
            do JavaScript ("document.forms[0].livedoor_id.value = \"" & livedoorID & "\"") in document 1
            do JavaScript ("document.forms[0].password.value = \"" & livedoorPassword & "\"") in document 1
            do JavaScript "document.forms[0].submit()" in document 1
        else
            display dialog "Connection failure." with icon 0 buttons {"OK"} default button 1
        end if
    end tell
end login

on pageLoaded(timeoutValue)
    set num to (time of (current date)) + timeoutValue
    delay 1
    repeat
        tell application "Safari"
            if (do JavaScript "document.readyState" in document 1) is "complete" then
                return true
            end if
        end tell
        if (time of (current date)) is greater than num then exit repeat
    end repeat
    return false
end pageLoaded

プロパティの livedoorID と livedoorPassword を書き換えてご利用ください。JavaScript と curl を使えば、たいていのことはなんでもできちゃいそうな気がしないでもないです。

ちなみに、livedoor Reader では RSS を登録している人の数が表示されるのですが、「ちゃらんぽらん」は 8 人でした(多分)。登録している方々、毎度ごひいきにしていただきありがとうございます。

Amazon Web サービス

Livedoor の次は Amazon かよ...って所ですが。既に気づいた方もいらっしゃるかもしれませんが、このサイトの右側の下の方に Amazon の商品を表示しています。すっかり、アフィリエイトづいています。

これ、今読んでいたりする本や聴いている CD などの関連商品です。Amazon Web サービスでは関連商品を調べることができるのですね。ここに表示される商品を見ると、なにに興味を持っているかが分かるっていうスンポー。

Livedoor の天気も Amazon を使った関連商品の表示も PHP で作っているのですが、一番手こずった部分は、実は JavaScript の部分ということは秘密。

REST で XML が返ってくるという部分は Livedoor お天気と同じなので AppleScript でも処理を行うことができます。ということで、今聴いている iTunes の曲のアーティストを使って Amazon で検索してみるスクリプトなんかを。

Script Editor で開く

property tmpFolder : path to temporary items folder from user domain
property tmpFile : POSIX path of ((tmpFolder as Unicode text) & "request.xml")

property LF : ASCII character 10

property baseurl : "http://webservices.amazon.co.jp/onca/xml"
property service : "AWSECommerceService"
property accesskey : "your access key is here." -- ここにアクセスキー
property associateid : "your associate id is here." -- ここにアソシエイト ID
property operation : "ItemSearch"
property SearchIndex : "Music"
property responsegroup : "Request,Small,Images"
property AWSVersion : "2006-05-17"
property pagenumber : "1"
property sort : "salesrank"

tell application "iTunes"
    set theArtist to artist of current track
end tell

set encodedText to my encodeURL(theArtist)

set keywords to encodedText

set request to baseurl
set request to request & "?Service=" & service
set request to request & "&AWSAccessKeyId=" & accesskey
set request to request & "&AssociateTag=" & associateid
set request to request & "&Operation=" & operation
set request to request & "&Keywords=" & keywords
set request to request & "&SearchIndex=" & SearchIndex
set request to request & "&ResponseGroup=" & responsegroup
set request to request & "&Page=" & pagenumber
set request to request & "&Version=" & AWSVersion
set request to request & "&Sort=" & sort

do shell script "curl -o " & quoted form of tmpFile & " " & quoted form of request

tell application "System Events"
    set xmlFile to XML file tmpFile

    set root to XML element 1 of xmlFile
    set itemsElement to XML element "Items" of root
    set html to ""
    repeat with thisElement in (XML elements of itemsElement whose name is "Item")
        set theURL to value of XML element "DetailPageURL" of thisElement
        set itemTitle to value of XML element "Title" of XML element "ItemAttributes" of thisElement
        set html to html & "<a href=\"" & theURL & "\">" & itemTitle & "</a>" & LF
    end repeat
end tell

html

on encodeURL(theText)
    if not ((theText starts with "'") and (theText ends with "'")) then
        set theText to quoted form of theText
    end if

    try
        do shell script "echo " & theText & " | " & "php -r 'echo rawurlencode(`cat -`);'"
    on error eMessage number eNumber
        tell application (path to frontmost application as Unicode text)
            activate
            display dialog (eNumber & return & eMessage & return & return) as Unicode text buttons {"OK"} default button 1 with icon 0
        end tell
        return ""
    end try
end encodeURL

検索結果を使って商品へのリンクタグを作っています。実際に利用するには Amazon でデベロッパー登録をしてアクセスキーをもらう必要があります。アフィリエイト ID は特に必要ないですが、あると検索結果の商品の URL にアフィリエイト ID が埋め込まれます。

検索結果がなかったときと Amazon でエラーが起きた時の処理は行っていませんので、その辺りご注意を。

天気予報

気がつけば、6 月も半ばを過ぎている...。ちょっとした驚きですね。

6 月に入った頃から、ADSL モデムの調子が悪くてネットに接続できないようになっていました。これは、モデムの故障で交換したらなおったのですが。こういったこともあったのですが、少し前から Web アプリケーションのことを調べていてそれに時間を取られていました。って、今も取られているのだけど。

Web アプリケーション、Web API、Web サービス...といった関連ですね。マッシュアップなんてものが流行っている昨今、私も流行に感化されました。

しかし、ネットに接続したいというときにモデムが故障して...。そうなれば、Ajax や Web サービスといったってネットにつなげられなきゃ、役立たずというごく当然の結論。これは、なかなか興味深い発見でした。

...いま、カーネルパニックが起きました。おい。なんか、調子悪いな...。

なんとなく理解(こういう中途半端が一番危ないと思うのですが)したので手頃な Web API がないかなと探していたところ、思い出した。livedoorWeather Hacks というのを公開していました。公開されてすぐに oomori.com さんがこのサービスを使った LiveOtenki という Cocoa アプリケーションを配布していましたが、これが手頃です。

仕様を読んでみる...。REST で XML が返ってくるんだな。よし、まずは、AppleScript で検証してみよう。

Script Editor で開く

property baseurl : "http://weather.livedoor.com/forecast/webservice/rest/v1"
property areaList : {"北海道地方", "東北地方", "関東地方", "信越・北陸地方", "東海地方", "近畿地方", "中国地方", "四国地方", "九州地方", "南西諸島地方"}
property prefList : {{"道北", "道央", "道東", "道南"}, {"青森県", "秋田県", "岩手県", "宮城県", "山形県", "福島県"}, {"茨城県", "栃木県", "群馬県", "埼玉県", "東京都", "千葉県", "神奈川県", "山梨県"}, {"富山県", "石川県", "福井県", "新潟県", "長野県"}, {"静岡県", "愛知県", "岐阜県", "三重県"}, {"滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県"}, {"岡山県", "広島県", "島根県", "鳥取県", "山口県"}, {"徳島県", "香川県", "愛媛県", "高知県"}, {"福岡県", "大分県", "長崎県", "佐賀県", "熊本県", "宮崎県", "鹿児島県"}, {"沖縄県"}}
property cityList : {{{"稚内", "旭川", "留萌"}, {"札幌", "岩見沢", "倶知安"}, {"網走", "北見", "紋別", "根室", "釧路", "帯広"}, {"室蘭", "浦河", "函館", "江差"}}, {{"青森", "むつ", "八戸"}, {"秋田", "横手"}, {"盛岡", "宮古", "大船渡"}, {"仙台", "白石"}, {"山形", "米沢", "酒田", "新庄"}, {"福島", "小名浜", "若松"}}, {{"水戸", "土浦"}, {"宇都宮", "大田原"}, {"前橋", "みなかみ"}, {"さいたま", "熊谷", "秩父"}, {"東京", "大島", "八丈島", "父島"}, {"千葉", "銚子", "館山"}, {"横浜", "小田原"}, {"甲府", "河口湖"}}, {{"富山", "伏木"}, {"金沢", "輪島"}, {"福井", "敦賀"}, {"新潟", "長岡", "高田", "相川"}, {"長野", "松本", "飯田"}}, {{"静岡", "網代", "三島", "浜松"}, {"名古屋", "豊橋"}, {"岐阜", "高山"}, {"津", "尾鷲"}}, {{"大津", "彦根"}, {"京都", "舞鶴"}, {"大阪"}, {"神戸", "豊岡"}, {"奈良", "風屋"}, {"和歌山", "潮岬"}}, {{"岡山", "津山"}, {"広島", "庄原"}, {"松江", "浜田", "西郷"}, {"鳥取", "米子"}, {"下関", "山口", "柳井", "萩"}}, {{"徳島", "日和佐"}, "高松", {"松山", "新居浜", "宇和島"}, {"高知", "室戸", "清水"}}, {{"福岡", "八幡", "飯塚", "久留米"}, {"大分", "中津", "日田", "佐伯"}, {"長崎", "佐世保", "厳原", "福江"}, {"佐賀", "伊万里"}, {"熊本", "阿蘇乙姫", "牛深", "人吉"}, {"宮崎", "延岡", "都城", "高千穂"}, {"鹿児島", "鹿屋", "種子島", "名瀬"}}, {{"那覇", "名護", "久米島", "南大東島", "宮古島", "石垣島", "与那国島"}}}
property cityIDList : {{{"1", "2", "3"}, {"4", "5", "6"}, {"7", "8", "9", "10", "11", "12"}, {"13", "14", "15", "16"}}, {{"17", "18", "19"}, {"20", "21"}, {"22", "23", "24"}, {"25", "26"}, {"27", "28", "29", "30"}, {"31", "32", "33"}}, {{"54", "55"}, {"56", "57"}, {"58", "59"}, {"60", "61", "62"}, {"63", "64", "65", "66"}, {"67", "68", "69"}, {"70", "71"}, {"75", "76"}}, {{"44", "45"}, {"46", "47"}, {"48", "49"}, {"50", "51", "52", "53"}, {"72", "73", "74"}}, {{"34", "35", "36", "37"}, {"38", "39"}, {"40", "41"}, {"42", "43"}}, {{"77", "78"}, {"79", "80"}, {"81"}, {"82", "83"}, {"84", "85"}, {"86", "87"}}, {{"88", "89"}, {"90", "91"}, {"92", "93", "94"}, {"95", "96"}, {"97", "98", "99", "100"}}, {{"101", "102"}, {"103"}, {"104", "105", "106"}, {"107", "108", "109"}}, {{"110", "111", "112", "113"}, {"114", "115", "116", "117"}, {"118", "119", "120", "121"}, {"122", "123"}, {"124", "125", "126", "127"}, {"128", "129", "130", "131"}, {"132", "133", "134", "135"}}, {{"136", "137", "138", "139", "140", "141", "142"}}}

on run
    set theResult to chooseIt(areaList, item 1 of areaList)
    if theResult is false then return
    repeat with i from 1 to count areaList
        if item i of areaList is theResult then exit repeat
    end repeat

    set areaNum to i
    set thisPref to a reference to item areaNum of prefList
    set theResult to chooseIt(thisPref, item 1 of thisPref)
    if theResult is false then return
    repeat with i from 1 to count thisPref
        if item i of thisPref is theResult then exit repeat
    end repeat

    set prefNum to i
    set thisCity to a reference to item prefNum of item areaNum of cityList
    set theResult to chooseIt(thisCity, item 1 of thisCity)
    if theResult is false then return
    repeat with i from 1 to count thisCity
        if item i of thisCity is theResult then exit repeat
    end repeat

    set cityID to item i of item prefNum of item areaNum of cityIDList
    set tmp to path to desktop folder as Unicode text
    set tmp to POSIX path of (tmp & "tmp")
    weatherQuery("today", cityID, tmp)
    lwws((tmp as POSIX file) as Unicode text)
end run

on weatherQuery(when, cityID, theFile)
    set query to baseurl & "?city=" & cityID
    set query to query & "&day=" & when

    do shell script "curl " & quoted form of query & " > " & theFile
end weatherQuery

on chooseIt(theList, defaultItems)
    tell application (path to frontmost application as Unicode text)
        activate
        set thisItem to choose from list theList default items defaultItems
        if thisItem is false then return false
        set thisItem to thisItem as Unicode text
    end tell
end chooseIt

on lwws(theFile)
    tell application "System Events"
        set xmlFile to XML file theFile

        set root to XML element 1 of xmlFile

        set area to value of XML attribute "area" of XML element "location" of root
        set pref to value of XML attribute "pref" of XML element "location" of root
        set city to value of XML attribute "city" of XML element "location" of root

        set forecastday to value of XML element "forecastday" of root
        if forecastday is "today" then
            set forecastday to "今日"
        else if forecastday is "tomorrow" then
            set forecastday to "明日"
        else
            set forecastday to "明後日"
        end if

        set forecastdate to value of XML element "forecastdate" of root
        set publictime to value of XML element "publictime" of root
        set telop to value of XML element "telop" of root
        set desc to value of XML element "description" of root
        set temperature to XML element "temperature" of root
        set max to value of XML element "celsius" of XML element "max" of temperature as Unicode text
        if max is "" then set max to "--"
        set min to value of XML element "celsius" of XML element "min" of temperature as Unicode text
        if min is "" then set min to "--"
        set imageURL to value of XML element "url" of XML element "image" of root

        return {area, pref, city, forecastday, forecastdate, publictime, telop, desc, max, min, imageURL}
    end tell
end lwws

なるほど。このまま日記の今日の天気欄に挿入したりして使えますね。感じがつかめたところで PHP で Web 上で動くものを作ってみる。それが、これ(...なくなりました)。

うむ、天気予報が表示されたところでどうってことはないな。これだけでは面白くも何ともないということが確認できました。やっぱり、なにかと組み合わせないと...。

しかし、Web アプリケーションを作るときの問題はコーディングではなくて、インターフェイスに時間がかかるということ。これは、私の向き不向きなのでしょうが、デザインって苦手だ。HTML と CSS でああでもない、こうでもないと。簡単に済ますなら、フォームでポップアップメニューを使って市町村の名前をずらずらと並べればいいのですが、これは避けたかった。だって、150 近くの市の中から目的のものなんて探したくないですから。でも、よく使われていますね。都道府県をメニューから選んだりするやつとか。あれでも 50 近くありますね。誕生日の日の選択とか。

どうすれば、簡単に目的のものを選択できるかを考えた結果、3 段階に分けたメニューにしたのですが...こうすると、動くブラウザと動かないブラウザがでてくるんですよね。

そんなわけで、AppleScript から少し離れている今日この頃でした。