dorapon2000’s diary

忘備録的な。セキュリティとかネットワークすきです。

Perlのfalsyな値の==とeqによる比較結果

Perlのfalsyな値を比較した時、True/Falseのどちらになるか整理します。検証するfalsyな値は以下の4つです。

  • 空文字""
  • 数字の0
  • 文字列の"0"
  • undef

検証コードはGitHubにあげています。

github.com

検証環境

==で比較

==は数字が等しいかの判定に使う演算子です。

f:id:dorapon2000:20220330234747p:plain

結果は明快で、すべてがTrueになりました。表のうち*をつけたところは数字の比較ではないという警告が出ました。0と"0"の比較だけは数字の比較とみなしてくれたみたいです。

eqで比較

eqは文字列が等しいかの判定に使う演算子です。

f:id:dorapon2000:20220330234757p:plain

こちらは、"0"と空文字""の2グループに分けて考えられそうです。数字の0と文字列の0は"0"のグループ、空文字""とundefは空文字""のグループで、同じグループ同士のeqはTrueになり、そうでないならFalseでした。*が付けている部分は、比較にundefを扱う部分で、変数を初期化していないという警告が出ました。

❯ perl -E 'say undef'

undefは出力すると何も表示されないので、空文字と同列というのはわかる話です。

()について

空Arrayの()もfalsyな値です。ただ、空Arrayは実質undefなので、結果中はundefとしてまとめています。

say(defined ()    ? 1 : 0); # -> 0
say(defined undef ? 1 : 0); # -> 0
say(defined ""    ? 1 : 0); # -> 1
say(defined "0"   ? 1 : 0); # -> 1
say(defined 0     ? 1 : 0); # -> 1

# my @l = ();
my @l = undef;
$l[0] = "Hello";
say $l[0]; # -> Hello

Twitterのリストをサブアカウントにコピーできるサービスをリリースした

2023/05/21追記:Twitter API料金体系変更に伴い、サービスを終了しました。想像以上の方に利用していただいてたようです。今までご利用ありがとうございました。

リリースしたといいつつ、もう公開して3ヶ月ほど経ちます。リスト移動とアンドロイドを掛けたListIDOというサービスです。自分がほしいからと、勉強も兼ねて作ったサービスですが、作ったからには自分と同じ悩みを抱えている人(いるかわからないけど)にも知ってもらいたいと思い、この記事で紹介します。

https://listido.dorapon2000.xyz/listido.dorapon2000.xyz

そもそもリスト機能とは

リストについて - Twitterヘルプセンター

Twitterの機能の1つで、アカウントのグループやフォルダみたいな機能です。友達アカウントのリストや趣味に関するアカウントのリストなどを作成して、TLのようにできます。

リスト機能は自分が使っているTweetDeckというTwitterクライアントと組み合わせた時に本領を発揮します。TweetDeckはTwitterの公式が提供しているWebサービスのため怪しいものではありません。

https://tweetdeck.twitter.com/

画像を見ればわかりますが、カラム形式にいくつもTLを作り出せます。TL以外に通知やアクティビティを並べたりもできますし、複数アカウントを管理することもできます。Twitter公式が提供しながらなぜか広告も一切表示されません。Twitter本家の機能が遅れて実装されるという欠点もありますが、PCでTwitterをするなら正直使わない手はないと思っています。

さて、このTweetDeckでカラムを作り出す方法として先程のリスト機能と相性がいいです。例えば、自分のTL、友達のリスト、趣味のリストと横に並べて一度に眺められます。複数アカウントでログインできることと組み合わせて、例えば、アカウントAのリストαとアカウントBのリストβを並べて表示できます。自分はそうやっていくつものリストを横に長々と並べています。

リストの引っ越し問題

ここからが自分の抱えていた問題で、かつてアカウントAで作成した趣味のリストを、趣味アカウントBを作ったからそちらに移行したいと思うことがありました。大抵そういうときは引っ越し用のツールがありますが、リストを移動する機能があるツールは見つかりませんでした。アカウントBで一から同じリストを作ることもできますが、100以上のアカウントを登録するのは大変です。

