Archive for the 'NSUTF8StringEncoding' Category

2017/10/13 指定クラスがどのFrameworkに所属しているか検索

文字列として与えたCocoaのClassがどのFrameworkに所属しているかを検索するAppleScriptです。Spotlight検索のためにShane Stanleyの「Metadata Lib」のインストールを必要とします。

割と切実に欲しかったAppleScriptです。よくShane Stanleyから「そのクラス呼ぶんだったら、このFrameworkをuseで宣言しとかないとダメだぞー」と小突かれているので、「自動でチェックしてえなぁ(涙)」と思っていたところでした。

use文によるFrameworkの使用宣言は、AppleScriptそれ自体で宣言していなくても、Script Editor側で使用宣言しているとそのまま動いてしまったりするパターンがあるため、割と見過ごしてしまう例がありました(Script Editor上でエラーメッセージ出ないし)。

わざわざAppleのWeb Referenceを検索したりと、本質的でない不毛な作業が必要。

そこで、Class名を文字列で与えるとどのFrameworkをuseしないといけないのかを調べる本AppleScriptを書いたわけです。

ながらく「書きたい」と思っていたわけですが、どこを手がかりにすればよいか長らくわかっていませんでした。

それが、macOS 10.13.0のScripting Bridgeのバグに直面して、Scripting Bridgeの「仕組み」そのものについて自分でも調べる機会がありました(正確に把握するため、どこがダメでどこでAppleがミスしたのか追調査)。

すると、macOS内の/System/Library/Frameworksフォルダ内にあるApple純正フレームワークの中に「(フレームワーク名).bridgesupport」という記述ファイルが存在しており、この中にScripting Bridge経由で機能を公開する内容が書いてあることが見て取れました。

macOS 10.13.0では、この.bridgesupportファイルの記述が間違っていた、というのがShane Stanleyの指摘です。

あれ??? この.bridgesupportファイルの中には当該フレームワークが他の言語に対して公開しているクラス名やメソッド名などの一覧が書かれているわけで、この中を検索すれば、目的のクラスを使うにはどのフレームワークをuseコマンドで参照するように宣言すればよいかわかる???? 

完全にもともとの用途とは違う使い道ですが、わかることはわかる(はず)。

冗談半分で書いてみたら、自分の目的を果たすことができました。1回の検索あたり0.7〜1.1秒程度です。結果をキャッシュするとか、検索するごとに毎回読むのをやめるとかすればもっと大幅に高速化はできるものと思われますが、そこまで気合いを入れる内容ではないのでこんな感じでしょうか。

ただし、例外で「CIColor」がみつかりません。ほかにも、この方法で見つけられないものがあるかもしれません。思いつきを形にしたぐらいの内容なので、ミスがあってもご容赦を。

AppleScript名:指定クラスがどのFrameworkに所属しているか検索
– Created 2017-10-13 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use mdLib : script "Metadata Lib" version "1.0.0"
–http://piyocast.com/as/archives/4894

property NSString : a reference to current application’s NSString
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding

set t1Res to searchInBridgeSupport("NSString") of me
–>  {"Foundation.framework"}

set t2Res to searchInBridgeSupport("CLLocation") of me
–>  {"MapKit.framework", "CoreLocation.framework"}

set t3Res to searchInBridgeSupport("NSRect") of me
–>  {"WebKit.framework", "Foundation.framework", "AppKit.framework", "ScreenSaver.framework", "Quartz.framework", "Quartz.framework", "Carbon.framework"}

on searchInBridgeSupport(aKeyword)
  set theFolder to "/System/Library/Frameworks/"
  
set theFiles to mdLib’s searchFolders:{theFolder} searchString:"kMDItemDisplayName ENDSWITH %@" searchArgs:{".bridgesupport"}
  
  
set keyList to {}
  
set the end of keyList to "<class name=’" & aKeyword & "’>"
  
set the end of keyList to aKeyword
  
  
repeat with ii in keyList
    set matchedList to {}
    
set targString to contents of ii
    
    
repeat with i in theFiles
      set j to contents of i
      
      
set aStr to (NSString’s stringWithContentsOfFile:j encoding:NSUTF8StringEncoding |error|:(missing value))
      
set aRange to (aStr’s rangeOfString:targString)
      
      
if aRange’s location() ≠ current application’s NSNotFound and (aRange’s location()) < 9.99999999E+8 then
        if j does not contain "PyObjC" then –ignore PyObjC’s bridge support
          set tmpStr to (current application’s NSString’s stringWithString:j)
          
set pathList to tmpStr’s pathComponents() as list
          
set pathRes to contents of item 5 of pathList
          
set the end of matchedList to pathRes
        end if
      end if
    end repeat
    
    
if length of matchedList > 0 then
      return matchedList
    end if
    
  end repeat
  
  
return {}
end searchInBridgeSupport

★Click Here to Open This Script 

2017/10/10 なろう小説APIで各カテゴリごとの集計を実行

「小説家になろう」サイトのAPI「なろう小説API」を呼び出して、カテゴリごとの該当件数を集計するAppleScriptです。

「なろう小説API」は事前にAPI Keyの取得も不要で、簡単に呼び出せるのでお手軽に使えます。

