日本語と英語のテキスト境界のスペースをどうするか?

こうゆう発想のワンライナースクリプトが大好き。

uedamac:MEMO ueda$ echo "わたしは aho です。4 さいです。" |
 sed 's/\([^a-zA-Z0-9]\) \([a-zA-Z0-9]\)/\1\2/g' |
 sed 's/\([a-zA-Z0-9]\) \([^a-zA-Z0-9]\)/\1\2/g'
わたしはahoです。4さいです。
出版社に送る原稿には日本語と英単語の間にスペースを入れない方が良いらしいので贖罪のためにシェル芸やります。 – 上田ブログ

なるほど、確かに日本語と英語の間にスペースを入れるということは、段落先頭の字下げでスペースを使うようなものかもしれない。理想は、ページフォーマットや CSS で制御されるべきものだと思う。... とここまで考えて、ちょっぴり思い直した。そう言えば、OSX 環境上は日本語と英語の境界にスペースを挿入していたはず。

  • 例えば、アップルメニューの1行目は「この Mac について」となっている。
  • Mac の両側には、半角スペースが挿入されているのだ。
  • その証拠に、この半角スペースを無視してはショートカットを登録できない。
  • 半角スペースを挿入した「この Mac について」で登録することで有効になる。

  • 半角スペースは「この Mac について」に限らず、すべてのメニュー、ダイアログ、ヘルプセンター等々で統一して使われているのだ。
  • 一方、アップルのホームページでは、日本語と英語の間に一切のスペースは存在しない。

  • そのかわりフォントデザインの違いで、日本語と英語の境界をアピールしているように見える。


アップルは環境に応じて使い分けている。これは一体どうゆうことなのか?


ところで、自分のページはどうなっているかと言えば、これがまったく統一されていない ...。

  • その時の気分で、スペースを入れたり、入れなかったり。一番ダメな状況である。
  • 将来の理想的な css の世界に備えて、日英間のすべてのスペースを取り除くか!と考えたが、現状のページデザインでは明らかに読み難くなること必至。
  • 日英間のスペースを取り除くスクリプトは既にある。(冒頭)
  • ならば、逆にスペースを追加するスクリプトを作ってみる。
  • そうすれば、スクリプトが自動でスペースの追加も削除もやってくれる。

時代に逆行するかもしれないけど、試してみた。

日本語と英語の間にスペースを追加するシェルスクリプト

冒頭のスクリプトの逆なのだからすぐできると思ったけど、やってみると想像以上に手がかかった ...。

基本
$ echo 私はzarigani toshです。| sed -E 's/([^A-Za-z0-9])([A-Za-z0-9])/\1 \2/g' | sed -E 's/([A-Za-z0-9])([^A-Za-z0-9])/\1 \2/g'
私は zarigani   tosh です。
  • いかん、いかん ...。そのまま逆にしただけでは、既に存在するスペースにも重ねてスペースを追加してしまう。
  • それに、自分のブログは英数字だけでなく、様々なスクリプト記号も頻繁に出現する。半角記号も英数字と同じように扱いたい。
  • アスキーコード表(man ascii)の見える文字すべて(= スペースから ~ チルダまで)を対象にしてみた。
$ echo 私はzarigani toshです。|sed -E 's/([^ -~])([ -~])/\1 \2/g'|sed -E 's/([ -~])([^ -~])/\1 \2/g'
私は zarigani tosh です。

$ echo ファイル~/.bashrcです。|sed -E 's/([^ -~])([ -~])/\1 \2/g'|sed -E 's/([ -~])([^ -~])/\1 \2/g'
ファイル ~/.bashrc です。
  • 既に存在するスペースに、重ねて追加されてしまう不具合も修正してみた。
$ echo 'ファイル ~/.bashrc です。'|sed -E 's/([^ -~])([ -~])/\1 \2/g'|sed -E 's/([ -~])([^ -~])/\1 \2/g'
ファイル  ~/.bashrc  です。

$ echo 'ファイル ~/.bashrc です。'|sed -E 's/([^ -~])([!-~])/\1 \2/g'|sed -E 's/([!-~])([^ -~])/\1 \2/g'
ファイル ~/.bashrc です。
  • ところで、数値の前後にまでスペースが追加されると、しつこい気がする。