「#ないなら作ればいいじゃない」ということで自分で作ろうと思ったのがListIDOというサービスになります。

ListIDOでできること

次のような場面の利用を想定しています。

  • サブアカウントを本アカウントに統合するために、サブアカウントが所有しているリストを本アカウントに移動したい
  • サブアカウントにも本アカウントと同じリストを作成したい
  • 自分が公開しているリストと同じものがほしいと友人に言われた
  • 同じリストを同じアカウントにコピーして、オリジナルとは違ったふうにカスタマイズしたい

ListIDOでできないこと

  • 三者のアカウントが所有するリストと同じリストを自分のアカウントにもほしい
  • 自分がフォローするリストをアップロードしたい

つまり、リストの移動元と移動先アカウントはどちらも自分が所有している必要があるということです。

ListIDOの詳しい使い方

グーグルスライドを作っているので、そちらを御覧ください。

FAQも用意しています。

https://listido.dorapon2000.xyz/faq

紹介は以上です。

Pythonで2列をテーブルのようにprintする

tabulateのような、テーブル表示をするためのライブラリもありますが、フォーマット文字列だけでも縦を揃えられます。ただし、日本語が入ると揃いません。

def print_align_left(label, value):
    print(f'{label:<20}: {value}')

# ラベルを右寄せ
def print_align_right(label, value):
    print(f'{label:>20}: {value}')


print_align_left('Country', 'Japan')
print_align_left('Anthem', 'Kimigayo')
print_align_left('Capital', 'Tokyo')
print_align_left('National language', 'Japanese')
print_align_left('Area', '377,975 km2')
print_align_left('Population', '126,226,568')

print('\n' + '*' * 40 + '\n')

print_align_right('Country', 'Japan')
print_align_right('Anthem', 'Kimigayo')
print_align_right('Capital', 'Tokyo')
print_align_right('National language', 'Japanese')
print_align_right('Area', '377,975 km2')
print_align_right('Population', '126,226,568')
Country             : Japan
Anthem              : Kimigayo
Capital             : Tokyo
National language   : Japanese
Area                : 377,975 km2
Population          : 126,226,568

****************************************

             Country: Japan
              Anthem: Kimigayo
             Capital: Tokyo
   National language: Japanese
                Area: 377,975 km2
          Population: 126,226,568

github.com

参考

Python spacing and aligning strings - Stack Overflow

Perlの文字化けとutf8とEncodeの関係

ブログの記事のサンプルコードを書いているときにPerlで文字化けに悩まされました。文字化けはせずとも、Wide character in say atという警告が表示されたりすると、記事のサンプルコードとしては締りが悪いです。もうPerlで日本語まったくわからん... そこで、いろいろ実験してPerlの文字化け・警告と仲良くなろうというのがこの記事の主旨です。use utf8;encode_utf8を使っているのに文字化けが直らない!という人がいれば、助けになるかも知れません。

目次

検証環境

文字化けに関わるコード

Perl 文字化け」で検索してヒットする記事で出てくるPerlのコード達です。

# コード中に埋め込まれた文字列リテラルが内部文字列として解釈されるようになる。
use utf8;

# それぞれ、utf8のバイナリを内部文字列に変換するメソッドと、内部文字列からutf8のバイナリに変換するメソッド
use Encode qw/ decode_utf8 encode_utf8 /;

# 標準入力からのutf8の文字列が内部文字列へ変換される
binmode STDIN,  ":utf8";

# 標準出力する際に、内部文字列がutf8のバイナリへ変換される
binmode STDOUT, ":utf8";

# 標準エラー出力する際に、内部文字列がutf8のバイナリへ変換される
binmode STDERR, ":utf8";

お、お手柔らかに...

これら以外にもファイルの読み書きに関するものもあります。今回は標準入出力とコード中の文字の埋め込みだけに絞って考えていきます。

内部文字列とutf8のバイナリ

