バランサーのヘルスチェックのリクエストヘッダー

AWS EC2のエラスティックロードバランサーをクラシックから付け替えているのだけど、アプリケーションロードバランサーにしたら、ヘルスチェックのリクエストが変わったみたいで、Webアプリ側のチェックで応答が悪くヘルスチェックに苦労した。
というわけで、ヘルスチェックのリクエストのヘッダーを見ましょう。
そのために、小さな Webサーバーをセットします、まあ、WEBrick でいいよね。小さな Webサーバは前にもいくつか話題にしています、時間の掛る Webサーバ - hs9587’s diary TLS1.2 サーバ - hs9587’s diary "小さい"Rackアプリケーション - hs9587’s diary

というわけで、このくらいかな

ruby -r webrick -e "WEBrick::HTTPServer.new(DocumentRoot: %q[./], Port: 8080, RequestCallback: Proc.new{|req,| req.raw_header.join.+(%Q[\n]).display }).start"

実行して、同じマシンで「lynx localhost:8080」とかすると、こうい風にリクエストのヘッダーが見えます。

(0)ec2-user@ip-172-31-<何か>[34] ruby -r webrick -e "WEBrick::HTTPServer.new(DocumentRoot: %q[./], Port: 8080, RequestCallback: Proc.new{|req,| req.raw_header.join.+(%Q[\n]).display }).start"
[2021-08-14 11:39:58] INFO  WEBrick 1.7.0
[2021-08-14 11:39:58] INFO  ruby 3.0.1 (2021-04-05) [x86_64-linux]
[2021-08-14 11:39:58] INFO  WEBrick::HTTPServer#start: pid=29917 port=8080
Host: localhost:8080
Accept: text/html, text/plain, text/css, text/sgml, */*;q=0.01
Accept-Encoding: gzip, bzip2
Accept-Language: en
User-Agent: Lynx/2.8.8dev.15 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/1.0.2k-fips

127.0.0.1 - - [14/Aug/2021:11:40:04 JST] "GET / HTTP/1.0" 200 77
- -> /

起動ディレクトリには「Hello world!」とあるだけの index.html を置いときます。

というわけで、ロードバランサーやターゲットグループにこの機を配置します。

Classic Load Balancer より

host: 172.31.<何か>:8080
User-Agent: ELB-HealthChecker/1.0
Accept: */*
Connection: keep-alive

172.31.<何か> - - [14/Aug/2021:14:14:53 JST] "GET /index.html HTTP/1.1" 200 77
- -> /index.html

Application Load Balancer の Target groups より

Host: 172.31.<何か>:8080
Connection: close
User-Agent: ELB-HealthChecker/2.0
Accept-Encoding: gzip, compressed

172.31.<何か> - - [14/Aug/2021:14:13:53 JST] "GET /index.html HTTP/1.1" 200 77
- -> /index.html

ということで、User-Agent: が 1.0、2.0 とか兎も角、Connection: の値や、Accept: か Accept-Encoding: など、微妙に違っています。
所要のWebアプリでは「[msg "Request Missing an Accept Header"]」と出ていたので、まさにそういう事でした。

Facebook や Instagram のデータのダウンロードで JSON の日本語

FacebookInstagram の投稿など、(個人)データをまとめてダウンロードすることが出来る、データ形式JSON もあるのだけど、その日本語が読めない。
日本語というか非ASCIIというかそういう文字。

  • Facebook - アカウント - 設定とプライバシー - 設定
    • あなたのFacebook情報 - 個人データをダウンロード - コピーをリクエスト - フォーマット - JSON
  • Instagram - 設定
    • プライバシーとセキュリティ - データのダウンロード - ダウンロードをリクエスト - 情報フォーマット - JSON

ダウンロードはこの辺から。

猫廼舎。2019年09月28日(土) 15:00:59

それで、この辺の投稿

フェイスブック
https://www.facebook.com/hi.shimura.9/posts/2627556467311596

インスタグラム
https://www.instagram.com/p/B28a5ANHUI6/?igshid=1qe54csg90kch

ちなみにツイッターの投稿はこちら
https://twitter.com/hs9587/status/1177825511520788485

