2021年11月1日月曜日

PowerShellで「文字」と「文字コード」を扱う

 エンコードされた文字のコードを調べたい、あるいは文字コードから文字を調べたいということはよくある。そういう場合、専用ツールを使う手もあるが、Windows PowerShellでも文字と文字コードの変換は可能だ。

 ただ、PowerShellは文字の扱いについて微妙なのと、ファイルのエンコード、デコードは可能だが、文字列や文字を対象にしたエンコード、デコードのコマンドを持っていないため、ちょっと分かりづらい。

 インターネット検索で「PowerShell 文字コード」や「PowerShell 文字コード変換」を調べても、ファイルの文字コード変換の方法や文字化け解消の方法しか引っかからない。

 そこで、PowerShellでの文字と文字コードの扱いについて解説ページを作ることにした。

文字コードを扱う

文字コードから文字へ

 PowerShellには、文字リテラルを表記する方法がないので、文字コードを表す整数を[char]にキャストする。 内部コードは、UTF-16LE(LE:Little Endian)で、これをPowerShellではUnicodeと呼ぶ。

※以下の実行例では、行頭が“> ”のものは入力コマンド、それ以外はコマンドの出力を表す

> [char]0x41
A
> [char]0x4e9c
亜
>  [char[]]@(0x41,0x42,0x43)
A
B
C

 文字列に文字コードで表現された文字を埋め込むには、

> "aaaa$([char]0x4e9c)bbbb"
aaaa亜bbbb

とする。これはサロゲート文字にも対応できるが2文字になる

> $y="$([char]0xd840)$([char]0xdc0b)"
> $y
𠀋
> [int[]][char[]]$y | %{ $_.tostring("X")}
D840
DC0B

 一部の特殊文字に関しては、逆クオート記法で表現できる。

表記 コード 意味
`0 0x00 NUL
`a 0x07 BEL
`b 0x08 BS
`f 0x0c FF
`n 0x0d 0x0a CR LF
`r 0x0d CR
`t 0x09 HT
`v 0x0b VT

 PowerShell 7.x以降では、

`u{ユニコード}

でユニコード文字を

`e

でエスケープ(0x1b/ESC)を表現できる

文字から文字コードへ

 PowerShell内部では16bit Unicode(UTF-16LE)で文字を扱っている。文字は、[char]'c' で扱えるので、これを[int]でキャストすれば、内部文字コードに変換できる。[int]として扱うので、エンディアンの影響を受けない。

> [int][char]'a'
97
> [int][char]'う'
12358
>([int][char]'う').tostring("X")
3046
> ([int][char]'a').tostring("X")
61

 ただし、Unicodeの場合、一部の漢字がサロゲートペアになり内部的には2文字扱いとなる。なので、キャストする場合には、[int[]][char[]]としなければならない。

 これはintの配列として返ってくる。このため、1文字としての対処が難しい。例えば、16進数に.ToString(書式)で変換する場合にForEach-Objectを使わねばならない。“%”は、ForEach-Objectのエイリアスである。

 > $x
 𠀋
 > [int[]][char[]]$x | %{$_.tostring("X")}
 D840
 DC0B
 > [int][char]$x
 値 "𠀋" を型 "System.Char" に変換できません。エラー: "String には一文字しか使用できません。"
 発生場所 行:1 文字:1
 + [int][char]$x
 + ~~~~~~~~~~~~~
 + CategoryInfo          : InvalidArgument: (:) []、RuntimeException
 + FullyQualifiedErrorId : InvalidCastParseTargetInvocation

文字に関するその他のこと

string.toCharArray()

 文字列は「string.toCharArray()」でcharの配列に分割可能である。分割すれば、すべての文字列は文字の配列として扱える。それぞれの文字を文字コード(内部コードなのでUTF-16)に変換できる。

> ('a日本語'.ToCharArray()) | %{([int][char]$_).ToString("x")}
61
65e5
672c
8a9e

エンコードを変換したい

 たとえば、UTF-8に変換したいなら.NET FrameworkのSystem.Text.Encodingクラスを使って変換を行う。具体的には、[System.Text.Encoding]::UTF8.GetBytes(<string>)メソッドを使う。引数の<string>がUTF-16(Unicode)文字列であることに注意。UTF-32にも変換できる。

 また、サロゲート文字にも対応可能。なお[クラス]::メソッドは、PowerShellで.NET Frameworkのクラスにあるメソッド/プロパティを実行するための記法。このやり方で、.NET Frameworkのクラスが利用できる。

 System.Text.Encodingクラスには、デコーダーも含まれるのでUTF-8/32を含め他の文字コードからUnicodeへの変換も可能。