Perlはどうやらプログラムで扱う専用の文字列と入出力用の文字列で異なるものとして扱うようです。前者を「内部文字列」、後者を「バイナリ」と呼びます。内部文字列はこちらとしてはどう管理されているか使っている側は気にしなくてもいいはずですが、utf8だそうです。

f:id:dorapon2000:20220223180737p:plain

つまり、Perlで文字列を扱う際は内部文字列にしてからバイナリに戻すという作業が必要ということになります。

ということで...

検証① decode_utf8とencode_utf8

内部文字列にしてからバイナリに戻してみる

decode_utf8encode_utf8を使います。

use 5.30.0;
use strict;
use warnings;

use Encode;

my $en = decode_utf8("English");
say encode_utf8($en), " : ", length($en), " 文字";

my $ja = decode_utf8("日本語");
say encode_utf8($ja), " : ", length($ja), " 文字";
English : 7 文字
日本語 : 3 文字

f:id:dorapon2000:20220223185100p:plain

文字列をdecode_utf8で内部文字列にしたあと、sayで標準出力する際にencode_utf8でバイナリに戻しています。length($en)は返り値が文字列ではなく数字なので(Perlでは文字列と数字を区別しませんが)、とくにencode_utf8する必要がないはずです。

では、"English""日本語"以外の" : "" 文字"の部分はencode_utf8しなくていいんでしょうか。これはしなくていいですね。" : "" 文字"は内部文字列ではなくバイナリです。"English""日本語"は文字数を数えるという処理のために、一旦内部文字列に変換する必要がありましたが、" : "" 文字"は特に処理の対象ではないです。

内部文字列にしない

内部文字列にしないと何が困るのでしょうか。試してみます。

my $en = "English";
say $en, " : ", length($en), " 文字";

my $ja = "日本語";
say $ja, " : ", length($ja), " 文字";
English : 7 文字
日本語 : 9 文字

「日本語」が9文字としてカウントされてしまいました。utf8は日本語1文字を3バイトで表現するので、3 x 3 = 9になったようです。

しかし、「English」の方は内部文字列にしていないにも関わらず、正しくカウントできています。これは内部文字列も実体はutf8で、バイナリもutf8で、utf8は英字1文字を1バイトで表現するという偶然の結果なのだと思います。しっかりするなら「English」だけであってもencode_utf8するべきなのでしょう。

文字化けさせてみる

文字化けさせてみましょう。引数に内部文字列を渡すべきencode_utf8でバイナリを渡せば文字化けしそうです。

my $en = "English";
say encode_utf8($en), " : ", length($en), " 文字";

my $ja = "日本語";
say encode_utf8($ja), " : ", length($ja), " 文字";
English : 7 文字
æ¥æ¬èª字

f:id:dorapon2000:20220223185320p:plain

予想通り、文字化けました。

警告を表示させる

Wide character in say atを表示させてみます。これは内部文字列をバイナリに変換せず直接出力しようとすると出るようです。

my $en = decode_utf8("English");
say $en, " : ", length($en), " 文字";

my $ja = decode_utf8("日本語");
say $ja, " : ", length($ja), " 文字";
English : 7 文字
Wide character in say at /Users/.../mojibake.pl line 14.
日本語 : 3 文字

f:id:dorapon2000:20220223191038p:plain

警告が出ましたが、表示としては問題ありません。これはutf8である内部文字列をutf8として画面上に表示しようとして、偶然文字化けしなかった形でしょうね。Shift-JIS環境などで表示すると文字化けると思います。

個人的には"English"の方も警告が表示されると予想していたので、警告が1つしか表示されないのは意外です。

encode_utf8とdecode_utf8を間違えて逆にする

my $en = encode_utf8("English");
say decode_utf8($en), " : ", length($en), " 文字";

my $ja = encode_utf8("日本語");
say decode_utf8($ja), " : ", length($ja), " 文字";
English : 7 文字
日本語 : 18 文字

f:id:dorapon2000:20220223194726p:plain

文字列の長さがおかしいので逆をやったことに気づけますが、そうでないと標準出力からは気づけないという罠です。エンコード→デコードの順に使いたくなってしまうのも非常にわかります。