それはこういう文字です

irb(main):009:0> "猫廼舎。".force_encoding('ASCII')
=> "\xE7\x8C\xAB\xE5\xBB\xBC\xE8\x88\x8E\xE3\x80\x82"

それが上記でダウンロードしたなかで、

フェイスブック

  {
    "timestamp": 1569650461,
    "attachments": [
      {
        "data": [
          {
            "media": {
              "uri": "photos_and_videos/InstagramPhotos_DG0b8PZadQ/71746085_2627556473978262_1018468751701442560_n_2627556467311596.jpg",
              "creation_timestamp": 1569650461,
              "title": "Instagram Photos",
              "description": "\u00e7\u008c\u00ab\u00e5\u00bb\u00bc\u00e8\u0088\u008e\u00e3\u0080\u0082"
            }
          }
        ]
      }
    ]
  },


インスタグラム

  {
    "media": [
      {
        "uri": "media/posts/201909/70725295_131712588184345_6958651618331221860_n_18004788103249812.jpg",
        "creation_timestamp": 1569650456,
        "media_metadata": {
 …… (写真のメタデータ少々)
        },
        "title": "\u00e7\u008c\u00ab\u00e5\u00bb\u00bc\u00e8\u0088\u008e\u00e3\u0080\u0082"
      }
    ]
  },

こんな感じ。
(それそれ一部切り出し、なかの uri の記述はそれぞれの同梱データでの画像ファイル名)

日本語文字列

というわけで

"\u00e7\u008c\u00ab\u00e5\u00bb\u00bc\u00e8\u0088\u008e\u00e3\u0080\u0082"

「猫廼舎。」

JSON なら UTF-8 のはずなんだけどそうじゃない、手近の JSONパーサはそう思って変換して読めなくなってしまう。(Rubyjsonライブラリです)

この辺参考に文字を読みます

UTF-8 の16進表記を2桁ずつにして「\u00」に繋げてるみたい、どういう形式(名前)のエンコーディングなんだろう。

というわけで、encoding: 'ascii' で読み、そのように変換します、Ruby です。

irb(main):026:0> neko
=> "              \"description\": \"\\u00e7\\u008c\\u00ab\\u00e5\\u00bb\\u00bc\\u00e8\\u0088\\u008e\\u00e3\\u0080\\u0082\"\n"
irb(main):027:0> neko.encoding
=> #<Encoding:US-ASCII>
irb(main):028:0> neko.gsub(/\\u00([a-f0-9]{2})/m){ "#{$1.to_i(16).chr}"}.force_encoding('UTF-8')
=> "              \"description\": \"猫廼舎。\"\n"

try catch のエラーと例外

ちょっとファイル参照の用があって、一度試して無かったら別のところを見に行くとかしたかった。キャッシュ見に行って無かったら本体とか、ローカルを見に行って無かったらネットを参照とか、そういう感じ。
ファイル参照は file_get_contents() PHP: file_get_contents - Manual を使えば良いでしょう。一度試すというの、PHP に try catch という枠組み PHP: 例外(exceptions) - Manual があるというのでちょっと検討する。
結論から言うと、try catch を使うのではなく、 file_get_contents() の結果を === false でチェックすれば良いのでした。 try catch のエラーと例外 - hs9587’s diary

try catch と例外(exceptions)

PHP: 例外(exceptions) - Manual より、PHP にも try catch という枠組みがあるということでこういうコードを書いてみる。

<?php$photo_local  $photo_s3 適宜 」

try {
  $photo = file_get_contents($photo_local);
} catch (Exception $e)  {
  $photo = file_get_contents($photo_s3);
}

  header('Content-Type: image/jpeg');
  echo $photo;
?>

うまくいかない。
エラーは発生するが、catch はされない、catch方面の二度目のファイル参照はなく、続行し 200 OK になるが何も出てこない。

PHP Warning: file_get_contents(「何か」): failed to open stream: No such file or directory in 「どこか」

PHP マニュアル前掲、例外(exceptions) のところに注意書きがあるが、まさにその事態だ。