本AppleScriptは、「小説家になろう」掲載作品を、大カテゴリと小カテゴリでコードを指定して、ループで存在件数の集計を行います。カテゴリごとに分布が偏っているようなので、該当件数が0件のカテゴリは結果出力しないようにしています。筆者の環境では集計に22〜25秒ぐらいかかっています(インターネット接続回線速度に依存)。

http headerにgzip転送リクエスト要求を書きつつ、実際のデータ自体もgzipで圧縮されているので、二重に圧縮している状態です。実測したところ、http headerでgzip指定を行なったほうがトータルで1秒程度速かったので「そんなもんかな」と思いつつ、そのままにしています。

Web APIからのデータ受信時のNSDataからのZip展開にオープンソースのフレームワーク「GZIP」(By Nick Lockwood)を利用しています。同プロジェクトはGithub上のXcodeプロジェクトをXcodeでビルドするとFrameworkが得られるので、ビルドして~/Library/FrameworksフォルダにGZIP.frameworkを入れてください。

ジャンルは数値で指定するようになっていますが、その数値が何を示しているかという情報はAPI側からの出力にはないので、Webサイト上から文字情報をコピペで取得し、AppleScript内に記載して(ハードコーディング)カテゴリコードリストと照合して出力しています。

実際に集計してみると、ノンカテゴリが53%ということで、カテゴリ分けの機能が有効に活用されていないことが見てとれます。

そのことについては運営側も重々承知しているようで、APIの検索オプションに「キーワードに異世界転生があるものを含む」といったものがあるなど、ジャンルよりもキーワード重視するようにしているようです。

そういいつつも、使われているキーワードについては若干の表記ゆらぎがあるようで、単純にこのオプションを指定しても「異世界転生もの」をすべて抽出できていないように見えます。キーワード自体にどの程度「表記揺れ」が存在しているのかを調べてみるとよいのかもしれません。

APIの仕様上、2,000件しか詳細データを取得できないように見えるので、そのあたりがちょっと気になります(どうも全数調査を行いにくい仕様)。

分析するまでもなく、異世界転生ものが多く、ノンジャンル作品でも異世界転生ものばっかりという印象です。掘り出しもので「ソ連の宇宙技術は最強過ぎたのだが、それを西側諸国が完全に理解したのはつい最近だった」という作品に行き当たり、これが強烈に面白いです。

AppleScript名:なろう小説APIで各カテゴリごとの集計を実行
– Created 2017-10-10 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4891
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “GZIP”
–https://github.com/nicklockwood/GZIP
–http://dev.syosetu.com/man/api/
–1日の利用上限は80,000または転送量上限400MByte???

property |NSURL| : a reference to current application’s |NSURL|
property NSString : a reference to current application’s NSString
property NSArray : a reference to current application’s NSArray
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSURLComponents : a reference to current application’s NSURLComponents
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection
property NSSortDescriptor : a reference to current application’s NSSortDescriptor
property NSNumber : a reference to current application’s NSNumber
property NSNumberFormatter : a reference to current application’s NSNumberFormatter
property NSNumberFormatterRoundUp : a reference to current application’s NSNumberFormatterRoundUp
property NSNumberFormatterRoundDown : a reference to current application’s NSNumberFormatterRoundDown

set invList to {}

set bgList to {1, 2, 3, 4, 99, 98}
set bigGnereLabel to {“恋愛”, “ファンタジー”, “文芸”, “SF”, “その他”, “ノンジャンル”}

set gList to {101, 102, 201, 202, 301, 302, 303, 304, 305, 306, 307, 401, 402, 403, 404, 9901, 9902, 9903, 9904, 9999, 9801}
set smlGenreLabel to {“異世界〔恋愛〕”, “現実世界〔恋愛〕”, “ハイファンタジー〔ファンタジー〕”, “ローファンタジー〔ファンタジー〕”, “純文学〔文芸〕”, “ヒューマンドラマ〔文芸〕”, “歴史〔文芸〕”, “推理〔文芸〕”, “ホラー〔文芸〕”, “アクション〔文芸〕”, “コメディー〔文芸〕”, “VRゲーム〔SF〕”, “宇宙〔SF〕”, “空想科学〔SF〕”, “パニック〔SF〕”, “童話〔その他〕”, “詩〔その他〕”, “エッセイ〔その他〕”, “リプレイ〔その他〕”, “その他〔その他〕”, “ノンジャンル〔ノンジャンル〕”}

–全体の件数取得
set aRec to {gzip:“5″, out:“json”, lim:“1″}
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
set wholeCount to (allCount of first item of aRESTres)

–カテゴリごとの集計
repeat with i in bgList
  repeat with ii in gList
    set aRec to {gzip:“5″, biggenre:i as string, genre:ii as string, out:“json”, lim:“1″}
    
set aRESTres to callNarouAPI(aRec, “1″, “1″) of me
    
set aTotal to allCount of first item of aRESTres
    
    
if aTotal is not equal to 0 then
      set big to contents of i
      
set small to contents of ii
      
set bigLabel to getLabelFromNum(bgList, bigGnereLabel, big) of me
      
