Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2014-08-15

Closure Compilerを利用するワンライナーとサービス

前回からの続き。

その後、JavaScriptを圧縮・整形するコマンドjs-compile.rbを何度か使っているうちに、あることに気付いた。js-compile.rbにオプション指定の選択肢はあるのだけど、自分が使う条件はたった3種類しかない。

  • --compilation_level=WHITESPACE_ONLY
  • --compilation_level=SIMPLE_OPTIMIZATIONS
  • --compilation_level=WHITESPACE_ONLY --pretty_print

現状の自分の使い方では、上記のオプション指定だけで足りてしまう。ならば、わざわざオプション解析するコマンドなんか作らなくても、Rubyワンライナーで足りてしまうのではないか?そう思って、さっそく試してみた。

1行コマンド

  • --compilation_level=WHITESPACE_ONLY
ruby -r net/http -r uri -e "puts Net::HTTP.post_form(URI('http://closure-compiler.appspot.com/compile'),js_code:URI.decode(STDIN.gets(nil)),compilation_level:'WHITESPACE_ONLY',output_format:'text',output_info:'compiled_code').body"

  • --compilation_level=SIMPLE_OPTIMIZATIONS
ruby -r net/http -r uri -e "puts Net::HTTP.post_form(URI('http://closure-compiler.appspot.com/compile'),js_code:URI.decode(STDIN.gets(nil)),compilation_level:'SIMPLE_OPTIMIZATIONS',output_format:'text',output_info:'compiled_code').body"

  • --compilation_level=WHITESPACE_ONLY --pretty_print
ruby -r net/http -r uri -e "puts Net::HTTP.post_form(URI('http://closure-compiler.appspot.com/compile'),js_code:URI.decode(STDIN.gets(nil)),compilation_level:'WHITESPACE_ONLY',output_format:'text',output_info:'compiled_code',formatting:'pretty_print').body"

できた、できた、これで十分である。

Aliasにしておく

  • さすがに毎回入力するには長過ぎて手間なので、aliasに設定しておくことにした。
alias js-white="ruby -r net/http -r uri -e \"puts Net::HTTP.post_form(URI('http://closure-compiler.appspot.com/compile'),js_code:URI.decode(STDIN.gets(nil)),compilation_level:'WHITESPACE_ONLY',output_format:'text',output_info:'compiled_code').body\""

alias js-simple="ruby -r net/http -r uri -e \"puts Net::HTTP.post_form(URI('http://closure-compiler.appspot.com/compile'),js_code:URI.decode(STDIN.gets(nil)),compilation_level:'SIMPLE_OPTIMIZATIONS',output_format:'text',output_info:'compiled_code').body\""

alias js-pretty="ruby -r net/http -r uri -e \"puts Net::HTTP.post_form(URI('http://closure-compiler.appspot.com/compile'),js_code:URI.decode(STDIN.gets(nil)),compilation_level:'WHITESPACE_ONLY',output_format:'text',output_info:'compiled_code',formatting:'pretty_print').body\""

  • 以下のコードをコピーして...
javascript:
(function(d,f,s){
  s=d.createElement('script');
  s.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js';
  s.onload=function(){f()};/* <---onload属性を追加 */
  d.body.appendChild(s);

  f=function(){
    /* 内部コード */
    alert(typeof CryptoJS);
    alert(CryptoJS.AES.encrypt('hello','1234'));
  };
})(document)

$ pbpaste | js-white
javascript:(function(d,f,s){s=d.createElement("script");s.src="//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js";s.onload=function(){f()};d.body.appendChild(s);f=function(){alert(typeof CryptoJS);alert(CryptoJS.AES.encrypt("hello","1234"))}})(document);

$ pbpaste | js-simple
(function(b,c,a){a=b.createElement("script");a.src="//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js";a.onload=function(){c()};b.body.appendChild(a);c=function(){alert(typeof CryptoJS);alert(CryptoJS.AES.encrypt("hello","1234"))}})(document);

$ pbpaste | js-pretty
javascript: (function(d, f, s) {
  s = d.createElement("script");
  s.src = "//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js";
  s.onload = function() {
    f();
  };
  d.body.appendChild(s);
  f = function() {
    alert(typeof CryptoJS);
    alert(CryptoJS.AES.encrypt("hello", "1234"));
  };
})(document);

余分なオプションを気にしなくていいので、

前回のコマンド版より使い勝手がいいかも。


  • STDIN.gets(nil)の部分を、ARGF.gets(nil)に修正した方が、使い勝手が良くなる。
    • getsはデフォルトでARGFから読み込むことになっているので、
    • "STDIN."の部分を削除して、gets(nil)に修正するだけでOK。
  • ファイルパスで指定できるようになるのだ!
$ js-white FILE_PATH
  • もちろん、今までどおり標準入力からも受け取れる。
$ pbpaste | js-white

サービスにしておく

Automatorサービスも作ってみた。

f:id:zariganitosh:20140814153203p:image:w450


ruby -r net/http -r uri -e "puts Net::HTTP.post_form(URI('http://closure-compiler.appspot.com/compile'),js_code:URI.decode(STDIN.gets(nil)),compilation_level:'WHITESPACE_ONLY',output_format:'text',output_info:'compiled_code',formatting:'pretty_print').body"
      • この日記の1行コマンドのコードがそのまま使える。

 on run {input, parameters}
   display notification "pretty_print" with title "クリップボードにコピーしました。" subtitle "Closure Compiler" sound name "Blow"
 end run


JavaScriptコードを選択して、二本指タップしてみると...

f:id:zariganitosh:20140814155601p:image:w371

これで素早く圧縮・整形できるようになった!

2013-11-01

日本語と英語のテキスト境界のスペースをどうするか?

こうゆう発想のワンライナー的スクリプトが大好き。

uedamac:MEMO ueda$ echo "わたしは aho です。4 さいです。" |
 sed 's/\([^a-zA-Z0-9]\) \([a-zA-Z0-9]\)/\1\2/g' |
 sed 's/\([a-zA-Z0-9]\) \([^a-zA-Z0-9]\)/\1\2/g'
わたしはahoです。4さいです。
出版社に送る原稿には日本語と英単語の間にスペースを入れない方が良いらしいので贖罪のためにシェル芸やります。 – 上田ブログ

なるほど、確かに日本語と英語の間にスペースを入れるということは、段落先頭の字下げでスペースを使うようなものかもしれない。理想は、ページフォーマットや CSS で制御されるべきものだと思う。... とここまで考えて、ちょっぴり思い直した。そう言えば、OSX 環境上は日本語と英語の境界にスペースを挿入していたはず。

  • 例えば、アップルメニューの1行目は「この Mac について」となっている。
  • Mac の両側には、半角スペースが挿入されているのだ。
  • その証拠に、この半角スペースを無視してはショートカットを登録できない。
  • 半角スペースを挿入した「この Mac について」で登録することで有効になる。

f:id:zariganitosh:20131101085545p:image:h225f:id:zariganitosh:20131101085546p:image:h225

  • 半角スペースは「この Mac について」に限らず、すべてのメニュー、ダイアログ、ヘルプセンター等々で統一して使われているのだ。

  • 一方、アップルのホームページでは、日本語と英語の間に一切のスペースは存在しない。

f:id:zariganitosh:20131101085547p:image:w450

  • そのかわりフォントデザインの違いで、日本語と英語の境界をアピールしているように見える。

アップルは環境に応じて使い分けている。これは一体どうゆうことなのか?


ところで、自分のページはどうなっているかと言えば、これがまったく統一されていない ...。

  • その時の気分で、スペースを入れたり、入れなかったり。一番ダメな状況である。
  • 将来の理想的な css の世界に備えて、日英間のすべてのスペースを取り除くか!と考えたが、現状のページデザインでは明らかに読み難くなること必至。
  • 日英間のスペースを取り除くスクリプトは既にある。(冒頭)
  • ならば、逆にスペースを追加するスクリプトを作ってみる。
  • そうすれば、スクリプトが自動でスペースの追加も削除もやってくれる。

時代に逆行するかもしれないけど、試してみた。

日本語と英語の間にスペースを追加するシェルスクリプト

冒頭のスクリプトの逆なのだからすぐできると思ったけど、やってみると想像以上に手がかかった ...。

基本
$ echo 私はzarigani toshです。| sed -E 's/([^A-Za-z0-9])([A-Za-z0-9])/\1 \2/g' | sed -E 's/([A-Za-z0-9])([^A-Za-z0-9])/\1 \2/g'
私は zarigani   tosh です。
  • いかん、いかん ...。そのまま逆にしただけでは、既に存在するスペースにも重ねてスペースを追加してしまう。
  • それに、自分のブログは英数字だけでなく、様々なスクリプト記号も頻繁に出現する。半角記号も英数字と同じように扱いたい。
  • アスキーコード表(man ascii)の見える文字すべて(= スペースから ~ チルダまで)を対象にしてみた。
$ echo 私はzarigani toshです。|sed -E 's/([^ -~])([ -~])/\1 \2/g'|sed -E 's/([ -~])([^ -~])/\1 \2/g'
私は zarigani tosh です。

$ echo ファイル~/.bashrcです。|sed -E 's/([^ -~])([ -~])/\1 \2/g'|sed -E 's/([ -~])([^ -~])/\1 \2/g'
ファイル ~/.bashrc です。

  • 既に存在するスペースに、重ねて追加されてしまう不具合も修正してみた。
$ echo 'ファイル ~/.bashrc です。'|sed -E 's/([^ -~])([ -~])/\1 \2/g'|sed -E 's/([ -~])([^ -~])/\1 \2/g'
ファイル  ~/.bashrc  です。

$ echo 'ファイル ~/.bashrc です。'|sed -E 's/([^ -~])([!-~])/\1 \2/g'|sed -E 's/([!-~])([^ -~])/\1 \2/g'
ファイル ~/.bashrc です。

  • ところで、数値の前後にまでスペースが追加されると、しつこい気がする。
2013年10月31日
2013 年 10 月 31 日
  • 自分は、数値の場合はスペースを追加しない方が好き。対応してみる。
$ echo '20131031'|sed -E 's/([^ -~])([!-/:-~])/\1 \2/g'|sed -E 's/([!-/:-~])([^ -~])/\1 \2/g'
20131031

  • しかし、OSX 10.8.5 のような表記に対しては、スペースを入れて欲しい。
  • 数値のみが連続しない場合は、スペースを入れるようにしてみた。
$ echo '早急にOSX 10.8.5をインストールしてください。'|sed -E 's/([^ -~])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~])/\1 \2/g'
早急に OSX 10.8.5 をインストールしてください。

$ echo '20131031'|sed -E 's/([^ -~])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~])/\1 \2/g'
20131031

  • 句読点に続く場合は、スペースを挿入して欲しくない。
$ echo 'まだ、OSX 10.9にアップデートしてません。'|sed -E 's/([^ -~])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~])/\1 \2/g'
まだ、 OSX 10.9 にアップデートしてません。

$ echo 'まだ、OSX 10.9にアップデートしてません。'|sed -E 's/([^ -~[:punct:]])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g'
まだ、OSX 10.9 にアップデートしてません。

  • 個人的なことだが、自分は(半角)と(全角)を使い分けたい。
  • 半角()は、その前後に余分な空間を空けたくない時に使う。
  • よって、半角()の境界にはスペースを追加して欲しくないのだ。
$ echo '私は(zarigani tosh)です。'|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
私は(zarigani tosh)です。

$ echo '私はzarigani toshです。'|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
私は zarigani tosh です。

$ echo '私は(zarigani tosh)です。'|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
私は(zarigani tosh)です。
  • [:punct:]効果のためか、全角()の内側境界にも余分なスペースが追加されなくて、いい感じ。

  • おまけで、数値はすべて半角に統一したい。
$ echo '2013年1031'|tr 0-9 0-9|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
20131031

ひとまず欲求は満たされた。

Automator のサービスにしておく
  • 上記のスクリプトをシェルスクリプト実行アクションにコピーすれば OK と思っていたのだけど、うまく動かない ...。
  • まったく同じスクリプトなのになぜ?と悩んでいたら、実行環境に違いがあることに気付いた。
    • ターミナルの bash は、デフォルトで LANG=ja_JP.UTF-8 な環境。
    • 一方、Automator の bash は、LANG 設定なしの環境。

というわけで ...

  • LANG に設定して、export するとちゃんと動いた!
export LANG=ja_JP.UTF-8
cat|tr 0-9 0-9|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"

f:id:zariganitosh:20131101085548p:image:w450

  • 失敗したときのことを考えて、元のテキストをクリップボードへコピーしてから、スペースの追加を実行するようにした。
  • とんでもない結果になってしまった場合は、command-v でペーストすれば、元のテキストが復活するのだ。

さらなる課題