注意:
PHP の内部関数の多くは エラー報告 を使っており、例外を使っているのは新しい オブジェクト指向 の拡張モジュールのみです。 しかし、ErrorException を使えば簡単にエラーを例外に変換することができます。 この変換テクニックが使えるのは、致命的でないエラーに限ります。

そこにサンプルも書いてある

<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
    throw new ErrorException($message, 0, $severity, $filename, $lineno);
}

set_error_handler('exceptions_error_handler');
?>

さっきのやつ、冒頭にこのコードを書き足すと catch 出来るようになる。

PHP Fatal error: Uncaught ErrorException: file_get_contents(asdf): failed to open stream: No such file or directory in /var/www/html/g「ええと」:15
Stack trace:
#0 [internal function]: exceptions_error_handler(2, 'file_get_conten...', '/var/www/html/g...', 15, Array)
#1 /var/www/html/g「ええと」(15): file_get_contents('「別の」')
#2 {main}
thrown in /var/www/html/g「ええと」 on line 15

15行目というのは二つ目の方、そっちまで来るようになった。
catch節でまた例外になり、特に補足してないので Fatal になる。

このコードでは全てのエラーを例外に throw してる、ちょっと従来のエラーの枠組みから離れすぎてるように感じるのでもう少しコードを整理する。

set_error_handler() と restore_error_handler()

PHPマニュアルより

filename が見つからない場合、maxlength がゼロより小さい場合、あるいはストリーム内での指定した offset へのシークが失敗した場合に E_WARNING レベルのエラーが発生します。

というわけで

  • 例外への変換を限局する
    • エラーレベル、折角 E_WARNING とわかってるのでそうする
    • set_error_handler() による変換の登録はファイル参照の直前にする
      • もう try の中でいいや
    • catch したらすぐに restore_error_handler() で例外への変換をやめる
    • catch できなくても finallyに restore_error_handler() で例外への変換をやめる
  • 折角 ErrorException を投げてるので catch もそれに限定する

の様な方針でコードを整理する。

<?php
  # https://www.php.net/manual/ja/language.exceptions.php 注意 エラーを例外に
  function exceptions_error_handler($severity, $message, $filename, $lineno) {
    throw new ErrorException($message, 0, $severity, $filename, $lineno);
  }
?>
<?php$photo_local  $photo_s3 適宜 」

  try { # catch (ErrorException $e)
      set_error_handler('exceptions_error_handler', E_WARNING);
      # 次行の失敗に備えてエラーを例外に変換して throwするようセットする
      $photo = file_get_contents($photo_local);
  } catch (ErrorException $e) {
      restore_error_handler(); # catch したのでエラーの例外を throwするの止める
      error_log($e);
      #error_log(var_export($e, true));
      #error_log(var_export(get_class($e), true));
      error_log("Try: file_get_contents($photo_s3)");
      $photo = file_get_contents($photo_s3);
  } finally {
      restore_error_handler(); # いずれにしてもエラーの例外を throwするの止める
  } # catch (ErrorException $e)

  header('Content-Type: image/jpeg');
  echo $photo;
?>

これで望んだように動く。
それでエラー見ながら様子をみてると、そうはいってもファイル参照失敗ではプログラム続行してて、200 OK 返す、まあ echo の中味なくてブラウザでは何も出て来ないが、或いは画像が壊れてるとか。

file_get_contents()

さて、前掲この関数の説明によると

読み込んだデータを返します。失敗した場合に false を返します。
警告
この関数は論理値 false を返す可能性がありますが、false として評価される値を返す可能性もあります。 詳細については 論理値の セクションを参照してください。この関数の返り値を調べるには ===演算子 を 使用してください。

ということでした。
失敗しても値を返してくれるので、それをチェックすれば try catch の俎上に載せる必要なかったか。

=== false

それならこれでいいや