set smlLabel to getLabelFromNum(gList, smlGenreLabel, small) of me
      
set aPerCentatge to roundingDownNumStr(((aTotal / wholeCount) * 100), 1) of me
      
set the end of invList to {biggenre:bigLabel, genre:smlLabel, totalNum:aTotal, percentage:aPerCentatge}
    end if
  end repeat
end repeat

set bList to sortRecListByLabel(invList, “totalNum”, false) of me –降順ソート
–> {{totalNum:274072, biggenre:”ノンジャンル”, percentage:53.1, genre:”ノンジャンル〔ノンジャンル〕”}, {totalNum:47121, biggenre:”ファンタジー”, percentage:9.1, genre:”ハイファンタジー〔ファンタジー〕”}, {totalNum:28883, biggenre:”恋愛”, percentage:5.6, genre:”現実世界〔恋愛〕”}, {totalNum:23217, biggenre:”文芸”, percentage:4.5, genre:”ヒューマンドラマ〔文芸〕”}, {totalNum:21320, biggenre:”ファンタジー”, percentage:4.1, genre:”ローファンタジー〔ファンタジー〕”}, {totalNum:17079, biggenre:”恋愛”, percentage:3.3, genre:”異世界〔恋愛〕”}, {totalNum:16798, biggenre:”その他”, percentage:3.2, genre:”その他〔その他〕”}, {totalNum:13892, biggenre:”その他”, percentage:2.6, genre:”詩〔その他〕”}, {totalNum:13341, biggenre:”文芸”, percentage:2.5, genre:”コメディー〔文芸〕”}, {totalNum:10120, biggenre:”文芸”, percentage:1.9, genre:”ホラー〔文芸〕”}, {totalNum:9502, biggenre:”その他”, percentage:1.8, genre:”エッセイ〔その他〕”}, {totalNum:8486, biggenre:”文芸”, percentage:1.6, genre:”純文学〔文芸〕”}, {totalNum:7211, biggenre:”文芸”, percentage:1.3, genre:”アクション〔文芸〕”}, {totalNum:6199, biggenre:”SF”, percentage:1.2, genre:”空想科学〔SF〕”}, {totalNum:5780, biggenre:”その他”, percentage:1.1, genre:”童話〔その他〕”}, {totalNum:3295, biggenre:”文芸”, percentage:0.6, genre:”推理〔文芸〕”}, {totalNum:3217, biggenre:”文芸”, percentage:0.6, genre:”歴史〔文芸〕”}, {totalNum:2606, biggenre:”SF”, percentage:0.5, genre:”VRゲーム〔SF〕”}, {totalNum:1471, biggenre:”SF”, percentage:0.2, genre:”パニック〔SF〕”}, {totalNum:1454, biggenre:”SF”, percentage:0.2, genre:”宇宙〔SF〕”}, {totalNum:190, biggenre:”その他”, percentage:0.0, genre:”リプレイ〔その他〕”}}

on callNarouAPI(aRec, callFrom, callNum)
  set reqURLStr to “http://api.syosetu.com/novelapi/api/” –通常API
  
  
–set aRec to {gzip:”5″, |st|:callFrom as string, out:”json”, lim:callNum as string}
  
set aURL to retURLwithParams(reqURLStr, aRec) of me
  
set aRes to callRestGETAPIAndParseResults(aURL) of me
  
  
set aRESCode to (responseCode of aRes) as integer
  
if aRESCode is not equal to 200 then return false
  
  
set aRESHeader to responseHeader of aRes
  
set aRESTres to (json of aRes) as list
  
end callNarouAPI

–GET methodのREST APIを呼ぶ
on callRestGETAPIAndParseResults(aURL)
  set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:“GET”
  
aRequest’s setValue:“gzip” forHTTPHeaderField:“Content-Encoding”
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
  
set rRes to bRes’s gunzippedData() –From GZIP.framework
  
  
set resStr to NSString’s alloc()’s initWithData:rRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
end callRestGETAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

–リストに入れたレコードを、指定の属性ラベルの値でソート
on sortRecListByLabel(aRecList as list, aLabelStr as string, ascendF as boolean)
  set aArray to NSArray’s arrayWithArray:aRecList
  
  
set sortDesc to NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF
  
set sortDescArray to NSArray’s arrayWithObjects:sortDesc
  
set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray
  
  
set bList to sortedArray as list
  
return bList
end sortRecListByLabel

on getLabelFromNum(aList, labelLIst, aNum)
  set aInd to offsetOf(aList, aNum) of me
  
set anItem to contents of item aInd of labelLIst
  
return anItem
end getLabelFromNum

on offsetOf(aList as list, aTarg)
  set aArray to current application’s NSArray’s arrayWithArray:aList
  
set aIndex to aArray’s indexOfObjectIdenticalTo:aTarg
  
return (aIndex + 1)
end offsetOf

on roundingDownNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundDown)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingDownNumStr

on roundingUpNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s doubleValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundUp)
  
set aStr to aFormatter’s stringFromNumber:aa
  
return (aStr as text) as real
end roundingUpNumStr

★Click Here to Open This Script 