喜んで使っていたのもつかの間、上記のサービスにはまだ問題が多い。

  • 文章の一部を選択して、狙った部分のみに日英間のスペースを追加する時は、うまく動く。
  • しかし、ひと通り書き終わった日記全体を選択して実行すると ...
    • pre タグの中までスペースが追加されてしまう。
    • pre タグの中はソースコードの場合がほとんどなので、無闇にスペースを追加して欲しくない。
    • blockquote タグの中も同様である。引用文は可能な限り原文のままとしたい。
    • HTML タグの中も同様である。
    • はてな記法の[]の中も同様である。
  • つまり、以下の記法の内側では、スペースの追加をしないようにしておきたい。
# ブロック要素のタグ
 >>〜〜<<
 >||〜〜||<
 >|〜〜|<
 <pre>〜〜</pre>
 <blockquote>〜〜</blockquote>

# インライン要素のタグ
 <〜〜>
 [〜〜]

Ruby コードに書き直す

  • 特定の範囲でスペースを追加しないようにするのは、ちょっと面倒だ。
  • 自分の現状の知識では、正規表現だけで実現するのは無理そう。
  • よって、正規表現にこだわらず、条件判定をしながら処理する方法でやってみる。
  • また、コードを書くならシェルより Ruby の方が慣れているので、書き直してみた。
$ echo '早急にOSX 10.8.5をインストールしてください。'|ruby -pe '
$_.tr!("-","0-9"); 
$_.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2"); 
$_.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2");
'
早急に OSX 10.8.5 をインストールしてください。
  • 基本的にシェルと同じ処理だが、シェルから Ruby を呼び出す都合上、エスケープの問題でその表現に悩んだ。
  • シェルスクリプトのシングルクォート内で、Ruby のシングルクォートを表現するために \u0027 を使っている。
  • Ruby における \1 や \2 は、シェルスクリプトの中では \\1、\\2 と表現する必要があった。
ruby -pe '何らかのコード; ...'
  • 上記コードの ruby -p オプションは、以下のコードと同じ。
while gets
  何らかのコード; ...
  print $_
end
  • $_ には、パイプ経由で渡されたテキストが1行ずつ代入される。(gets がテキストを1行ずつ読み取る)
  • 行処理の最後に $_ を自動的に出力してくれるので、$_ に対する破壊的メソッドの連続で処理するのだ。

ブロック要素のタグ内ではスペースを追加しない

  • これが修正前の元コード
export LANG=ja_JP.UTF-8

cat | ruby -pe '
$_.tr!("-","0-9"); 
$_.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2"); 
$_.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2");
'

  • pre タグや blockquote タグ内でスペースを追加しないようにするため、以下のように修正してみた。
export LANG=ja_JP.UTF-8

cat | ruby -pe '
BEGIN{block_point = 0}

block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|<pre *.*>|<blockquote *.*>)/
if block_point == 0 then
  $_.tr!("-","0-9")
  $_.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2")
  $_.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|<\/ *pre>|<\/ *blockquote>)/
'

  • ちなみに BEGIN ブロックよって、while gets ループ手前に、初期化するコードが追加される。
  • また、END ブロックも用意されており、while gets ループ脱出後に、締めのコードが追加される。
ruby -pe 'BEGIN{初期化コード}; 何らかのコード; ... END{締めのコード};'
  • 上記コードは、以下のように展開されるイメージ。
初期化コード

while gets
  何らかのコード; ...
  print $_
end

締めのコード

インライン要素のタグ内でもスペースを追加しない

  • さらなる難題は、インライン要素の特定の範囲でもスペースを追加しないようにすること。
  • 具体的には、半角の<>と[]の内側では、スペースの追加は行わないようにしたい。
  • 悩んだのち、1行ずつ<>と[]の正規表現をマッチさせて、
    • マッチした部分($&)
    • マッチした部分の手前($`)
    • マッチした部分の後方($~.post_match)
  • 以上3つの部分に分けて、地道に繰り返し処理することにした。
export LANG=ja_JP.UTF-8

cat | ruby -ne '
BEGIN{block_point = 0}

r=$_
block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|^<pre *.*>|^<blockquote *.*>)/
if block_point == 0 then
  r = ""
  e = $_
  $_ =~ /(\[.*\]|<.*>)/
  while $`
    s,m,e = $`,$&,$~.post_match
    s.tr!("-","0-9")
    s.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2")
    s.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
    r << s + m
    e =~ /(\[.*\]|<.*>)/
  end
  e.tr!("-","0-9")
  e.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2")
  e.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
  r << e
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|^<\/ *pre>|^<\/ *blockquote>)/
puts r
'

  • ループで重複する条件判定を一つにまとめた。
  • HTMLタグ境界でも、内側のテキストと連続させて判定し、スペースを追加できるように修正した。
export LANG=ja_JP.UTF-8

cat | ruby -ne '
BEGIN{block_point = 0}

r=$_
block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|^<pre *.*>|^<blockquote *.*>)/
if block_point == 0 then
  r = ""
  s, e = "", $_
  while e =~ /(<a *.+?<\/ *a>|<.*?>|\[.*?\])/
    s, m, e = $`, $&, $~.post_match
    s << e[0]
    s.tr!("-","0-9")
    s.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
    s.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
    s.slice!(-1)
    r << s + m
  end
  e = (s[-1] || " ") + e
  e.tr!("-","0-9")
  e.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
  e.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
  e.slice!(0)
  r << e
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|^<\/ *pre>|^<\/ *blockquote>)/
puts r
'

実験

  • 以上のコードを Automator に追加して、text-spacing サービスを作ってみた。

f:id:zariganitosh:20131101085549p:image:h450

  • そして、今書いているこの日記自体を処理してみるのだ。幸運を祈りながら ...。
  • ちなみに、この日記は日本語と英語の間のスペースは、一切入れずに書いた。

____ここからtext-spacing未処理____

  • 若干修正。
  • preタグ、blockquoteタグが、その要素自身の中でマッチして、その後の動作がおかしくなっていた。
    • 「preタグやblockquoteタグ内でスペースを追加しないようにするため、以下のように修正してみた。」の直下のコードが怪しい。
  • そこで、行頭から始まるpreタグ、blockquoteタグのみを対象にするよう修正した。(自分の作業環境では、ほぼ問題ない)
  • よって、preタグ、blockquoteタグが行頭から始まっていないと、その中の日本語と英語の境界にもスペースが追加されてしまう。

その結果が、今の状態。

フォントを修正

その後...

  • アップルのページに習って、英数字はLucida Grandeが優先されるように修正してみた。
body { font-family:'Lucida Grande','Hiragino Kaku Gothic ProN', Meiryo, sans-serif; }
  • もしかしたら、無駄なスペースの挿入なんて止めて、Lucida Grandeで満足できるかもしれない。

またしても、無駄なコードとサービスが増えてしまったか...。

参考ページ

以下のページがたいへん参考になりました。感謝です!


text-spacingサービスで挿入したスペースを取り除く(text-spacing-none)

  • 挿入したスペースは取り除けるようにしておきたいと思ったので。(ほとんどtext-spacingと同じ)
export LANG=ja_JP.UTF-8

cat | ruby -ne '
BEGIN{block_point = 0}

r=$_
block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|^<pre *.*>|^<blockquote *.*>)/
if block_point == 0 then
  r = ""
  s, e = "", $_
  while e =~ /(<a *.+?<\/ *a>|<.*?>|\[.*?\])/
    s, m, e = $`, $&, $~.post_match
    s << e[0]
    s.tr!("-","0-9")
    s.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
    s.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
    s.slice!(-1)
    r << s + m
  end
  e = (s[-1] || " ") + e
  e.tr!("-","0-9")
  e.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
  e.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
  e.slice!(0)
  r << e
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|^<\/ *pre>|^<\/ *blockquote>)/
puts r
'

  • text-spacingとtext-spacing-noneの差分
(master)$ diff -u text-spacing.rb.sh text-spacing-none.rb.sh
--- text-spacing.rb.sh	2013-11-02 15:58:32.000000000 +0900
+++ text-spacing-none.rb.sh	2013-11-02 15:58:26.000000000 +0900
@@ -12,15 +12,15 @@
     s, m, e = $`, $&, $~.post_match
     s << e[0]
     s.tr!("-","0-9")
-    s.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
-    s.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
+    s.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
+    s.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
     s.slice!(-1)
     r << s + m
   end
   e = (s[-1] || " ") + e
   e.tr!("-","0-9")
-  e.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
-  e.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
+  e.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
+  e.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
   e.slice!(0)
   r << e
 end

2013-03-13

チャプターマーカー付きオーディオブックの作り方

オーディオブックとは

  • オーディオブックとは、元々は書籍を朗読した音声データを保存するための、MPEG4オーディオなファイル形式である。
  • 拡張子はm4b。でも、内部のデータ構造は基本的にm4aと変わらないらしい。
  • 但し、m4aと違って、iTunesやiPhoneから操作したとき、以下の機能を備えている。
    • チャプターマーカーによる頭出しができる。
    • iPhoneなどで再生速度を好みの速さに変更できる*1

チャプターマーカーによる頭出し

QuickTime Playerによる頭出し

f:id:zariganitosh:20130313154126p:image:w450

  • command-shift-←・→のショートカットでも、チャプター間を素早く移動できる。
iTunesによる頭出し
  • メニューから頭出し

f:id:zariganitosh:20130313154127p:image:w450


  • オーディオブック ウィンドウから頭出し

f:id:zariganitosh:20130313154125p:image:w450

  • command-shift-←・→のショートカットでも、チャプター間を素早く移動できる。
  • もちろん、iPhoneなどのiOSデバイスに同期すれば、iPhoneなどからも頭出し操作ができるのだ。

f:id:zariganitosh:20130314080041p:image:h300

iPhoneなどで再生速度の変更

  • もう1つの素晴らしい機能が、再生速度の変更である。
  • iPhoneなどのiOSデバイスで再生するとき、1/2×のアイコンをタップするのだ。

f:id:zariganitosh:20130313154130p:image:h300 f:id:zariganitosh:20130313154128p:image:h300 f:id:zariganitosh:20130313154129p:image:h300

  • 語学や情報番組の視聴をする時に、とても重宝しそう。
  • 残念なのは、iTunesからは再生速度を変更できないようだ...。

オーディオブックを作る

  • オーディオブックとするには、拡張子を .m4a から .m4b に変更してからiTunesに取り込んだり、
  • すでにiTunesに取り込まれている曲なら、以下の設定をするだけで、オーディオブックとなる。
    • 情報を見る(command-I) >> オプション >> メディアの種類:オーディオブック
    • 情報を見る(command-I) >> 情報 >> アルバム項目を設定する
      • 忘れずにアルバム項目を設定しておかないと、同期してもiPhoneなどから見つけられなかった...。
  • 但し、上記の簡易的な方法では、チャプターマーカーが埋め込まれない。
    • iPhoneなどで再生速度を変更することはできる。
  • オーディオブックの素晴らしいところは、チャプターマーカだと思っている。
  • チャプターマーカーのないオーディオブックなんて、目次のない本のようなものだ。

Audiobook Binder

  • radikoから予約録音しているJ-WAVEのVisionが溜まってきたので、オーディオブックにまとめてみる。
    • 1日ごとにまとめた.m4aファイルをドラッグ&ドロップして、
    • 登録されたファイル名すべてを選択して、command-S。
    • チャプター分割されるので、チャプター名称を好みに変更。
    • 最後に[Bind]ボタンを押すと、オーディオブックの書き出しが始まった!

f:id:zariganitosh:20130313173232p:image:w450

  • しばらくしてオーディオブックの作成が完了したので、開いてみた。

f:id:zariganitosh:20130313173231p:image:w450

素晴らしい!ちゃんとチャプターマーカー付きのオーディオブックができた!

abbinderコマンド+Automator

  • ところで、ダウンロードしたAudiobook Binderには、abbinderというコマンドも含まれていた。
  • このabbinderコマンドを使うと、オーディオブックを作成するAutomatorサービスが簡単にできるのだ。

f:id:zariganitosh:20130313173233p:image:w450

  • コードの中身は以下の3行。
  • 1行目と3行目は、作業開始と終了を告げる通知なので、正味は真ん中の1行のみ。
    • 作業開始と終了を告げる通知=terminal-notifierコマンドが必要。
/usr/bin/terminal-notifier -title 'bind_to_AudioBook' -message 'running...'
/usr/local/bin/abbinder -E '%t' ~/Desktop/audiobook.m4b "$@"
/usr/bin/terminal-notifier -title 'bind_to_AudioBook' -message 'complete! Put on Desktop!'
  • Finderでオーディオファイルを選択して、「bind_to_AudioBook」を選択するだけでOK。
  • ファイル名がそのままチャプター名となり、オーディオブックが生成されるのだ!
  • オーディオブックはデスクトップに出力される仕様。(~/Desktop/audiobook.m4b)

abbinderコマンド+AppleScript

  • 上記はFinderからファイル名を選択する必要があるが、
  • iTunesの曲を複数選択してオーディオブックを作成したいこともある。
  • それを実現するAppleScriptも作ってみた。