<?php$photo_local  $photo_s3 適宜 」

  function error_log_mine($message, $line) {
    error_log("PHP Mine. $message in " .  __FILE__ . " on line $line");
  }

  error_log_mine("Try: file_get_contents($photo_local)", __LINE__+1);
  $photo = file_get_contents($photo_local);
  if (! $photo === false) {
    error_log_mine("Success: file_get_contents($photo_local)", __LINE__-2);
  } # if (! $photo === false)

  if ($photo === false) {
    error_log_mine("Try: file_get_contents($photo_s3)", __LINE__+1);
    $photo = file_get_contents($photo_s3);
    if (! $photo === false) {
      error_log_mine("Success: file_get_contents($photo_s3)", __LINE__-2);
    } # if (! $photo === false)
  } # if ($photo === false)

  header('Content-Type: image/jpeg');
  echo $photo;
?>

ここでは注意書き通り === で false と比較した。
読み込み先が空ファイルだったりすると、正常に読み込んだ結果が空文字列になり PHPの文脈では偽という事になる。それを再読み込み側に回したいときはその様にチェックしましょう。

error_log のメッセージはちょっと書き過ぎてるかも、想定環境で多いほうの分岐とか、事前(Try)、事後(Success)のどちらかとか、適宜コメントアウトしましょう。

ちなみに

apple_health_export 出力の整形

前回
iPhone のヘルスケア apple_health_export - hs9587’s diary
iPhone に入れたヘルスケア情報を csv に出来た。
そうするとこんな感じになる

20200418053000+0900,35.9,degC,HKQuantityTypeIdentifierBodyTemperature
20200417211100+0900,36,degC,HKQuantityTypeIdentifierBodyTemperature
20200417155800+0900,36.1,degC,HKQuantityTypeIdentifierBodyTemperature
20200417055000+0900,36.2,degC,HKQuantityTypeIdentifierBodyTemperature
20200416221500+0900,36,degC,HKQuantityTypeIdentifierBodyTemperature
20200416054000+0900,35.9,degC,HKQuantityTypeIdentifierBodyTemperature
20200415052400+0900,35.9,degC,HKQuantityTypeIdentifierBodyTemperature

十分わかり易い形だが、コンピューターで読むのではなく、人が見るならもう少し整形しても良い。

format_cda.rb

require 'csv'
require 'time'

CSV::Converters.merge!( {
  row3: ->(cell, info){ info.index != 3 ? cell : cell[24..-1]   },
  row1: ->(cell, info){ info.index != 1 ? cell : '%2.1f' % cell },
  row0: ->(cell, info){ info.index != 0 ? cell : \
          Time.parse(cell).strftime('%Y-%m-%d %H:%M %z')[2..-3] },
} )

CSV.filter(out_col_sep: "\t", converters: [:row0, :row1, :row3]) {}

そうするとこんな感じ

20-04-18 05:30 +09      35.9    degC    BodyTemperature
20-04-17 21:11 +09      36.0    degC    BodyTemperature
20-04-17 15:58 +09      36.1    degC    BodyTemperature
20-04-17 05:50 +09      36.2    degC    BodyTemperature
20-04-16 22:15 +09      36.0    degC    BodyTemperature
20-04-16 05:40 +09      35.9    degC    BodyTemperature
20-04-15 05:24 +09      35.9    degC    BodyTemperature

CSV(library csv (Ruby 2.7.0 リファレンスマニュアル)) の普段僕が余り使わない機能を使ったので少しコードの説明。

普段 CSV.parse とか CSV.read で読むと配列の配列になるのでいろいろしていた。
今回は入力を少し整形してすぐ出力するフィルターが良いと思った、 CSV.filter
https://docs.ruby-lang.org/ja/latest/class/CSV.html#S_FILTER があった。

カンマ区切りはコンピュータとのやり取りには良いのだけど、人が見るにはちょっとうるさいかな、出力はタブにしましょう、オプションに「out_col_sep: "\t"」。
入出力オプションほかのはこの辺
class CSV (Ruby 2.7.0 リファレンスマニュアル) を参考に、出力なので頭に「out_」か「output_」を付けるとのこと。

書式の変換には converters: オプションと CSV::Converters
https://docs.ruby-lang.org/ja/latest/class/CSV.html#C_-CONVERTERS を使ってみる。
コンヴァーターの実装

  row3: ->(cell, info){ info.index != 3 ? cell : cell[24..-1]   },
  row1: ->(cell, info){ info.index != 1 ? cell : '%2.1f' % cell },
  row0: ->(cell, info){ info.index != 0 ? cell : \
          Time.parse(cell).strftime('%Y-%m-%d %H:%M %z')[2..-3] },

