Perl: use utf8 と use encoding と \w

2008年1月16日(水) 22時26分 by level
B ?

Perl で UTF8 を処理するのにかれこれ半年ほどはまっています。Perl 5.8.x Unicode関連などを参考にあれこれ試行錯誤しているのですが、結局問題は以下の URL エンコードを行うコードが期待通り動かないということです。

#!/usr/bin/perl -w
use strict;
use encoding "utf8";
sub url_encode{
  my $url=shift;
  utf8::encode($url);
  $url =~ s/([^\w\/\.\?:=&#])/sprintf("%%%02X", unpack("C", $1))/eg;
  return $url;
}

"あいうえお" を変換させると以下のように変換されます。

�%81%82�%81%84�%81%86�%81%88�%81%8A

あれこれやっていて、わかったのは、以下のコードはちゃんと動くということ。

#!/usr/bin/perl -w
use strict;
use encoding "utf8";
sub url_encode1{
  my $url=shift;
  utf8::encode($url);
  $url =~ s/([^\w])/sprintf("%%%02X", unpack("C", $1))/eg;
  return $url;
}

結果は以下のとおり。

%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A

もちろん url_encode1 はエンコードする対象範囲が異なりますが、日本語はきちんと処理できます。どうも、正規表現の [^\w] の部分に何か一文字でも追加するとだめのようです。

まさかと思って以下のコードを試してみると、ちゃんと動きます。

#!/usr/bin/perl -w
use strict;
use encoding "utf8";
sub url_encode2{
  my $url=shift;
  utf8::encode($url);
  $url =~ s/([^a-zA-Z0-9_\/\.\?:=&#])/sprintf("%%%02X", unpack("C", $1))/eg;
  return $url;
}

はて? 大抵の正規表現のチュートリアルには、\w[a-zA-Z0-9_] と等価であると書いてありますが、どうやら utf8 を有効にすると \w の意味が変ってしまうようです。しかし、それにしては url_encodeurl_encode1 の動作が異なるのが納得がいきません。

\w については、冒頭の参照ページで、宣言によっては「変数名にマルチバイト文字が使える」とあったのを思い出しました。

Perl 5.8.x Unicode関連

use utf8; と use encoding は、ソースコードの文字列を指定し、ソースコード中の文字列にUTF8フラグをたてるところは同じです。

違いは?

 use utf8;use encoding;
ソースコードの文字列にUTF8フラグたてるたてる
PerlIOレイヤなしSTDIN,STDOUTが指定した文字コードに
変数名にマルチバイト使えるFILTER=>1 が必要

変数名にマルチバイトが使えるということは、\w がマルチバイト文字にマッチするように拡張されているのでは?と思ったのですが、use encoding の場合は変数名にマルチバイト文字は使えないので、どうもそれとも違うようです。

そこで、試しに、use encoding ではなく use utf8 に変えてみると、、、

#!/usr/bin/perl -w
use strict;
use utf8;
sub url_encode{
  my $url=shift;
  utf8::encode($url);
  $url =~ s/([^\w\/\.\?:=&#])/sprintf("%%%02X", unpack("C", $1))/eg;
  return $url;
}

あら不思議、ちゃんと動きます。でも両方宣言するとだめです。

いったいどうなっているのか、さっぱりわかりません。もうギブアップです。どなたかお助けを!(ちなみに Perl は V5.8.8 を使っています)

1/19 更新

コメントを頂いた内容などを参考に実験してみたところ、use encoding "utf8" の時に UTF8 フラグを落とした文字列に対する [\w][\wa] の動作が異なるようです。

#!/usr/bin/perl -w
use strict;
use encoding "utf8";
use Devel::Peek;

my $org="abc+-あいう";
utf8::encode($org);
my @str = ( $org =~ m/[\wa]/g );
my $str = join('', @str);
Dump $str;

結果:

SV = PV(0x10011ef0) at 0x10022614
  REFCNT = 1
  FLAGS = (PADBUSY,PADMY,POK,pPOK)
  PV = 0x10030510 "abc\343\343\343"\0
  CUR = 6
  LEN = 8

正規表現を [\w] にするか、use encoding "utf8"; を使用しなければ期待通りに動作します。

SV = PV(0x10011d88) at 0x10022614
  REFCNT = 1
  FLAGS = (PADBUSY,PADMY,POK,pPOK)
  PV = 0x100d3890 "abc"\0
  CUR = 3
  LEN = 4

これが

Perl 5.8.x Unicode関連

ですが、弾さんによると、encoding プラグマはお手軽だけど副作用が大きいとのことです。

のひとつなのでしょうか。

最終更新: 2008年1月19日(土) 16時57分

コメント (6)

1 1/17 11:00 foo
(c1) [2008/01/17 11:00:56] by foo

ややこしくて私も理解できていませんが、とりあえず
use encoding "utr8",Filter => 1;
とすればurl_condeも期待の結果を返すようですよ。

2 1/18 11:19 junichiro
(c2) [2008/01/18 11:19:36] by junichiro

$1 で取得できる正規表現でキャプチャした値が、
use utf8; の時と
use encoding 'utf8'; の時とで
異なることが確認できました。
理由はわかりませんが、ヒントになればと思います。

use Devel::Peek;
my @str = ( url =~ m/([^\w\/\.\?:=&#])/g );
my $str = join('', @str);
Dump $str;

こんな感じで確認しました。

3 1/19 09:18 takeshi
(c3) [2008/01/19 09:18:02] by takeshi

バグじゃないですか?
use encoding "utf8";した上でpack "C", $iでUTF8フラグを落とした00-FFの文字列を作って確認したところ、/[^\w]/では80-FF全てにマッチします(つまり\wでないと認識される)が、/[^\wa]/ではいくつかがマッチしません(ちょうどlatin-1として判別したような感じです。つまりlatin-1のコード表でaに飾りの付いたような文字とかは\wと認識され、掛け算記号や割り算記号は\wでないと認識されています)。
記載されたコードでは、たまたま81や82は\wでないと認識されたので変換され、E3は\wと認識されたので変換されません。

> \w がマルチバイト文字にマッチするように拡張されているのでは?

これはちゃんと定義されています。UTF8フラグあり文字列のマッチングでは、全角数字とかもマッチするようになってます。ただ、UTF8フラグなしの場合の\wの定義とか見つけられなかったので、この場合の仕様はあやしいです。使わないでa-z...と書き下すのが吉かと。

ちなみに上記テストでuse utf8;は影響しませんでした。

4 1/19 16:04 level
(c4) [2008/01/19 16:04:50] by level

junichiroさん、takeshi さん、ありがとうございます。
こちらでも試してみましたが、use encoding "utf8";の場合、
[\w]と[\wa]の動作が異なりますね。どうもバグっぽいです。

5 2/01 12:18 通りすがり。
(c5) [2008/02/01 12:18:22] by 通りすがり。

URI::Escape::uri_escape または URI::Escape::uri_escape_utf8 ではだめなのでしょうか?

6 9/06 05:42 nobuoka
(c6) [2008/09/06 05:42:39] by nobuoka

初めまして。 今更だと思いますが、私もいましがた似たようなことで悩んでいたので書き込みさせて貰います。
私が思うに、
 utf8::encode($url);
で $url を binary strings に変換しているにもかかわらず、
 $url =~ s/([^\w\/\.\?:=&#])/sprintf("%%%02X", unpack("C", $1))/eg;
の右辺が text strings になっているのが問題なのではないかと思います。
以下のように置換部分だけを binary strings で処理するようにすると問題は起きないと思います。
(うちの環境 (perl version 5.10.0) では問題ありませんでした。)

#!/usr/bin/perl -w
use strict;
use encoding "utf8";
sub url_encode{
my $url=shift;
utf8::encode($url);
no encoding; # use encoding 無効化
$url =~ s/([^\w\/\.\?:=&#])/sprintf("%%%02X", unpack("C", $1))/eg;
use encoding "utf8"; # 再設定
return $url;
}

まあ use utf8; ならいけたりする辺り微妙にバグなのかなーとも思いますけど。
ただ binary strings と text strings をごちゃ混ぜにして使うと思わぬ事が起こることが多いですから混ぜないように心がけるのが Perl 5.8 以降の重要なところだと思います。

トラックバック

トラックバックは検索対象外です。

この記事にリンクしているページ < >

  1. データがありません。