message("bind_to_AudioBook", "処理中です...")
 do shell script "/usr/local/bin/abbinder -E '%t' ~/Desktop/audiobook.m4b " & join(iTunes_selection_path_list(), space)
 message("bind_to_AudioBook", "完了しました!")
 
 
 
 
 on message(title, msg)
   do shell script "/usr/bin/terminal-notifier -title " & quoted form of title & " -message " & quoted form of msg
 end message
 
 on iTunes_selection_path_list()
   tell application "iTunes"
     set sel_list to selection
     set res_list to {}
     repeat with sel in sel_list
       set res_list's end to quoted form of (sel's location as alias)'s POSIX path
     end repeat
   end tell
   res_list
 end iTunes_selection_path_list
 
 on join(sourceList, delimiter)
   set oldDelimiters to AppleScript's text item delimiters
   set AppleScript's text item delimiters to delimiter
   set theText to sourceList as text
   set AppleScript's text item delimiters to oldDelimiters
   theText
 end join

  • 上記スクリプトを ~/Library/Scripts/Applications/iTunes/bind_to_AudioBook.scpt として保存しておくと、
  • iTunesを利用しながら、スクリプトメニューの上の方、操作しやすい位置に表示されるのだ。
  • オーディオブックはデスクトップに出力される仕様。(~/Desktop/audiobook.m4b)

これからはオーディオブックが便利そう!

*1:これはオーディオブックの機能というより、iPhoneなどの再生環境の性能と言えるが。でも、オーディオブックでないと再生速度を変更できない仕様のようだ。

2013-02-22

音楽ファイルから好みの部分を切り出すサービス

以前Visionを連続視聴する時に、ffmpegを使って余分なCMをカットして、聴きたい部分だけの音楽ファイルに編集した。その時はコマンドラインで操作していたのだけど、意外と便利なのでAutomatorサービスに登録してみた。

作業環境

  • MacBook Pro Retina15 OSX 10.8.2
  • ffmpeg
$ ffmpeg -version
ffmpeg version 1.1
built on Jan 10 2013 16:57:58 with Apple clang version 4.1 (tags/Apple/clang-421.11.66) (based on LLVM 3.1svn)
configuration: --prefix=/usr/local/Cellar/ffmpeg/1.1 --enable-shared --enable-gpl --enable-version3 --enable-nonfree --enable-hardcoded-tables --cc=cc --host-cflags= --host-ldflags= --enable-libx264 --enable-libfaac --enable-libmp3lame --enable-libxvid
libavutil      52. 13.100 / 52. 13.100
libavcodec     54. 86.100 / 54. 86.100
libavformat    54. 59.106 / 54. 59.106
libavdevice    54.  3.102 / 54.  3.102
libavfilter     3. 32.100 /  3. 32.100
libswscale      2.  1.103 /  2.  1.103
libswresample   0. 17.102 /  0. 17.102
libpostproc    52.  2.100 / 52.  2.100

Automatorでの作業

f:id:zariganitosh:20130222091521p:image:w450

  • ファイル >> 新規 >> サービス を選択。
    • サービスは次の選択項目を受け取ります:オーディオファイル
    • 検索対象:Finder.app

f:id:zariganitosh:20130222091520p:image:w450

  • AppleScriptを実行のアクションを追加。(ドラッグ&ドロップ)
  • 以下のAppleScriptコードを貼り付けて完成。
    • ファイル名=曲トリム_範囲指定 で保存した。

AppleScriptコード

on run {input, parameters}
   
   (* Your script goes here *)
   set f to input's item 1's POSIX path
   activate
   set res to display dialog "-ss 開始時間 -t 継続時間 を入力してください。" default answer "-ss 00:00:00 -t 09:00:00"
   set opt to res's text returned
   "f=" & quoted form of f & ";/usr/local/bin/ffmpeg -y -i \"$f\"  -acodec copy " & opt & " \"${f%.*}_2.${f##*.}\""
   do shell script result
   
   return input
 end run

使い方

  • 音楽ファイルを選択して二本指タップすると、さっそく上記で登録したサービスが見える!

f:id:zariganitosh:20130222093347p:image:h450


  • 開始時間と継続時間を指定すると、その部分だけを音楽ファイルとして書き出すのだ。

f:id:zariganitosh:20130222093348p:image:w368

  • 継続時間はデフォルトで9時間となっているので、開始時間だけ指定すれば、そこから最後までの音楽ファイルとして保存されるのだ。

これで1ヶ月後の自分も忘れずに使えそう!

2012-11-20

スクリーンショットを最高の1枚に仕上げるサービス

前回以降もshadowコマンドは修正を重ね、現在の仕様は以下のように落ち着いてきた。

Usage: shadow [-a ALPAH_VALUE(0-1)] [-b BLUR_RADIUS(0<=)] [-s SUFFIX] [-owh] [FILE ...]
  -a ALPAH_VALUE    影の透明度(0 <= ALPAH_VALUE <= 1 の少数値、デフォルト=0.5)
  -b BLUR_RADIUS    影のぼけ具合(0 <= BLUR_RADIUS の整数値、デフォルト=8)
  -s 'SUFFIX'       出力画像のファイル名に付加する文字列
  -o                輪郭なし
  -w                同じ画像ファイルに上書きする
  -h                このヘルプを表示する

Example:
  shadow test.png             ->  Default shadow(= shadow -a0.5 -b8 test.png)
  shadow -b4 test.png         ->  Nano shadow
  shadow -b2 test.png         ->  Line shadow
  shadow -b0 -a0 test.png     ->  None shadow
  shadow -b56 -a0.8 test.png  ->  OS X shadow
  shadow test.png -s '-nano'  ->  Output file name is 'test-nano.png'.
  shadow test.png -w          ->  Output file name is 'test.png' .(original is over written)

準備は整った。このshadowコマンドも「イメージを圧縮する」サービスに組み込んで、ブログ用のスクリーンショットを最高の一枚に仕上げるサービスにしてみようと思う。

準備すること

第一階層のサービスにする
  • 二本指クリックのサービスメニューが第二階層になってしまうと、その操作性は著しく低下する。(と感じている)
  • だから、常に第一階層のサービスメニューとして表示されるように、以下のおまじないをしておくのである。
  • OSXデフォルトでは4つ以上サービスが登録されていると、二本指クリックのコンテクストメニューに「サービス」という項目が作成されて、2階層目にサービスが展開されてしまう。
  • 自分にとってサービスメニューはよく使う操作なので、2階層目のメニューを選択するのは、マウスの軌跡を階段状に移動させなくてはならず、煩わしい...。

そこで...

# 99項目を超えるとサブメニューになる(99項目まで第一階層に表示される)
$ defaults write -g NSServicesMinimumItemCountForContextSubmenu -int 99
# 4項目を超えるとサブメニューになる(デフォルト)
$ defaults delete -g NSServicesMinimumItemCountForContextSubmenu

99項目まで第一階層の設定をした。

メッセージ通知
  • terminal-notifierを利用して、イメージの圧縮が完了したら通知するのだ。
  • 何も通知しないと、ちゃんと圧縮できたのかどうか不安になってしまう...。
  • リアクションは大事。
  • イメージを圧縮する.workflowは、terminal-notifierを利用して、メッセージを通知する。
  • terminal-notifierをインストルしておいた方が、より満足度の高いサービスとなるのだ。

terminal-notifierをインストールした。

画像圧縮ツール
  • Retina対応の画像は単純に考えただけで、そのままでは4倍(=縦横2倍)のファイルサイズになる。
  • Retina以前と同じように軽快に表示される環境を維持するためには、何らかの圧縮をするしかない。
  • 圧縮と品質はトレードオフの関係だが、自分の使い方では多くの場合、以下のツールを使う限りそれほど気にならない。
  • 但し、記事の内容によっては、どうしても高品質を維持したい時もある。そんな時だけは、圧縮するのをやめれば良い。

ImageAlphaとImageOptimをインストールした。

影の調整
  • 無駄に広すぎる影の領域を削除するということは、画像サイズを小さくすることにもなるので、高品質を保ったまま圧縮することと同じである。(素晴らしい)
  • 但し、影がまったくない画像というのもなんだかメリハリのない印象になってしまうので、必要最小限の影を残しておいた方が良さそう。

shadowコマンドを開発&インストールした。

スクリーンショットを影なしで撮影
  • shadowコマンドは影付き画像の影も調整できるのだが、その過程で影が混ざる部分のアンチエイリアス処理を削除している。
    • ウィンドウコーナー部分の若干の画質低下が今のところ避けられない。(人によっては気にならないかもしれないが)
  • 一方、影なしの画像に対しては、いったん影を削除する処理を省略できるので、高品質なアンチエイリアスを維持したまま、好みの影に調整できる。
  • OSXデフォルトでもoptionキーを押しながら撮影すれば影なしのスクリーンショットが撮れるのだが、押し忘れや面倒臭さがある。
  • よって、ウィンドウのスクリーンショット(command-shift-4、space)を常に影なしで撮影する設定にしてしまう。
# ウィンドウのスクリーンショットを常に影なしで撮影する
$ defaults write com.apple.screencapture com.apple.screencapture disable-shadow -bool yes
$ killall SystemUIServer

# ウィンドウのスクリーンショットを影ありで撮影する(デフォルトに戻す)
$ defaults delete com.apple.screencapture disable-shadow
$ killall SystemUIServer

常に影なしで撮影する設定にした。

サービスにまとめる

基本的に前回作った「イメージを圧縮する」サービスにshadowコマンドを組み込んだだけなのだが、以下の部分で若干手こずった。

shadowコマンドは二回使う
  • 基本的に影なしの画像を圧縮するはずなのだが、稀に影ありの画像を扱いたいこともあるかもしれない。
  • 影ありのまま画像サイズを指定してしまうと、影のない部分の画像が目指すサイズよりも小さくなってしまう...。
  • 画像サイズを指定する前に影の調整をしてしまうと、画像とともに影も縮小されて、影のイメージも変化してしまう...。
  • そこで、画像サイズを指定する前に、shadowコマンドで影の部分を削除した画像を生成して、その画像に対してサイズを指定している。
  • そしてリサイズ後、もう一度shadowコマンドを実行して、好みの影に調整している。
  • その後、二つの画像圧縮をして、できる限りコンパクトな画像に仕上げている。
    • ちなみに、処理の最後に影を付けると、せっかく圧縮した画像が馬鹿でかいサイズに戻ってしまった。
    • きっと影付けの過程でNSImageに再描画しているので、事前の圧縮が台無しになってしまうのだと思う。

20121119162646

AppleScriptシェルスクリプトが混在する時の注意

今回のようにAutomatorでAppleScriptとシェルスクリプトを混在させて使う時は、次のアクションに渡すファイルパスの形式に注意しておく必要があった。

  • シェルスクリプトは、エイリアス形式であろうが、UNIX形式であろうが、どちらのパスも良きに計らいUNIX形式に変換してくれる。
  • 一方、AppleScriptは、エイリアス形式、UNIX形式、どちらも変換なしにそのまま利用しようとする。
  • すると、エイリアス形式のパスを受け取って処理する仕様のAppleScriptの前にシェルスクリプトを挿入してしまうと、
  • シェルスクリプトは次にUNIX形式のパスを渡すので、AppleScriptが正常に動作しなくなって悩んでしまうのだ...。
  • 同じファイルを示すパスを渡しているのに、形式の違いによって動かなくなってしまうのは著しく不便である。
  • そこで、AppleScript側にもどちらの形式のパスを受け取ってもエイリアスに変換してから処理する仕組みを追加した。

on run {input, parameters}
   set input to alias_list(input)
   (* input = {alias, alias, ...} *)
   
   --以下に必要な処理を続ける
   
 end run
 
 on alias_list(fs)
   set a_list to {}
   repeat with f in fs
     set a_list's end to _alias(f)
   end repeat
   a_list
 end alias_list
 
 on _alias(f)
   try
     tell application "Finder" to (f as alias)
   on error
     tell application "Finder" to (f as POSIX file as alias)
   end try
 end _alias

これで、パスの形式の違いによるエラーで悩む必要がなくなった!

シュルスクリプトでファイルパスを連携させる方法
  • Automatorのサービスとは、選択されたファイルパスあるいはテキストを引数として、ワークフローを実行する仕組みである。
  • 今回のように画像ファイルに対して複数の処理を繰り返す時には、同じファイルパスを次のアクションにも渡してあげる必要がある。
  • AppleScriptなら「input」で受け取ったものを「return input」するだけで用が足りる。
  • では、シェルスクリプトではどうすべきなのだろう?
    • echo "$@"では一つの連続した引数となってしまった。
  • ならばループを回して、必要な回数分echoするようにしてみた。
for f in "$@"; do		echo "$f";		done

これで次のアクションにも確実にファイルパスのリストが渡せた!

サービスをテストする方法
  • サービスは2本指タップから起動できてとても便利なのだけど、開発中にこの方法で起動してテストしても、エラーが発生した箇所が分からなくて困る。
  • エラー箇所を特定するためには、ワークフロー書類の実行ボタンを押して起動する必要があるのだけど、
  • サービスの場合、引数となるファイルを指定する必要があるので、そのままではエラーになってしまう。
  • そんな時は、最初のアクションに「選択されたFinder項目を取得」アクションを追加すればいい。

