テキストエディタに正規表現でマッチしたものだけ抽出する機能がほしい

# 概要

ほとんどの正規表現を備えているテキストエディタは正規表現にマッチした箇所をハイライトにするとかまたは別の文字列に置換することは可能ですが、マッチしたものだけエディタに残す機能がない。もし有ったらそのテキストエディタを教えて欲しいです。Vscodeの拡張機能でも良いです。

追記)
@takahasinaoki さんのコメントより
> vscodeなら正規表現で検索し、
コマンドパレットで(Select All Occurrences of Find Match「一致するすべての出現箇所を選択」)を実行（ショートカットキーは Alt + Enter）
Ctrl + C でコピー
Ctrl + A で全選択
Ctrl + V で貼り付け
### 例
```text
住所:東京都中央区1-1-1 Tel:03-1234-5678
住所:東京都中央区1-1-2 Tel:03-1234-5679
```
このようなテキストが有ったとき電話番号だけ抽出したい。つまり、次のような正規表現で抽出すると、その次のような結果がエディタ上に残って欲しいのです。もし、"-"も不要ならエディタ上に残ったものに"-"を""に置換すれば"-"なしの電話番号が得られます。　
```text
Tel:((\d+-?)+)
```
```text
03-1234-5678
03-1234-5679
```
当然、これだけなら"住所.*Tel:"を""に置換してもできます。
しかし、もっと複雑なデータだと電話番号以外のところを消すのが面倒で、いままではPythonなどのプログラムを作ってデータ加工や抽出を行っていまいした。テキストエディタで出来るとその場すぐに確認ができ、何回か抽出や置換を繰り返すことで最終的に自分が欲しいデータ形式にすることが可能です。

こんなことが出来たら良いなというものを不慣れなHTML + Javascriptでプロトタイプを作ってみました。誰かがこういう機能を持ったテキストエディタあるいは専用のアプリを作ってくれたら嬉しいなという気持ちです。それでも、まあまあなプロトタイプにはなったかなと思います。

# プロトタイプ

HTML,CSS,Javascriptは勉強してから時間が経っていて今は最新の仕様や使い方がわからないので変なところがあるかもしれません。マッチング処理は行単位で行っています。

## テスト

正規表現`Tel\d*:((\d+-?)+)`と抽出・置換`$1,`で抽出ボタンをクリックする左下に正規表現にマッチした電話番号が表示されました。
`$1`はグループの1番目です。`$0`は正規表現マッチした全体が表示されますので1行目なら`Tel:03-1234-5678,Tel2:03-1234-5679,`と表示されます。
ちなみにこの正規表現では`$2`もあり1行目なら`5678,5679,`が対応します。理由は()が入れ子になっているからです。外側が`$1`で内側が`$2`です。

![regexjavascript01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/e0d06b28-0e25-4803-99fa-4213ad5418cd.png)


上記の結果を入力欄に移したいときは`入力へ移動`ボタンをクリックします。そうすると次のような画面になります。

![regexjavascript02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/a046f60b-532c-4a0a-9be9-8a2a2e7c6bdf.png)



そして行末の","を""に置換します。次の画面で`置換`ボタンをクリックします。
これ以降は`上へ移動`ボタンは押しませんので。

![regexjavascript03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/e9f63057-8bf0-4d0f-80ab-ed7457f4fcc1.png)



次の画面は`company2`で`行を残す`ボタンをクリックすると`company2`が含まれる行だけが表示されます。

![regexjavascript04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/f3b5d2e9-34fd-4f54-bced-31223a8e66a0.png)



次の画面は`company2`で`行を消す`ボタンをクリックすると`company2`が含まれる行だけが消されて表示されます。

![regexjavascript05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/8a01c2e0-5297-4d3b-9330-3ede24aec861.png)



次の画面は`$n`を使った置換の例で、2桁-4桁-4桁の電話番号が3桁-3桁-4桁の電話番号に変わったのでそれを変更するものです。

![regexjavascript06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/1e09ed76-5404-4df3-8562-b11e686d6ec2.png)


次の画面はドメインの`.co.jp`と`.com`で`検索`ボタンを押すと該当する箇所が最初の`.`を除いて赤色になって出力されます。`.`を含めて検索しないと`company`の`com`も該当します。

