［Perl/PowerShell/Python］正規表現でマッチした文字列を関数で加工した内容に置換する (UNIX Epoch変換)

# はじめに

正規表現によるパターンマッチと文字列置換だけで実装しようとするとどうしても複雑になってしまいがちな処理を、マッチした文字列を別途実装した関数に処理させて、その処理結果で置換してしまうとすっきりするよ、というお話。

ついでなので、どうしても実装がうろ覚えになりがちな時刻処理を題材としました。
処理内容は「UNIX時刻(UNIX Epoch)をYYYY.mm.dd HH:MM:SS形式に変換」で、サンプルは、`1560663000 [info] 0.0.0.0:8080 server started.`というログ的な文字列を人が見てわかるようにするプログラムです。

```console
zaki@mascarpone% date -d @1560663000 "+%Y.%m.%d %H:%M:%S"
2019.06.16 14:30:00
```

# Perl

置換先に指定する内容(`s/pattern/__この部分__/`)は通常は文字列リテラルだが、正規表現のオプションに`e`を付加すればPerlコードとして解釈される。

```perl:sample.pl
#!/usr/bin/perl

$logstr = "1560663000 [info] 0.0.0.0:8080 server started.";
$logstr =~ s/^(\d+)/epoch2ymdhms($1)/e;

print $logstr, "\n";

sub epoch2ymdhms {
    my @time = (localtime shift)[5,4,3,2,1,0];
    $time[0] += 1900;
    $time[1] += 1;
    return sprintf("%04d.%02d.%02d %02d:%02d:%02d", @time);
}
```

実行結果

```console
zaki@mascarpone% ./sample.pl
2019.06.16 14:30:00 [info] 0.0.0.0:8080 server started.
```

コードを直接書けるので関数呼び出しでなくパターン置換構文内にワンライナーで書くこともできる。
また、使い捨てフィルタのように書式指定の必要がなく単に`localtime()`に突っ込んだ結果でもよければ、以下のように雑に実装しても良い。

```perl
$logstr = "1560663000 [info] 0.0.0.0:8080 server started.";
$logstr =~ s/^(\d+)/localtime $1/e;
```

結果

```console
zaki@mascarpone% ./sample.pl
Sun Jun 16 14:30:00 2019 [info] 0.0.0.0:8080 server started.
```

# PowerShell

PowerShell標準の`-replace`演算子ではコードとして評価することはできないが、C#の`regex`を使用することで、コードを実行することができる。(スクリプトブロックを指定可能)

また、PowerShellではUNIX Epoch基準の時刻処理は対応していないため、基準となる日時(1970年1月1日)をハードコーディングしてやる必要がある…模様。

```powershell:sample.ps1
function Get-Epoch2Date {
    param($epoch)
    Get-Date([datetime]"1970/1/1 0:0:0 GMT").AddSeconds($epoch) -Format "yyyy.MM.dd HH:mm:ss"
}

$logstr = "1560663000 [info] 0.0.0.0:8080 server started."
$logstr = [Regex]::Replace($logstr, "^(\d+)", { Get-Epoch2Date $args.value })

echo $logstr
```

実行結果

```text
PS C:\Users\zaki\src\regexp> .\sample.ps1
2019.06.16 14:30:00 [info] 0.0.0.0:8080 server started.
```

PowerShell(C#)ではスクリプトブロックを指定できるので、これもワンライナーで書ける。前述のPerlの簡潔な例と同様、書式指定が不要であれば以下のようにも書ける。

```powershell
$logstr = "1560663000 [info] 0.0.0.0:8080 server started."
$logstr = [Regex]::Replace($logstr, "^(\d+)", { ([datetime]"1970/1/1 0:0:0 GMT").AddSeconds($args.value)})
```

結果

```text
PS C:\Users\zaki\src\regexp> .\powershell.ps1
06/16/2019 14:30:00 [info] 0.0.0.0:8080 server started.
```

なんでmonth/day/year表記なんだろ。どっかにロケール情報とかあるのかなぁ…

# Python

Pythonは、前述Perl/PowerShellと少し異なり、[マッチオブジェクト一つのみを引数にとる関数名のみ指定(C的に言うと関数ポインタを指定する感じかな？)](https://docs.python.org/ja/3/library/re.html#re.sub)することができる。
~~そのためPerl/PowerShellより自由度はちょっと低く、インラインでコードを記述することはできない。~~
(9/6 コメント欄より) lambda式を使ってインラインで記述することもできる (TODO:あとでコードも書く)

```python:sample.py
#!/usr/bin/python

import re
from datetime import datetime

def epoch2ymdhms(m):
    return datetime.fromtimestamp(float(m.group(0))).strftime("%Y.%m.%d %H:%M:%S")

if __name__ == '__main__':
    logstr = "1560663000 [info] 0.0.0.0:8080 server started."
    logstr = re.sub('^(\d+)', epoch2ymdhms, logstr)

    print(logstr)
```

実行結果 (Python 2.7.16でも同様)

```console
zaki@mascarpone% ./sample.py
2019.06.16 14:30:00 [info] 0.0.0.0:8080 server started.
```

~~インラインでは書けないが、~~ 書式指定を省略するとこんな感じ

```python
def epoch2ymdhms(m):
    return str(datetime.fromtimestamp(float(m.group(0))))
```

結果

```console
zaki@mascarpone% ./sample.py
2019-06-16 14:30:00 [info] 0.0.0.0:8080 server started.
```

型変換多いな…

---

# 参考情報

- [PowerShell/UNIXTIMEから通常の日時に変換する方法 - Windowsと暮らす](https://win.just4fun.biz/?PowerShell/UNIXTIMEから通常の日時に変換する方法)
- [PowerShellで日付書式にカスタム書式パターンを指定する - tech.guitarrapc.cóm](https://tech.guitarrapc.com/entry/2013/02/09/030226)
- [Powershellでの高度な置換 - Qiita](https://qiita.com/acuo/items/a4f83d886c4b8a7fcf52)
- [Python の datetime オブジェクトと Unix 時間の相互変換方法のまとめ – ymyzk’s blog](https://blog.ymyzk.com/2015/04/python-datetime-unix-time/)
- [日付フォーマット(datetime⇔文字列） | Python Snippets](https://python.civic-apps.com/date-format/)