このページは福井県立大学の田中求之が2006年1月まで運用していた Mac のサーバ運用に関する会議室 「Web Scripter's Meeting」の記録です。情報が古くなっている可能性がありますのでご注意ください。

AppleScriptでbyte長分の文字数を取得するには?

発言者:野本夏俊
( Date Saturday, May 09, 1998 11:31:29 )


1バイト、2バイト文字が混在するテキストの頭からnバイト分の文字数を
取り出すための、効率的なアルゴリズム、またはOSAXはないでしょうか?

具体的にはFaceSpanで作った簡易エディタに検索機能を付け加えようとしている
のですが、テキストボックスの内容をインサーションポイントで切り分けるのに、
結構時間がかかってしまいます。
たとえば、テキストが“野本夏俊”で“本”のあとにインサーションポイントが
ある場合にテキストを“野本”と“夏俊”にきり分けて“夏俊”だけを検索対象に
したいわけです。
このときにselectionを取り出すと{4,4}となります。頭から4バイト分のデータが
何文字なのかがわかれば、
text (i + 1) thru -1 of Alltext
とすれば簡単に“夏俊”が得られる訳ですが、テキストのバイト数を取得するのは
byteLengthでできますが、その逆はなかなかできません。
今のところ以下のようなスクリプトでできてはいるのですが、とても時間が
かかってしまいます。何かいい方法がないもんでしょうか?

tell textbox 1 of document 1
   set AllText to contents as text
   set Sele to selection
   set {S1, S2} to {item 1 of Sele, item 2 of Sele}
   if S2 > 1 then
      set C to (S2 div 2)
      repeat
         set L to byteLength (text 1 thru C of AllText)
         if (L ウ S2) then
            exit repeat
         else
            set C to C + ((S2 - L) div 2) + ((S2 - L) mod 2)
         end if
      end repeat
      set SeleCont to text (C + 1) thru -1 of AllText
   else if S2 = 1 then
      set SeleCont to text 2 thru -1 of AllText
   else
      set SeleCont to AllText
   end if
end tell

これでSeleContにインサーションポイント後のテキストが入ります。
とりあえずインサーションポイントがテキストの前半にあるのかどうかで
前から追うか後ろから追うかに分ければ少しは速くなりそうな気もしますが、
もっと画期的な解決方法はないでしょうか?

田中求之 さんからのコメント
( Saturday, May 09, 1998 14:42:55 )

えっと、パラメーターで渡した文字列の先頭から、指定したバイト分の文字を返す
というのでいいわけですね?(2バイト文字を考慮して)

でしたら AppleScript の length および characater が2バイト文字を
ちゃんと認識することを利用して、以下のようなハンドラーで済みますよ。

on byteSplit(myText, xByte)
  set dx to (length of myText)
  repeat with x from 1 to dx
    set dt to text from character 1 to x of myText
    if (MT Byte Length dt) ウ xByte then exit repeat
  end repeat
  return dt
end byteSplit

これを使えば、

set myText to "今日は very fine な天気です"
byteSplit(myText, 8)

---> "今日は v"

になります。なお、2バイト文字にひかかった時には、その文字も含めるように
なってますので、

set myText to "今日は very fine な天気です"
byteSplit(myText, 5) --- 5 バイト目は "は" の1バイト目にあたる

---> "今日は"

となります。

* AppleScript の length は、2バイト文字も1文字として数えた、文字数を
返します(これに困って (?)  ByteLength osax を作ったわけです)


野本夏俊 さんからのコメント
( Saturday, May 09, 1998 16:31:22 )

お答えいただいてありがとうございます。
しかし、田中さんのスクリプトはすっきりしてはいますが、遅すぎます。
おそらく最も時間のかかるやり方だと思います。
以下のようにして比較して見ましたが、バイト数が増えるぶんだけ余計に時間が
かかってしまいます。履歴に書き込めないと困るので、後半部分の先頭20文字と
所要時間を記録します。(2.0はまだ使っていないのでbyteLengthに戻しました)

property len : 10000

open {choose file}