20121120085259

  • これで、そのとき最前面のFinderウィンドウで選択されているファイルを対象にワークフローを実行できるのだ。

ちなみに、

  • テキストを選択して起動するサービスの場合も同じ。「指定されたテキストを取得」アクションを追加するのだ。
      • 注意:このアクションの場合は、選択する予定のテキストを入力しておく必要がある。

20121120085225

ダウンロード


写真を撮ることは好きだ。今の時期なら、今にも落葉しそうな紅葉した葉っぱと、秋晴れの空の青とのコントラストがじつに感動的に見える。スクリーンショットも同じ。コンパクトな影でキリリと引き締まったスクリーンショットが並んでいると、なんだか嬉しくなるのだ!

      • ところで、前から気になっていたのだけど、画像の下に紫のラインがなぜか表示されてしまう...。
      • これはcssの設定を弄れば非表示にできるのだろうか?一体どうやって削除したら良いのだろう?

2012-11-03

イメージ圧縮してDropboxで公開するサービス

DropboxのPublicフォルダとは

  • 今時のDropboxには、Publicフォルダがある。
  • Publicフォルダに入れたファイルには、「パブリックリンク」と呼ばれるURLが与えられる。
  • そのURLをブラウザに与えれば、画像を表示したり、ファイルをダウンロードしたりできるのである。

  • 「パブリックリンク」として与えられるURLの仕組みを見てみると、以下の要素の組み合わせになっている。
    • Publicフォルダを表現する、ユーザーごとのID番号*1
    • Publicフォルダを起点とする、ファイルパス。
https://dl.dropbox.com/u/ユーザーごとのID番号/ファイルパス

  • もしPublicフォルダ内にimagesフォルダを作って、その中にscreenshot.pngを入れたとしたら、
  • 自分のDropboxでは、以下のURLでアクセスできるようになる。(システム環境設定の画像)
https://dl.dropbox.com/u/2281410/images/screenshot.png

  • そして、上記のURL使ってimgタグを生成すれば、webページに画像が表示される。
  • 幅を450pxに制限することで、Retina対応の画像にしている。(元画像900px)
<img src='https://dl.dropbox.com/u/2281410/images/screenshot.png' width= 450.0 />

イメージをそのままDropboxで公開する

  • 上記一連の操作を素早く実行するAutomatorサービスにしてしまえば、この上ない幸せを感じられるはず。さっそくやってみた。
  • Finderで画像ファイルを選択して、「イメージをそのままDropboxで公開」サービスを実行すると、
    • 画像ファイルは Dropbox/Public/images へ移動する。
    • クリップボードにはimgタグがコピーされた状態になる。
  • 好みの場所にペーストすれば、パブリックリンクをURLとするimgタグの出来上がり。webページに画像が表示されるのだ。

以下、各アクションの詳細

  • 「Finder項目を移動」アクションで、Dropbox/Public/imagesを指定しておく。

--クリップボードにimgタグを生成
 property dropbox_public_url : "https://dl.dropbox.com/u/0000000/images/" --自分のDropbox/Public/imagesへのURLを設定しておく
 
 on run {input, parameters}
   (* input = {alias, alias, ...} *)
   if input = {} then tell application "Finder" to set input to selection
   set the clipboard to join(img_tags(input), return)
   return input
 end run
 
 on img_tags(flist)
   set imgs to {}
   repeat with an_alias in flist
     tell application "Image Events"
       launch
       set myImage to open file (an_alias as text)
       set {w, h} to myImage's dimensions
     end tell
     if w > h then
       set imgs's end to "<img src='" & dropbox_public_url & fname(an_alias) & "' width=" & w / 2 & " />"
     else
       set imgs's end to "<img src='" & dropbox_public_url & fname(an_alias) & "' height=" & h / 2 & " />"
     end if
   end repeat
   imgs
 end img_tags
 
 on fname(f)
   try
     tell application "Finder" to (f as alias)'s name
   on error
     tell application "Finder" to (f as POSIX file as alias)'s name
   end try
 end fname
 
 on join(sourceList, delimiter)
   set oldDelimiters to my text item delimiters
   set my text item delimiters to delimiter
   set theText to sourceList as text
   set my text item delimiters to oldDelimiters
   theText
 end join

--完了のメッセージを通知
 on run {input, parameters}
   (* input = {alias, alias, ...} *)
   message(my name, "Dropboxで公開しました。" & return & join(fname_list(input), ", "))
   return input
 end run
 
 on message(a_title, a_msg)
   try
     "/usr/bin/terminal-notifier " & " -title " & quoted form of a_title & " -message " & quoted form of a_msg
     do shell script result
   on error
     activate
     display dialog a_title & return & a_msg buttons {"OK"} with icon 1 giving up after 5
   end try
 end message
 
 on fname_list(alias_list)
   set a_list to {}
   repeat with an_alias in alias_list
     set a_list's end to fname(an_alias as text)
   end repeat
   a_list
 end fname_list
 
 on fname(f)
   try
     tell application "Finder" to (f as alias)'s name
   on error
     tell application "Finder" to (f as POSIX file as alias)'s name
   end try
 end fname
 
 on join(sourceList, delimiter)
   set oldDelimiters to my text item delimiters
   set my text item delimiters to delimiter
   set theText to sourceList as text
   set my text item delimiters to oldDelimiters
   theText
 end join

インストール1

  • 事前にDropboxをインストールしておく。
  • 事前にDropbox/Public/imagesフォルダを作成する。

  • 「Automatorで開く」ボタンを押す。
  • 「Finder項目を移動」アクションで、上記Dropbox/Public/imagesを設定する。
  • AppleScriptアクション「クリップボードにimgタグを生成」で、1行目のproperty dropbox_public_url: に、自分のパブリックリンクのURLを書き込む。
    • パブリックリンクのURL=Publicフォルダ内のファイルを2本指クリックして、Dropbox >> パブリックリンクのコピー、で確認できる。
  • 一旦、ワークフローを保存して、再び二本指クリックで開く。
  • 「インストール」ボタンを押して、インストールが完了する。

イメージを圧縮してDropboxで公開する

  • さらに、前回のイメージを圧縮する.workflow + 今回のイメージをそのままDropboxで公開.workflowイメージを圧縮してDropboxで公開.workflow にしてみる。

  • Automatorの素晴らしいところは、各アクションの独立性を高めてコーディングしておけば、ほとんど手直しなしで合体や分離できるところ。
  • ファイルパスがalias形式のリストなのか、unix形式のファイルパスのリストなのか、注意しておく必要があった。
    • AppleScript アクションにはalias形式のリストが渡される。
    • シェルスクリプト アクションにはunix形式のファイルパスのリストが渡される。
  • 一旦unix形式のファイルパスになってしまったら、自分でalias形式に変換しない限りunix形式のファイルパスが渡される。
  • シェルスクリプト アクションの後は、たとえAppleScriptアクションが続いていても、unix形式のファイルパスが渡される。
  • AppleScriptアクション側でalias形式・unix形式の両方に対応しておけば、シェルスクリプト アクションが混ざっても問題なく動く。

インストール2

*1:ユーザーごとのID番号は、実際に「パブリックリンクをコピー」してどこかにペーストしてみれば、簡単に確認できる。

2012-11-02

素早く軽快なRetina対応のイメージにするサービス

前回、はてなフォトライフでも十分Retina対応のイメージを出力できると理解したが、一つだけ気がかりがある。それはひと月当たりのアップロード容量が30MBに制限されていること。このブログのスクリーンショットを撮影して、それぞれの画像サイズは以下のようになった。

画像ファイル30MB制限で
アップロードできる枚数
450px122KB245枚
900px388KB77枚
1280px682KB43枚
2758px(元画像)1400KB21枚
  • 450pxと900pxでは、ファイル容量に3倍強の差がある。
  • 30MB制限でアップロード可能な枚数を見ると、245枚から77枚になってしまう...。
  • たいていの月は77枚で足りると思うが、記事の内容によってはそれ以上の容量が欲しいこともあるかもしれない。
  • また、450pxに縛られる必要はないのだから、VGAサイズ(640px)でRetina対応しても良いのかもしれない。
    • 1280pxの画像を用意しておいて、それをimgタグで640pxに縮小表示する。
    • その方が、全画面のスクリーンショットでも細部の文字がつぶれずに判読可能なレベルになった。
  • すると、さらにアップロード可能枚数は減って43枚。これではちょっと足りない気がする。
  • それに、アップロード制限にはなんとか収まったとしても、1画像サイズは3倍強、5倍強の大きさになってしまう。
  • ブラウザで表示した時の画像表示の重さが気になる。
    • iPhone・iPadで閲覧する可能性もあるし、
    • 外出先では高速な回線に繋がらないかもしれない。
    • そもそも、自分が常用する回線自体が低速である。

画像のファイル容量をダイエットする必要がありそう!

PNG画像を圧縮する

  • OSX・iOS環境のスクリーンショットは、自分の環境ではPNG画像で撮影される。
  • スクリーンショットのPNG画像は背景が透明になるので、ブログで利用しやすい。
  • しかし、プレビュー.appで確認しても、JPEGとは違って画質を調整するスライダーがない。
  • 果たしてPNG画像の更なる圧縮は可能なのだろうか?

調べてみると、素晴らしいツールが見つかった!

  • 2つのアプリケーションをダウンロードして、それぞれのファイルに適用してみる。すると...
      • PNG画像をDockのImageAlphaアイコンにドラッグ&ドロップして、

      • ↑デフォルト + 一応 IE6-frendly alpha にもチェックを入れてみた。

      • ↑Save As...の時に、Optimize with ImageOptimにチェックを入れて保存することで、ImageOptimと連携できるのだ。

素晴らしい!すべて元ファイルの1/3以下になった。

画像ファイルImageAlpha&
ImageOptim圧縮
30MB制限で
アップロードできる枚数
450px122KB39KB245枚 → 769枚
900px388KB115KB77枚 → 260枚
1280px682KB195KB43枚 → 153枚
2758px(元画像)1400KB366KB21枚 → 81枚
  • 素晴らしい圧縮率。900pxは、PNG圧縮前の450pxよりも小さくなった!
  • 圧縮前後の画質を確認してみた。(左:圧縮前 / 右:圧縮後)

  • 画像が小さいこともあるが、ほとんど違いに気付かないレベル!
  • 自分が利用するスクリーンショットにおいては、十分な品質だ!
  • ちなみに、圧縮の仕組みは...
    • ImageAlphaは、256色に減色しているようだ。
    • ImageOptimは、何をやっているかよく分からない。(メタデータの削除?)

コマンドから使う

  • GUIのアプリケーションは取っ付きやすいのだけど、ブログを素早く更新するルーチンワークとして使い始めると、一連の操作が一気に面倒くさくなる...。
  • ところが、これらのアプリケーションはAppleScriptには対応していないのだが、コマンドラインから簡単に実行できることが分かった!
    • pngquantについては、オプション設定によって画質の細かな調整も可能。以下のコマンドは一例。
$ /Applications/ImageAlpha.app/Contents/Resources/pngquant -iebug -speed 3 -force -ext .png 256 ファイル名.png
$ /Applications/ImageAlpha.app/Contents/Resources/pngquant --iebug --speed 3 --force --ext .png 256 ファイル名.png
$ open -a ImageOptim.app ファイル名.png

サービスに仕上げる

  • ならば、上記のコマンドをAutomatorのサービスに組み込んでしまえば、二本指クリック一発でPNG画像を圧縮可能になるのだ!


  • それぞれのAppleScriptとシェルコマンドの詳細は、以下のようなコードになっている。