2013年10月31日
2013 年 10 月 31 日
  • 自分は、数値の場合はスペースを追加しない方が好き。対応してみる。
$ echo '2013年10月31日'|sed -E 's/([^ -~])([!-/:-~])/\1 \2/g'|sed -E 's/([!-/:-~])([^ -~])/\1 \2/g'
20131031
  • しかし、OSX 10.8.5 のような表記に対しては、スペースを入れて欲しい。
  • 数値のみが連続しない場合は、スペースを入れるようにしてみた。
$ echo '早急にOSX 10.8.5をインストールしてください。'|sed -E 's/([^ -~])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~])/\1 \2/g'
早急に OSX 10.8.5 をインストールしてください。

$ echo '2013年10月31日'|sed -E 's/([^ -~])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~])/\1 \2/g'
20131031
  • 句読点に続く場合は、スペースを挿入して欲しくない。
$ echo 'まだ、OSX 10.9にアップデートしてません。'|sed -E 's/([^ -~])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~])/\1 \2/g'
まだ、 OSX 10.9 にアップデートしてません。

$ echo 'まだ、OSX 10.9にアップデートしてません。'|sed -E 's/([^ -~[:punct:]])([0-9]*[!-/:-~][0-9]*)/\1 \2/g'|sed -E 's/([0-9]*[!-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g'
まだ、OSX 10.9 にアップデートしてません。
  • 個人的なことだが、自分は(半角)と(全角)を使い分けたい。
  • 半角()は、その前後に余分な空間を空けたくない時に使う。
  • よって、半角()の境界にはスペースを追加して欲しくないのだ。
$ echo '私は(zarigani tosh)です。'|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
私は(zarigani tosh)です。

$ echo '私はzarigani toshです。'|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
私は zarigani tosh です。

$ echo '私は(zarigani tosh)です。'|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
私は(zarigani tosh)です。
  • [:punct:]効果のためか、全角()の内側境界にも余分なスペースが追加されなくて、いい感じ。
  • おまけで、数値はすべて半角に統一したい。
$ echo '2013年10月31日'|tr 0-9 0-9|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"
20131031

ひとまず欲求は満たされた。

Automator のサービスにしておく
  • 上記のスクリプトシェルスクリプト実行アクションにコピーすれば OK と思っていたのだけど、うまく動かない ...。
  • まったく同じスクリプトなのになぜ?と悩んでいたら、実行環境に違いがあることに気付いた。
    • ターミナルの bash は、デフォルトで LANG=ja_JP.UTF-8 な環境。
    • 一方、Automatorbash は、LANG 設定なしの環境。

というわけで ...

  • LANG に設定して、export するとちゃんと動いた!
export LANG=ja_JP.UTF-8
cat|tr 0-9 0-9|sed -E "s/([^ -~[:punct:]])([0-9]*[!-'*-/:-~][0-9]*)/\1 \2/g"|sed -E "s/([0-9]*[!-'*-/:-~][0-9]*)([^ -~[:punct:]])/\1 \2/g"

  • 失敗したときのことを考えて、元のテキストをクリップボードへコピーしてから、スペースの追加を実行するようにした。
  • とんでもない結果になってしまった場合は、command-v でペーストすれば、元のテキストが復活するのだ。

さらなる課題

喜んで使っていたのもつかの間、上記のサービスにはまだ問題が多い。

  • 文章の一部を選択して、狙った部分のみに日英間のスペースを追加する時は、うまく動く。
  • しかし、ひと通り書き終わった日記全体を選択して実行すると ...
    • pre タグの中までスペースが追加されてしまう。
    • pre タグの中はソースコードの場合がほとんどなので、無闇にスペースを追加して欲しくない。
    • blockquote タグの中も同様である。引用文は可能な限り原文のままとしたい。
    • HTML タグの中も同様である。
    • はてな記法の[]の中も同様である。
  • つまり、以下の記法の内側では、スペースの追加をしないようにしておきたい。
# ブロック要素のタグ
 >>〜〜<<
 >||〜〜||<
 >|〜〜|<
 <pre>〜〜</pre>
 <blockquote>〜〜</blockquote>

# インライン要素のタグ
 <〜〜>
 [〜〜]