【準備】サロゲート文字を$yに定義

#$yはサロゲート文字
> $y="$([char]0xd840)$([char]0xdc0b)"
> [int[]][char[]]$y | %{ $_.tostring("X4")}
D840
DC0B
> $y
𠀋

UTF-16LEに変換

 バイト配列で扱うとエンディアンが見えるようになる。

> [System.Text.Encoding]::Unicode.GetBytes($y) | %{ $_.tostring("X2")}
40
D8
0B
DC

UTF-32に変換

[System.Text.Encoding]::UTF32.GetBytes($y) | %{ $_.tostring("X2")}
0B
00
02
00

UTF-8に変換

> [System.Text.Encoding]::UTF8.GetBytes($y) | %{ $_.tostring("X2")}
F0
A0
80
8B

GetBytesは文字列も受け付け可能

 GetBytesは[byte[]]になるので文字列を与えることも可能

[System.Text.Encoding]::UTF8.GetBytes('日本語') | %{ $_.tostring("X2")}
E6
97
A5
E6
9C
AC
E8
AA
9E

 毎回、コマンドを打つのも面倒なので、エンコーダーを変数に定義しておくと便利。

> $U8Encoder=[System.Text.Encoding]::UTF8
> $U32Encoder=[System.Text.Encoding]::UTF32
> $U16Encoder=[System.Text.Encoding]::Unicode
> $U8Encoder.GetBytes($y)
240
160
128
139

利用頻度が高いならプロファイル($PROFILE)で定義しておくこともできる。

Unicode(UTF-16LE)以外でエンコードされた文字を表示させたい

 [System.Text.Encoding]::<エンコード>.GetString(byte[])を使えば、他のエンコード方式のbyte配列を文字列(=Unicode)として出力できる。

> $y
𠀋
> [System.Text.Encoding]::UTF8.GetBytes($y)
240
160
128
139
> [System.Text.Encoding]::UTF8.GetString([System.Text.Encoding]::UTF8.GetBytes($y))
𠀋

 これも同じく、変数に定義したエンコーダーを使うと短く表示できる

> $utf8bytes=$U8Encoder.GetBytes($y)
> $utf8bytes
240
160
128
139
> $U8Encoder.GetString($utf8bytes)
𠀋

文字をUnicode(内部コード)で16進ダンプしたい

 文字列を.ToCharArray()で分解したあとに[int[]][char[]]をキャストして書式指定文字"X"を使って表示書式を変更する。内部コードなのでUnicodeとなる。

> [int[]][char[]]('う日本語'.ToCharArray()) | %{$_.ToString("X")}
3046
65E5
672C
8A9E

文字をUTF-8で16進ダンプしたい

 文字をUTF-8にエンコードしたあと、文字ごとに区切って、UTF-8の先頭バイトを確認して16進数に変換して出力する。文字ごとに区切らなければ、前記の「UTF-8に変換」でよい。
> $U8Encoder.GetBytes("aう日本語$($y)") | %{ if($_ -ge 0xC2){ Write-Host "" } Write-Host ('0x{0:X2} ' -f $_) -NoNewline }
0x61
0xE3 0x81 0x86
0xE6 0x97 0xA5
0xE6 0x9C 0xAC
0xE8 0xAA 0x9E
0xF0 0xA0 0x80 0x8B

文字を“U+XXXX”形式で文字コードを表示したい

 UTF-32のbyte列に変換して、BitCoverterで4つごとにint32に変換、これを16進数で表示する。

> 'U+'+[System.BitConverter]::ToInt32($U32Encoder.GetBytes($y),0).toString("X")
U+2000B

 [System.BitConverter]は静的クラスであり、オブジェクト(インスタンス)を作ることができないため[System.Text.Encoding]のように変数には格納できない。毎回打つか、関数を作る。

function global:U_Code($x) {
	return ('U+{0:X4}' -f [System.BitConverter]::ToInt32($U32Encoder.GetBytes($x),0))
}

実行例

> U_Code('a')
U+61
> U_Code('う')
U+3046
> U_Code($y)
U+2000B
> U_Code("$([char]0xd840)$([char]0xdc0b)")
U+2000B

 Unicodeのコードポイント表記は、旧Unicodeの定義では、「U+<4桁の16進数>」となっているため、書式を"X4”とした。ただし、のちに32bitに拡張されたとき、「U-<8桁の16進数>」が追加で定義されている。しかし、ここでは、接頭辞を「U+」として、最低限4桁の16進数を出力するものとした。

参考

The Unicode Standard, Version 3.0,1991 Preface,Page xxix

PDF

0 件のコメント:

コメントを投稿