![regexjavascript07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/d5bf9ba9-721f-464a-ba28-875a2d099392.png)


次の画面は電話番号とメールアドレスを`:`付きで表示します。正規表現が一部隠れていますが次のように入力しています。
```
((?<=Tel\d*):(\d+-?)+)+|(?<=mail):(\w+@(\w+\.?)+)
```
抽出欄は空白にしているので`$0`を指定したとみなします。`:`を出力するようにしているのはそうしないと項目間にデリミタが無くくっついて表示されてしまうからです。最終的にはこれを上に移動して`:`を`,`に置換すれば良いと思います。
それよりも正規表現が複雑なのでもっと簡単な正規表現で抽出と置換を何度も繰り返した方が楽かもしれません。

![regexjavascript08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/599fee43-767d-42ae-96db-fb0c70def6b5.png)

次はJavaScriptのコードを書いて実行させる例です。
```js:ユーザ書いたコード
let matches = [...line.matchAll(/Tel\d*:((\d+-?)+)/g)]
if (matches.length == 0) {
    return null;
} else {
    let out = "";
    matches.forEach(match => {
        out += match[1] + ",";
    });
    return out.slice(0,-1);
}
```
これを入力して`実行`ボタンを押します。このコードには`line`変数に１行分のテキストが渡され、それを加工して`return`します。`null`を返すと出力されません。
実際のプログラムは以下のようなラムダ式になっています。
```js 
let func = eval("line => {ユーザが書いたコード}");
....
let ret = func(1行分のデータ);
```

![regexjavascript09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/85a1f433-44f6-4c65-93a0-85814636247a.png)



## ソースコード

使い方はローカルの`regex.html`をブラウザで表示させ、加工したいデータをコピペするだけす。サーバーも不要です。`regex.html`をダブルクリックすれば使えます。

