DTPab

DTPにまつわるあれこれ

JavaScriptの検索とInDesignの検索

【4/5 21:56 戻り値の表記にアドバイスをいただいたので反映させました】
【4/6 09:10 findTextメソッドとfindGrepメソッドの設定をクリアするコードを追記し、indexOfメソッドとlastIndexOfメソッドのコードのミスを修正しました】
この前のもくもく会で、JavaScript正規表現検索について話があったので、改めて記事にしました。
先に断っておきますが、メソッドについての紹介のみで、正規表現それ自体やキャプチャグループ、フラグなどについては説明を割愛しています。

JavaScriptで検索として使えるメソッド

RegExp.testメソッド

検索したい文字列(正規表現)が、検索対象文字列に「あるかどうか」だけを調べたい場合はRegExp.testメソッドを使ったほうがスマートです。

var regexp = /hello world/i;
var str = "HeLLo WorlD!";
$.writeln(regexp.test(str)); //true

文字列が見つかればtrue、そうでなければfalseが返ります。至極単純なやり方です。

String.matchメソッド

こちらは正規表現オブジェクト(RegExpオブジェクト)ではなく、文字列オブジェクト(Stringオブジェクト)が持つメソッドです。なので、RegExp.testメソッドとは使い方が違います。検索したい文字列(正規表現)を引数とし、検索対象となる文字列にドット演算子を付けます。

var regexp = /hello world/i;
var str = "HeLLo WorlD!";
$.writeln(str.match(regexp)); //HeLLo WorlD:実際には配列なので ["HeLLo WorlD"]

ちなみに、Stirng.matchメソッドの引数(検索したい文字列)は文字列型オブジェクトを渡しても動作します。

var findWord = "L";
var str = "HeLLo WorlD!";
$.writeln(str.match(findWord)); //L:実際には配列なので ["L"]

これは、文字列が引数として渡された場合、JavaScriptが暗黙的にnew RegExp(objct)による正規表現オブジェクト(RegExpオブジェクト)への変換を行っているためです。
また、このコードでは、検索対象文字列に「L」が2つあるにも関わらず、1つしか検出されていませんよね。JavaScriptが暗黙的に変換したRegExpオブジェクトにフラグが設定されていないため、「L」を1つ見つけて処理が終わってしまったためです。
new RegExp(objct)による正規表現オブジェクト(RegExpオブジェクト)への変換を行っているということで、String.macthメソッドの引数には、RegExpコンストラクタと同じく、第二引数にフラグを定義できます*1

var findWord = "L";
var str = "HeLLo WorlD!";
$.writeln(str.match(findWord, "g")); //L, L:実際には配列なので ["L", "L"]

こうすることで、検索対象文字列に含まれるすべての「L」を見つけ出すことができます。
ただし、String.macthメソッドの戻り値には注意してください。検索対象文字列に検索したい文字列(正規表現)が見つからなければnullが戻りますが、なにか検索マッチした場合は配列オブジェクトが戻ります。この例では、str.match(findWord, "g")の戻り値は配列になっていますので、検索マッチした文字列を1つだけ抽出する場合は、str.match(findWord, "g")[0]ないしstr.match(findWord, "g")[1]などとするといいです。

RegExp.execメソッド

String.macthメソッドと似て非なるメソッドとして、RegExp.execメソッドがあります。これは正規表現オブジェクト(RegExpオブジェクト)のメソッドなので、RegExp.testメソッドと同じく、正規表現.exec(文字列)という使い方をします。
String.macthメソッドとどちらを使うかはケース・バイ・ケースですが、基本的に前者で事足りると思います。僕自身、基本的にexecメソッドはそんなに使いませんが、execメソッドらしい使い方が求められるケースではとても便利に使えます。
この前のもくもく会では、「検索語がマッチするindexを知りたい」という話があったのでこのメソッドを紹介しました。こういう「indexを知りたい」「頭から順に処理して任意の場所で止めたい」というケースで役立ちます。