--指定px以上の画像のみリサイズする
 on run {input, parameters}
   (* input = {alias, alias, ...} *)
   set output to {}
   set {num, act} to gets()
   repeat with an_alias in input
     set output's end to resize(an_alias, num, act)
   end repeat
   tell application "Finder" to delete input
   
   return output
 end run
 
 on resize(an_alias, limit, act)
   set {fdir, fname, fext} to parse_file(an_alias)
   set resize_path to fdir & fname & "_" & limit & fext
   tell application "Image Events"
     launch
     set myImage to open file (an_alias as text)
     set {w, h} to myImage's dimensions
     if w > limit or h > limit then
       if limit > 1 then
         scale myImage to size limit
       else if limit > 0 then
         scale myImage by factor limit
       end if
       
       save myImage in file (resize_path) as PNG
       close myImage
       POSIX file resize_path as alias
     end if
   end tell
 end resize
 
 on gets()
   repeat
     activate
     "900  = 900pxにリサイズ
 0.5  = 50%にリサイズ
 空白  = リサイズしない"
     display dialog result default answer "900" with icon 1
     try
       set res to result
       set n to res's text returned as number
       set B to res's button returned
       exit repeat
     end try
   end repeat
   {n, B}
 end gets
 
 on parse_file(fpath)
   if fpath's classtext then set fpath to (fpath as alias)'s POSIX path
   {do shell script "ruby -e \"puts File.dirname(" & fpath's quoted form & ")+'/'\"", ¬
     do shell script "ruby -e \"puts File.basename(" & fpath's quoted form & ", '.*')\"", ¬
     do shell script "ruby -e \"puts File.extname(" & fpath's quoted form & ")\""}
 end parse_file

for f in "$@"; do		echo "$f";		done

/Applications/ImageAlpha.app/Contents/Resources/pngquant --iebug --speed 3 --force --ext .png 256 "$@" >/dev/null 2>&1; exit 0
for f in "$@"; do		echo "$f";		done

open -a ImageOptim.app "$@"

--完了のメッセージを通知
 on run {input, parameters}
   (* input = {alias, alias, ...} *)
   message(my name, "減色&メタ削除で最適化しました。" & return & join(fname_list(input), ", "))
   return input
 end run
 
 on message(a_title, a_msg)
   try
     "/usr/bin/terminal-notifier " & " -title " & quoted form of a_title & " -message " & quoted form of a_msg
     do shell script result
   on error
     activate
     display dialog a_title & return & a_msg buttons {"OK"} with icon 1 giving up after 5
   end try
 end message
 
 on fname_list(alias_list)
   set a_list to {}
   repeat with an_alias in alias_list
     set a_list's end to fname(an_alias as text)
   end repeat
   a_list
 end fname_list
 
 on fname(f)
   try
     tell application "Finder" to (f as alias)'s name
   on error
     tell application "Finder" to (f as POSIX file as alias)'s name
   end try
 end fname
 
 on join(sourceList, delimiter)
   set oldDelimiters to my text item delimiters
   set my text item delimiters to delimiter
   set theText to sourceList as text
   set my text item delimiters to oldDelimiters
   theText
 end join

インストール

  • 「インストール」ボタンを押せば、インストールが完了する。

使い方

  • FinderでPNG画像を二本指クリックすると、サービスメニューが表示される。

  • 「イメージを圧縮する」を選択すれば、指定した画像サイズにリサイズしてから、ImageAlpha&ImageOptim圧縮が始まる。
  • 圧縮後、元画像はゴミ箱に移動される。もし圧縮画像が気に入らなかったら、Finderでcommand-Zすると復活できる。

terminal-notifier

  • イメージを圧縮する.workflowは、terminal-notifierを利用して、メッセージを通知する。
  • terminal-notifierをインストルしておいた方が、より満足度の高いサービスとなるのだ。

第一階層のサービスメニューにする

  • OSXデフォルトでは4つ以上サービスが登録されていると、二本指クリックのコンテクストメニューに「サービス」という項目が作成されて、2階層目にサービスが展開されてしまう。
  • 自分にとってサービスメニューはよく使う操作なので、2階層目のメニューを選択するのは、マウスの軌跡を階段状に移動させなくてはならず、煩わしい...。

そこで...

# 99項目を超えるとサブメニューになる
defaults write -g NSServicesMinimumItemCountForContextSubmenu -int 99
# 4項目を超えるとサブメニューになる(デフォルト)
defaults delete -g NSServicesMinimumItemCountForContextSubmenu

これで、サービスメニュー1クリックでPNG画像がWebページに最適化されるようになった!

サービスにショートカットを設定しておけば、キー操作一発でも圧縮が始まる。快適である。

2012-01-10

あらゆる操作を実行可能マウスカーソルを動かす方法いろいろ

GUIなOS環境では、マウスを操作してカーソルを移動することで、操作対象を選択して、命令を実行する。通常マウスは人の手で操作するのだけど、もしコードで自由に制御できれば、面倒な一連のマウス操作を自動化できるのだ。

但し、人がマウスを操作する時は画面の状況を確認しながら操作できるけど、マウスカーソルを自動制御する時には、そうはいかない。今時のOSXでは、ウィンドウがしまわれていたり、アプリが隠れていたり、Spacesでスペースがいくつもあったりと、考慮しておくことが多くて大変なのだ。

一方、マウスを操作して実行する命令には、大抵ショートカットが用意されていたり、同等のコマンドがあったりする。AppleScriptでも操作可能かもしれない。可能な限りマウスカーソルの自動制御以外の方法で操作した方が、GUIの状況に左右されず、素早く、確実に、命令が完了するはずである。

そうは言っても、マウス以外の操作方法が見当たらないことも多々ある。あるいは、GUIスクリプティングがどうにも上手く動いてくれないとか、コードの書き方がよくわからないとか。そんな時、マウスカーソルを自動制御する方法を知っていれば、かなり楽して幸せになれる。マウスカーソルをコードで制御するのは、冗長で、環境にも思いきり依存し、そこでしか使えない限定的なツールになりがちだが、覚えておいて損はないと思われる。調べてみた。

AppleScriptの click atコマンド

  • 以下のように click atコマンドを使うことで、目標の座標(x=36, y=33)がクリックされる。
    • 座標(x=36, y=33)は、AppleScriptエディタのウィンドウのオレンジボタン。
    • 実行すると、Dockにウィンドウがしまわれる。(アニメーション付き)
  • 但し、目標の座標をクリックするイベントが発生するだけで、現在のマウスカーソルの位置は変化しない。
  • 目標の座標は、画面左上を原点(0, 0)とした絶対座標で指定する。
    • MacBook13インチの場合は、画面右下(1280, 800)までの範囲となる。
  • ちなみに、絶対座標は、スクリーンショットcommand-shift-4のショートカットで簡単に計測できる。

 tell application "System Events"
   tell process "AppleScript Editor"
     click at {36, 33}
   end tell
 end tell

Automatorで記録する

  • Automatorの「記録」ボタンを押すと、簡単にGUI操作がワークフローとして記録される。
  • 記録したワークフローは、「実行」ボタンを押すことで再生される。
  • 以下のワークフローは、アップルメニューのクリックを記録したもの。

f:id:zariganitosh:20120110144825p:image

  • 再生速度を×10にしておくと、ほとんど待ち時間なく、テキパキと実行してくれる。
  • マウス操作を記録したワークフローを実行すると、現在のマウスカーソルの位置がワークフローに従って動く。
  • ちなみに、記録されたイベントを、ワークフローエリアにドラッグ&ドロップすると、AppleScriptに変換される。
  • 未知のGUIスクリプティングの記述方法を知る便利な方法でもある。

cliclickコマンド

$ cliclick -h

 cliclick - Command Line Interface Click
 Version 1.3.1
 Carsten Bluem, 2010-04-13
 http://www.bluem.net/en/mac/cliclick/

 Usage: cliclick [-v] [-r] [-q] [-w n] x y [x2 y2] [x3 y3] [...]
   x and y are integer numbers which specify the screen coordinate(s)
   where the mouse click(s) should be emulated. (Upper left corner is 0 0)
   Instead of a number, you may pass "m" as x and / or value to
   use the current x and / or y position.

   If you need a doubleclick, prefix the x coordinate with "d".
   If you need a control click, use prefix "c".

 Options:
   -w <n> You can pass multiple coordinate pairs as argument. But if you do,
          it is often useful to have a small delay between events -- that is
          what the -w option is for: It will cause cliclick to wait for the
          specified number of milliseconds after each event.
   -q     Instead of clicking, print the current mouse pointer.
          location on the screen (format "x,y") and exit.
   -r     Restore initial mouse location after performing the clicks.
   -v     Makes cliclick more verbose.

 Examples:
   'cliclick 26 12' will click the apple menu
   ((x=26, y=12)の座標をクリックする。つまり、アップルメニューをクリックする。)

   'cliclick 50 60 c70 80' will click at 50/60, then Control-click at 70/80
   (cに続く座標はControl-click、つまり右クリックと同等になる。)

   'cliclick d50 60' will doubleclick at 50/60
   (dに続く座標はダブルクリックになる。)

   'cliclick dm m' will doubleclick the current mouse location
   ((m,m)は現在のマウスカーソルの座標、と読まれる。)

   'cliclick c500 m' will control-click at x position 500 and the mouse's current y position.
   (500,m)は、x=500、y=現在のy座標、と読まれる。)

   'cliclick -w 50 26 11 26 33' will open the "About this Mac" panel
   (-wに続く数値はミリ秒指定。連続クリックする時の間隔が指定される)

   'cliclick -r 26 12' will click the apple menu and, afterwards, restore the initial mouse location.
   (-rオプションは、cliclickコマンド実行後、実行前のマウスカーソル位置に戻す)

   'cliclick -q' will print the current mouse location.
   (-qオプションは、現在のマウス座標を返す)
$ cliclick -q
317,515
  • cliclickコマンドでは、クリックの連続技を簡潔なワンライナーで記述できるところが好き。

クリックせずにカーソルだけ移動する

  • 上記はすべてマウスクリックの操作だったが、クリックせずにマウスカーソルの移動のみは出来ないのだろうか?
MacRuby版
  • OSX標準搭載のMacRubyを利用することで、以下のようにAppleScriptから操作できた。
    • OSX標準搭載は、MacRubyでなく、CocoaRubyだった。
    • MacRubyを利用するためには環境のインストールが必要。
  • Re: Move cursor with Applescript(サンプルコードの参考ページ。感謝です!)

 do shell script "/usr/local/bin/macruby -e \"framework 'carbon'\" -e \"framework 'ApplicationServices'\" -e \"CGWarpMouseCursorPosition(CGPointMake(" & 26 & "," & 12 & "))\""

  • 上記AppleScriptは、マウスカーソルを座標(x=26, y=12)、つまりアップルメニュー アイコンの真ん中に移動するのだ。
  • 以下のようにmove_mouseハンドラとして登録しておけば、使いやすいかもしれない。

 move_mouse(26, 12)
 
 on move_mouse(x, y)
   do shell script "/usr/local/bin/macruby -e \"framework 'carbon'\" -e \"framework 'ApplicationServices'\" -e \"CGWarpMouseCursorPosition(CGPointMake(" & x & "," & y & "))\""
 end move_mouse


CocoaRuby版(あるいはRubyCocoaと呼ばれる)

 do shell script "/usr/bin/ruby -e \"require 'osx/cocoa'\" -e \"OSX::CGWarpMouseCursorPosition(OSX::CGPointMake(" & 26 & "," & 12 & "))\""

 move_mouse(26, 12)
 
 on move_mouse(x, y)
   do shell script "/usr/bin/ruby -e \"require 'osx/cocoa'\" -e \"OSX::CGWarpMouseCursorPosition(OSX::CGPointMake(" & x & "," & y & "))\""
 end move_mouse


マウスボタンを押す・放す

  • 「クリックせずにカーソルだけ移動する」方法では、確かにマウスカーソルは移動するが、実際に手でマウスを握って動かすのと状況は異なる。
  • move_mouse()を使ってメニュー項目上に移動しても、ハイライト表示もされず、サブメニューも表示されないのだ。
  • おそらくCGWarpMouseCursorPositionは、マウスカーソルを特定の位置にセットするだけで、マウスを動かしたというイベントは発生させないのだ。
  • キー操作でメニュー選択をする方法もあるが、フルキーボードアクセスを有効にしても操作できないメニュー項目もあったりする。
    • メニューバー右側のアイコンメニューの中に、フルキーボードアクセスでは届かない項目が存在することが多い。
  • cliclickコマンドを利用しても良いのだが、クリックする一歩手前で項目をハイライトした状態で停止させておきたい需要もある。

こうなったら、マウスボタンのキーダウン・キーアップをシミュレートするしかない!

 --左のマウスボタンをダウン(押し下げ)
 left_mouse_down(26, 12)
 left_mouse_down(46, 174)
 left_mouse_down(370, 61)
 (*
 --間隔を空けてマウスボタンをアップ(放す)すればクリックになる
 delay 0.5
 left_mouse_up(370, 61)
 *)
 
 (*  
 --右クリック
 right_mouse_down(261, 527)
 delay 0.1
 right_mouse_up(261, 527)
 *)
 
 on left_mouse_down(x, y)
   set cocoa_ruby to "require 'osx/cocoa'; event=OSX::CGEventCreateMouseEvent(nil, 1, OSX::CGPointMake(" & x & "," & y & "), 0); OSX::CGEventPost(0,event);"
   set pt to do shell script "/usr/bin/ruby -e " & quoted form of cocoa_ruby
 end left_mouse_down
 
 on left_mouse_up(x, y)
   set cocoa_ruby to "require 'osx/cocoa'; event=OSX::CGEventCreateMouseEvent(nil, 2, OSX::CGPointMake(" & x & "," & y & "), 0); OSX::CGEventPost(0,event);"
   set pt to do shell script "/usr/bin/ruby -e " & quoted form of cocoa_ruby
 end left_mouse_up
 
 on right_mouse_down(x, y)
   set cocoa_ruby to "require 'osx/cocoa'; event=OSX::CGEventCreateMouseEvent(nil, 3, OSX::CGPointMake(" & x & "," & y & "), 0); OSX::CGEventPost(0,event);"
   set pt to do shell script "/usr/bin/ruby -e " & quoted form of cocoa_ruby
 end right_mouse_down
 
 on right_mouse_up(x, y)
   set cocoa_ruby to "require 'osx/cocoa'; event=OSX::CGEventCreateMouseEvent(nil, 4, OSX::CGPointMake(" & x & "," & y & "), 0); OSX::CGEventPost(0,event);"
   set pt to do shell script "/usr/bin/ruby -e " & quoted form of cocoa_ruby
 end right_mouse_up

  • AppleScriptからマウスボタンを押す・放す、という非常に低レベルなイベント操作まで出来てしまうのであった!
  • さらに、以下のページで紹介されているmouseClickハンドラは、あらゆるマウスイベントを発生させる。
  • mouseClickハンドラを活用すれば...
    • commandキーやoptionキーなども併用し、
    • ダブルクリック・ドリプルクリックも簡単に行えるのだ!
    • しかも、現在のマウスカーソル位置を活用することもできる!

