おっぱいスクリプト with AppleScript

ゆけ。リビドーの赴くままに。

...もう旬が過ぎた気もするし、三連休が終わって明日から仕事なんだろうけど AppleScript によるおっぱいスクリプトの実装を。

他の言語ではどのように実装されているか、といったことを比べながら読むと AppleScript の長所や短所がよく分かる気がします。

最大の違いはどの言語も最初からライブラリが豊富だったり、追加できたり...と。AppleScript は他の言語がライブラリを利用して実装している部分をすべて自分で作らなければだめなのです。ゆえに、コードが長い...。

なんの奇もてらっていないので、AppleScript をこれから覚えるんだ!といった中学、高校生以外にはなんの面白みもないものになっていますのであしからず。

Yahoo の検索 API を使っているので、Yahoo!デベロッパーネットワーク で登録して、アプリケーション ID を取得しておいてください。

Script Editor で開く

property APP_ID : "ここに Yahoo のアプリケーション ID を入れてちょ"
property API_URL : "http://search.yahooapis.jp/ImageSearchService/V2/imageSearch"
property SEARCH_WORD : "おっぱい"

on run
    main()
end run

on main()
    set my SEARCH_WORD to text returned of (display dialog "Enter search word:" default answer SEARCH_WORD with icon 1)
    set dataFolder to choose folder

    set pageCount to 1
    set downloadCount to 1

    repeat until pageCount is greater than 20
        set queryList to {{"appid", APP_ID}, {"query", SEARCH_WORD}, {"results", "20"}, {"adult_ok", "1"}, {"format", "jpeg"}, {"site", "tumblr.com"}, {"type", "any"}, {"start", ((pageCount - 1) * 20 + 1) as text}}

        set theURL to my makeQuery(queryList)
        set xml to my urlopen(API_URL & "?" & theURL)
        set xmlDocument to my openXML(xml)
        set root to my getFirstNode(xmlDocument)

        if my getNodeName(root) is "ResultSet" then
            repeat with thisItem in my getElementsByTagName("Result", root)
                set imageURL to my getFirstNodeTextByTagName("Url", thisItem)
                my downloadFile(imageURL, dataFolder)
                set downloadCount to downloadCount + 1
            end repeat
        end if
        set pageCount to pageCount + 1
    end repeat
end main

on encodeURL(theText)
    try
        return do shell script "python -c \"import sys,urllib; print urllib.quote(sys.argv[1])\" " & quoted form of theText
    on error mes number num
        return {num, mes}
    end try
    return theText
end encodeURL

on urlopen(thisURL)
    try
        set theResult to do shell script "curl " & quoted form of thisURL
    on error msg number num
        set theResult to {msg, num}
    end try

    return theResult
end urlopen

on downloadFile(fileURL, downloadFolder)
    do shell script "cd " & quoted form of (POSIX path of downloadFolder) & ";curl -O -L " & quoted form of fileURL
end downloadFile

on makeQuery(queryList)
    set theList to {}
    repeat with thisItem in queryList
        set thisItem to {item 1 of thisItem, my encodeURL(item 2 of thisItem)}
        set end of theList to my join(thisItem, "=")
    end repeat

    return my join(theList, "&")
end makeQuery

on join(theList, delimiter)
    tell (a reference to text item delimiters)
        set {tid, contents} to {contents, delimiter}
        set {theText, contents} to {theList as text, tid}
    end tell

    return theText
end join

on openXML(theFile)
    tell application "System Events"
        delete every XML data
        set xmlDocument to ""
        try
            set xmlDocument to contents of XML file theFile
        on error msg number num
            if num is -1728 then
                if (class of theFile) is text then
                    set xmlDocument to make new XML data with properties {text:theFile}
                else
                    return {num, msg}
                end if
            else
                return {num, msg}
            end if
        end try
    end tell

    return xmlDocument
end openXML

on getFirstNode(xmlDocument)
    tell application "System Events"
        return first XML element of xmlDocument
    end tell
end getFirstNode

on getNodeName(node)
    tell application "System Events"
        return name of node
    end tell
end getNodeName

on getElementsByTagName(tagName, node)
    tell application "System Events"
        if exists (XML elements of node whose name of it is tagName) then
            return XML elements of node whose name of it is tagName
        else
            return missing value
        end if
    end tell
end getElementsByTagName

on getFirstNodeTextByTagName(tagName, node)
    tell application "System Events"
        if exists (XML elements of node whose name of it is tagName) then
            set firstNode to my getFirstNodeByTagName(tagName, node)
            set nodeText to value of firstNode
            try
                nodeText
            on error
                return ""
            end try
        else
            return ""
        end if
    end tell
end getFirstNodeTextByTagName