var regexp = /[0-9]+/g;
var string = "qwas75edrftgy1h41u2jikol;p6";
var find;

while (find = regexp.exec(string)){
    $.writeln("match: " + find);
    $.writeln("index: " + find.index);
    $.writeln("lastIndex: " + regexp.lastIndex);
    $.writeln("---");
    }

結果のコンソール

match: 75
index: 4
lastIndex: 6
---
match: 1
index: 13
lastIndex: 14
---
match: 41
index: 15
lastIndex: 17
---
match: 2
index: 18
lastIndex: 19
---
match: 6
index: 26
lastIndex: 27
---

このように、execメソッドはindexを更新しながら順に検索していきます。indexとlastIndexは持っている親のオブジェクトが違うので、そこだけ注意してください。それと、indexの値は0が^でヒットする単語の先頭になります。したがって、indexで得られた数値はn文字目の後ろとなります。
せっかくなので、もう少し実用性のあるコードも用意してみました。単位が入り混じった計算を行うスクリプトです。

var regexp = /([0-9.]+)(pt|mm)?/g;
var string = "10 + 12.5mm + 3pt + 1.3pt";
var find, cul;

while (find = regexp.exec(string)){
    if (!find[2]) continue;
    switch (find[2]){
        case "pt": cul = parseFloat(find[1])*25.4/72; break;
        case "mm": cul = parseFloat(find[1]); break;
        }
    string = string.replace(find[1]+find[2], cul);
    }

$.writeln( eval(string) ); //24.0169444444444

このスクリプトは、単位がついてなければ置換せず、単位がついていればmm単位に変換します。その上で、計算式をevalメソッドで計算しています。このスクリプトではptしか含めていませんが、間のswitch文に計算に許容する単位とmmへの換算式を書き加えていけば、オリジナルの単位表記混在計算機のできあがりです。
あまり使う機会の多くないRegExp.execメソッドですが、使い方を知っておくと、たまーに役立つことがあるかもしれません^^;;

String.searchメソッド

このメソッドは、上述のString.matchメソッドのように正規表現を使って検索を行うことができます。ただ、戻り値が数値型です。マッチしなければ-1が、マッチすればそのマッチした語句のindexが戻ります。

var str = "hoge hogeee hoggge hohogege hhhoooogeeee";
$.writeln(str.search("e")); //3
$.writeln(str.search("n")); //-1
$.writeln(str.search(/eee/g)); //正規表現が使えます…結果は8

また、String.matchメソッドと同じく、引数が文字列の場合は暗黙的にRegExpコンストラクタ関数を通りますので、正規表現リテラルの記述以外にも、「文字列, フラグ」という渡し方ができます。

String.indexOfメソッドとString.lastIndexOfメソッド

この項、ちょっとネタみたいなものだと思ってください。あまりお勧めしたくないのですが、こういうメソッドもありますという感じで読んでいただければ。
これらは正規表現が使えない、原則として文字列の完全一致のみをしらべるメソッドです。見つからなければ-1が戻りますが、見つかれば見つかった文字の最初のindexが戻ります。indexOfとlastIndexOfの違いは、indexOfは文字列の最初から検索するのに対し、lastIndexOfは最後から検索します。

var str = "11/[a-z]/11111/[a-z]/1";
$.writeln(str.indexOf(/[a-z]/)); //2 //正規表現リテラルっぽく記述しても、正規表現は使えないです…。恐らく、new String(obj)のようなコンストラクタ関数が呼ばれている模様
$.writeln(str.lastIndexOf("/[a-z]/")); //14

見てわかるとおり、lastIndexOfは最後から検索しますが、得られるindexは単語の先頭になります。
これらのメソッドは、第二引数に「何番目の文字から調べるか?」というfromIndexを渡すこともできます。

var str = "11/[a-z]/11111/[a-z]/1";
$.writeln(str.indexOf("/[a-z]/", 4)); //14

この例では4文字目から検索を開始しているので、最初に出てくる/[a-z]/にはマッチしません。なので2番目に出てくる/[a-z]/にマッチし、そのindexを取得しています。
このfromIndexをうまく利用すると、RegExp.execメソッドと似たような更新されるindex単位での処理も可能です。