```html:regex.html
<html lang="ja">
<head>
<meta http-equiv="Content-Type" 
      content="text/html; charset=utf-8">
</head>
<style>
    textarea{
        font-family: monospace;
        background-color:moccasin;
    }
    input{ 
        background-color:moccasin;
        font-family: monospace;
    }
    button{ 
        background-color:lightgreen;
        color: blue;
    }
    .col2{
        padding-left: 20px;
    }
    textarea{
        overflow: scroll;
    }
    div{
        font-family: monospace;
    } 
    .output{
        white-space:pre;
    } 

    c{
        color: red;
    }

    .main{
        display: flex;  
    }
</style>
<body>
<div class="main">
    <div class="col1">
        <textarea id="input" name="input" rows="20" cols="80" wrap="off"></textarea>
    </div>
    <div class="col2">
        検索<br />
        <input type="text" id="find" name="find"
        maxlength="100"  size="40" />
        <br />
        抽出・置換<br />
        <input type="text" id="replace" name="replace" 
        maxlength="100"  size="40" />
        <br />
        <input type="checkbox" id="regex" name="regex" 
            onClick="on_click_checkbox('regex');" checked />
        <label for="regex">正規表現</label>
        <input type="checkbox" id="global" name="global" checked />
        <label for="global">すべて</label>
        <input type="checkbox" id="caseSensitive" name="caseSensitive" 
            onClick="on_click_checkbox('caseSensitive');" checked/>
        <label for="caseSensitive">Aa区別</label>
        <br /><br />
        <button type=”button” onclick="btn_click('extract');">抽出</button>
        <button type=”button” onclick="btn_click('replace');">置換</button>
        <button type=”button” onclick="btn_click('find');">検索</button>
        <button type=”button” onclick="btn_click('keep');">行を残す</button>
        <button type=”button” onclick="btn_click('delete');">行を消す</button>
        <button type=”button” onclick="btn_click('move');">入力へ移動</button>
        <br /><br />
        スクリプト<button type=”button” onclick="btn_click('exec')">実行</button><br />
        <textarea id="exec" name="exec" cols="50" rows="10"></textarea><br />
        <a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Regular_expressions"
        target="_blank">JavaScript正規表現</a>
        <a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_expressions"
    target="_blank">JavaScript正規表現メソッド</a>    
    </div>
</div>
<hr />
<div id="output" name="output" class="output"></div>

<script>
g_global = true;
const input = document.getElementById('input');
const find = document.getElementById('find');
const output = document.getElementById('output');
const replace = document.getElementById('replace');
const exec = document.getElementById('exec');
const check_regex = document.getElementById('regex');
const check_global = document.getElementById('global');
const check_caseSensitive = document.getElementById('caseSensitive');
const btn_func = {
    "extract": btn_extract,
    "replace": btn_replace,
    "find": btn_find,
    "keep": btn_keep,
    "delete": btn_delete,
    "move": btn_move,
    "exec": btn_exec,
};
function get_input() {
    return input.value.split("\n");
}
function btn_click(kind) {
    try {
        btn_func[kind]();
    } catch (error) {
        output.innerText = error;
    }
}
function btn_extract() {
    let outlines = [];
    let re = make_regex();      
    get_input().forEach(line => {
        let outl = ""
        if (g_global) {
            var matches = line.matchAll(re);
        } else {
            var matches = [line.match(re)];
            if (matches[0]== null) return;
        }
        matches.forEach(match => {
            let repl_value = replace.value;
            if (replace.value == "")
                repl_value = "$0"            
            let str = repl_value.replaceAll(/\$(\d)/g,"\${match[$1]}");
            outl += eval("`"+str+"`");

        });
        if (outl != "") {
            outlines.push(outl);
        } 
    });
    set_output(outlines);
}
function btn_find() {
    flags = "";
    if (check_global.checked)
        flags += "g";
    if (!check_caseSensitive.checked)
        flags += "i";
    if (check_regex.checked){
        var re = new RegExp("("+find.value+")", flags);
        var repl = "<c>"+"$1</c>";
    } else {
        var re = find.value;
        var repl = "<c>" + find.value + "</c>";
    }
    let outlines = [];
    get_input().forEach(line => {
        outlines.push(line.replace(re, repl));
    });
    set_output(outlines, true);

}
function btn_replace() {
    let outlines = [];
    var re = make_regex();
    get_input().forEach(line => {
        outlines.push(line.replace(re, replace.value));
    });
    set_output(outlines);
}

function btn_keep(keep=true) {
    let outlines = [];
    let re = make_regex();    
    get_input().forEach(line => {
        re.lastIndex = 0;
        let match = re.test(line);
        if ((match && keep) || (!match && !keep))
            outlines.push(line);
    });
    set_output(outlines);
}
function btn_delete() {
    btn_keep(false);
}

function btn_move() { 
    input.value = output.innerText;
}
function btn_exec() {
    let outlines = [];
    let func = eval("line => {\n"+exec.value+"\n}");
    get_input().forEach( line => {
        let ret = func(line);
        if (ret != null) {
            outlines.push(ret);
        }
    });
    set_output(outlines);
}
function make_regex(){
    let options = "";
    g_global = false;
    if (check_global.checked) {
        options += "g";
        g_global = true;
    }
    if (!check_caseSensitive.checked) 
        options += "i";
    return new RegExp(find.value, options);
}
function set_output(outlines, color=false) {
    if (color)
        output.innerHTML = outlines.join("<br />");    
    else
        output.innerText = outlines.join("\n");    
}
function on_click_checkbox(kind){
    if (!check_regex.checked && !check_caseSensitive.checked) {
        alert("Aa区別の指定しないときは正規表現が指定が必須です");
        check_regex.checked = true;
    }
}
</script>
</body>
</html>
```

- 検索や置換は行単位で行っていますので`get_input`で行単位に分割しています
- ボタンのクリック処理は`btn_click`で全てを受けて、引数の`kind`によって処理関数を分けています
    - try ~ catchを一箇所にしたかったため