素晴らし過ぎる!AppleScriptからここまで出来るとは!感動した!

  • 中身は、cocoaRubyを使ってCocoaの機能を利用しているのだけど、
  • AppleScriptとして配布すれば、OSX10.6標準の環境で実行できてしまうのである。
  • 余分なインストールなしに、そのまま実行できてしまう所が最大の魅力だと思う。

カーソルの座標を取得する

  • マウスカーソルを操作していると、現在のカーソル位置を活用したくなることがよくある。そんな時に...

 mouse_position()
 --mouse_position()'s x
 --mouse_position()'s y
 --mouse_position() as list
 
 --マウスカーソル位置{x:100, y:100}を返す
 on mouse_position()
   set cocoa_ruby to "require 'osx/cocoa'; pt=OSX::NSEvent.mouseLocation; puts pt.x, OSX::NSScreen.mainScreen.frame.size.height-pt.y;"
   set pt to do shell script "/usr/bin/ruby -e " & quoted form of cocoa_ruby
   {x:pt's paragraph 1 as number, y:pt's paragraph 2 as number}
 end mouse_position

  • もはや、NS(NextStep)系の古いオブジェクトより、最新のCG(コア・グラフィック?)系のオブジェクトを利用するべきかもしれない。

 --別解
 getMouseLocation()
 
 on getMouseLocation()
   set theRubyScript to "require 'osx/cocoa'; pt=OSX::CGEventGetLocation(OSX::CGEventCreate(nil)); puts pt.x, pt.y"
   set thePtText to do shell script "/usr/bin/ruby -e " & quoted form of theRubyScript
   {paragraph 1 of thePtText as number, paragraph 2 of thePtText as number}
 end getMouseLocation

AppleScriptであれこれする

マウスカーソルを制御して幸せな環境を作ろう!

2010-10-14

Amazonの商品発送メールに反応して配送状況をSafariで表示するワークフローとAppleScript

今の自分のMacBookは、ルールで賢く仕事するMacBookにしておいたので、購入先から商品発送しましたメールが届くと、自動的にSafariが宅配便の照会ページを開いてくれる。

多くのショップが伝票No.と照会ページのリンクを別々に記載している状況では、

  • 配送状況を照会するために伝票No.をコピーして、
  • 照会ページに移動して、
  • しかるべき場所に伝票No.をペーストする。

という手順が必要で、煩わしいと思っていた。だから個人的にはこの自動環境をかなり気に入っている。メールに配送伝票No.と宅配便会社名が記載されていれば、メールを受信するだけでSafariの照会ページが表示されるのだから。(たまには間違えて、関係ない番号を照会してしまうこともあるけど...。)

ところで、この宅配便の自動照会ルールは、Amazonの配送には反応しない。そもそも自動照会ルールを作ったきっかけが、メール本文中のリンクをクリックしただけでは配送伝票を照会できないことに不便を感じたから。Amazonの場合はリンクをクリックしていけば、どうにか照会ページには辿り着ける。

しかし、自動照会ルールの恩恵に慣れてしまった今、Amazonでの照会リンクのクリックさえ面倒に感じてきた。Amazonで配送状況を確認する場合、たぶん以下の手順が必要になると思う。

  • アカウントサービスを開いて、
  • 購入履歴を見る、
  • サインインする、
  • 注文履歴から目指す商品の、配送状況を確認する、
  • さらに詳細を確認したければ、配送状況の詳細を確認する。

最大5回のクリックが必要。一方、自動照会ルールなら、すでに照会ページは開いている。この差は大きい...。最も頻繁に利用するAmazonの照会が自動化されなくては、自分にとって快適な通販生活はあり得ないのだ...。(笑)

Amazonも自動照会ルールに組み込むべく、試行錯誤してみた。

宅配便会社の照会ページ

  • Amazonはどこかの宅配便会社を利用している訳で、配送方法の履歴を調べてみると以下のようになった。
    • 配送方法:カトーレック便
    • 配送方法:佐川飛脚便
    • 配送方法:佐川メール便
    • 配送方法:JPX
    • 配送方法:お急ぎ便ヤマト
    • 配送方法:日通ペリカン便
    • 配送方法:Pelican
    • 配送方法:Sagawa Mail
  • 最近は、ほとんどがカトーレック便と佐川メール便。
  • JPXはJPエクスプレスのことだろうか?
  • であれば、郵便事業株式会社に吸収合併されているので、今後は日通ペリカン便・Pelicanを含めて配送方法としては存在しなくなるはず。

以上のことを考慮して、以前作った自動照会ルールに追加を試みた。しかし...

  • 佐川メール便については...
    • 佐川のページでは照会できなかった。(お問い合わせのデータは登録されておりません)
    • そもそも、佐川メール便は照会できない仕様らしい。これは今時とっても不便に感じる。最低でも、配送が完了したかどうか(ポストに投函されたかどうか)くらいは知りたい。
  • カトーレックについては...

カトーレックのページで照会するAppleScript

  • しょうがないので、カトーレックの照会ページで、指定した伝票No.を検索するスクリプト。
  • search_katolec()は、AppleScriptからJavaScriptを実行して、照会ページを操作している。

 (* 照会_カトーレック *)
 my search_katolec("771011888888")
 
 on search_katolec(num)
   tell application "Safari"
     tell window 1 --タブを操作するためにはwindowを指定する必要あり
       {URL:"http://www6.katolec.com/tracking/amzn/tracking.aspx"}
       set current tab to make new tab with properties result
     end tell
     delay 1 --新規タブがロードされる待ち時間が必要
     do JavaScript "
   var element=document.getElementById('inputDenpyo');
   element.value=" & num's quoted form & ";
   document.trackingForm.submit();
   " in document 1 --現在アクティブなページ(タブ)でjavascriptを実行する
   end tell
 end search_katolec

漏れなく照会するための作戦

  • 宅配便会社の照会ページだけに頼る方法では、佐川メール便のように照会できない仕様だったりして、不便を感じることもある。