2017/10/07 なろう小説APIを呼び出す v2(Zip展開つき)

「小説家になろう」サイトのAPI「なろう小説API」を呼び出してデータを取得するAppleScriptです。

「なろう小説API」は事前にAPI Keyの取得も不要で、簡単に呼び出せるのでお手軽に使えます。用意されているのはデータ取得のメソッドであり、結果の保持や集計、しぼりこみについてはローカル側で勝手に行う放任仕様です条件抽出のパラメータもあります。

ただし、本APIが用意しているZip圧縮を利用するのに少々骨が折れました。

http header中でContent-Encodingに「gzip」を指定する程度では対応できませんでした。Zip圧縮転送指定時(パラメータにgzipを指定)APIから返ってくるデータそのものがZip圧縮されており、APIから返ってきたデータそのもの(NSData)を展開する必要がありました。

そこで、NSDataをそのままZip展開できるオープンソースのフレームワーク「GZIP」(By Nick Lockwood)を利用してみました。同プロジェクトはGithub上のXcodeプロジェクトをXcodeでビルドするとFrameworkが得られるので、ビルドして~/Library/FrameworksフォルダにGZIP.frameworkを入れてください。

とりあえず呼び出して500件のデータを取得していますが、500件ずつ取得してループするように書き換えるとよいでしょう。

AppleScript名:なろう小説API v2(Zip展開つき)
– Created 2017-10-03 by Takaaki Naganoya
– 2017 Piyomaru Software
–http://piyocast.com/as/archives/4890
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “GZIP”
–https://github.com/nicklockwood/GZIP
–http://dev.syosetu.com/man/api/
–1日の利用上限は80,000または転送量上限400MByte???

property |NSURL| : a reference to current application’s |NSURL|
property NSString : a reference to current application’s NSString
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSURLComponents : a reference to current application’s NSURLComponents
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property NSURLConnection : a reference to current application’s NSURLConnection

set reqURLStr to “http://api.syosetu.com/novelapi/api/” –通常API
–set reqURLStr to “http://api.syosetu.com/novel18api/api/”–18禁API

set aRec to {gzip:“5″, out:“json”, lim:“500″}

set aURL to retURLwithParams(reqURLStr, aRec) of me

set aRes to callRestGETAPIAndParseResults(aURL) of me

set aRESCode to (responseCode of aRes) as integer
if aRESCode is not equal to 200 then return false

set aRESHeader to responseHeader of aRes
set aRESTres to (json of aRes) as list

–GET methodのREST APIを呼ぶ
on callRestGETAPIAndParseResults(aURL)
  set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:“GET”
  
aRequest’s setValue:“gzip” forHTTPHeaderField:“Content-Encoding”
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
  
set rRes to bRes’s gunzippedData() –From GZIP.framework
  
  
set resStr to NSString’s alloc()’s initWithData:rRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
end callRestGETAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

★Click Here to Open This Script 

2017/09/19 Notesで選択中のnoteやfolderの情報を取得する?

defaultsコマンド経由でNotes.app(日本語ローカライズ名:メモ.app)で選択中のnoteやfolderの情報を取得する(参考になるかもしれない)AppleScriptです。

selectionとかselected folderなどの用語が用意されていないことでおなじみのNotes.app。「現在表示中のノート」を処理したいのはやまやまですが、用語辞書に書かれていない機能は、まっとうな手段では呼び出せません。

となると、「まっとうでない方法」を検討することになります。

まっとうでない手段を選ぶことで、スピード低下、安定性のなさ、メンテナンスの手間など負の側面と向き合う必要が出てきますが、問題自体の解決は行えます。

(1)GUI Scripting
まっとうでない方法の筆頭。画面上から無理やり情報を取得して、選択状態にあるGUI部品のタイトルを取得することで、選択中のノートの情報を特定できそうです。興味と必要性がないので深追いしていませんが、Notes.appの各種表示状態(アカウント情報一覧を表示するとか、表示を隠すとか)に合わせて数パターンの処理を用意しておくことで、実現できるメドは立っています。

ただし、GUI Scriptingのつねとして、「スピードは通常のAppleScriptの100倍以上遅い」(Cocoa経由の処理と比較すると数万倍遅い)「予期しないアプリケーション側の表示には追従できない」「OSやアプリケーションのバージョンアップにともない、表示内容やGUI部品の階層が変わったぐらいで書き換えが必要になる」ということから、多用は避けたいところです。

(2)defaultsコマンド
まっとうでない方法として有名。アプリケーションの設定情報を直接読んで処理します。アプリケーションやOSがバージョンアップして内部情報が変更されると動かなくなるのはGUI Scripting同様。

defaultsコマンドから得られる情報をAppleScriptネイティブのデータ形式(recordとか)に変換するのが面倒でしたが、Cocoaの機能を利用して割と手軽に読み込めるようになってきました。

Terminal上で、

  defaults read com.apple.Notes

を実行してみたところ、設定情報の中に現在選択中のnoteおよびfolderを記録している箇所があったので、実際にAppleScript中に記述して取り出してみました。