その適用時、列数の指定とかはないみたい。どの列にもみんな適用されるので第二引数から列数を確認する。「?」三項演算子(
https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html#cond
)、本当は否定の条件は避けるべきなんだが、可変部を後ろにした方が分かり易いかとそうした。二列目 row2 は儘なので書かない。

コンヴァーターを使うことにしたら、CSV.filter のブロックでやることが無くなったので空っぽのブロックを付ける「 {} 」

CSV.filter(out_col_sep: "\t", converters: [:row0, :row1, :row3]) {}

空っぽのブロックって、他になんか書き方無いのかな。

iPhone のヘルスケア apple_health_export

iPhone のヘルスケア アプリ、体重とか体温とか入力してるんだけど、外には書き出せないのかな。

  1. ヘルスケア アプリ
    1. 「概要」画面
      1. 右肩 人型マーク
    2. 人型マーク 画面
      1. 画面最下部に「すべてのヘルスケアデータを書き出す」リンク
        1. 押下
      2. 「ヘルスケアデータを書き出す」ダイアローグ
        1. 「書き出す」釦
          1. 押下
      3. 書き出し中
      4. 「書き出したデータ」zipデータ
        1. 送り先選択
        • メールとかメッセージかなあ、あるいは"ファイル"に保存
        • ファイル名注意「書き出したデータ.zip」前のがあると置き換える
      5. 保存しました
    3. 人型マーク画面「完了」
      1. 押下
    4. 「概要」画面

として、データがファイルで手に入る。何とかして PCに持って来よう。

Windows PowerShell

> Expand-Archive .\書き出したデータ.zip
\書き出したデータ> tree /F
フォルダー パスの一覧
……
└─apple_health_export
        export.xml
        export_cda.xml

XML データ。
見たいあたり CSV にしましょうか。

export_cda.xml が入力データっぽい、今日はそれを読む。export.xml の方はサイズが大きい量も多く、IPhone で自動収集されるデータ、歩数とか、入ってるみたい。

helth_care_data.rb

require 'rexml/document'

REXML::Document.new(ARGF.read) \
  .root \
  .get_elements('//component') \
  .map do |comp|
    {
      high:  [:attributes, :values],
      value: [:get_text,   :value ],
      unit:  [:get_text,   :value ],
      type:  [:get_text,   :value ],
    } \
    .map do |key, (method, value)|
      comp.get_elements("*//#{key}").first.send(method).send(value)
    end \
    .join(',')
  end \
#  .sort \
  .join("\n") \
  .display

XML項目の説明は特にしないが、日付 observation/effectiveTime/high は属性値に値があるので #attributes.values で値を取る、そうすると配列になるのだが、属性が一つしかないのでまあ何とかなる。

>ruby helth_care_data.rb 書き出したデータ\apple_health_export\export_cda.xml

こんな感じかな。

PowerShell とパイプと文字コード

Windows PowerShell でテキストをパイプでやり取りするときなんか日本語化けた。
ちょっと事情よく分かってなくて、取り敢えず対症療法とメモ。

$OutputEncoding = [Text.Encoding]::Default

PowerShell 起動時、$OutputEncoding はこうなってる

PS C:\Users\hs9587> $OutputEncoding


IsSingleByte      : True
BodyName          : us-ascii
EncodingName      : US-ASCII
HeaderName        : us-ascii
WebName           : us-ascii
WindowsCodePage   : 1252
IsBrowserDisplay  : False
IsBrowserSave     : False
IsMailNewsDisplay : True
IsMailNewsSave    : True
EncoderFallback   : System.Text.EncoderReplacementFallback
DecoderFallback   : System.Text.DecoderReplacementFallback
IsReadOnly        : True
CodePage          : 20127

そして

PS C:\Users\hs9587> [Text.Encoding]::Default


