perlのutfフラグとpackの確認整理

perlでUnicode文字列を扱う時に、毎度毎度混乱するのでメモ。

unpackとpackで文字列をバラしたり戻したりしたい場合がある。utfフラグがついていて文字コードが判ってる場合はシンプルだし、今どきは「utf8」で決め打ちしてもいいと思うけど、pack unpackの挙動の確認と整理。

『perlpacktut - pack と unpack のチュートリアル』
https://perldoc.jp/docs/perl/5.8.8/perlpacktut.pod#unicode

↑ 公式のドキュメントがすべて。以下は素人のわたしの確認と整理のためのものです。

【確認したこと】

  • スクリプトはutf8で書かれていて、スクリプトの中の文字列にはutf8のutfフラグがついている
  • utfフラグがついている文字列「おはようabcâë」
    • おはよう ← 日本語
    • abc ← アルファベット
    • âë ← アクサン、ウムラウト
  • unpack も pack もテンプレート「U」で、対象をバイナリとして評価する。
  • utfフラグがついた状態と外した状態で出力。

フラグあり/なしで、元の文字コード「utf8」「euc-jp」「sjis」の3種類を出力して比較した。

perlの以下のスクリプトで確認

#!/usr/bin/perl
use strict;
use utf8;
use Encode;
use Encode::Guess qw/ euc-jp shiftjis 7bit-jis utf8 /;
$Encode::Guess::NoUTFAutoGuess = 1; # utf16 utf32 を候補から外す
use Data::Dumper;
binmode STDOUT=>":utf8";

my $str = 'おはようabcâë';

print "\nflag[utf8]=============================\n";
foreach (unpack("U*", $str)){
	print $_ . " ";
	printf qq{U+%04X }, $_;
	print pack("U*", $_) . "\n";
}
print "\nflag[euc]=============================\n";
my $euc = Encode::encode('euc-jp', $str);
$euc = Encode::decode('euc-jp', $euc);
foreach (unpack("U*", $euc)){
	print $_ . " ";
	printf qq{U+%04X }, $_;
	print pack("U*", $_) . "\n";
}
print "\nflag[sjis]=============================\n";
my $sjis = Encode::encode('sjis', $str);
$sjis = Encode::decode('sjis', $sjis);
foreach (unpack("U*", $sjis)){
	print $_ . " ";
	printf qq{U+%04X }, $_;
	print pack("U*", $_) . "\n";
}

print "\n[utf8]=============================\n";
my $none = Encode::encode('utf8', $str);
foreach (unpack("U*", $none)){
	print $_ . " ";
	printf qq{U+%04X\n}, $_;
}

print "\n[euc]=============================\n";
my $none_euc = Encode::encode('euc-jp', $str);
foreach (unpack("U*", $none)){
	print $_ . " ";
	printf qq{U+%04X\n}, $_;
}
print "\n[sjis]=============================\n";
my $none_sjis = Encode::encode('sjis', $str);
foreach (unpack("U*", $none_sjis)){
	print $_ . " ";
	printf qq{U+%04X\n}, $_;
}

utfフラグをつけたもの は文字種関係なく、出力されたコードはすべて同じ。unpack packの「U」はバイナリとして評価するから当然。

12362 U+304A お
12399 U+306F は
12424 U+3088 よ
12358 U+3046 う
97 U+0061 a
98 U+0062 b
99 U+0063 c
226 U+00E2 â
235 U+00EB ë

コードは同じだけどShiftJISだけはアクサンが文字化けして表示できなかったはしかたがない。

utfフラグを外したもの はShiftJISだけ日本語とアクサンのコードが違っていた。

[utf8 euc]=============================
227 U+00E3
129 U+0081
138 U+008A
227 U+00E3
129 U+0081
175 U+00AF
227 U+00E3
130 U+0082
136 U+0088
227 U+00E3
129 U+0081
134 U+0086
97 U+0061
98 U+0062
99 U+0063
195 U+00C3
162 U+00A2
195 U+00C3
171 U+00AB