Ruby コードに書き直す

  • 特定の範囲でスペースを追加しないようにするのは、ちょっと面倒だ。
  • 自分の現状の知識では、正規表現だけで実現するのは無理そう。
  • よって、正規表現にこだわらず、条件判定をしながら処理する方法でやってみる。
  • また、コードを書くならシェルより Ruby の方が慣れているので、書き直してみた。
$ echo '早急にOSX 10.8.5をインストールしてください。'|ruby -pe '
$_.tr!("0-9","0-9"); 
$_.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2"); 
$_.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2");
'
早急に OSX 10.8.5 をインストールしてください。
  • 基本的にシェルと同じ処理だが、シェルから Ruby を呼び出す都合上、エスケープの問題でその表現に悩んだ。
  • シェルスクリプトのシングルクォート内で、Ruby のシングルクォートを表現するために \u0027 を使っている。
  • Ruby における \1 や \2 は、シェルスクリプトの中では \\1、\\2 と表現する必要があった。
ruby -pe '何らかのコード; ...'
  • 上記コードの ruby -p オプションは、以下のコードと同じ。
while gets
  何らかのコード; ...
  print $_
end
  • $_ には、パイプ経由で渡されたテキストが1行ずつ代入される。(gets がテキストを1行ずつ読み取る)
  • 行処理の最後に $_ を自動的に出力してくれるので、$_ に対する破壊的メソッドの連続で処理するのだ。

ブロック要素のタグ内ではスペースを追加しない

  • これが修正前の元コード
export LANG=ja_JP.UTF-8

cat | ruby -pe '
$_.tr!("0-9","0-9"); 
$_.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2"); 
$_.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2");
'
  • pre タグや blockquote タグ内でスペースを追加しないようにするため、以下のように修正してみた。
export LANG=ja_JP.UTF-8

cat | ruby -pe '
BEGIN{block_point = 0}

block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|<pre *.*>|<blockquote *.*>)/
if block_point == 0 then
  $_.tr!("0-9","0-9")
  $_.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2")
  $_.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|<\/ *pre>|<\/ *blockquote>)/
'
  • ちなみに BEGIN ブロックよって、while gets ループ手前に、初期化するコードが追加される。
  • また、END ブロックも用意されており、while gets ループ脱出後に、締めのコードが追加される。
ruby -pe 'BEGIN{初期化コード}; 何らかのコード; ... END{締めのコード};'
  • 上記コードは、以下のように展開されるイメージ。
初期化コード

while gets
  何らかのコード; ...
  print $_
end

締めのコード

インライン要素のタグ内でもスペースを追加しない

  • さらなる難題は、インライン要素の特定の範囲でもスペースを追加しないようにすること。
  • 具体的には、半角の<>と[]の内側では、スペースの追加は行わないようにしたい。
  • 悩んだのち、1行ずつ<>と[]の正規表現をマッチさせて、
    • マッチした部分($&)
    • マッチした部分の手前($`)
    • マッチした部分の後方($~.post_match)
  • 以上3つの部分に分けて、地道に繰り返し処理することにした。
export LANG=ja_JP.UTF-8

cat | ruby -ne '
BEGIN{block_point = 0}

r=$_
block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|^<pre *.*>|^<blockquote *.*>)/
if block_point == 0 then
  r = ""
  e = $_
  $_ =~ /(\[.*\]|<.*>)/
  while $`
    s,m,e = $`,$&,$~.post_match
    s.tr!("0-9","0-9")
    s.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2")
    s.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
    r << s + m
    e =~ /(\[.*\]|<.*>)/
  end
  e.tr!("0-9","0-9")
  e.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\/:-~][0-9]*)/, "\\1 \\2")
  e.gsub!(/([0-9]*[!-\u0027*-\/:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
  r << e
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|^<\/ *pre>|^<\/ *blockquote>)/
puts r
'


  • ループで重複する条件判定を一つにまとめた。
  • HTMLタグ境界でも、内側のテキストと連続させて判定し、スペースを追加できるように修正した。
export LANG=ja_JP.UTF-8

cat | ruby -ne '
BEGIN{block_point = 0}

r=$_
block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|^<pre *.*>|^<blockquote *.*>)/
if block_point == 0 then
  r = ""
  s, e = "", $_
  while e =~ /(<a *.+?<\/ *a>|<.*?>|\[.*?\])/
    s, m, e = $`, $&, $~.post_match
    s << e[0]
    s.tr!("0-9","0-9")
    s.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
    s.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
    s.slice!(-1)
    r << s + m
  end
  e = (s[-1] || " ") + e
  e.tr!("0-9","0-9")
  e.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
  e.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
  e.slice!(0)
  r << e
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|^<\/ *pre>|^<\/ *blockquote>)/
puts r
'

