日本語と英語のテキスト境界のスペースをどうするか?
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 について」に限らず、すべてのメニュー、ダイアログ、ヘルプセンター等々で統一して使われているのだ。
- 一方、アップルのホームページでは、日本語と英語の間に一切のスペースは存在しない。
- そのかわりフォントデザインの違いで、日本語と英語の境界をアピールしているように見える。
アップルは環境に応じて使い分けている。これは一体どうゆうことなのか?
- text-spacing が待ち遠しい | Unformed Building
- いずれは css でコントロールされる時代になるのだと思う。
ところで、自分のページはどうなっているかと言えば、これがまったく統一されていない ...。
- その時の気分で、スペースを入れたり、入れなかったり。一番ダメな状況である。
- 将来の理想的な 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' 2013年10月31日
- しかし、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' 2013年10月31日
- 句読点に続く場合は、スペースを挿入して欲しくない。
$ 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" 2013年10月31日
ひとまず欲求は満たされた。
Automator のサービスにしておく
- 上記のスクリプトをシェルスクリプト実行アクションにコピーすれば OK と思っていたのだけど、うまく動かない ...。
- まったく同じスクリプトなのになぜ?と悩んでいたら、実行環境に違いがあることに気付いた。
というわけで ...
- 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>〜〜</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で満足できるかもしれない。
またしても、無駄なコードとサービスが増えてしまったか...。
参考ページ
以下のページがたいへん参考になりました。感謝です!
- Rubyでワンライナーを書く方法のまとめ | UnNatural Language Processing Blog
- Rubyワンライナー入門 - maeharinの日記
- [Ruby]putsとprintの挙動の違いを確かめてみる : THINK PINK
- 組み込み変数 - Rubyリファレンスマニュアル
- 正規表現
- 日本語とアルファベットとの間に半角スペースを入れるか否か。 :: キミガタメ「ハ」
- text-spacing が待ち遠しい | Unformed Building
- [css3-text] text-spacing プロパティ (text-trim + text-autospace) from Koji Ishii on 2011-04-16 (public-html-ig-jp@w3.org from April 2011)
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