[sjis]=============================
130 U+0082
168 U+00A8
130 U+0082
205 U+00CD
130 U+0082
230 U+00E6
130 U+0082
164 U+00A4
97 U+0061
98 U+0062
99 U+0063
63 U+003F
63 U+003F

utf8とeucは日本語が3バイト、アクサンが2バイト。ShiftJISは日本語が2バイト、アクサンが1バイト。

[utf8 euc]
「お」→ 「U+00E3」「U+0081」「U+008A」
「â」→ 「U+00C3」「U+00A2」

[ShiftJIS]
「お」→ 「U+0082」「U+00A8」
「â」→ 「U+003F」

これはしかたない、か。全部同じだとすっきりしてたんだけど…。

utfフラグさえついていれば、unpackされた文字はpackで簡単に復元できる。
unpackした「お」の文字コードポイント「12362」をpackに渡せばOK。

pack("U*", '12362'); # 「お」

テンプレートの「U」以外、たとえば16進数にする「h」でも同じことができる。

my $h_str = unpack("h*", Encode::encode('utf8',$str));
print $h_str . "\n";
print Encode::decode('utf8',pack("h*", $h_str)) . "\n";

ただ、これは文字コードが事前にわかっている必要がある。
文字列をunpackに渡す時にのutfフラグを外す。
packに渡して戻ってくる文字列にはutfフラグをつける。

ということで、文字種に合わせた対応が必要になってくる。その点、「U」を使えば「utf8」「euc」「sjis」など気にする必要がない。


なんでまたこんなことをダラダラ確認してたかというと。

ブログなんかのCMSで入力されたテキストの中にCMSで余計なHTMLタグをつけられると困る部分があったら、とりあえずその部分を一時的に退避して、ひと通り処理した後で復元するような時。

↓ こんなテキストが入力されたら

自分のブログからちょっと以下引用すると
<blockquote>
「サーバーとデータベース知識も必須か::ひまつぶし雑記帖」
<a href="https://t2aki.doncha.net/?id=1134732831">https://t2aki.doncha.net/?id=1134732831</a>

↑ 元の雑記がこれなんだけど、頑張ってサーバーとかデータベースを勉強するぞ! という主旨のところじゃなくて、飲み屋のカウンターで酔っ払いがなんか言ってるぞレベルのネタのところがリアクション対象。
</blockquote>
これはこれでありだと思う。

「<blockquote>」と「</blockquote>」で挟まれている部分のテキストはそのまま表示したい。

だけど、ブログのCMSでは入力されたテキストを
「<p>」テキスト「</p>」
など「Pタグ」なんかのHTMLのタグを付けることがほとんど。「blockquote」なのに、それだと意図しない表示になってしまう。

なので「<blockquote>」と「</blockquote>」で挟まれている部分のテキストを、とりあえず別のものに変換しておく、というのはありがちだ。
別のものというのは16進数など、ほかと被らなくて扱いやすい文字列だったらなんでもいい。

ここで、unpackとpackの出番となる。

□ blockquoteの中のテキストをunpackに渡して退避

my $flg;
$flg->{bq} = ($body =~ s!<blockquote[^>]*>!\tBLOCK_LTAG\t!g);
if( $flg->{bq} ){
    $body =~ s!</blockquote>!\tBLOCK_RTAG\t!g;
    $body =~ s!(\tBLOCK_LTAG\t)([^\t]+)(\tBLOCK_RTAG\t)!sprintf("%s%s%s",$1,$to_num->($2),$3)!seg;
}

□ pack「U*」で退避と復元

  • 「U」でコードポイントが一文字ずつ「文字列」として出力される
  • コードポイント文字列の長さは文字種によって違う
  • packで復元する時に一文字のコードポイントが必要になる