実験

  • 以上のコードを Automator に追加して、text-spacing サービスを作ってみた。

  • そして、今書いているこの日記自体を処理してみるのだ。幸運を祈りながら ...。
  • ちなみに、この日記は日本語と英語の間のスペースは、一切入れずに書いた。


____ここからtext-spacing未処理____

  • 若干修正。
  • preタグ、blockquoteタグが、その要素自身の中でマッチして、その後の動作がおかしくなっていた。
    • 「preタグやblockquoteタグ内でスペースを追加しないようにするため、以下のように修正してみた。」の直下のコードが怪しい。
  • そこで、行頭から始まるpreタグ、blockquoteタグのみを対象にするよう修正した。(自分の作業環境では、ほぼ問題ない)
  • よって、preタグ、blockquoteタグが行頭から始まっていないと、その中の日本語と英語の境界にもスペースが追加されてしまう。


その結果が、今の状態。

フォントを修正

その後...

  • アップルのページに習って、英数字はLucida Grandeが優先されるように修正してみた。
body { font-family:'Lucida Grande','Hiragino Kaku Gothic ProN', Meiryo, sans-serif; }
  • もしかしたら、無駄なスペースの挿入なんて止めて、Lucida Grandeで満足できるかもしれない。

またしても、無駄なコードとサービスが増えてしまったか...。

text-spacingサービスで挿入したスペースを取り除く(text-spacing-none)

  • 挿入したスペースは取り除けるようにしておきたいと思ったので。(ほとんどtext-spacingと同じ)
export LANG=ja_JP.UTF-8

cat | ruby -ne '
BEGIN{block_point = 0}

r=$_
block_point += 1 if $_ =~ /(?:^>.*>$|^>\|$|^>\|.*\|$|^<pre *.*>|^<blockquote *.*>)/
if block_point == 0 then
  r = ""
  s, e = "", $_
  while e =~ /(<a *.+?<\/ *a>|<.*?>|\[.*?\])/
    s, m, e = $`, $&, $~.post_match
    s << e[0]
    s.tr!("0-9","0-9")
    s.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
    s.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
    s.slice!(-1)
    r << s + m
  end
  e = (s[-1] || " ") + e
  e.tr!("0-9","0-9")
  e.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
  e.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
  e.slice!(0)
  r << e
end
block_point -= 1 if $_ =~ /(?:^<<$|^\|<$|^\|\|<$|^<\/ *pre>|^<\/ *blockquote>)/
puts r
'
  • text-spacingとtext-spacing-noneの差分
(master)$ diff -u text-spacing.rb.sh text-spacing-none.rb.sh
--- text-spacing.rb.sh	2013-11-02 15:58:32.000000000 +0900
+++ text-spacing-none.rb.sh	2013-11-02 15:58:26.000000000 +0900
@@ -12,15 +12,15 @@
     s, m, e = $`, $&, $~.post_match
     s << e[0]
     s.tr!("0-9","0-9")
-    s.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
-    s.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
+    s.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
+    s.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
     s.slice!(-1)
     r << s + m
   end
   e = (s[-1] || " ") + e
   e.tr!("0-9","0-9")
-  e.gsub!(/([^ -~[:punct:]])([0-9]*[!-\u0027*-\-:-~][0-9]*)/, "\\1 \\2")
-  e.gsub!(/([0-9]*[!-\u0027*-.:-~][0-9]*)([^ -~[:punct:]])/, "\\1 \\2")
+  e.gsub!(/([^ -~]) ([!-\u0027*-~])/, "\\1\\2")
+  e.gsub!(/([!-\u0027*-~]) ([^ -~])/, "\\1\\2")
   e.slice!(0)
   r << e
 end