ここで、Amazonで購入した商品の配送状況を追跡するベストな手段を見つけ出すために、照会できる・できない条件を見直してみた。

  • Amazon自身が発送する商品は...
    • Amazonの配送状況の確認ページで必ず照会できる。
    • 宅配便会社のページでは、照会できる場合と、できない場合がある。
    • 例:カトーレック=照会OK、佐川メール便=照会NG。
    • 商品発送メールの件名:Amazon.co.jp ご注文の発送 (123-1234567-1234567)
  • Amazonマーケットプレイスの出品者が発送する商品は...
    • Amazonの配送状況の確認ページでは照会できない。
    • 宅配便会社のページなら照会できる。
    • 商品発送メールの件名:Amazon.co.jp ご注文商品の発送 (#123-1234567-1234567)
    • 商品発送メールは、Amazon・出品者の両方から通知され、どちらにも伝票No.が記載されている。

  • つまり、Amazon自身が発送する商品だけAmazonの配送状況の確認ページを利用すれば良さそう。
  • Amazonマーケットプレイスの出品者が発送する商品は、出品者も商品発送メールで通知してくるので、現状の自動照会ルールで対応可能。
  • Amazon自身かマーケットプレイス出品者かは、商品発送メールの件名で区別できる。
    • Amazon.co.jp ご注文の発送 (123-1234567-1234567) ...... Amazon自身
    • Amazon.co.jp ご注文商品の発送 (#123-1234567-1234567) ...... マーケットプレイス出品者
  • マーケットプレイス出品者の場合、太字の部分が付加されていた。

Amazonのページで照会する作戦

  • 何はともあれ、伝票No.からAmazonの配送状況の確認ページを開くスクリプトが必要になる。
配送状況を確認するURL
  • 太字の部分がパラメーターとして、条件によって変化する値。
https://www.amazon.co.jp/gp/css/history/orders/view.html/ref=oss_track_no_js?trackShipmentID=DD7XXXXXX&trackOrderID=123-1234567-1234567#tracking-DD7XXXXXX
配送状況の詳細を確認するURL
  • 配送状況の詳細を確認するページは開けるが...
    • 詳細ページでは、商品名が載っていない。
    • 発送日が常に1970/1/1になってしまう。
    • latestArrivalDate・shipmentDateもちゃんと設定すれば改善されると思う。
    • しかし、その値はどこから引っ張ってくるのだろう?
https://www.amazon.co.jp/gp/css/shiptrack/view.html?addressID=npllsXXXX&trackingNumber=123456789012&latestArrivalDate=0000000000&shipMethod=SAGAWA_MAIL&orderID=123-1234567-1234567&ref=&shipmentDate=0000000000

  • 手っ取り早く目的を達成するためには、Amazonの注文番号から配送状況を確認するURLを生成するスクリプトを作れば良さそう。
  • しかし、それでは自分のMacBook以外の環境では役に立たないスクリプトになってしまう可能性がある。
    • trackShipmentID・addressIDなどが、ユーザーや利用マシンによって変化する可能性があるので。
  • たった1台のマシン専用のコードを書くというのは、コードを書く人として、できる限り避けたい、という思いがある。
  • それにしても、Amazonの配送状況の確認をするためのパラメーターは多い。伝票No.だけでOKな宅配便会社とは対照的である。
  • おそらく、Amazonではログインして、リンクを辿って照会することを想定している。(URLでダイレクトに照会することは想定されていないのだ)
  • であれば、アカウントサービスのページからリンクを辿る手作業を忠実に再現した方が、あらゆる環境で利用するには都合が良さそうな気がしてきた。
  • MacBookの中には、OSX標準でAutomator君が居る。
  • Automator君でGUIの操作を記録して、再現させれば、ほとんどのことは自動操作可能になる。
  • 早速やってみた。

Automatorで記録した操作をAppleScriptに変換する

  • 例えば、「注文履歴を見る」ボタンを押す操作をAutomatorで記録して、ドラッグ&ドロップすると...

f:id:zariganitosh:20101015102258p:image

  • 以下のように、「注文履歴を見る」ボタンを押す操作がAppleScriptに展開されるのだ。

f:id:zariganitosh:20101015102259p:image

  • 「注文履歴を見る」ボタンを押す操作自体は、おそらく以下の部分のコードとして表現されていると想像できる。
    • click image 1 of UI Element 30 of UI Element 1 of scroll area 1 of group 3 of window 1 of application process "Safari"
  • しかし、このコードをそのまま実行したのでは、うまくボタンが押されないことが多い。
  • 幾度か試行錯誤していると、普遍的な部分と、条件によって変化する部分があることに気付く。
  • どうも、UI Element 30の部分が、実行の度にUI Element 26, 31, ...などに変化しているのだ。
  • なぜ変化するのか、どんな条件でその値になるのかは分からない。
  • でも、変化するのであればその部分をすべて調べて、目指す「注文履歴を見る」ボタンの時だけクリックするようにしてみた。
  • このページでは「注文履歴を見る」ボタンはたった1つしかないので、その他の無効なオブジェクト参照はすべてエラーになる。

 tell application "System Events"
   tell UI element 1 of scroll area 1 of group 3 of window 1 of application process "Safari"
     repeat with a_element in UI elements
       tell a_element
         try
           if image 1's description is "注文履歴" then
             click image 1
           end if
         end try
       end tell
     end repeat
   end tell
 end tell

  • これで、UI Element 30の部分が変化しても、確実に「注文履歴を見る」ボタンを押せるようになった。

  • ところで、webページを完全に表示するまでには、読み込むための時間が必要である。
  • 読み込み途中で上記スクリプトを実行しても、「注文履歴を見る」ボタンがまだ存在しなかったりして、エラーになる。
  • エラーの1秒後に完全なアカウントサービスのページが表示されても、「注文履歴を見る」ボタンは永久に押されないのだ。
  • そんな不運な状況を回避するための単純な方法としては、適当な待ち時間を作って、その後実行する手順が考えられる。
    • 具体的には、1行目に Delay 3 を追記して、3秒経過するのを待って、その後「注文履歴を見る」ボタンを押すのだ。
  • しかし、適当な待ち時間は3秒で良いのだろうか?もしかしたら、4秒必要かもしれない。あるいは2秒でも十分かもしれない。
  • 待ち時間が不足すればエラーになり、過剰であれば無駄な時間にイライラすることになる。
  • また、同じMacBookでも、その時のCPUの負荷や、回線の状況によって、最適な待ち時間は変化するはずだ。
  • だから固定的に Delay 3とするのは、あまりおすすめの方法とは言えない。
  • 現状のベストな方法は、Automator自身が生成するGUIスクリプティングなAppleScriptにあった。
    • Automatorで操作を記録して、ドラッグ&ドロップして生成されるAppleScript。
  • 最大の待ち時間を決めて、その待ち時間内はエラーが発生しても何度でも再試行する方法である。
  • それを参考に、以下のようにしてみた。

 on run {input, parameters}
   
   (* Amazon注文履歴を見る *)
   delay 1
   repeat 4 times
     delay 1
     if my job() then exit repeat
   end repeat
   
 end run
 
 on job()
   do shell script "/usr/local/bin/growlnotify -m '注文履歴を見る...' "
   try
     tell application "Safari" to set sign_in_url to document 1's URL
     if sign_in_url begins with "https://www.amazon.co.jp/gp/css/homepage.html" then
       tell application "System Events"
         tell UI element 1 of scroll area 1 of group 3 of window 1 of application process "Safari"
           repeat with a_element in UI elements
             tell a_element
               try
                 if image 1's description is "注文履歴" then
                   click image 1
                   return true
                 end if
               end try
             end tell
           end repeat
         end tell
       end tell
     end if
   end try
   false
 end job

  • 最初に1秒待って、その後1秒おきに4回、再試行するのである。

これで無駄な時間を待たずに、かなり確実に「注文履歴を見る」ボタンを押せるようになった!

Amazonのページで照会するワークフロー

以上の要領で、必要な操作をアップルスクリプトに生成して、並べてみると以下のようになった。

アカウントサービスを開く

f:id:zariganitosh:20101015102246p:image

注文履歴を見る
  • すると、ページはこんな状態。

f:id:zariganitosh:20101015102247p:image

  • 「注文履歴を見る」ボタンを押す。

f:id:zariganitosh:20101015102248p:image

サインイン
  • すると、サインインを求められる。

f:id:zariganitosh:20101015102249p:image

  • ユーザー名とパスワードの自動入力を有効にしてあるので、自分のMacBookではすでに入力済みの状態になっている。
  • 「サインイン」ボタンを押す。

f:id:zariganitosh:20101015102250p:image

配送状況を確認する
  • すると、注文履歴のページへ移動する。

f:id:zariganitosh:20101015102251p:image

  • 目指す商品の「配送状況を確認する」ボタンを押す。

f:id:zariganitosh:20101015102252p:image

f:id:zariganitosh:20101015102253p:image

  • 実際にはボタンを押さずに、リンク先アドレスにジャンプしている。
  • 忠実にボタンを押す操作だと、JavaScriptによるページ遷移なしの処理になる。
  • すると、次の「配送状況の詳細を確認する」ページから戻った時に、配送状況の表示なしの状態に戻ってしまうのだ。
  • 一方、ページ遷移を伴うリンク先アドレスにジャンプする方法なら、戻った時の配送状況の表示も保持されるのだ。
  • また、ページ遷移を伴うリンク先アドレスにジャンプする方法だと、配送状況が見える位置まで自動的にスクロールしてくれるという利点もある。

  • 以下の操作は余分かもしれない。必要に応じて、自分で「配送状況の詳細を確認する」ボタンを押せば良いのだから。
  • しかし、AppleScriptで自動操縦する方法の記録として、せっかくなのでメモとして残してしまった。
配送状況の詳細を確認する
  • すると、配送状況が追記表示される。

f:id:zariganitosh:20101015102254p:image

  • さらに「配送状況の詳細を確認する」リンクも押す。

f:id:zariganitosh:20101015102255p:image

ブラウザの戻る操作
  • すると、配送状況の詳細ページへ移動する。

f:id:zariganitosh:20101015102257p:image

  • このままでは商品名が分からないので、一旦ブラウザの戻る操作を実行する。

f:id:zariganitosh:20101015102256p:image

  • これで、ブラウザの戻る・進む操作で、詳細ページとの間を素早く行き来できるのだ。

以上のワークフローは、「Amazon配送状況の詳細を確認する.workflow」として保存した。

ルールに組み込む

  • 以前の宅配便の自動照会のルールに、Amazonの処理を追加して、以下のようにしてみた。

 property LIB : load script file ((path to scripts folder as text) & "_lib.scpt")
 
 --Mail.appのルールで駆動する
 using terms from application "Mail"
   on perform mail action with messages theMessages for rule theRule
     tell application "Mail"
       repeat with aMessage in theMessages
         set body to LIB's replace(aMessage's content, "'", "’")
         set title to LIB's replace(aMessage's subject, "'", "’")
         try
           if my delivery_com(body) is "Amazon.co.jp" then
             set the clipboard to my order_id(title)
             do shell script "open -b com.apple.AutomatorRunner $HOME/Library/Scripts/Amazon配送状況の詳細を確認する.workflow"
           else
             open location my query_url(my slip_no(body), my delivery_com(body))
           end if
         end try
       end repeat
     end tell
   end perform mail action with messages
 end using terms from
 
 --メール本文から伝票No.を抽出する(ruby正規表現を利用)
 --数字が12桁、あるいは4-4-4桁のパターンを抽出する(13桁以上の数字に含まれる場合は除外する)
 --    例:123456781234
 --    例:1234-5678-1234
 on slip_no(str)
   LIB's do_ruby_jcode_u({"/\\D\\d{12}\\D|\\D\\d{4}\\-\\d{4}\\-\\d{4}\\D/ =~ " & quoted form of str, "$~"})
   LIB's do_ruby_jcode_u({"/\\d{12}|\\d{4}\\-\\d{4}\\-\\d{4}/ =~ " & quoted form of result, "$~"})
 end slip_no
 
 on order_id(str)
   LIB's do_ruby_jcode_u({"/\\D\\d{3}\\-\\d{7}\\-\\d{7}\\D/ =~ " & quoted form of str, "$~"})
   LIB's do_ruby_jcode_u({"/\\d{3}\\-\\d{7}\\-\\d{7}/ =~ " & quoted form of result, "$~"})
 end order_id
 
 --メール本文から宅配便の会社を調べる
 on delivery_com(str)
   set delivery_names to {"Amazon.co.jp", "クロネコ", "ヤマト", "JPエクスプレス", "佐川", "sagawa", "ゆうバック"}
   repeat with a_name in delivery_names
     if a_name is in str then
       return a_name as text
     end if
   end repeat
 end delivery_com
 
 --伝票No.と宅配便の会社名から検索URLを生成する
 on query_url(slip, company)
   try
     "{" & ¬
       "|クロネコ|:\"https://jizen.kuronekoyamato.co.jp/jizen/servlet/crjz.b.NQ0010?id=\"," & ¬
       "|ヤマト|:\"https://jizen.kuronekoyamato.co.jp/jizen/servlet/crjz.b.NQ0010?id=\"," & ¬
       "|西濃|:\"https://track.seino.co.jp/cgi-bin/gnpquery.pgm?GNPNO1=\"," & ¬
       "|カンガルー|:\"https://track.seino.co.jp/cgi-bin/gnpquery.pgm?GNPNO1=\"," & ¬
       "|佐川|:\"https://k2k.sagawa-exp.co.jp/p/web/okurijosearch.do?okurijoNo=\"," & ¬
       "|sagawa|:\"https://k2k.sagawa-exp.co.jp/p/web/okurijosearch.do?okurijoNo=\"," & ¬
       "|JPエクスプレス|:\"https://info.jpexpress.jp/confirm/confirmList.html?denpyoNo=\"," & ¬
       "|ペリカン|:\"https://info.jpexpress.jp/confirm/confirmList.html?denpyoNo=\"," & ¬
       "|ゆうパック|:\"http://tracking.post.japanpost.jp/service/singleSearch.do?searchKind=S002&locale=ja&SVID=023&reqCodeNo1=\"," & ¬
       "|福山通運|:\"http://www4.fukutsu.co.jp/inq/INQJS120?toino=&toino=&toino=&toino=&toino=&toino=&toino=&toino=&toino=&toino=\"," & ¬
       "|書留|:\"http://tracking.post.japanpost.jp/service/singleSearch.do?searchKind=S002&locale=ja&SVID=023&reqCodeNo1=\"," & ¬
       "|翌朝10時郵便|:\"http://tracking.post.japanpost.jp/service/singleSearch.do?searchKind=S003&locale=ja&SVID=023&reqCodeNo1=\"," & ¬
       "|EMS|:\"http://tracking.post.japanpost.jp/service/singleSearch.do?searchKind=S004&locale=ja&SVID=023&reqCodeNo1=\"" & ¬
       "}'s"
     set hash_code to result
     run script hash_code & "|" & company & "|"
     result & slip
   end try
 end query_url


  • 変更箇所は以下の部分。Amazonであれば、上記の「Amazon配送状況の詳細を確認する.workflow」を実行するのだ。
    • 現状は、Amazon.co.jpと注文番号を含むすべてのメールを照会する仕様になってしまっている。
    • 本来は、メールの件名から商品発送元が、Amazonであるか、マーケットプレイス出品者であるか判定して、無駄な照会を避けるようにするべき。(あとでやる)
  • AppleScriptからAutomatorへのAmazonの注文番号のやり取りに、クリップボードを利用している。
    • だから、コピー作業の真っ最中に、Amazonメールに反応してしまうと、ペーストした結果がAmazonの注文番号になってしまうのだ...。

...(中略)...
 if my delivery_com(body) is "Amazon.co.jp" then
   set the clipboard to my order_id(title)
   do shell script "open -b com.apple.AutomatorRunner $HOME/Library/Scripts/Amazon配送状況の詳細を確認する.workflow"
 else
   open location my query_url(my slip_no(body), my delivery_com(body))
 end if
...(中略)...

  • また、Amazonの注文番号を抽出するハンドラも追加した。

 on order_id(str)
   LIB's do_ruby_jcode_u({"/\\D\\d{3}\\-\\d{7}\\-\\d{7}\\D/ =~ " & quoted form of str, "$~"})
   LIB's do_ruby_jcode_u({"/\\d{3}\\-\\d{7}\\-\\d{7}/ =~ " & quoted form of result, "$~"})
 end order_id


  • 実行するには、ユーザ・スクリプト・フォルダに、_LIB.scptもインストールしておく必要あり。

Mail.appのルール

  • Mail.appのルールは以下のようになっている。

f:id:zariganitosh:20101015150302p:image

f:id:zariganitosh:20101015145916p:image


これで快適な通販生活になりそうな予感!

  • 意味もなく、何かAmazonで買ってみたくなってきた...。

  • どうやら「配送状況を確認する」ボタンを押す操作が調子悪い...。(以下の部分)

f:id:zariganitosh:20101015102252p:image

f:id:zariganitosh:20101015102253p:image

  • 調べてみると、「WebページからリンクURLを取得」アクションで、URLの一部しか取得できないことがある。
  • しかし、何度か試していると、ちゃんとすべてのURLを取得できることもある。
  • Automatorのアクション自体に問題があるのかもしれないが、原因は分からない。
  • 「WebページからリンクURLを取得」アクションに頼らない方法を考えてみた。

 on run {input, parameters}
   
   (* クリップボードのorderIDを含む配送状況を確認する *)
   delay 1
   repeat 4 times
     delay 1
     if my job() then exit repeat
   end repeat
   
 end run
 
 on job()
   do shell script "/usr/local/bin/growlnotify -m '配送状況を確認する...' "
   tell application "System Events"
     tell UI element 1 of scroll area 1 of group 3 of window 1 of application process "Safari"
       repeat with a_table in tables
         tell a_table
           try
             static text 1 of UI element (the clipboard as text) of group 1 of list 1 of UI element 1 of row 1
             click image 1 of UI element 1 of group 1 of UI element 2 of row 2
             return true
           end try
         end tell
       end repeat
     end tell
   end tell
   false
 end job

  • GUIスクリプティングでは、「配送状況を確認する」ボタンのURLを取得することはできなかったのだが、
  • 同じテーブル内の注文番号なら取得できることに気付いた。
  • 注文番号へのオブジェクト参照を実行してエラーが発生しなければ、
  • そのテーブルの「配送状況を確認する」ボタンを押すことで、目指す商品の配送状況が表示されるのだ。

これで確実に「配送状況を確認する」ボタンを押せるようになった!

  • しかし、ページ遷移なしのJavaScript更新なので、詳細ページから戻った時に配送状況が閉じてしまう...。
  • ほとんどがAppleScriptになってしまったので、ワークフローにしておく価値がなくなってしまったかも。
  • すべてAppleScriptで書いておけば、引数のやり取りにクリップボードを使うような技も不要になる。(あとで書き直す)

以上のワークフローとAppleScriptは以下のリンクからダウンロードできる。

2010-09-30

sed・grepで濁点と改行をまともに扱う方法

#sed 's/^/-/g'			# 行頭に-を付加する
#sed 's/$/-/g'			# 行末に-を付加する
#sed 's/\(xxxx\)/[\1]/g'	# xxxxを[]で囲う
#sed 's/xxxx/oooo/g'		# xxxxをooooに置き換える
  • 必要なコマンドラインのコメントマーク#を削除して、便利に使うつもりでいた。
  • さらに、これは便利と思い、気を良くしてgrepバージョンも作って喜んでいた。