packで復元するために、一文字分ずつコードポイント文字列を取り出せるように「コードポイント文字列の長さ」も記載しておく。 (unpackで文字列を取り出す時のテンプレート用文字列、「a5」などと「a」+「コードポイント文字列の長さ」)

to_numで退避。to_strで復元

my $to_num = sub{ my $s = shift;
    my $str; my $templ;
    foreach ( unpack("U*", $s) ){
        $str .= $_;
        $templ .= 'a' . length($_);
    }
    return $templ . ':' . $str;};
my $to_str = sub{ my $s = shift;
    my($templ, $str) = split(':', $s);
    return join('',map{ pack("U", $_) } unpack($templ,$str));};

□ 退避した文字列(途中折り返し)

a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a2a2a5a5a5a5a5a5a5a5a5a2a2a2a2a3a3a3a3a2a2a3a3a3a3a3a2a2a2a3a2a2a3a3a2a3a3a3a2a3a2a2a3a3a3a2a2a3a3a2a2a2a2a2a2a2a2a2a2a2a2a2a3a3a3a3a3a2a2a2a3a2a2a3a3a2a3a3a3a2a3a2a2a3a3a3a2a2a3a3a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a4a2a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a2a2a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5:
123001246912540124961254012392124871254012479125051254012473306933567212418245173892012363585812402124141238812406123753860935352240861230110609732104114101102613410411611611211558474711650971071054610011111099104974611010111647631051006149495152555150565149346210411611611211558474711650971071054610011111099104974611010111647631051006149495152555150565149604797625910108593322080312398386093535212364123711242812394124351238412369123931228938929243731238712390124691254012496125401239212363124871254012479125051254012473124342119324375123771242712382333212392123561235820027260881239812392123711242912376124191239412367123901228939154124152362712398124591245412531124791254012391372041238725173123561236412394124351236335328123871239012427123821252412505125231239812493124791239812392123711242912364125221245012463124711251912531235503593712290

□ pack「h*」で退避と復元

入力される文字コードがわかってるならこっちの方がシンプル

my $to_num = sub{ my $s = shift;
    return unpack("h*", Encode::encode('utf8', $s));};
my $to_str = sub{ my $s = shift;
    return Encode::decode('utf8', pack("h*", $s));};

□ 退避した文字列

3e08c83e285b3e38cb3e38093e38cb3e188a3e38783e38cb3e28fb3e38993e38cb3e289b7ef95a8eda893e28285efb589e0a883e18b8a3a33e182b3e18eb3e184a3e186b3e18799eb9198e8a895e8b693e08d8a0c3160286275666d3228647470737a3f2f2472316b696e246f6e6368616e2e65647f2f39646d31313334373332383331322e38647470737a3f2f2472316b696e246f6e6368616e2e65647f2f39646d313133343733323833313c3f216e3b3a0a02e6819025e58383e18ea9eb9198e8a893e18c83e18393e28c83e18aa3e28393e180a3e18193e189a3e08189e0a195ecb5b3e183a3e186a3e285b3e38cb3e38093e38cb3e188a3e18b83e38783e38cb3e28fb3e38993e38cb3e289b3e28295eb8985ecb7b3e18993e28b83e18e912023e188a3e18483e18684e8bbb6e798a3e18ea3e188a3e18393e28d83e18893e28383e18aa3e18f83e186a3e08189e3a2b3e18fb5e1bb83e18ea3e28ba3e286a3e383b3e28fb3e38cb3e187a9e58493e183a6e98593e18483e18c83e18aa3e28393e18b88e8a083e183a3e186a3e28b83e18e93e38ca3e38993e38ba3e18ea3e38d83e28fb3e18ea3e188a3e18393e28d83e18c83e38aa3e282a3e28fa3e287b3e387a3e383b5efaeb8e1b1a3e0828

……Unicodeはほんと伏魔殿

[2026-02-14 14:54:47]v0.0.0

Menu