on open FS
   set Fref to open for access (item 1 of FS)
   set TXT to read Fref
   close access Fref

   set LastDate to current date
   byteSplit2(TXT, len)
   set Lap to current date
   log Lap - LastDate
   byteSplit(TXT, len)
   log (current date) - Lap
end open

on byteSplit2(AllText, S2)
   set C to (S2 div 2)
   repeat
      set L to byteLength (text 1 thru C of AllText)
      if (L ウ S2) then
         exit repeat
      else
         set C to C + ((S2 - L) div 2) + ((S2 - L) mod 2)
      end if
   end repeat
   set SeleCont to text (C + 1) thru -1 of AllText
   log text 1 thru 20 of SeleCont
end byteSplit2

on byteSplit(myText, xByte)
   set dx to (length of myText)
   repeat with X from 1 to dx
      set dt to text from character 1 to X of myText
      if (byteLength dt) ウ xByte then exit repeat
   end repeat
   log text (X + 1) thru (X + 20) of myText
end byteSplit

同様の処理をやってくれるOSAXとかご存じではないですか?

ところで、2.0のコマンドの頭の“MT”って何か特別な意味があるんですか?

田中求之 さんからのコメント
( Saturday, May 09, 1998 18:17:22 )

>しかし、田中さんのスクリプトはすっきりしてはいますが、遅すぎます。