BodyName          : iso-2022-jp
EncodingName      : 日本語 (シフト JIS)
HeaderName        : iso-2022-jp
WebName           : shift_jis
WindowsCodePage   : 932
IsBrowserDisplay  : True
IsBrowserSave     : True
IsMailNewsDisplay : True
IsMailNewsSave    : True
IsSingleByte      : False
EncoderFallback   : System.Text.InternalEncoderBestFitFallback
DecoderFallback   : System.Text.InternalDecoderBestFitFallback
IsReadOnly        : True
CodePage          : 932
PS C:\Users\hs9587> [Text.Encoding]::GetEncoding('shift_jis')


BodyName          : iso-2022-jp
EncodingName      : 日本語 (シフト JIS)
HeaderName        : iso-2022-jp
WebName           : shift_jis
WindowsCodePage   : 932
IsBrowserDisplay  : True
IsBrowserSave     : True
IsMailNewsDisplay : True
IsMailNewsSave    : True
IsSingleByte      : False
EncoderFallback   : System.Text.InternalEncoderBestFitFallback
DecoderFallback   : System.Text.InternalDecoderBestFitFallback
IsReadOnly        : True
CodePage          : 932
PS C:\Users\hs9587> [Text.Encoding]::GetEncoding('utf-8')


BodyName          : utf-8
EncodingName      : Unicode (UTF-8)
HeaderName        : utf-8
WebName           : utf-8
WindowsCodePage   : 1200
IsBrowserDisplay  : True
IsBrowserSave     : True
IsMailNewsDisplay : True
IsMailNewsSave    : True
IsSingleByte      : False
EncoderFallback   : System.Text.EncoderReplacementFallback
DecoderFallback   : System.Text.DecoderReplacementFallback
IsReadOnly        : True
CodePage          : 65001
PS C:\Users\hs9587> [Text.Encoding]::GetEncoding('euc-jp')


BodyName          : euc-jp
EncodingName      : 日本語 (EUC)
HeaderName        : euc-jp
WebName           : euc-jp
WindowsCodePage   : 932
IsBrowserDisplay  : True
IsBrowserSave     : True
IsMailNewsDisplay : True
IsMailNewsSave    : True
IsSingleByte      : False
EncoderFallback   : System.Text.InternalEncoderBestFitFallback
DecoderFallback   : System.Text.InternalDecoderBestFitFallback
IsReadOnly        : True
CodePage          : 51932
PS C:\Users\hs9587> [Text.Encoding]::GetEncoding('iso-2022-jp')


BodyName          : iso-2022-jp
EncodingName      : 日本語 (JIS)
HeaderName        : iso-2022-jp
WebName           : iso-2022-jp
WindowsCodePage   : 932
IsBrowserDisplay  : False
IsBrowserSave     : False
IsMailNewsDisplay : True
IsMailNewsSave    : True
IsSingleByte      : False
EncoderFallback   : System.Text.InternalEncoderBestFitFallback
DecoderFallback   : System.Text.InternalDecoderBestFitFallback
IsReadOnly        : True
CodePage          : 50220
PS C:\Users\hs9587> [Text.Encoding]::GetEncoding('us-ascii')


IsSingleByte      : True
BodyName          : us-ascii
EncodingName      : US-ASCII
HeaderName        : us-ascii
WebName           : us-ascii
WindowsCodePage   : 1252
IsBrowserDisplay  : False
IsBrowserSave     : False
IsMailNewsDisplay : True
IsMailNewsSave    : True
EncoderFallback   : System.Text.EncoderReplacementFallback
DecoderFallback   : System.Text.DecoderReplacementFallback
IsReadOnly        : True
CodePage          : 20127

デフォルトがそうなんだから最初からデフォルトにしとけば良いのに

丸数字をよける

Shift_JIS の文脈で丸数字をちょっとよけたかった

# coding: Windows-31J
(''..'').inject([ARGF.read.force_encoding('Windows-31J'), 1]) do |(input, i), n|
  [input.gsub(n, "(#{i})"), i + 1]
end.first.display

こんな感じか。

もう少し何とか、
丸数字一つずつじゃなくてまとめてとかどうかないかな、String#tr では変換先一文字だっけ

追記 (2019/11/17)

コメントにアドレスあげたようにスラックでいくつか助言をいただく