var str = "hoge hogeee hoggge hohogege hhhoooogeeee";
var myIndex = str.indexOf("e");

while (myIndex !== -1){
    myIndex = str.indexOf("e", myIndex + 1);
    $.writeln(myIndex);
    }

結果のコンソール

8
9
10
17
24
26
36
37
38
39
-1

まぁ、あまり使う機会はないかと思います(上に紹介したほかのメソッドたちのほうがよほど使いやすいので)。

InDesignで検索

JavaScriptにあるいろいろな検索方法について見てきましたが、最後はInDesignでこれらをどのように利用するかと、そもそもInDesignの検索置換パネルで行う検索と何か違うのか?ということを書きたいと思います。

オブジェクト型に注意する

上に紹介してきたJavaScriptの検索メソッドたちは、RegExpオブジェクトで使える、もしくはStringオブジェクトで使えるという厳密な決まりがあります。

app.activeDocument.match(/hoge/g); //悪い例

なので、こういうふうにはできません。matchメソッドを使いたければ、文字列を探してきて、それに.matchを付けないといけないのです。

app.activeDocument.selection[0].contents.match(/hoge/g); //文字列を選択していればOK

contentsプロパティの中身はStringオブジェクトなので、この処理は問題ありません。

検索置換パネルを使う

InDesignの標準機能として備わっている検索置換パネルによる検索(・置換)と、JavaScriptの検索(・置換)では、処理速度に雲泥の差があります。前者のほうが圧倒的に早いです。なので、ドキュメント上のテキストを検索したり置換したりという場合は、InDesignの検索を使います。上述のとおり、JavaScriptのmatchメソッドなどを使う場合オブジェクトのcontentsプロパティなどにアクセスしてから検索を行うので、パフォーマンスが著しく低下します。
また、コード自体もInDesignの検索機能を使ったほうが圧倒的に楽です。手数が少なくて済みます。

//InDesignでテキストを検索する場合
app.findTextPreferences = NothingEnum.nothing; //設定をクリア
app.findTextPreferences.findWhat = "hoge";
var result = app.findText();

これで、開いている全てのドキュメントから「hoge」というテキストを検索します。result変数には、見つかったものが配列で入ります。ちなみにこのfindTextPreferencesfindTextは、テキストの検索です。正規表現検索は以下のようになります。

//InDesignで正規表現検索する場合
app.findGrepPreferences = NothingEnum.nothing; //設定をクリア
app.findGrepPreferences.findWhat = "hoge"; //正規表現検索パネル
var result = app.findGrep(); //正規表現検索の実行

また、検索の対象は、どのオブジェクトにfindTextないしfindGrepメソッドを使うかによって違います。

  • app.findText():アプリケーション全体=すべてのドキュメントに対して検索を実行
  • app.activeDocument.findText():前面のドキュメントに対して検索を実行
  • app.selection[0].findText():選択しているオブジェクト(の1つめ)に対して検索を実行

findTextメソッドやfindGrepメソッドは、他にもいろいろなオブジェクトが持っているので、オブジェクトモデルビューアで調べてみてください。InDesignから単純に検索するよりもかなり細かい範囲で検索することができます。

一方、JavaScriptのmatchメソッドを使おうとすると……。

  1. すべてのテキストフレームから、contentsを抜き出す
  2. 抜き出したcontentsから、検索したい文字列をmatchメソッドで検索する

という順序を踏むのですが(コードを用意することすら面倒すぎたw)、いざmatchメソッドで語句が見つかったとしてもそのテキストがどこにあるのか、文字サイズはいくつでどんな段落スタイルがあたっているか、みたいな情報を取り出すのは至難の業です。InDesignで文字列の検索を行う場合は、特別な事情がない限り標準機能の検索を使ったほうがメリットが大きいです。

*1:ExtendScriptECMAScript 3準拠のため、フラグのうちstikyフラグ「y」が使えません。