- btn_extract関数（抽出）の以下のコードについて
    - ```js
            let repl_value = replace.value;
            if (replace.value == "")
                repl_value = "$0"            
            let str = repl_value.replaceAll(/\$(\d)/g,"\${match[$1]}");
            outl += eval("`"+str+"`");
      ```
    - `抽出・置換`項目が入力なしの場合は`$0`とする
    - `$0`など`$n`(ただしnは１文字)を`${match[n]}`に置換する
        - `\${match[$1]}`の`$1`は前の正規表現の`(\d)`に相当、つまり`$n`の`n`。これはreplaceの機能です
        - Javascriptで`$n`が使えるのはreplaceだけなので、上のコードはmatch,matchAllが返すデータに`$n`が使えるようにしている。実際のマッチしたデータは配列変数`match`に入っている
    - eval(\`変数名を含んだ文字列\`)は変数名を実際の値に置き換えてくれる
        - ただし、変数名は`${変数名}`とする
- btn_find関数（検索）
    - 対象の部分を`<c></c>`で囲むようにして色をつけている
    - 色付きにするため出力はHTMLにしているので`innerHTML`を使っている
- btn_replace関数（置換)
    - replace関数は`global`オプションがあれば全てに対しても置換するので、replaceAllを使っていない

# 終わりに
テストで使ったようなデータが約1万行でも処理はすぐ終わったが、加工した1万行を全選択してコピペしようとしたらすごい時間が掛かっていてCPUは100%になっていた。これは使っていたSafariの問題のようでChromeでも同じことをしても全然問題なかった。なので、この投稿には間にあいませんでしたが、クリップボードへのコピーボタンを作ってJavascriptでやれば良さそうです。
本当はPythonが使いたいけどわざわざサーバーを立てるのも面倒なので今回はやめました。しかし、`pywebview`というものが有ってPythonからWebviewを操作できるようなので、Python + HTML + Javascriptで作れそうです。もし、出来たらまた投稿します。

# 修正版のソース

<details><summary>2025/5/25修正版ソース</summary>

```regex.html
<html lang="ja">
<head>
<meta http-equiv="Content-Type" 
      content="text/html; charset=utf-8">
</head>
<style>
    textarea{
        font-family: monospace;
        background-color:moccasin;
    }
    input{ 
        background-color:moccasin;
        font-family: monospace;
    }
    button{ 
        background-color:lightgreen;
        color: blue;
    }
    .col2{
        padding-left: 20px;
    }
    textarea{
        overflow: scroll;
    }
    div{
        font-family: monospace;
    } 
    .output{
        white-space:pre;
    } 

    c{
        color: red;
    }

    .main{
        display: flex;  
    }
</style>
<body>
<div class="main">
    <div class="col1">
        <textarea id="input" name="input" rows="20" cols="80" wrap="off"></textarea>
    </div>
    <div class="col2">
        検索<br />
        <input type="text" id="find" name="find"
        maxlength="100"  size="40" />
        <br />
        抽出・置換<br />
        <input type="text" id="replace" name="replace" 
        maxlength="100"  size="40" />
        <br />
        <input type="checkbox" id="regex" name="regex" 
            onClick="on_click_checkbox('regex');" checked />
        <label for="regex">正規表現</label>
        <input type="checkbox" id="global" name="global" checked />
        <label for="global">すべて</label>
        <input type="checkbox" id="caseSensitive" name="caseSensitive" 
            onClick="on_click_checkbox('caseSensitive');" checked/>
        <label for="caseSensitive">Aa区別</label>
        <br /><br />
        <button type=”button” onclick="btn_click('extract');">抽出</button>
        <button type=”button” onclick="btn_click('replace');">置換</button>
        <button type=”button” onclick="btn_click('find');">検索</button>
        <button type=”button” onclick="btn_click('keep');">行を残す</button>
        <button type=”button” onclick="btn_click('delete');">行を消す</button>
        <button type=”button” onclick="btn_click('move');">入力へ移動</button>
        <button type=”button” onclick="btn_click('clipboard');">クリップボード</button>
        <br /><br />
        スクリプト<button type=”button” onclick="btn_click('exec')">実行</button><br />
        <textarea id="exec" name="exec" cols="50" rows="10"></textarea><br />
        <a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Regular_expressions"
        target="_blank">JavaScript正規表現</a>
        <a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_expressions"
    target="_blank">JavaScript正規表現メソッド</a>    
    </div>
</div>
<hr />
<div id="output" name="output" class="output"></div>

<script>
g_global = true;
const input = document.getElementById('input');
const find = document.getElementById('find');
const output = document.getElementById('output');
const replace = document.getElementById('replace');
const exec = document.getElementById('exec');
const check_regex = document.getElementById('regex');
const check_global = document.getElementById('global');
const check_caseSensitive = document.getElementById('caseSensitive');
const btn_func = {
    "extract": btn_extract,
    "replace": btn_replace,
    "find": btn_find,
    "keep": btn_keep,
    "delete": btn_delete,
    "move": btn_move,
    "clipboard": btn_clipboard,
    "exec": btn_exec,
};
function get_input() {
    return input.value.split("\n");
}
function btn_click(kind) {
    try {
        btn_func[kind]();
    } catch (error) {
        output.innerText = error;
    }
}
function btn_extract() {
    if (!check_regex.checked) {
        alert("抽出は正規表現にチェックが必要です");
        return;
    }
    let outlines = [];
    let re = make_regex();      
    get_input().forEach(line => {
        let outl = ""
        if (g_global) {
            var matches = line.matchAll(re);
        } else {
            var matches = [line.match(re)];
            if (matches[0] == null) return;
        }
        matches.forEach(match => {
            let repl_value = replace.value;
            if (replace.value == "")
                repl_value = "$0"            
            let str = repl_value.replaceAll(/\$(\d)/g,"\${match[$1]}");
            outl += eval("`"+str+"`");

        });
        if (outl != "") {
            outlines.push(outl);
        } 
    });
    set_output(outlines);
}
function btn_find() {
    let re = make_regex(true);
    let outlines = [];
    if (check_regex.checked) {
        get_input().forEach(line => {
            outlines.push(line.replace(re, "<c>$1</c>"));
        });
    } else {
        let repl = "<c>" + re + "</c>";
        if (g_global) {
            get_input().forEach(line => {
                outlines.push(line.replaceAll(re, repl));
            });
        } else {
            get_input().forEach(line => {
                outlines.push(line.replace(re, repl));
            });
        }

    }
    set_output(outlines, true);
}
function btn_replace() {
    let outlines = [];
    let re = make_regex();
    if (g_global) {
        get_input().forEach(line => {
            outlines.push(line.replaceAll(re, replace.value));
        });
    } else {
        get_input().forEach(line => {
            outlines.push(line.replace(re, replace.value));
        });
    }

    set_output(outlines);
}

function btn_keep(keep=true) {
    let outlines = [];
    let re = make_regex();    
    if (check_regex.checked) {
        get_input().forEach(line => {
            re.lastIndex = 0;
            let match = re.test(line);
            if ((match && keep) || (!match && !keep))
                outlines.push(line);
        });
    } else {
        get_input().forEach(line => {
            let match = false;
            if (line.includes(re)) 
                match = true;
            if ((match && keep) || (!match && !keep))
                outlines.push(line);
        });
    }
    set_output(outlines);
}
function btn_delete() {
    btn_keep(false);
}

function btn_move() { 
    input.value = output.innerText;
}
function btn_clipboard() {
    navigator.clipboard.writeText(output.innerText)
}
function btn_exec() {
    let outlines = [];
    let func = eval("line => {\n"+exec.value+"\n}");
    get_input().forEach( line => {
        let ret = func(line);
        if (ret != null) {
            outlines.push(ret);
        }
    });
    set_output(outlines);
}
function make_regex(find_proc=false){
    let options = "";
    g_global = false;
    if (check_global.checked) {
        options += "g";
        g_global = true;
    }
    if (!check_caseSensitive.checked) 
        options += "i";
    let find_str = find.value;
    if (check_regex.checked) {
        if (find_proc)
            find_str = "(" + find_str + ")";
        return new RegExp(find_str, options);
    } else
        return find_str;
    
}
function set_output(outlines, color=false) {
    if (color)
        output.innerHTML = outlines.join("<br />");    
    else
        output.innerText = outlines.join("\n");    
}
function on_click_checkbox(kind){
    if (!check_regex.checked && !check_caseSensitive.checked) {
        alert("Aa区別の指定しないときは正規表現が指定が必須です");
        check_regex.checked = true;
    }
}
</script>
</body>
</html>
```
</details>