0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-23

概要

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

追記)
@takahasinaoki さんのコメントより

vscodeなら正規表現で検索し、
コマンドパレットで(Select All Occurrences of Find Match「一致するすべての出現箇所を選択」)を実行(ショートカットキーは Alt + Enter)
Ctrl + C でコピー
Ctrl + A で全選択
Ctrl + V で貼り付け

住所:東京都中央区1-1-1 Tel:03-1234-5678
住所:東京都中央区1-1-2 Tel:03-1234-5679

このようなテキストが有ったとき電話番号だけ抽出したい。つまり、次のような正規表現で抽出すると、その次のような結果がエディタ上に残って欲しいのです。もし、"-"も不要ならエディタ上に残ったものに"-"を""に置換すれば"-"なしの電話番号が得られます。 

Tel:((\d+-?)+)
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

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

regexjavascript02.png

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

regexjavascript03.png

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

regexjavascript04.png

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

regexjavascript05.png

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

regexjavascript06.png

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

regexjavascript07.png

次の画面は電話番号とメールアドレスを:付きで表示します。正規表現が一部隠れていますが次のように入力しています。

((?<=Tel\d*):(\d+-?)+)+|(?<=mail):(\w+@(\w+\.?)+)

抽出欄は空白にしているので$0を指定したとみなします。:を出力するようにしているのはそうしないと項目間にデリミタが無くくっついて表示されてしまうからです。最終的にはこれを上に移動して:,に置換すれば良いと思います。
それよりも正規表現が複雑なのでもっと簡単な正規表現で抽出と置換を何度も繰り返した方が楽かもしれません。

regexjavascript08.png

次はJavaScriptのコードを書いて実行させる例です。

ユーザ書いたコード
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変数に1行分のテキストが渡され、それを加工してreturnします。nullを返すと出力されません。
実際のプログラムは以下のようなラムダ式になっています。

let func = eval("line => {ユーザが書いたコード}");
....
let ret = func(1行分のデータ);

regexjavascript09.png

ソースコード

使い方はローカルのregex.htmlをブラウザで表示させ、加工したいデータをコピペするだけす。サーバーも不要です。regex.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関数(抽出)の以下のコードについて
    •       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は1文字)を${match[n]}に置換する
      • \${match[$1]}$1は前の正規表現の(\d)に相当、つまり$nn。これは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で作れそうです。もし、出来たらまた投稿します。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?