Googleスプレッドシート 検索と置換を使いこなそう!5(GAS TextFinderで神検索)
Googleスプレッドシートの 検索と置換シリーズ 5回目です。
前回は GASのTextFinderクラスの便利さを実感いただき、基本のコードと改行への一括置換、合わせて GAS実行用のUI周りのコードなどを学びました。
シリーズ最終回となる今回は、TextFinderの応用例を色々考えていきましょう。
Googleドキュメント、Googleスライドの検索と置換
Googleスプレッドシートの「検索と置換」をずっと掘り下げてきましたが、GoogleWorkspace の他のオフィスアプリケーションはどうなってるんでしょうか?
シリーズ最後なんで、少しだけGoogleスプレッドシート以外の
Googleドキュメント ・・・ いわゆる Google版Word
Googleスライド ・・・ いわゆる Google版 Powerpoint
の検索と置換にも少し触れておきましょう。
Googleドキュメントの検索と置換
Googleドキュメントの 検索と置換は、スプレッドシートに近い感覚で利用できます。
検索範囲という概念はありませんが、正規表現が使えるので スプレッドシート同様に 高度な応用検索も可能。先読み・後読みも使えます。
ただしヘルプにも記載がありますが、スプレッドシートの検索と置換では大活躍のキャプチャグループが使えません。これが結構痛い。
Googleドキュメントが キャプチャグループが使えないことによる、任意の箇所への 文字挿入が出来ないデメリットについては、いきなり答える備忘録さんが具体例を挙げて書かれています。
Googleスライドの検索と置換
Googleスライドの検索と置換は、さらに簡易版といった感じで 正規表現も使えません。
オプションに大文字と大文字の区別はありますが、もう一つのオプションは「ラテン文字の発音区分符号を無視する」というもので、この2つだけです。
ラテン文字の設定は 日本人的にはピンときませんが、世界的にはニーズがあるってことなんでしょうか?
ドキュメント、スライドのどちらも Ctrl + F による簡易検索は使えます。
一方、現在学び中の GASの TextFinderに該当するものは、ドキュメントやスライドにはありません。
GoogleWorkspaceのオフィススィート系と比べると、Googleスプレッドシートの「検索と置換」はかなり機能が充実していて、高度な処理が出来るというのがわかりますね。(Excelに比べて機能が少ないとか ボヤいてる場合じゃない!!)
それでは前向きな気持ちでw本編いってみましょう!
TextFinderの findAll() を使おう
検索と置換には「すべて置換」というボタンがあり、これは GASのTextFinderだと replaceAllWith(replaceText) に該当します。
しかし、検索と置換には「すべて検索」というボタンはありません。
でも TextFinderには「すべて検索」に該当する findAll() というメソッドがあるんです。
しかも、この findAll()が めっちゃ使えるヤツなんです!
まずは、この findAll() の活用を考えてみましょう。
findAll()で 検索にマッチしたセル数を取得する
findAll() の返り値は
Range[] ・・・ Rangeオブジェクトの配列
となります。
これはこのまま出力しても意味がありません。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function sscountif() {
const SEARCH_WORD = "りんご";
const textFinder = ss.createTextFinder(SEARCH_WORD).findAll();
console.log(textFinder);
}
たとえばこんなコードを書いて出力しても
このようになるだけ。
Rangeオブジェクトのまま出力しても意味がないので、加工が必要ってわけです。
でも、このままでも使える価値あるものが1つあります。
それは length で取得した findAll() の 配列の要素数です。
これは ss(スプレッドシート 全シート)に対して、「りんご」を含むで検索にマッチしたセルの個数を返しています。
ブック全体(全シート)を対象に検索した際の「マッチしたセルの数の取得」。これはExcelでは普通に出来るんですが、Googleスプレッドシートは標準機能では出来ませんでした。
簡単なことなんですが、GASの TextFinderクラスで findAll() メソッドを使うことで、ようやくこれを取得できたわけです。
全シートを対象とした COUNTIF関数みたいなものと言ってもよいかもしれません。
GAS 自作関数:全シートを対象とした COUNTIF関数
じゃあ、せっかくなんでこれを使って自作関数を作ってみましょう。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
/**
* 全シートを対象に word と一致、または含むセル数を返す GAS関数
* @param {"りんご"} word 検索ワード (セル参照可)
* @param {false} flag セルに完全一致で検索するか? (初期値は false で 含む )
* @return {カウント数} 検索結果のセル個数
* @customfunction
*/
function sscountif(word,flag=false) {
const textFinder = ss.createTextFinder(word).matchEntireCell(flag).findAll();
return textFinder.length
}
コードはこんな感じ。説明書き部分の方が長いですねw
このGASで作成した自作関数の説明書きについては、シート情報を出力する自作関数作成を解説した noteで 触れております。
これを入れておくことで シート上で関数入力時に、以下のように説明が表示されます。
使ってみると
このようにスプレッドシート内の全シートを対象に検索し、マッチしたセル数がセルに出力されます。
第2引数のflag は セルとの完全一致の設定なので
省略した場合は false扱いで キーワードを含むセル数
flagをtrue指定した際は、完全一致のセル数
を 返しています。
もちろん、キーワードをセル参照させた場合は、そのセル(今回の場合は画像の A2 やA3)もカウントに含んでしまうので注意。
そしてもう一点、この関数は常にシート全体をウォッチしているわけではないので、検索ワードの数が増減しても再計算されない点も注意です。
手動で再計算は Dleteしてから 戻るボタン(Ctrl +Z)が簡単です。
findAll()の中身を map で加工して活用
次は findAll() の返り値の Rangeオブジェクト配列を 活用できる形に加工してみましょう。
配列の個々の要素に対して処理をして、同じ要素数の配列を得たい場合は、map + アロー関数 を使うのがおススメです。
例えばあまり意味はありませんが、検索にマッチしたセルの値を全て取得したい場合は
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function findValues() {
const SEARCH_WORD = "ばなな";
let textFinder = ss.createTextFinder(SEARCH_WORD).findAll();
textFinder = textFinder.map(range => range.getValue());
console.log(textFinder);
}
このようにすれば良いです。
まあ、これは意味ないですよね。
でも、rangeが取得できているということは、例えば検索でマッチしたセルの一つ隣のセルの値を取得 なんてことも出来るわけです。
ここで活用できるのが シート関数とほぼ同じ感覚で使える offset です。
GAS 自作関数:全シートを対象とした vlookup というかFILTER
この offsetを組み合わせることで、いわゆる シート関数における vlookupやxlookup のような処理を 検索列を指定せず、むしろシート全体に対しても行うことができます。
※よくよく考えるとこの処理は、LOOKUP系というよりは FILTER関数的と言えるかも
シート全体だとわかりにくいので、開いているシート(sheet)を対象とした場合、
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function zlookup() {
const SEARCH_WORD = "ごりら";
let textFinder = sheet.createTextFinder(SEARCH_WORD).matchEntireCell(true).findAll();
textFinder = textFinder.map(range => range.offset(0,1).getValue());
console.log(textFinder);
}
↑ zlookup という関数をつくれば
このように、検索対象の列(もシートを)も指定する必要なく検索にマッチした 「ごりら」と一致するセルの 一つ隣のセルの値を返すことができます。
もちろん、先ほどの sscountif と同じようにシート上で使える自作関数化も可能です。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function zlookup(SEARCH_WORD,offsetrow=0,offsetcol=0) {
//const SEARCH_WORD = "ごりら";
let textFinder = sheet.createTextFinder(SEARCH_WORD).matchEntireCell(true).findAll();
textFinder = textFinder.map(range => range.offset(offsetrow,offsetcol).getValue());
return textFinder;
}
ただし、先ほどの ssconut と同じく 対象のシートやスプレッドシートに変化があっても 結果は連動しては変化しませんので注意。
offset の第3引数(高さ 行数)、第4引数(幅 列数)をセットして マッチしたセルを起点とした範囲を取得することも出来ますが、その場合はシートに出力させる際に少し工夫する必要があります。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function zlookup(SEARCH_WORD,offsetrow=0,offsetcol=0,numRows=1, numColumns=1) {
//const SEARCH_WORD = "ごりら";
let textFinder = sheet.createTextFinder(SEARCH_WORD).matchEntireCell(true).findAll();
textFinder = textFinder.map(range => range.offset(offsetrow, offsetcol, numRows, numColumns).getValues());
textFinder = textFinder.flat();
return textFinder;
}
範囲を取得するので getValues() に変える必要があるのはわかりますね。
ただ、getValues()で取得した 個々の要素が二次元配列となるので、全体としては 三次元配列になってしまいシート上に出力できません。
これをシートに出力させる為に flat() で1つ次元を落として二次元配列としています。
これ、getValuesで二次元配列を取得してから同様の処理をやろうとすると、結構面倒というか初心者には難しかったりします。
検索部分は、indexOf や findIndex、filterを使う方法もありますが、配列系メソッドが理解できていないと、なかなか扱いが難しい・・・。
さらに大量データだったり、対象データが単体シートではなくスプレッドシートファイル(ブック)全体だったりすると処理時間という点でも、TextFinderを使った方がメリットが出せることも多いんで おススメです。
シート内の検索にマッチしたセルを全てアクティブにする
これもExcelの「検索と置換」では出来るのにGoogleスプレッドシートでは出来なくて、はだしのゲンのごとく「ギギギ」と言いたくなる思いをしていました。
この検索にマッチしたセルの一括アクティブも、 findALl()を使うことでサクッと出来ちゃいます。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function matchCellActive(){
const SEARCH_WORD = "ごりら";
let textFinder = sheet.createTextFinder(SEARCH_WORD).matchEntireCell(true).findAll();
textFinder = textFinder.map(range => range.getA1Notation());
console.log(textFinder);
sheet.getRangeList(textFinder).activate();
}
飛び飛びのセルに対して処理をする際に有効なのが getRangeList() というメソッドです。
公式リファレンスの パラメーターの箇所を確認すると
このように記載があります。
つまり シートの個々のセル位置を A1表示で表した一元配列を用意してあげれば、この getRangeList( この中に入れる )が使えるということです。
それを生成している部分が
ここですね。実行すると
このように
と、対象シートで検索にマッチしたセルのA1表記文字列が配列になっているのが確認できます。
そして最後の sheet.getRangeList(textFinder).activate() で、シート上で「ごりら」と一致したセルが全て選択状態(アクティブ)になってますね。
この状態になればこっちのもんです。
あとは 煮るなり焼くなり 二宮〇也って感じで、手動でお好きな一括処理ができちゃいます。
飛び飛びで選択したセルを一括処理
飛び飛びで選択状態(アクティブ)になったセルに対して一括処理をする時は、アクティブとなっている最後のセルがポイントとなります。
ツールバーからの一括操作、たとえばセルの色やフォント操作は 気にする必要はありません。
しかし、対象セルを含む行や列を非表示にするなどの 右クリックから操作する処理の場合は、最後のセルを意識する必要があります。
特に列は間違いやすいで注意です。
今回の場合 一番右側は K列なのですが 最後のアクティブセルが G28である為、Ctrlを押しながら G列を右クリックする必要があります。
このように、「ごりら」に一致するセルが存在する列を一括で非表示にすることができました。
これが何が凄いかわかりますか?
実は 「条件に合致したセルが存在する列を 一括で非表示にする」という処理は結構厄介なんです。
GAS無しでやる場合、行の非表示ならフィルタを使うことで簡単に出来ます。一方列に関しては残念ながらGASを使う以外には目視作業しかありません。
さらに GASでは飛び飛びの行や列の非表示は、一括処理が出来ません。
sheetクラスには hideRows(index, num) や hideColumns(index, num) といったメソッドがありますが、これらは連続する行(列)をまとめて非表示にする為のものです。
飛び飛びの 行や列の非表示はループ処理を回すしかないのです。
そして、GASはシート上でAPIを叩くループ処理は極端に遅いという弱点があります。
この 飛び飛びの 行や列の一括非表示は、対象が選択状態にさえなっていれば、手動なら出来る処理です。件数によっては 一連の流れをGASでループ処理するよりも、GAS+手動の方が早かったりします。
GASでは出来なくても手動なら出来るってのが、結構あるんですよね。
紹介したコードは、GASのTextFinderを使って「あえて」条件に合致するセルのアクティブ化 までの処理としたものです。
これは、最終的に使用者がざっと目視でチェックしたり、最終的な処理(色付けや列非表示)を一括でスピーディーに実行できる。こんなメリットがあるかと思います。
GASの一括セルアクティブ化をカスタムメニュー化する
前回の改行置換と同じく、この検索マッチセルの一括アクティブも標準機能では出来ないけど欲しい機能なのでカスタムメニューに追加しておきましょう。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
//起動時にカスタムメニュー追加
function onOpen() {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu('検索と置換+α') // メニューを作成
.addItem('改行置換', 'inputSearchWord') // 改行置換の2つの関数は先週の noteを参照
.addItem('検索結果アクティブ', 'inputSearchWord2') //一括アクティブ関数
.addToUi();
}
//ワード入力と結果表示
function inputSearchWord2(){
const words = Browser.inputBox("検索したい文字を入力");
if(words == "cancel") return; //キャンセルの時は処理終了
const result = matchCellActive(words);
Browser.msgBox(`シート内の${words}と一致した${result} 件のセルを アクティブにします`);
}
//検索結果 一括アクティブ化
function matchCellActive(words){
const cellMatch = true; //完全一致するセルを検索する場合は ture, 含むで検索は false
let textFinder = sheet.createTextFinder(words).matchEntireCell(true).findAll();
textFinder = textFinder.map(range => range.getA1Notation());
sheet.getRangeList(textFinder).activate();
return textFinder.length;
}
ついでに msgBoxで 検索結果の件数も出力させました。
カスタムメニューの箇所とinputBoxを使った入力UI関数 inputsearchWord2 の記述については、先週の 改行置換の説明を参照ください。
これを使うと
こんな感じになりました~。
ちなみに HTMLを書く必要があってGASの違う話に飛んじゃうんで説明は割愛しますが、この findAll() を応用すれば、
こんな感じで、前回紹介した 有料アドオンの Advanced Find and Replace のようなことも可能です。
機会があれば、もう少し作りこんだ自作の検索サイドバーについてもnoteで触れられればと思います。(さすがに有料記事か?)
findAll() めっちゃお得~。ってのをまずは紹介しました。
TextFinderの findNext()はどこで使うべきか?
検索と置換の「検索」ボタンに該当するメソッドが findNext() です。
この findNext()は 次に一致するセルを検索するメソッドで、基本っちゃ基本なんですが、findAll()の方が使い勝手良くそっちで事足りることも多いんですが・・・。一応紹介しておきましょう!
検索で見つからない場合は null (false扱い)を返すので、whileの繰り返しと相性が良いです。
findNext() サンプルコード
たとえば
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
//検索ワード入力、結果出力
function inputSearchWord3(){
const words = Browser.inputBox("検索したい文字を入力");
if(words == "cancel") return; //キャンセルの時は処理終了
const result = stepSearch(words);
Browser.msgBox(`検索結果は ${result} `);
}
//検索でマッチしたセルの値を返す 繰り返し処理
function stepSearch(words){
let textFinder = sheet.createTextFinder(words);
let next;
let i=0;
while(next=textFinder.findNext()){
i++;
next.activate();
const msg = Browser.msgBox(next.getValue());
//ダイアログ外クリック(キャンセル時)のループ脱出
if(msg == "cancel") return i + "件で終了しました" ;
}
return i + "件でした"
}
こんなコードを書いて、inputSearchWord3 を実行すると
このように、検索にマッチしたセルを一つずつアクティブにしつつ、セルの値をメッセージボックスに返すことが出来ます。
でも、これも findAll()を forEach で処理すれば、同じこと出来るんですよね。。
TextFinderの検索は1枚目のシートのA1から探索する
上のコードの
let textFinder = sheet.createTextFinder(words);
この部分を
let textFinder = ss.createTextFinder(words);
とすれば、全シート検索になります。
ただし、この時の挙動に 機能の「検索と置換」と少しだけ違いがあります。
検索と置換の機能は、現在開いているシート(アクティブなシート)の選択セル(アクティブなセル)を検索のスタート位置とします。
そこから 右方向に検索をして 一つ下の行に下がっていき、全シートが検索対象の場合はアクティブシートが終わったら 一つ右 のシートへと移ります。
つまり、検索の起点はアクティブセルってことです。
一方 GAS TextFinderの場合は
シート6の B4セルがアクティブな状態で 全シートを対象に検索をかけると上のgif画像の通り 検索開始位置は1枚目のシート (シート1)のA1セルとなります。
findAll() の場合は気にしなくてもよいでしょうが、検索で見つかった一つ目だけが必要といった findNext () を使うケースでは、この開始位置を意識する必要があるわけです。
この検索開始位置を指定するメソッドが startFrom(startRange) です。
今アクティブなセルを検索開始位置としたい場合は
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
//検索ワード入力、結果出力
function inputSearchWord3(){
const words = Browser.inputBox("検索したい文字を入力");
if(words == "cancel") return; //キャンセルの時は処理終了
const result = stepSearch(words);
Browser.msgBox(`検索結果は ${result} `);
}
//検索でマッチしたセルの値を返す 繰り返し処理
function stepSearch(words){
// ↓ここが変更点!
//アクティブなセルを開始位置としてスプレッドシート全体を検索
let textFinder = ss.createTextFinder(words).startFrom(sheet.getActiveRange());
let next;
let i=0;
while(next=textFinder.findNext()){
i++;
next.activate();
const msg = Browser.msgBox(next.getValue());
//ダイアログ外クリック(キャンセル時)のループ脱出
if(msg == "cancel") return i + "件で終了しました" ;
}
return i + "件でした"
}
↓ ここだけ、このように変更すればOK.。
//アクティブなセルを開始位置としてスプレッドシート全体を検索
let textFinder = ss.createTextFinder(words).startFrom(sheet.getActiveRange());
検索の開始位置をアクティブセルに指定できました~。
findNext()は、検索して見つかった一つ目のrangeに対してのみ処理を実行する。といった使い方におススメです。
その際ポイントとなるが、startFrom() で検索開始位置の指定です。
覚えておきましょう!
TextFinderによる置換処理の応用
TextFinderの検索の活用例を見てきましたが、前回紹介した改行への置換以外の置換処理の活用例も見ていきましょう。
GAS、というかプログラミングにおいて 価値を発揮する処理の一つが繰り返し処理です。
変換リストに基づいて ループ置換したい
たとえば、右のD2:E12 の変換表に基づいて、A列の文字列を置換したいという場合
GAS無しで 検索と置換でこの処理をやろうとすると、手動で何度も置換処理をする以外に方法はありません。
しかし、GASのTextFinderを使えば ループ処理で検索と置換が出来ます。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function loopReplace(){
const LIST_RANGE = "シート1!D3:E12"; //置換リスト範囲
const TARGET_RANGE = "シート1!A:A"; //検索と置換対象範囲
const listArray = ss.getRange(LIST_RANGE).getValues();
const target = ss.getRange(TARGET_RANGE);
listArray.forEach(list => target.createTextFinder(list[0]).replaceAllWith(list[1]));
}
ちなみにsheetを指定してから getRange()するのが一般的な書き方ですが、シート内での参照と同じように
シート1!D3:E12
といった文字列を使うことで、ss(スプレッドシート)から直接 getRange()することが出来ます。
この記述にすると、変換表が別シートだった時でも
ここだけ修正すればいいんで、記述がシンプルで済みますね。
このネタは最近「微風 on the web」さんでも紹介されていました。
繰返し文は forEachの 1行コードと、こちらも非常にシンプルです。
実際の動きを見てみましょう。
このように変換表に沿って、ループ処理で置換の繰り返しができました。
これは結構需要があるんじゃないでしょうか?
TextFinderで実現!リッチテキストを保持した GASによる文章追加
最後に少し変わった活用例を紹介しましょう。
たとえば、こんな要望があったとします。
この処理は getValueで取得した値に、改行と文字を追加して setValueというコードを書くのが一般的です。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function addText(){
const target ="A1";
const range = sheet.getRange(target);
//getValueして
let value = range.getValue();
const addValue = Browser.inputBox("追加する文字を入力");
//改行と文字を追加して
value = value +"\n" + addValue;
//setValueする
range.setValue(value);
}
問題なく動いてますね。
しかし、後から
こんなことを言ってくるかもしれません。(QAサイトあるある)
このように setValue() しちゃうと、元のリッチテキストが失われてしまいます。
じゃあどうするか?
これを保持する為には RichTextValue クラスを使うことになるのですが、まぁこれが文字の開始と終了位置ごとに リッチテキストを設定するビルダー系で、初心者には結構ハードルが高かったりするんです。
というわけで、この処理をセル内の文字の最後に 改行 + 文字を追加するではなく、文末を 改行 + 文字に置換すると捉えれば、検索と置換の TextFinderを正規表現で使えば出来そう!って発想にたどり着くわけです。
そもそも改行を除けば、文末への追加は GAS無しで 検索と置換でも出来るってのは、シリーズの最初に紹介しましたね。
今回の処理を TextFinderを使ってコーディングすると
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
function addText(){
const target ="A1";
const range = sheet.getRange(target);
const addValue = Browser.inputBox("追加する文字を入力");
if(addValue == "cancel") return;
//末尾を改行+addValueに置換
range.createTextFinder("$").useRegularExpression(true).replaceAllWith("\n"+addValue);
}
このようになります。
文末を $ で取得するのに正規表現を使うので
useRegularExpression(useRegEx)
が登場しました~。(true で正規表現を利用)
これを実行してみると
このように元のリッチテキストを保持した状態で、文末に改行 + 追加文字を挿入できましたね。
こんな一見、検索と置換では無さそうなケースでも TextFinderが使えたりするという 応用例の紹介でした。
TextFinder replaceWith() には注意
ちなみに1件だけの置換なら
replaceAllWith じゃなくて replaceWith でいいんじゃね?
って思うかもしれませんが、
このように replaceWith の方はあくまでも マッチした現在のセルに対して使えるメソッドなんです。
だから上の処理の replaceAllWith をそのまま replaceWith に差し替えても、
Exception: Service error: Spreadsheets とエラーが出てしまいます。
replaceWith を使うには、一度 findNext() をした実行した textFinder(返り値ではない)を使う必要があるようです。
腹落ちしないところもありますが、
//末尾を改行+addValueに置換
let textFinder = range.createTextFinder("$").useRegularExpression(true);
textFinder.findNext();
textFinder.replaceWith("\n"+addValue);
こんな記述にすれば動きます。でも面倒ですね~。
しかも!! mirも理由はよくわかりませんが
このように replaceWithによる置換は リッチテキストを破壊してしまいます。
なんでだー???
というわけで、普通に置換する分には replaceAllWith()だけ使っときましょう。
検索と置換(TextFinder)で スプレッドシート上のGAS処理は広がる!
検索と置換を GAS無しで3週、GASの TextFinderクラスで2週と 5回に渡って、かなりガッツリ取り上げてみました。
それでも今回はかなり詰め込んじゃったんで、コード解説もすっとばしてるところが多々あり、
ちょっと難しいし、わかづらいよ~
と、不親切に感じる方も多いかもしれません。(反省)
また、機会があれば個々の処理の GASを TextFinderを使わない場合を取り上げた際にでも、TextFinderにも再度触れていければと思います。
とりあえず今回のまとめメッセージとしては
GASのTextFinderクラスは意外と簡単
他の方法よりもシンプルなコードで処理も早いことが多い
とりあえずfindAll()とreplaceAllWith()が使えればよい
ってとこでしょうか。
検索と置換、GAS の TextFInder 是非色々試してみてください!
次回はまたシート関数ネタを書きたいと思います。