今回掲載したのが、そのdefaultsコマンドを利用した情報取得のサンプルです。このデータをもとにSQLiteのデータベースを検索してみればいいかも、などと思ってデータベースを漁ってみたものの、

notes_sqlite3_resized.png

defaultsから得られたような形式のID(UUIDっぽい?)はDB中に記録されていないようで、ちょっと残念です(SQLiteではなくCoreData経由で調べるべき?)。もう少し調べてみると手がかりが見つかるかもしれませんが、出先の電車の中で調べた程度だったのでこのぐらいです。

AppleScript名:Notesで選択中のnoteやfolderの情報をdefaults経由で取得する
– Created 2016-10-31 by Takaaki Naganoya
– 2016 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
–http://piyocast.com/as/archives/4827

set sRes to do shell script ” defaults read com.apple.Notes ArchivedUIState”
set aDict to deserializeToPlistString(sRes) of me

set curNote to (aDict’s valueForKey:“currentNote”) as string
–>  ”860327F4-4E6F-44FB-92EC-720B47CDD38A”

set curNoteFol to (aDict’s valueForKey:“currentNoteFolder”) as string
–>  ”184daff620aca4ad23fd2bf70d0b76af”

set folderListHidden to (aDict’s valueForKey:“folderListHidden”) as integer as boolean
–>  false

return {curNote, curNoteFol, folderListHidden}

–XML-format plist string–> list or record
on deserializeToPlistString(aStr as string)
  set deStr to current application’s NSString’s stringWithString:aStr
  
set theData to deStr’s dataUsingEncoding:(current application’s NSUTF8StringEncoding)
  
set aList to current application’s NSPropertyListSerialization’s propertyListWithData:theData options:(current application’s NSPropertyListMutableContainersAndLeaves) |format|:(missing value) |error|:(missing value)
  
return aList
end deserializeToPlistString

★Click Here to Open This Script 

2017/08/27 文の意味類似度の評価

Apitore「文の意味類似度の評価」APIを呼び出して、文単位で意味的な近さを評価するAppleScriptです。

ApitoreのREST API呼び出しのために、Apitoreにユーザー登録してAccess Tokenを取得し、retAccessToken()内に記述しておいてください(自分はKeychainにAccessTokenを記録して呼び出すライブラリを利用しています)。

掲載リストそのままでAccess Tokenを記入していない状態だとエラーになります。

「文の意味類似度の評価」APIはWord2Vecを用いているわけですが、Word2Vecの処理のためにWikipediaのダンプ内容を学習させており、ローカルでもWord2Vecの演算を行いたいところ。もう、いっそのことOSにWikipediaのローカルダンプを含めて配布してほしい今日このごろです(OSインストーラのサイズが倍ぐらいになりそうですが)。

AppleScript名:文の意味類似度の評価
– Created 2017-08-16 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version "2.5"
use scripting additions
use framework "Foundation"
–http://piyocast.com/as/archives/4792

property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSMutableData : a reference to current application’s NSMutableData
property NSNumberFormatter : a reference to current application’s NSNumberFormatter
property NSURLRequestReloadIgnoringLocalCacheData : a reference to current application’s NSURLRequestReloadIgnoringLocalCacheData
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property |NSURL| : a reference to current application’s |NSURL|
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSString : a reference to current application’s NSString
property NSURLConnection : a reference to current application’s NSURLConnection
property NSURLComponents : a reference to current application’s NSURLComponents
property NSNumber : a reference to current application’s NSNumber
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSNumberFormatterRoundDown : a reference to current application’s NSNumberFormatterRoundDown

set targStr1 to "戦場の絆REV.4において、ガンダムはコスト250の格闘機である"
set targStr2 to "戦場の絆REV.2において、ガンダムはかつてコスト280の近距離機であった"
set aRes to calcSimiliarity(targStr1, targStr2) of me

set targStr3 to "この秋、戦場の絆REV.4にGブルとザクレロが宇宙ステージ専用機体として登場するらしい。"
set targStr4 to "バスが遅くて、非力なUプロセッサで、放熱機構が弱い、鈍足なMacBook Air 2011。"
set bRes to calcSimiliarity(targStr3, targStr4) of me

return {aRes, bRes}
–> {0.9, 0.0}–1.0に近いほど類似度が高い。0に近いほど類似度が低い

on calcSimiliarity(targStr1, targStr2)
  set reqURLStr to "https://api.apitore.com/api/53/sentence-similarity/eval"
  
set accessToken to retAccessToken() of me –Access Token
  
  
set aReq to {text1:targStr1, text2:targStr2}
  
set aRec to {access_token:accessToken}
  
set aURL to retURLwithParams(reqURLStr, aRec) of me
  
  
set aRes to callRestPOSTAPIAndParseResults(aURL, aReq) of me
  
set aRESCode to responseCode of aRes
  
set aRESHeader to responseHeader of aRes
  
set aRESTres to (json of aRes)
  
return roundingDownNumStr(aRESTres’s similarity, 2) of me
end calcSimiliarity

–POST methodのREST APIを呼ぶ
on callRestPOSTAPIAndParseResults(aURL, aReq)
  set {theData, theError} to NSJSONSerialization’s dataWithJSONObject:aReq options:0 |error|:(reference)
  
