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

alias nkf

alias nkf='ruby -rnkf -e "opt=[]; opt << ARGV.shift while ARGV.first.to_s[0]==%q[-]; NKF.nkf(opt.size>0 ? opt.join(%q[ ]) : %q[-w], ARGF.read).display;" --'

ちょっと、ruby 入ってるけど nkf 入ってないところでエイリアスで書いてみるとこんな感じか。
出力文字コード指定、デフォルトで '-w' 入れるようにしてるけど、明示的にオプション指定したときは無効になるのでその時はこれもきちんと書くこと。

何もオプション書かなかったら出力指定ユニコードになるけど、入力指定とかでオプション書くときは出力指定も書くように「-Sw」とか。

入出力コード固定なら

alias nkfSw='ruby -ne "\$_.encode(Encoding::UTF_8,Encoding::CP932).display"'

でも良いかもしれない。

コード推定は

alias guess='ruby -rnkf -e "NKF.guess(ARGF.read).display"'

かな。
file コマンドあるならその方が改行まで分かるか。