検証② utf8プラグマ

decode_utf8を使わず内部文字列にする

use utf8;はコード中に埋め込まれている文字列リテラルを内部文字列として解釈するためのプラグマです。これを使うとdecode_utf8のひと手間が要らなくなります。

use 5.30.0;
use strict;
use warnings;

use Encode;
use utf8;

my $en = "English";
say encode_utf8($en . " : " . length($en) . " 文字");

my $ja = "日本語";
say encode_utf8($ja . " : " . length($ja) . " 文字");
English : 7 文字
日本語 : 3 文字

f:id:dorapon2000:20220223192615p:plain

utf8プラグマとencode_utf8だけで内部文字列を介したやり取りができました。ポイントは、検証①ではencode_utf8していなかった" : "" 文字"もencode_utf8の中に入れる必要があるということです。これらはプログラムの処理の対象ではないですが、utf8プラグマは彼らも内部文字列に変換してしまうのです。

警告を表示させる

確認のため、" : "" 文字"をencode_utf8せず標準出力すると、Wide character in sayが出るはずです。

use utf8;

my $en = "English";
say $en, " : ", length($en), " 文字";

my $ja = "日本語";
say $ja, " : ", length($ja), " 文字";
Wide character in say at /Users/.../mojibake.pl line 11.
English : 7 文字
Wide character in say at /Users/.../mojibake.pl line 14.
Wide character in say at /Users/.../mojibake.pl line 14.
日本語 : 3 文字

f:id:dorapon2000:20220223193119p:plain

出ました。

文字化けさせてみる

use utf8;しているのにdecode_utf8すると文字化けするはずです。

use utf8;

my $en = decode_utf8("English");
say encode_utf8($en . " : " . length($en) . " 文字");

my $ja = decode_utf8("日本語");
say encode_utf8($ja . " : " . length($ja) . " 文字");
English : 7 文字
Wide character at /Users/.../mojibake.pl line 13.

f:id:dorapon2000:20220223193740p:plain

何も出力されませんでした笑。おかしな使い方であることは間違いないです。

検証③ binmode STDOUT

encode_utf8を使わずに標準出力する

binmode STDOUT, ":utf8";とすると、標準出力される際に、内部文字列をutf8のバイナリに変換して出力されます。

use 5.30.0;
use strict;
use warnings;

use utf8;
binmode STDOUT, ":utf8";

my $en = "English";
say $en, " : ", length($en), " 文字";

my $ja = "日本語";
say $ja, " : ", length($ja), " 文字";
English : 7 文字
日本語 : 3 文字

f:id:dorapon2000:20220223195859p:plain

今までで一番すっきりしましたね。

文字化けさせる

ここまでの検証を踏まえれば、文字化けの方法も無数に気づけます。例えばutf8プラグマを使わなければ、標準出力の際にバイナリをバイナリに変換というおかしなことになります。

binmode STDOUT, ":utf8";

my $en = "English";
say $en, " : ", length($en), " 文字";

my $ja = "日本語";
say $ja, " : ", length($ja), " 文字";
English : 7 æå­
æ¥æ¬èªå­

"English"”日本語”にだけdecode_utf8を適応しても、" : "" 文字"が文字化けします。

use Encode;
binmode STDOUT, ":utf8";

my $en = decode_utf8("English");
say $en, " : ", length($en), " 文字";

my $ja = decode_utf8("日本語");
say $ja, " : ", length($ja), " 文字";
English : 7 æå­
日本語 : 4 æå­

f:id:dorapon2000:20220223200243p:plain

まとめ

この記事を書きながら、かなりPerlの文字の扱いについて理解が深まりました。

最初で紹介したようにいろいろ文字を扱う方法はあるけれど、すべてを適応しても文字化けを誘うだけということがよくわかります。今回は記事が長くなってしまったので扱いませんでしたが、ファイル入出力の文字コード指定しかりです。正直なところ、use utf8;が一番それっぽいので、これだけですべての文字化けが解決してくれると嬉しいんですけどねぇ。

参考