if theData is missing value then error (theError’s localizedDescription() as text) number -10000
  
set postBody to NSMutableData’s |data|()
  
postBody’s appendData:theData
  
  
–Request
  
set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:"POST"
  
aRequest’s setCachePolicy:(NSURLRequestReloadIgnoringLocalCacheData)
  
aRequest’s setHTTPShouldHandleCookies:false
  
aRequest’s setTimeoutInterval:600
  
aRequest’s setValue:"application/json" forHTTPHeaderField:"Content-Type"
  
aRequest’s setHTTPBody:postBody
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
set resStr to NSString’s alloc()’s initWithData:bRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
  
end callRestPOSTAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

on retAccessToken()
  return "xxXXXxxX-XxXx-XXXX-xXXX-XXxXXxxXxxXx" –API Tore Access Token
end retAccessToken

on roundingDownNumStr(aNum as string, aDigit as integer)
  set a to NSString’s stringWithString:aNum
  
set aa to a’s floatValue()
  
set aFormatter to NSNumberFormatter’s alloc()’s init()
  
aFormatter’s setMaximumFractionDigits:aDigit
  
aFormatter’s setRoundingMode:(NSNumberFormatterRoundDown)
  
set aStr to aFormatter’s stringFromNumber:(NSNumber’s numberWithFloat:aa)
  
return (aStr as text) as real
end roundingDownNumStr

★Click Here to Open This Script 

2017/08/23 指定フォルダ以下のLocalのHTMLのテキストエンコーディングをUTF-8に書き換えて保存

Localの指定フォルダ以下にあるHTMLをすべてSpotlightで抽出し、見つかったすべてのHTMLのテキストエンコーディングをUTF-8に変更して上書き保存するAppleScriptです。

本Script単体では動作確認できないので、確認のためにアプレットを用意しておきました。HTMLへの要素アクセスにはオープンソースのHTMLReader.framework(By Nolan Waite)を、Spotlight検索にはShane Stanleyの「Metadata Lib」を、日本語テキストファイルのエンコーディング自動検出には自作の「japaneseTextEncodingDetector」を用いています。これらをすべて含んだアプレット「htmlutf8rewriter.app」を用意しておきました(Code Signずみ)。

–> Download htmlutf8rewriter.zip

実際にテストデータ(7,979 files)で実験してみたところ、MacBook Air 2011(Core i5, 1.6GHz)上で702 Seconds、12分で処理できました(macOS 10.13beta 7で実行)。

バスが遅くて、非力なUプロセッサで、放熱機構が弱い、鈍足なMacBook Air 2011でこの程度の速度なので、手元のMacBook Pro 2012で実行すると半分ぐらいの時間で実行できるものと思われます。

Dual Core(4 thread)のMacBook Airでも1〜2thread分は処理に余裕があったので、よりコア数の多いMac上で実行する場合には並列実行するとマシンパワーを絞り出せるものと思われます。

用意したテストデータには行儀の悪いものも混在していたので、300程度のエラーが発生。こうしたエラーデータに対しては、別途何らかのテキストエディタをコントロールして書き換え&保存を行なってもよいのかもしれません。

最初からテキストエディタをコントロールして書き換えを行うと並列処理できないですし、AppleScript単体で実行するよりも処理時間がかかります(現状だと秒間11ファイルぐらい処理できていますが、外部のテキストエディタを制御して処理すると秒間2ファイルぐらいまで落ちるはずです)。

AppleScript名:指定フォルダ以下のLocalのHTMLのテキストエンコーディングをUTF-8に書き換えて保存
– Created 2017-08-23 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “HTMLReader” –https://github.com/nolanw/HTMLReader
use jLib : script “japaneseTextEncodingDetector”
use mdLib : script “Metadata Lib” version “1.0.0″
–http://piyocast.com/as/archives/4789

property NSString : a reference to current application’s NSString
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property HTMLDocument : a reference to current application’s HTMLDocument

set theFolder to choose folder
set theRecord to mdLib’s searchFolders:{theFolder} searchString:“kMDItemFSName ENDSWITH[c] ’.html’” searchArgs:{}
set aCount to 1

repeat with i in theRecord
  set hRes to overWriteHTMLWithUTF8Encoding(i) of me
  
if hRes = true then
    set aCount to aCount + 1
  end if
end repeat

return aCount

on overWriteHTMLWithUTF8Encoding(aPOSIX)
  set aRes to readJapanesTextFileWithGuessingEncoding(aPOSIX) of jLib
  
set bRes to retSpecifiedElementsFromHTMLString(aRes, “meta”) of me
  
