アラートダイアログ上にWkWebViewを配置し、Google Chartsを用いてCalendar Chartを表示するAppleScriptです。
自分の開発環境(MacBook Pro Retina 2012, Core i7 2.6GHz)で100日間のアクセス履歴を処理して7秒強ぐらいで描画が終了します。
調子に乗って300日分のアクセス履歴を処理したところ、表示まで1分ほどかかりました。あまり長い期間の描画を行わせるのは(このプログラムの書き方だと)向いていないと感じます。いまのところテストしただけで実用性は考えていませんが、この程度のグラフなら自前でNSImage上にボックスを描画して表示しても大した手間にはならないでしょう。
Safariのアクセス履歴は例によってsqliteのDatabaseにアクセスして取得していますが、AppleScriptのランタイム環境によっては、アクセス権限がないというメッセージが出てアクセスできないことがあります。ASObjC Explorer 4上では実行できませんでしたし、Switch Control上でも実行できません。
# 管理者権限つきで実行しても(with administrator privileges)実行できません → Switch Controlでも実行できるようになりました
最初に掲載したバージョンでは、グラフ化したときに表示月が1か月ズレるという問題がありました。
Google Chartsのドキュメントを確認したところ、
Note: JavaScript counts months starting at zero: January is 0, February is 1, and December is 11. If your calendar chart seems off by a month, this is why.
JavaScriptでMonthはJanuaryが0らしく、monthを-1する必要があるとdocumentに書かれていました。うわ、なにその仕様?(ーー;;;
AppleScript名:アラートダイアログ上にWebViewでGoogle Chartを表示(Calendar Charts)v1a.scptd |
— – Created by: Takaaki Naganoya – Created on: 2020/05/07 — – Copyright © 2020 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use framework "AppKit" use framework "WebKit" use scripting additions property |NSURL| : a reference to current application’s |NSURL| property NSAlert : a reference to current application’s NSAlert property NSString : a reference to current application’s NSString property NSButton : a reference to current application’s NSButton property WKWebView : a reference to current application’s WKWebView property WKUserScript : a reference to current application’s WKUserScript property NSURLRequest : a reference to current application’s NSURLRequest property NSRunningApplication : a reference to current application’s NSRunningApplication property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding property WKUserContentController : a reference to current application’s WKUserContentController property WKWebViewConfiguration : a reference to current application’s WKWebViewConfiguration property WKUserScriptInjectionTimeAtDocumentEnd : a reference to current application’s WKUserScriptInjectionTimeAtDocumentEnd property returnCode : 0 script spd property aRes : {} property bRes : {} end script –Calculate Safari access frequency for (parameter days) set (aRes of spd) to calcMain(100) of safariHistLib set (bRes of spd) to "" repeat with i in (aRes of spd) set {item1, item2, item3} to parseByDelim(theName of (contents of i), "-") of me set newLine to " [ new Date(" & (item1 as string) & ", " & ((item2 – 1) as string) & ", " & (item3 as string) & "), " & (numberOfTimes of i) & "]," & (string id 10) set (bRes of spd) to (bRes of spd) & newLine end repeat set (bRes of spd) to text 1 thru -3 of (bRes of spd) –Pie Chart Template HTML set myStr to "<!DOCTYPE html> <html lang=\"UTF-8\"> <head> <div id=\"calendarchart\"></div> <script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script> <script type=\"text/javascript\"> // Draw the chart and set the chart values dataTable.addColumn({ type: ’number’, id: ’Web Access’ }); dataTable.addRows([ %@ ]); var chart = new google.visualization.Calendar(document.getElementById(’calendar_basic’)); var options = { title: \"Web Activity\", height: 350, }; chart.draw(dataTable, options); } </script> </head> <body> <div id=\"calendar_basic\" style=\"width: 1000px; height: 350px;\"></div> </body> </html>" set aString to current application’s NSString’s stringWithFormat_(myStr, (bRes of spd)) as string set paramObj to {myMessage:"Calendar Chart Test", mySubMessage:"This is a simple calendar chart using google charts", htmlStr:aString} –my browseStrWebContents:paramObj–for debug my performSelectorOnMainThread:"browseStrWebContents:" withObject:(paramObj) waitUntilDone:true on browseStrWebContents:paramObj set aMainMes to myMessage of paramObj set aSubMes to mySubMessage of paramObj set htmlString to (htmlStr of paramObj) set aWidth to 1000 set aHeight to 300 –WebViewをつくる set aConf to WKWebViewConfiguration’s alloc()’s init() –指定HTML内のJavaScriptをFetch set jsSource to pickUpFromToStr(htmlString, "<script type=\"text/javascript\">", "</script>") of me set userScript to WKUserScript’s alloc()’s initWithSource:jsSource injectionTime:(WKUserScriptInjectionTimeAtDocumentEnd) forMainFrameOnly:true set userContentController to WKUserContentController’s alloc()’s init() userContentController’s addUserScript:(userScript) aConf’s setUserContentController:userContentController set aWebView to WKWebView’s alloc()’s initWithFrame:(current application’s NSMakeRect(0, 0, aWidth, aHeight – 100)) configuration:aConf aWebView’s setNavigationDelegate:me aWebView’s setUIDelegate:me aWebView’s setTranslatesAutoresizingMaskIntoConstraints:true set bURL to |NSURL|’s fileURLWithPath:(POSIX path of (path to me)) aWebView’s loadHTMLString:htmlString baseURL:(bURL) — set up alert set theAlert to NSAlert’s alloc()’s init() tell theAlert its setMessageText:aMainMes its setInformativeText:aSubMes its addButtonWithTitle:"OK" –its addButtonWithTitle:"Cancel" its setAccessoryView:aWebView set myWindow to its |window| end tell — show alert in modal loop NSRunningApplication’s currentApplication()’s activateWithOptions:0 my performSelectorOnMainThread:"doModal:" withObject:(theAlert) waitUntilDone:true –Stop Web View Action set bURL to |NSURL|’s URLWithString:"about:blank" set bReq to NSURLRequest’s requestWithURL:bURL aWebView’s loadRequest:bReq if (my returnCode as number) = 1001 then error number -128 end browseStrWebContents: on doModal:aParam set (my returnCode) to (aParam’s runModal()) as number end doModal: on viewDidLoad:aNotification return true end viewDidLoad: on fetchJSSourceString(aURL) set jsURL to |NSURL|’s URLWithString:aURL set jsSourceString to NSString’s stringWithContentsOfURL:jsURL encoding:(NSUTF8StringEncoding) |error|:(missing value) return jsSourceString end fetchJSSourceString on pickUpFromToStr(aStr as string, s1Str as string, s2Str as string) set a1Offset to offset of s1Str in aStr if a1Offset = 0 then return false set bStr to text (a1Offset + (length of s1Str)) thru -1 of aStr set a2Offset to offset of s2Str in bStr if a2Offset = 0 then return false set cStr to text 1 thru (a2Offset – (length of s2Str)) of bStr return cStr as string end pickUpFromToStr –リストを任意のデリミタ付きでテキストに on retArrowText(aList, aDelim) set aText to "" set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to aDelim set aText to aList as text set AppleScript’s text item delimiters to curDelim return aText end retArrowText on array2DToJSONArray(aList) set anArray to current application’s NSMutableArray’s arrayWithArray:aList set jsonData to current application’s NSJSONSerialization’s dataWithJSONObject:anArray options:(0 as integer) |error|:(missing value) –0 is set resString to current application’s NSString’s alloc()’s initWithData:jsonData encoding:(current application’s NSUTF8StringEncoding) return resString end array2DToJSONArray on parseByDelim(aData, aDelim) set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to aDelim set dList to text items of aData set AppleScript’s text item delimiters to curDelim return dList end parseByDelim script safariHistLib property parent : AppleScript use scripting additions use framework "Foundation" property |NSURL| : a reference to current application’s |NSURL| script spd property sList : {} property nList : {} property sRes : {} property dRes1 : {} property dRes2 : {} end script on calcMain(daysNum) set (dRes1 of spd) to dumpSafariHistoryFromDaysBefore(daysNum) of me set (dRes2 of spd) to {} repeat with i in (dRes1 of spd) copy (first item of i) as string to dStr set convDstr to first item of (parseByDelim(dStr, {" "}) of me) set the end of (dRes2 of spd) to convDstr end repeat –日付ごとに登場頻度集計 set cRes to countItemsByItsAppearance2((dRes2 of spd)) of me return cRes as list end calcMain –NSArrayに入れたレコードを、指定の属性ラベルの値でソート on sortRecListByLabel(aArray, aLabelStr as string, ascendF as boolean) –ソート set sortDesc to current application’s NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF set sortDescArray to current application’s NSArray’s arrayWithObjects:sortDesc set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray –NSArrayからListに型変換して返す set bList to (sortedArray) as list return bList end sortRecListByLabel on dumpSafariHistoryFromDaysBefore(daysBefore) –現在日時のn日前を求める using terms from scripting additions set origDate to (current date) – (daysBefore * days) end using terms from set dStr to convDateObjToStrWithFormat(origDate, "yyyy-MM-dd hh:mm:ss") of me set aDBpath to "~/Library/Safari/History.db" set pathString to current application’s NSString’s stringWithString:aDBpath set newPath to pathString’s stringByExpandingTildeInPath() set aText to "/usr/bin/sqlite3 " & newPath & " ’SELECT datetime(history_visits.visit_time+978307200, \"unixepoch\", \"localtime\"), history_visits.title || \" @ \" || substr(history_items.URL,1,max(length(history_items.URL)*(instr(history_items.URL,\" & \")=0),instr(history_items.URL,\" & \"))) as Info FROM history_visits INNER JOIN history_items ON history_items.id = history_visits.history_item where history_visits.visit_time>(julianday(\"" & dStr & "\")*86400-211845068000) ORDER BY visit_time ASC LIMIT 999999;’" using terms from scripting additions set (sRes of spd) to do shell script aText end using terms from set (sList of spd) to (paragraphs of (sRes of spd)) repeat with i in (sList of spd) set j to contents of i –Parse each field set j2 to parseByDelim(j, {"|", "@ "}) of me set the end of (nList of spd) to j2 end repeat return (nList of spd) end dumpSafariHistoryFromDaysBefore on parseByDelim(aData, aDelim) set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to aDelim set dList to text items of aData set AppleScript’s text item delimiters to curDelim return dList end parseByDelim –出現回数で集計 on countItemsByItsAppearance2(aList) set aSet to current application’s NSCountedSet’s alloc()’s initWithArray:aList set bArray to current application’s NSMutableArray’s array() set theEnumerator to aSet’s objectEnumerator() repeat set aValue to theEnumerator’s nextObject() if aValue is missing value then exit repeat bArray’s addObject:(current application’s NSDictionary’s dictionaryWithObjects:{aValue, (aSet’s countForObject:aValue)} forKeys:{"theName", "numberOfTimes"}) end repeat –出現回数(numberOfTimes)で降順ソート set theDesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:"theName" ascending:true bArray’s sortUsingDescriptors:{theDesc} return bArray end countItemsByItsAppearance2 on convDateObjToStrWithFormat(aDateO as date, aFormatStr as string) set aDF to current application’s NSDateFormatter’s alloc()’s init() set aLoc to current application’s NSLocale’s currentLocale() set aLocStr to (aLoc’s localeIdentifier()) as string aDF’s setLocale:(current application’s NSLocale’s alloc()’s initWithLocaleIdentifier:aLocStr) aDF’s setDateFormat:aFormatStr set dRes to (aDF’s stringFromDate:aDateO) as string return dRes end convDateObjToStrWithFormat end script |