on getFirstNodeByTagName(tagName, node)
    tell application "System Events"
        if exists (XML elements of node whose name of it is tagName) then
            return (first item of (XML elements of node whose name of it is tagName))
        else
            return missing value
        end if
    end tell
end getFirstNodeByTagName

140 行ほどですか。長いですね。長いです。このハンドラでまとめている部分のほとんどが、他の言語ではライブラリとして用意されていたりします。ファイルのダウンロードすら現在では OSAX がなくなり、AppleScript では処理できないですからね。

健全な青少年も利用できるように検索語句は利用者が入力できるようにしています。

改良できる部分は多々あると思います。現状、リクエストに対しエラーが返ってきたら無視しているので、結果が返ってくるまでリクエストを繰り返してもいいと思います。また、画像の大きさを調べて小さいものはダウンロードしないといったことをしてもいいかも。

では、また。

AppleScript でクロージャ

クロージャがなにかってことはイマイチよく分かっていないのだけど。

Python クックブック 第2版を読んでいて、載っていたスクリプトを AppleScript で書き直したら、動いたって話なんだけど。JavaScript ほど多彩なことができるわけではないのだけど。

まぁ、どこでどのように使うのかというギモンは残るんだけどね。

AppleScript の場合、関数(ハンドラ)の中に関数(ハンドラ)は作れない。これはご存知だと思います。しかし、スクリプトオブジェクトなら作ることができます。

Script Editor で開く

on makeObject(i)
    script MyObject
        property counter : i

        on countUp()
            set counter to counter + 1
        end countUp

        on getCounter()
            my counter
        end getCounter

        on run
            countUp()
        end run
    end script
end makeObject

set theObject to makeObject(0)

tell theObject
    run
    display dialog getCounter()
    run
    display dialog getCounter()
end tell

単純な例ですが、実行するたびにカウントを行うスクリプトオブジェクトです。makeObject ハンドラ実行時に引数を渡し、それが MyObject の属性に初期値として設定されています。これはこれで特に問題はないのですが、MyObject の属性 counter は書き換え可能です。

tell theObject
    run
    display dialog getCounter() -- 1
    run
    display dialog getCounter() -- 2
    set counter of it to 100 -- 値を書き換えてみる
    run
    display dialog getCounter() -- 101
end tell

スクリプトオブジェクトの属性は読み書き可能。これは、AppleScript では当たり前のことですね。では、次のように書き換えてみましょう。

Script Editor で開く

on makeObject()
    set counter to 0
    script MyObject
        on countUp()
            set counter to counter + 1
        end countUp

        on getCounter()
            return counter
        end getCounter

        on run
            countUp()
        end run
    end script
end makeObject

set theObject to makeObject()

tell theObject
    run
    display dialog getCounter() -- 1
    run
    display dialog getCounter() -- 2
    try
        set counter of it to 100 -- 値を書き換えてみる
    on error msg number num
        error number num
    end try
    run
    display dialog getCounter() -- 3
end tell

makeObject ハンドラ実行時の引数をなくし、MyObject の属性 counter を makeObject ハンドラ内のローカル変数にしてみました。ハンドラ実行時のローカル変数がハンドラ実行後も保持され、スクリプトオブジェクト内から参照可能です。が、スクリプトオブジェクトの外から counter の値を変更することも参照することもできません。

もう少し分かりやすく書くと以下のようになります。

Script Editor で開く

on DataObject(theKey, value)
    script
        on getKey()
            return theKey
        end getKey

        on getValue()
            return value
        end getValue
    end script
end DataObject

set theData to DataObject("id", "someone")
theData's getKey() -- "id"
theData's getValue() -- "someone"

変数 value や theKey にアクセスするにはハンドラを利用する以外ありません。もちろん、グローバル変数を利用すると話は別ですが(または、スクリプトオブジェクト内に setter を用意するとか)。

と、にハンドラ内のローカル変数が保持されることは分かったのですが、これがクロージャなのかといわれるとよくわからなかったりする。以下のようにしておけば、スクリプトオブジェクトの初期化処理を複数回呼び出すことを防いだりできるかな?

Script Editor で開く

on makeObject()
    set instance to missing value
    script MyObject
        on init()
            if instance is missing value then
                (* ここに初期化処理 *)
                -- ハンドラのローカル変数に自身を割り当てる
                set instance to me
            else
                -- 初期化されてるよ
                display dialog "Initialize OK"
            end if
        end init

        on greeting()
            display dialog "Hello"
        end greeting
    end script

    init() of MyObject
    return instance
end makeObject

set x to makeObject()
init() of x
greeting() of x

init ハンドラ自体を呼び出せないようにしたいけど、そこまで求めるのは無理があったりなかったり。