–>  {(HTMLElement) 0 children>, (HTMLElement) 0 children>, ….
  
  
–Search Element
  
set aCount to 0
  
set hitF to false
  
repeat with i in bRes
    set tRes to i’s attributes()
    
–> (NSDictionary) {http-equiv:”Content-Type”, content:”text/html; charset=euc-jp”}
    
    
set kList to tRes’s allKeys() as list
    
–> {”http-equiv”, “content”}
    
    
if kList contains “http-equiv” then
      set hitF to true
      
exit repeat
    end if
    
    
set aCount to aCount + 1
  end repeat
  
  
if hitF = false then return false –Element Not Found
  
  
–Text Encoding を書き換えたHTMLElementを作成する
  
set aHTML to HTMLDocument’s documentWithString:aRes
  
set anElement to (aHTML’s nodesMatchingSelector:“meta”)’s objectAtIndex:aCount
  
anElement’s setObject:“text/html; charset=UTF-8″ forKeyedSubscript:“content”
  
set aRootHTMLStr to anElement’s |document|()’s serializedFragment()
  
  
–Overwrite HTML
  
set aRes to (aRootHTMLStr’s writeToFile:aPOSIX atomically:false encoding:(NSUTF8StringEncoding) |error|:(missing value)) as boolean
  
return aRes
end overWriteHTMLWithUTF8Encoding

–与えられたHTML文字列のうち、指定されたタグ要素で囲まれた文字要素を返す
on retSpecifiedElementsFromHTMLString(anNSString, aTag)
  set aHTML to HTMLDocument’s documentWithString:anNSString
  
set anElement to (aHTML’s nodesMatchingSelector:aTag)
  
return anElement as list
end retSpecifiedElementsFromHTMLString

–与えられたHTML文字列のうち、指定されたタグ要素で囲まれた文字要素を返す
on retSpecifiedElementStringFromHTMLString(anNSString, aTag)
  set aHTML to HTMLDocument’s documentWithString:anNSString
  
set anElement to (aHTML’s nodesMatchingSelector:aTag)’s firstObject()’s textContent()
  
return anElement as string
end retSpecifiedElementStringFromHTMLString

–指定されたローカルのHTMLファイルを、指定文字エンコーディングで読み込んで、指定されたタグ要素で囲まれた文字要素を返す
on retSpecifiedElementStringFromLocalHTML(aPOSIX, anEncoding, aTag)
  set aPath to NSString’s stringWithString:aPOSIX
  
set aData to NSString’s stringWithContentsOfFile:aPath encoding:(anEncoding) |error|:(missing value)
  
if aData = missing value then return false
  
  
set aHTML to HTMLDocument’s documentWithString:aData
  
set anElement to (aHTML’s nodesMatchingSelector:aTag)’s firstObject()’s textContent()
  
return anElement as string
end retSpecifiedElementStringFromLocalHTML

★Click Here to Open This Script 

2017/08/11 日本語形態素解析【新語対応】_ipadic_neologd(POST版)

apitoreのREST API「日本語形態素解析【Neologd対応】」のPOST対応版APIを呼び出すAppleScriptです。

apitore上の既存の形態素解析APIがPOST対応し、1MBまでのサイズのテキストの形態素解析が行えるようになったので、ためしに呼んでみることにしました。

ただし、テキストのサイズが大きくなった場合の処理時間が読めないので、いきなりMAXの1MBのテキストを形態素解析させるのは得策ではないでしょう(自分も様子見中)。

本Scriptをテストするためには、apitoreにサインアップしてAccess Tokenを取得して、Script末尾の伏字部分にコピー&ペーストしてください(掲載リストをそのまま実行してもエラーになります)。

個人的にはREST APIのAccess TokenをmacOSのKeychainに入れて、アカウント名とサイト名でKeychainに問い合わせる「keychain Lib」AppleScript Librariesを用いています。

AppleScript名:日本語形態素解析【新語対応】_ipadic_neologd(POST版)
– Created 2017-08-11 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.5″
use scripting additions
use framework “Foundation”
–use keychainLib : script “keychainLib”
–http://piyocast.com/as/archives/4773

property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSMutableData : a reference to current application’s NSMutableData
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property |NSURL| : a reference to current application’s |NSURL|
property NSURLRequestReloadIgnoringLocalCacheData : a reference to current application’s NSURLRequestReloadIgnoringLocalCacheData
property NSURLConnection : a reference to current application’s NSURLConnection
property NSString : a reference to current application’s NSString
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSURLComponents : a reference to current application’s NSURLComponents

(*)
tell current application
  set aTargStr to read (choose file) as «class utf8»–Read text as UTF-8
end tell
*)

–2017/7/3 Ver: POST対応。1MBまでのテキストを一気に形態素解析できるようになった
set aTargStr to “「ACE COMBAT INFINITY」3周年記念キャンペーンを実施。記念エンブレムをプレゼント”
set aTargList to paragraphs of aTargStr

set reqURLStr to “https://api.apitore.com/api/7/kuromoji-ipadic/tokenize”
set accessToken to retAccessToken() of me —Access Token
set aReq to {texts:aTargList}
set aRec to {access_token:accessToken}
set aURL to retURLwithParams(reqURLStr, aRec) of me

set aRes to callRestPOSTAPIAndParseResults(aURL, aReq) of me
set aRESCode to responseCode of aRes
set aRESHeader to responseHeader of aRes
set aRESTres to (json of aRes)
return aRESTres as record

–POST methodのREST APIを呼ぶ
on callRestPOSTAPIAndParseResults(aURL, aReq)
  set {theData, theError} to NSJSONSerialization’s dataWithJSONObject:aReq options:0 |error|:(reference)
  