するとこんな感じ

gsub をハッシュで

# coding: Windows-31J
ms = (''..'').inject([{}, 1]){ |(h, i), m| h[m] = "(#{i})"; [h, i + 1] }.first
ARGF.set_encoding('Windows-31J').read.gsub(/[#{ms.keys.join}]/, ms).display

一連なりで書くとこんな

# coding: Windows-31J
ARGF.set_encoding('Windows-31J').read \
  .gsub(*((''..'').each_with_object([{}, 1]) do |m, h_i|
    h_i[0][m], h_i[1] = "(#{h_i[1]})", h_i[1] + 1
  end.first.each_with_object([//, {}]) do |(k, v), ks_h|
    ks_h[0], ks_h[1][k] = /[#{ks_h[0].source}#{k}]/, v
  end)).display

さらに unicode_normalize を使う

# coding: Windows-31J
ms = (''..'').each_with_object({}) \
  { |m, h| h[m] = "(#{m.encode('utf-8').unicode_normalize(:nfkc)})" }
ARGF.set_encoding('Windows-31J').read.gsub(/[#{ms.keys.join}]/, ms).display

一連なりで書くとこんな

# coding: Windows-31J
ARGF.set_encoding('Windows-31J').read \
  .gsub(*((''..'').each_with_object({}) do |m, h|
    h[m] = "(#{m.encode('utf-8').unicode_normalize(:nfkc)})"
  end.each_with_object([//, {}]) do |(k, v), ks_h|
    ks_h[0], ks_h[1][k] = /[#{ks_h[0].source}#{k}]/, v
  end)).display

メソッドチェインで一連なりに書くのは返って長くなるし分かり難いかな。

分かり難い所説明少し追記 (/11/23)

メソッドチェインで一連なりに書くの分かり難い辺りについて。

gsub の引数、正規表現と置換先ハッシュの配列をとって、配列の頭に「*」を付けて2引数としている。

その引数や正規表現と置換先ハッシュの配列を作るのも分かり難い。

まず、丸数字の並びからそれをキーに括弧数字を値にするハッシュを作る、#unicode_normalize を使うかどうかでちょっと分かれる、ハッシュへの値の代入がハッシュ自身を返すわけじゃないので #each_with_object が便利。

(''..'').each_with_object([{}, 1]) do |m, h_i|
    h_i[0][m], h_i[1] = "(#{h_i[1]})", h_i[1] + 1
  end.first
(''..'').each_with_object({}) do |m, h|
    h[m] = "(#{m.encode('utf-8').unicode_normalize(:nfkc)})"
  end

「m」は丸数字、「h」は結果のハッシュ、「i」は番号の整数、「h_i」はその二つ繋げたののつもり。

そうやって作った一つのハッシュから、そのキーを集めた正規表現と自身と同じハッシュの二つを要素にする配列を作る、自身とそこから派生するなにかと二つの要素を集めてくるのどうしたらいいのだろう、今回はハッシュ自身をもう一度作り直した。

.each_with_object([//, {}]) do |(k, v), ks_h|
    ks_h[0], ks_h[1][k] = /[#{ks_h[0].source}#{k}]/, v
 end

正規表現で文字クラスのネスト注意、そうすると全部フラットにまとめた文字クラスと同じ意味になる 正規表現 (Ruby 2.6.0)
「k」はキー、「v」は値、「ks」はキー複数、「h」は結果のハッシュ、「ks_h」はその二つ繋げたののつもり。

いずれも多重代入注意、ブロック引数受けるとき括弧でくくっておくと分解してくれるの便利、結果オブジェクト側では分解しすぎるとオブジェクトに反映しないので前記のように繋げた名前の変数で受ける。

さらに追記、gsub のブロック (/12/1)

#unicode_normalize があるので数字を数える必要ないなら、.gsub の置換先、わざわざハッシュにすることもなかった、ブロックで良かった。

# coding: Windows-31J
ARGF.set_encoding('Windows-31J').read \
 .gsub(/[①-⑳]/){ |m| "(#{m.encode('utf-8').unicode_normalize(:nfkc)})" }.display