概要
ほとんどの正規表現を備えているテキストエディタは正規表現にマッチした箇所をハイライトにするとかまたは別の文字列に置換することは可能ですが、マッチしたものだけエディタに残す機能がない。もし有ったらそのテキストエディタを教えて欲しいです。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
です。
上記の結果を入力欄に移したいときは入力へ移動
ボタンをクリックします。そうすると次のような画面になります。
そして行末の","を""に置換します。次の画面で置換
ボタンをクリックします。
これ以降は上へ移動
ボタンは押しませんので。
次の画面はcompany2
で行を残す
ボタンをクリックするとcompany2
が含まれる行だけが表示されます。
次の画面はcompany2
で行を消す
ボタンをクリックするとcompany2
が含まれる行だけが消されて表示されます。
次の画面は$n
を使った置換の例で、2桁-4桁-4桁の電話番号が3桁-3桁-4桁の電話番号に変わったのでそれを変更するものです。
次の画面はドメインの.co.jp
と.com
で検索
ボタンを押すと該当する箇所が最初の.
を除いて赤色になって出力されます。.
を含めて検索しないとcompany
のcom
も該当します。
次の画面は電話番号とメールアドレスを:
付きで表示します。正規表現が一部隠れていますが次のように入力しています。
((?<=Tel\d*):(\d+-?)+)+|(?<=mail):(\w+@(\w+\.?)+)
抽出欄は空白にしているので$0
を指定したとみなします。:
を出力するようにしているのはそうしないと項目間にデリミタが無くくっついて表示されてしまうからです。最終的にはこれを上に移動して:
を,
に置換すれば良いと思います。
それよりも正規表現が複雑なのでもっと簡単な正規表現で抽出と置換を何度も繰り返した方が楽かもしれません。
次は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行分のデータ);
ソースコード
使い方はローカルの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)
に相当、つまり$n
のn
。これはreplaceの機能です - Javascriptで
$n
が使えるのはreplaceだけなので、上のコードはmatch,matchAllが返すデータに$n
が使えるようにしている。実際のマッチしたデータは配列変数match
に入っている
-
- eval(`変数名を含んだ文字列`)は変数名を実際の値に置き換えてくれる
- ただし、変数名は
${変数名}
とする
- ただし、変数名は
-
- btn_find関数(検索)
- 対象の部分を
<c></c>
で囲むようにして色をつけている - 色付きにするため出力はHTMLにしているので
innerHTML
を使っている
- 対象の部分を
- btn_replace関数(置換)
- replace関数は
global
オプションがあれば全てに対しても置換するので、replaceAllを使っていない
- replace関数は
終わりに
テストで使ったようなデータが約1万行でも処理はすぐ終わったが、加工した1万行を全選択してコピペしようとしたらすごい時間が掛かっていてCPUは100%になっていた。これは使っていたSafariの問題のようでChromeでも同じことをしても全然問題なかった。なので、この投稿には間にあいませんでしたが、クリップボードへのコピーボタンを作ってJavascriptでやれば良さそうです。
本当はPythonが使いたいけどわざわざサーバーを立てるのも面倒なので今回はやめました。しかし、pywebview
というものが有ってPythonからWebviewを操作できるようなので、Python + HTML + Javascriptで作れそうです。もし、出来たらまた投稿します。