確かに (^_^; あまりにまっとうすぎましたね。

ま、改良するとしたら、以下のようにするべきでしょう(2バイト文字の切れ目に
なったときの処理が異なりますが)。

on byteSplit(myText, xByte)
  repeat with x from xByte to 1 by -1
    set dt to text from character 1 to x of myText
    if (MT Byte Length dt) イ xByte then exit repeat
  end repeat
  return dt
end byteSplit

もっとも、これでもそんなに速くないかな(本来はこれにエラーチェックがいりますしね)。


>同様の処理をやってくれるOSAXとかご存じではないですか?

ないと思います。アメリカ人は、ScriptManger を呼んでキースクリプトを
見るような osax はまず作らないでしょうから、日本人(2バイト文字圏)の
人間が作らないかぎり出てこないでしょう。

osax としては非常に簡単に作れるものですが、あいにく私の開発環境では
ScriptManger 関連のコールがちゃんと行えないという問題を抱えてますので
(なぜだか原因は不明)、Tanaka's osax には入りません (^_^;


>ところで、2.0のコマンドの頭の“MT”って何か特別な意味があるんですか?

私の名前の頭文字です。 1.0 のコマンド名をいかに変更すべきかについて
Main Event (Scripter の発売元)の Cal Simone さんとメールで
やり取りしていたときに「コマンド名が他の osax やアプリケーションに
ぶつからないようにするために、先頭に頭文字をつけたらいい」という
アドバイスをもらいましたので、それに沿ったというわけです。
(それ以外にも、山のように注文を付けられた (^_^;; )

野本夏俊 さんからのコメント
( Saturday, May 09, 1998 20:09:34 )

>ScriptManger を呼んでキースクリプトを見るような osax

てことは、もしかしたら飯森さんのosaxをうまく使えばできそうな感じですね。
いろいろ試して見ます。(実はもともと飯森さんのmgrepOSAXを、検索機能に
組み込むためのスクリプトなんです。)

で、とりあえずやって見ました。

on byteSplit3(AllText, S2)
  set TXT2 to xReplace AllText search return replace "0"
  set X to mgrep TXT2 regex ("(.{" & (S2 as text) & "})") scriptCode 0
  set T to (subMatchStr of X) as text
  
  set SeleCont to text ((length of T) + 1) thru -1 of AllText
  log text 1 thru 20 of SeleCont
end byteSplit3

時間的にはbyteSplit2と似たり寄ったりですが、スクリプトはすっきりしましたし、
正しい結果を返してくれます。
しかし、改行を置き換えないでやる方法はないもんですかね?
(改行が多いとxReplaceで時間を食いそう)

>「コマンド名が他の osax やアプリケーションにぶつからないようにするために、
>先頭に頭文字をつけたらいい」というアドバイスをもらいましたので、それに
>沿ったというわけです。

なるほど、Motoyuki Tanakaでしたか!
コマンドは1wordでできてる方が選択しやすいので好きなんですが、いろんな
ガイドラインみたいなものが有るんでしょうね。

野本夏俊 さんからのコメント
( Monday, May 11, 1998 17:56:15 )

>しかし、改行を置き換えないでやる方法はないもんですかね?

できました!

on byteSplit3(AllText, S2)
   set X to mgrep AllText regex ("([\\x00-\\xff]{" & (S2 as text) & "})") scriptCode 0
   set T to (subMatchStr of X) as text
   
   set SeleCont to text ((length of T) + 1) thru -1 of AllText
   log text 1 thru 20 of SeleCont
end byteSplit3

やっぱりすごい飯森さん! 感謝です。30万バイトの取得で約4秒です。
もとのテキストの内容に影響を受けないし、1行で記述可能なのが最高です。
うれしいな〜。

田中求之 さんからのコメント
( Monday, May 11, 1998 18:42:46 )

な〜るほど、そういう手があったか…

野本夏俊 さんからのコメント
( Wednesday, May 13, 1998 11:47:15 )

昨日ちょっと思ったのですが、田中さんの場合はOSAXの開発に関しては
Roman環境でやっているのと、ほとんど変わらないということですよね?
だとしたら、テキストの先頭からnバイト分を返すというのは、ScriptManger
(ずっとScriptManagerだと思ってました)とは無関係にできることのように
思うのですが、どうなんでしょう?(ScriptMangerというのがどんなものなのか
まるで知らないんですが....)

アメリカ人がこのようなOSAXを作らないのは、Roman環境では、ただ単に

text 1 thru n of TheText

とすれば用が足りてしまうからですよね?
たとえば、同様のことをTanaka's osaxだけを使って、

on byteSplit4(AllText, S2)
   set X to charsToNumList AllText
   return NumListToChars (items 1 thru S2 of X)
end byteSplit4

とすることもできます。この場合は、もちろん相当時間がかかりますが、
text 1 thru N of TheText だけのことなら、一瞬でできてしまうのではないですか?
2バイト文字の途中で切れないようにしようと思うと、適切なスクリプトコードを
設定して...ってことになるんでしょうが、途中で切れても構わないのなら、
スクリプトコードを考慮する必要は無いんじゃないですか?

田中求之 さんからのコメント
( Wednesday, May 13, 1998 15:09:23 )

>途中で切れても構わないのなら、
>スクリプトコードを考慮する必要は無いんじゃないですか?

そうです。単純に、文字列から指定したバイト範囲のものを切り出すだけでよいのであれば
osax が作ってありますので、とりあえず登録しておきました。 MT Hex To Dec と
いっしょにして、Fukui osax という名前にしておきました。

この中に MT Extract string というコマンドが入っています。

from で何バイト目(オフセットではなく)から切り出すか(省略時はデータの先頭)
to で何バイト目まで切り出すか(省略時はデータの末尾)

を指定します。また、どちらのパラメーターも、マイナスの数を与えると、データの末尾
から数えて何バイト目かを意味します。ですから、

MT Extract String myData from -1024

で、データの最後の1Kバイトを取り出せます。

また、cut を true にすると、指定した範囲を切り取った、残りの文字列を返し
ます。

MT Extract string myData to -512 with cut

で、データの先頭の 512 バイトをカットしたものを返します。

→  fukui_osax.hqx

田中求之 さんからのコメント
( Wednesday, May 13, 1998 15:10:54 )

あ、間違い

>MT Extract string myData to -512 with cut

正しくは

MT Extract string myData to 512 with cut

野本夏俊 さんからのコメント
( Wednesday, May 13, 1998 16:53:05 )

これこそまさしく欲しいと思っていたものです!
感激です!ありがとうございます!!

from -.....や、範囲の指定もできるなんて、もう最高です!

もともとインサーションポイントのオフセット値参照しているので
2バイト文字が途中で切れてしまうことはないんですが、切れてしまうかどうか
わからない場合は、返値の文字数で元のテキストから改めて切り出せばいいの
ですから、全然問題有りません。

さっそく使わせていただきます。ありがとうございました!!!