grep -i 'xxxx'
#-i	大文字と小文字を区別しない
#-v	パターンに一致しない行を表示する
#-n	パターンに一致した行のファイル内での行番号を表示する
#-c	パターンに一致した行の行数のみを出力する
#-b	パターンに一致した行の先頭からのバイト数を表示する
  • ところが、自分は生粋の日本人。OSXもアプリケーションも日本語環境で利用している。一つ問題が発生した...。
sed 's/\(folder\)/[\1]/g'  #正常に機能する
sed 's/\(フォルダ\)/[\1]/g'  #置き換えてくれない

grep -i 'folder' #OK
grep -i 'フォル' #OK
grep -i 'フォルダ' #NG エラー発生
  • そう、英語(半角英数)なら調子良く動いていたコマンドも、日本語を入力したとたん使い物にならなくなってしまった...。
  • それにしても、「フォル」がOKで「フォルダ」はNG。ファイルによっては、英数字も日本語もすべてNGだったりする。
  • いったい、何が原因でこのような現象になるのか?徹底的に調べてみた。

現象

  • テキストエディット.appで、デスクトップに以下のようなファイル(folder.txt)を用意した。
applications folder
documents folder
アプリケーションフォルダ
書類フォルダ
  • また、Finderでファイル名を選択してコピー、それをテキストエディットにペーストしたファイル(services.txt)を用意した。
照会_西濃運輸(カンガルー便).workflow
照会_〒EMS.workflow
照会_佐川急便.workflow
照会_JPエクスプレス(ペリカン便・日本通運).workflow
照会_〒翌朝10時郵便.workflow
照会_〒書留.workflow
照会_〒ゆうパック.workflow
照会_ヤマト運輸.workflow
ターミナル
$ cat ~/Desktop/folder.txt | sed s/folder/OOOOOO/g #OK

$ cat ~/Desktop/folder.txt | sed s/フォルダ/OOOOOO/g #OK

$ cat ~/Desktop/services.txt | sed s/workflow/OOOOOO/g #NG 異常出力
照会_ヤマト運輸.OOOOOOOOOO ン便・日本通運).OOOOOO

$ cat ~/Desktop/services.txt | sed s/照会/OOOOOO/g #NG 異常出力
OOOOOO_ヤマト運輸.workflowowow 便・日本通運).workflow


$ cat ~/Desktop/folder.txt | grep 'folder' #OK

$ cat ~/Desktop/folder.txt | grep 'フォルダ' #OK

$ cat ~/Desktop/services.txt | grep -i 'jp' #NG 異常出力
照会_ヤマト運輸.workflowowow 便・日本通運).workflow

$ cat ~/Desktop/services.txt | grep -i '西濃' #NG 異常出力
照会_ヤマト運輸.workflowowow 便・日本通運).workflow

$ cat ~/Desktop/services.txt | grep -i 'カンガルー' #NG 抽出なし
$ cat ~/Desktop/services.txt | grep -i 'カン' #NG 異常出力
照会_ヤマト運輸.workflowowow 便・日本通運).workflow
Automator&「シェルスクリプトを実行」アクション
cat ~/Desktop/folder.txt | sed s/folder/OOOOOO/g #OK
cat ~/Desktop/folder.txt | sed s/フォル/OOOOOO/g #OK
cat ~/Desktop/folder.txt | sed s/フォルダ/OOOOOO/g #NG 置き換えなし
cat ~/Desktop/services.txt | sed s/workflow/OOOOOO/g #OK
cat ~/Desktop/services.txt | sed s/照会/OOOOOO/g #OK

cat ~/Desktop/folder.txt | grep -i 'folder' #OK
cat ~/Desktop/folder.txt | grep -i 'フォル' #OK
cat ~/Desktop/folder.txt | grep -i 'フォルダ' #NG エラー発生
cat ~/Desktop/services.txt | grep -i 'jp' #NG 全部抽出
cat ~/Desktop/services.txt | grep -i '西濃' #NG 全部抽出
cat ~/Desktop/services.txt | grep -i 'カンガルー' #NG 全部抽出
AppleScript&do shell script

 do shell script "cat ~/Desktop/folder.txt | sed s/folder/OOOOOO/g" --OK
 do shell script "cat ~/Desktop/folder.txt | sed s/フォルダ/OOOOOO/g" --OK
 do shell script "cat ~/Desktop/services.txt | sed s/workflow/OOOOOO/g" --OK
 do shell script "cat ~/Desktop/services.txt | sed s/照会/OOOOOO/g" --OK
 
 do shell script "cat ~/Desktop/folder.txt | grep -i 'folder'" --OK
 do shell script "cat ~/Desktop/folder.txt | grep -i 'フォルダ'" --OK
 do shell script "cat ~/Desktop/services.txt | grep -i 'jp'" --NG(全部抽出)
 do shell script "cat ~/Desktop/services.txt | grep -i '西濃'" --NG(全部抽出)
 do shell script "cat ~/Desktop/services.txt | grep -i 'カンガルー'" --NG(エラー発生)
 do shell script "cat ~/Desktop/services.txt | grep -i 'カン'" --NG(全部抽出)

Automator&「AppleScriptを実行」アクション&do shell script
  • AppleScript&do shell script と同じ結果

分析

データは揃った。以上の結果から、考えられる原因を想像してみた。

改行コード
  • folder.txt(LF)とservices.txt(CR)の違いは、改行コードの違いだった。(CotEditorで開いて気付いた)
    • LF:UNIXで利用される改行コード。
    • CR:Macで利用される改行コード。
    • ちなみにWindowsでは、CR・LFを行末に二つ記入している。
  • fileコマンドで確認すると、以下のように表示される。
$ file ~/Desktop/folder.txt
/Users/zari/Desktop/folder.txt: UTF-8 Unicode text

$ file ~/Desktop/services.txt
/Users/zari/Desktop/services.txt: UTF-8 Unicode text, with CR line terminators
  • ターミナルでservices.txtがことごとく異常出力されてしまうのは、改行されずに同一行で上書き表示されてしまう現象が発生しているようだ。
  • sedの置き換え自体は、うまく処理されていた。
    • 但し、ターミナル画面では表示が重なってしまうため、異常な出力に見えてしまっていた。
    • ファイルに出力して、CRを改行と見なしてくれるエディタで確認すると、問題なかった。
  • grepの抽出では、テキスト全体が一行と認識されてしまうため、一つでもマッチすれば、テキスト全体が抽出される結果になってしまったようだ。
    • おまけに、表示が重なってしまうため、異常な出力に見えてしまう。(sedと同様)

以下のページがたいへん参考になりました。感謝です!

テキストエンコーディング

改行コードがLFであるfolder.txtについては...

  • Automatorの「シェルスクリプトを実行」アクションでのみ、「フォル」がOKで、「フォルダ」がNGという面白い結果になった。
  • ターミナル、あるいはAppleScript&do shell script では正常に処理される。

おそらく、文字コードに由来する原因だと思うが、どうして上記のような現象になるのかは、分からなかった。

  • fileコマンドで調べた限り、同じUTF-8であるはずなのに...。

ところで、改行コードがCRであるservices.txtについて、CRに問題があることが分かっているので、LFに変換してやり直してみた。

  • ターミナル
$ cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | grep -i 'カンガルー' #NG 抽出なし
$ cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | grep -i 'カン' #OK
照会_西濃運輸(カンガルー便).workflow
照会_JPエクスプレス(ペリカン便・日本通運).workflow
  • AppleScript&do shell script

 do shell script "cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | grep -i 'カンガルー'" --NG(エラー)
 do shell script "cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | grep -i 'カン'" --OK
 --結果↓
 --"照会_西濃運輸(カンガルー便).workflow
 --照会_JPエクスプレス(ペリカン便・日本通運).workflow"

    • Automator&「シェルスクリプトを実行」アクション
cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | grep -i 'カンガルー' #OK
cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | grep -i 'カン' #OK

つまり、改行コードがCRであるservices.txtについては...

  • Automatorの「シェルスクリプトを実行」アクションでのみ、正常に処理される。
  • ターミナル、あるいはAppleScript&do shell script では、「カン」はOKで、「カンガルー」はNGというfolder.txtとは全く逆の結果になった。

さらに面白いことに...

  • 最終行に自分自身が手入力した「照会_西濃運輸(カンガルー便).workflow2」を追記してみた。
  • すると、今まで抽出なしだったターミナルでの grep -i 'カンガルー' で、抽出されたしまったのである!
$ cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | grep -i 'カンガルー'
照会_西濃運輸(カンガルー便).workflow2

同じ「カンガルー」のはずなのに、ファイル名のコピーと、手入力に一体、何の違いがあるというのだ?こうなったら最後の手段、HexEditorを使って、バイトコードの並びを比較してみることにした。

# (カンガルー便) の部分のバイトコード
ef bc 88 e3 82 ab e3 83 b3 e3 82 ab e3 82 99 e3 83 ab e3 83 bc e4 be bf ef bc 89 #ファイル名をコピーしたもの
ef bc 88 e3 82 ab e3 83 b3 e3 82 ac e3          83 ab e3 83 bc e4 be bf ef bc 89 #手入力したもの

なんと!ファイル名をコピーした方は3バイト分余分なコードが付加されていたのだ!

UTF-8-MAC

その答えは、ここにあった。

  • びんずめ堂(たいへん参考になりました。感謝です!)
  • 同じUTF-8なのに、以下の違いがあったのだ。
    • UTF-8-MAC: 濁点や半濁点が付いた文字を文字+濁点という2文字を合成したものとして扱う。
    • 普通のUTF-8: 濁点の付いた文字も一文字として扱う。

  • おそらく、ファイル名をコピーすると、UTF-8-MACな文字コードとして記録される。
  • 一方、自分自身で手入力した文字は、普通のUTF-8な文字コードとして記録される。
  • たぶん、Automator&「シェルスクリプトを実行」アクションは、UTF-8-MAC環境。
  • 一方、ターミナルやAppleScript&do shell scriptは、普通のUTF-8環境。

上記のように考えれば、「フォルダ」と「フォル」、「カンガルー」と「カン」の奇妙な現象も説明できる。

コード修正

理由が分かれば、方針を決めて、修正するだけである。

  • 改行コードはLF(UNIX形式)に統一する。
cat '入力ファイル' | nkf -Lu
  • 文字コードは普通のUTF-8、あるいはUTF-8-MACのどちらかに統一する。
    • AutomatorならTF-8-MACに統一した方が良さそう。
    • ターミナル、AppleScript&do shell scriptならUTF-8に統一した方が良さそう。
cat '入力ファイル' | iconv -f UTF-8-MAC -t UTF-8
# あるいは
cat '入力ファイル' | iconv -f UTF-8 -t UTF-8-MAC

  • Automatorの修正は簡単で、以前のコードに改行コードの変換とUTF-8-MACの変換をする「シェスルクリプトを実行」アクションを追加するだけで完了した。
/opt/local/bin/nkf -w -Lu|iconv -f UTF-8 -t UTF-8-MAC

f:id:zariganitosh:20100930173957p:image f:id:zariganitosh:20100930174121p:image


  • ターミナルやAppleScript&do shell script なら、パイプを挟んでこんな感じ。
$ cat ~/Desktop/folder.txt | /opt/local/bin/nkf -w -Lu | iconv -f UTF-8-MAC -t UTF-8 | sed 's/\(フォルダ\)/\1/g'
applications folder
documents folder
アプリケーション【フォルダ】
書類【フォルダ】

$ cat ~/Desktop/folder.txt | /opt/local/bin/nkf -w -Lu | iconv -f UTF-8-MAC -t UTF-8 | grep -i 'フォルダ'
アプリケーションフォルダ
書類フォルダ

$ cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | iconv -f UTF-8-MAC -t UTF-8 | sed 's/\(カンガルー\)/\1/g'
照会_西濃運輸(【カンガルー】便).workflow
照会_〒EMS.workflow
照会_佐川急便.workflow
照会_JPエクスプレス(ペリカン便・日本通運).workflow
照会_〒翌朝10時郵便.workflow
照会_〒書留.workflow
照会_〒ゆうパック.workflow
照会_ヤマト運輸.workflow
照会_西濃運輸(【カンガルー】便).workflow2

$ cat ~/Desktop/services.txt | /opt/local/bin/nkf -w -Lu | iconv -f UTF-8-MAC -t UTF-8 | grep -i 'カンガルー'
照会_西濃運輸(カンガルー便).workflow
照会_西濃運輸(カンガルー便).workflow2

以上で、sed・grepで濁点と改行をまともに扱えるようになった!


nkfコマンド

  • iconvはOSX標準でインストールされているコマンドだが、nkfは自分でインストール必要がある。
  • MacPortが利用できる環境なら、以下の一行でインストールは完了する。
$ sudo port install nkf
  • もし、MacPortがまだインストールされていなければ、MacPortをインストールするのが近道だと思う。
  • MacPortをインストールするには、Xcodeのインストールが必要。
  • Xcodeは、MacBook付属のDVD、あるいはSnow LeopardのDVDで、インストールできる。