見出し画像

全角と半角に挟まれたスペースを一括削除するには

テキストファイルの単純加工には sed コマンドを使うこと。
Linux を学んだことがある人なら、そう習っていると思います。

テキストファイルが手元にあって、その中に登場するスペースをすべて削除してください。
そう言われたら、間違いなく sed の出番です。

# sed “s/ //g” <(cat before.txt) > after.txt

しかしすべてのスペースを削除する代わりに全角文字と半角文字に挟まれたスペースだけを削除したい、としたらどうでしょうか。

例えば↓

Power Automate と Power Automate Desktop の違い

という一文があったとして、これを↓以下のように変換してほしい、という具合です。

Power AutomateとPower Automate Desktopの違い

このような条件を満たすスペースだけを削除したいとしたら、どうするのが正解なのでしょうか?

なんだかワンライナーで書けそう

まずは sed で試してみることにしました。
置換に使う s コマンドの検索パターンを工夫して解決するのが、一番近道に感じたからです。
ちなみに今回の検証環境は Docker Desktop for Windows 上の Debian ベースのコンテナーで、シェルは dash です。

# sed --version
sed (GNU sed) 4.7
Packaged by Debian
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Jay Fenlason, Tom Lord, Ken Pizzini,
Paolo Bonzini, Jim Meyering, and Assaf Gordon.
GNU sed home page: <https://www.gnu.org/software/sed/>.
General help using GNU software: <https://www.gnu.org/gethelp/>.
E-mail bug reports to: <bug-sed@gnu.org>.

全角文字を正規表現でマッチさせる

最初の問題はそもそも正規表現で全角文字をマッチさせられるかでした。
sed はとても歴史あるコマンドのため、そもそも utf8 に対応しているかも気掛かりでした。

これはいくつかのサイトを漁ってみた結果、次の正規表現が比較的安定だということが分かりました。

全角文字 → [^\x01-\x7e]

正規表現の文字クラス(角カッコ)に ASCII 文字を 16 進数で指定して、その否定(ハット)を指定すれば、それはおおよそ全角文字ということになりますね。
ちょっと乱暴にも感じますが、なるほど。早速試してみましょう。

# echo "aあbいcう" | sed "s/[^\x01-\x7e]/x/g"
xxxxxx

上手く動作しません。
入力文字列が 6 文字で、出力文字列も 6 文字になってしまいました。

ふと気になって LANG を指定しながら、いくつか実験してみました。

# LANG=C echo -n "aあbいcう" | wc
      1       1      12
# LANG=en_US.UTF-8 echo -n "aあbいcう" | wc
      1       1      12
# LANG=ja_JP.UTF-8 echo -n "aあbいcう" | wc
      1       1      12
# echo -n "aあbいcう" | LANG=C sed "s/./\n/g" | wc
     12       0      12
# echo -n "aあbいcう" | LANG=en_US.UTF-8 sed "s/./\n/g" | wc
      6       0       6
# echo -n "aあbいcう" | LANG=ja_JP.UTF-8 sed "s/./\n/g" | wc
     12       0      12

ふむ。この入力と出力から分かることは次の通りです。

  • echo コマンドは LANG 指定によらず、出力は半角文字なら 1 文字、全角文字(日本語)なら 3 文字として扱う
    → おそらくは utf8 エンコーディングが使われている?

  • GNU sed は LANG 指定によって、1 文字の扱いが異なる

    • en_US.UTF-8 のときは、全角文字を 1 文字として扱う

    • C のときと ja_JP.UTF-8 のときは、全角文字を 3 文字として扱う

これを踏まえて LANG 指定を追加してみたら、うまく動作しました。
これで一件落着です。

# echo "aあbいcう" | LANG=C sed "s/[^\x01-\x7e]/x/g"
axxxbxxxcxxx
# LANG=ja_JP.UTF-8
# echo "aあbいcう" | sed "s/[^\x01-\x7e]/x/g"
axxxbxxxcxxx

前後の文字を条件としてマッチさせる

次の問題は正規表現を駆使して、条件を満たすスペースにだけマッチしつつ、その前後の文字に制約を加えることができるかです。
これができれば sed の s コマンドを使って目的のスペースを削除するときに、スペース前後にある全角文字と半角文字の部分には触れずに済みます。これは肯定後読みというレアな表現を使えば解決できるはずです。

肯定後読みは (?<=pattern) という書き方をします。
使用例は以下のとおりです。

/(?<=p1)p2(?<=p3)/

↑このように書くと、まず p2 にマッチする部分を見つけて、その直前が p1 にマッチ、かつその直後が p3 にマッチしたなら、全体としてマッチしたことになります。
このときマッチする範囲は p2 の部分だけになります。

では sed を使って、全角文字と肯定後読みを組み合わせて、、、
という目論見でしたが、どう頑張ってもこれは実現しませんでした

いろいろ遠回りして分かったことですが、↓ということのようです。

  • 肯定後読みなどの記法は PCRE(Perl-compatible regular expressions)でのみ使える

  • GNU sed では BRE(基本正規表現)と ERE(拡張正規表現)をサポートしているが、PCRE をサポートしていない

では代わりにスペース前後の文字も含めてマッチさせつつ、後方参照(\1 とか \2)を使って置換することにしてはどうでしょうか。

# echo "a あ b い c d う え" | LANG=C sed "s/\([^\x01-\x7e]\) \([\x01-\x7e]\)/\1\2/g"
a あb いc d う え

「全角文字+スペース+半角文字」を「全角文字+半角文字」に置換しています。
これはうまく動作しました。

最後に「半角文字+スペース+全角文字」を置換する正規表現も追加して、以下のようなワンライナーが完成しました。

# echo "a あ b い c d う え" | LANG=C sed -e "s/\([^\x01-\x7e]\) \([\x01-\x7e]\)/\1\2/g" -e "s/\([\x01-\x7e]\) \([^\x01-\x7e]\)/\1\2/g"
aあbいc dう え

全角文字と半角文字に挟まれたスペースは削除しつつ、半角文字同士に挟まれたスペースと、全角文字同士に挟まれたスペースは維持されています。
すべて意図通りです。
やったね。おつかれさまでした。

おまけ

実のところ「手元のテキストファイル」というやつが Markdown 形式になっていまして、できれば Markdown の中ではこれまでどおり「全角と半角の間にはスペースを入れる」カタチで執筆作業を続けつつ、必要なタイミングで「全角と半角に挟まれたスペースを削除したテキストファイルをコマンドで生成したい」、というのが最初のモチベだったのでした。

Markdown 形式を守る上では、章題を示す # 記号やリストを示す * などは置換時に注意が必要なので、実際のところは以下のワンライナーを使っています。

# echo "### あ b い c d う え" |
> LANG=C sed \
> -e "s/\([#*-] \)\|\([0-9][0-9]*\. \)/\1\2 /g" \
> -e "s/\([^\x01-\x7e]\) \([\x01-\x7e]\)/\1\2/g" \
> -e "s/\([\x01-\x7e]\) \([^\x01-\x7e]\)/\1\2/g"
### あbいc dう え

参考文献


いいなと思ったら応援しよう!