if theData is missing value then error (theError’s localizedDescription() as text) number -10000
  
set postBody to NSMutableData’s |data|()
  
postBody’s appendData:theData
  
  
–Request
  
set aRequest to NSMutableURLRequest’s requestWithURL:(|NSURL|’s URLWithString:aURL)
  
aRequest’s setHTTPMethod:“POST”
  
aRequest’s setCachePolicy:(NSURLRequestReloadIgnoringLocalCacheData)
  
aRequest’s setHTTPShouldHandleCookies:false
  
aRequest’s setTimeoutInterval:600
  
aRequest’s setValue:“application/json” forHTTPHeaderField:“Content-Type”
  
aRequest’s setHTTPBody:postBody
  
  
set aRes to NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
  
set resList to aRes as list
  
  
set bRes to contents of (first item of resList)
  
set resStr to NSString’s alloc()’s initWithData:bRes encoding:(NSUTF8StringEncoding)
  
  
set jsonString to NSString’s stringWithString:resStr
  
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
  
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
  
  
–Get Response Code & Header
  
set dRes to contents of second item of resList
  
if dRes is not equal to missing value then
    set resCode to (dRes’s statusCode()) as number
    
set resHeaders to (dRes’s allHeaderFields()) as record
  else
    set resCode to 0
    
set resHeaders to {}
  end if
  
  
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
  
end callRestPOSTAPIAndParseResults

on retURLwithParams(aBaseURL, aRec)
  set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
  
  
set aKeyList to (aDic’s allKeys()) as list
  
set aValList to (aDic’s allValues()) as list
  
set aLen to length of aKeyList
  
  
set qList to {}
  
repeat with i from 1 to aLen
    set aName to contents of item i of aKeyList
    
set aVal to contents of item i of aValList
    
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
  end repeat
  
  
set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
  
aComp’s setQueryItems:qList
  
set aURL to (aComp’s |URL|()’s absoluteString()) as text
  
  
return aURL
end retURLwithParams

on retAccessToken()
  return “XXXXxxxX-xxxx-XXXx-xxxX-XxxxXxXxxXXx” –API Tore Access Token
end retAccessToken

★Click Here to Open This Script 

2017/08/07 LocalのHTMLからJavaScriptを除去する

オープンソースのHTMLReader.framework(By Nolan Waite)を用いて、ローカルのHTML書類からJavaScriptの記述(タグ要素)を削除するAppleScriptです。

JavaScriptを削除しているのは単に処理サンプル掲載のためであり、実際にはJavaScriptにかぎらずタグ名で要素を指定できるようになっているので、とくにtitleだろうがtableだろうがcssだろうが、問題なく削除できます。

また、サンプル掲載のためにわざとHTMLのテキストエンコーディングを固定で呼び出していますが、こちらもAppleScript単独で日本語テキストのエンコーディング自動判別できるライブラリを整備してあるので、そちらを利用するとよいでしょう(実際にやっています)。

HTMLReader.frameworkについては、実際に使いきれないぐらいの機能が入っているので、いろいろ調べていますが、処理サンプルでいちばん参考にしているのはHTMLReader.framework自体のソースコードです。Objective-Cで書かれたソース自体を読みつつ、AppleScriptで呼び出しています。

本Script実行のためにはHTMLReader.frameworkをXcode上でビルドして、~/Library/Frameworksフォルダに入れて実行する必要があります。

本Scriptを実際に処理すると、与えたHTMLからJavaScript要素を削除したHTMLのテキストを返してきます。適宜、ファイルに保存するなりしてください。

HTMLに対して単に文字列処理して加工するのではなく、こうしたHTMLReaderのようなフレームワークを併用して高度な処理が行えるのはとても便利です。

AppleScript名:LocalのHTMLからJavaScriptを除去する
– Created 2017-08-04 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “HTMLReader” –https://github.com/nolanw/HTMLReader
–http://piyocast.com/as/archives/4765

property NSString : a reference to current application’s NSString
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSJapaneseEUCStringEncoding : a reference to current application’s NSJapaneseEUCStringEncoding
property HTMLDocument : a reference to current application’s HTMLDocument

set aPOSIX to POSIX path of (choose file)
set cRec to removeSpecifiedElementFromLocalHTML(aPOSIX, NSJapaneseEUCStringEncoding, “script”) of me

–指定されたローカルのHTMLファイルを、指定文字エンコーディングで読み込んで、指定されたタグ要素をすべて削除する
on removeSpecifiedElementFromLocalHTML(aPOSIX, anEncoding, aTag)
  set aPath to NSString’s stringWithString:aPOSIX
  
set aData to NSString’s stringWithContentsOfFile:aPath encoding:(anEncoding) |error|:(missing value)
  
if aData = missing value then return false
  
  
set aHTML to HTMLDocument’s documentWithString:aData
  
set elemList to (aHTML’s nodesMatchingSelector:aTag) as list
  
repeat with i in elemList
    i’s removeFromParentNode()
  end repeat
  
  
return aHTML’s serializedFragment() as string
end removeSpecifiedElementFromLocalHTML

★Click Here to Open This Script