読者です 読者をやめる 読者になる 読者になる

ZIP中のファイル名の文字化け(Ruby編)

tmtms.hatenablog.com

という記事を書きましたが、今回はRubyでZIPファイルを作る時の話を。

RubyでZIPファイルを作るには、rubyzip というライブラリを使います。

% gem install rubyzip

次のようにしてZIPにファイルを追加できます。

require 'zip'

Zip::File.open('hoge.zip', Zip::File::CREATE) do |zip|
  zip.add('いろはにほへと.txt', '/path/to/いろはにほへと.txt')
end

Zip::File#add の第一引数はZIP内に記録されるファイル名、第二引数は実際のファイルのパスです。この二つのファイル名は同じである必要はありません。

ただし、この場合はUTF-8フラグがセットされません。つまりWindowsの標準機能で開くと文字化けしてしまいます。

UTF-8フラグを立てるには次のように Zip.unicode_names = true を指定します。

require 'zip'

Zip.unicode_names = true
Zip::File.open('hoge.zip', Zip::File::CREATE) do |zip|
  zip.add('いろはにほへと.txt', '/path/to/いろはにほへと.txt')
end

このように作成されたZIPファイルは最近のWindowsで(それとたぶんMacでも)開くことができます。

ただし、パッチがあたってない Windows 7 や、もう世界のどこでも動いていないはずの古いバージョンの Windows は UTF-8 に対応していないようなので、文字化けしてしまいます。

その場合はUTF-8フラグを立てずにファイル名をシフトJIS(CP932)に変換してやればいいです。

require 'zip'

Zip::File.open('hoge.zip', Zip::File::CREATE) do |zip|
  zip.add('いろはにほへと.txt'.encode('cp932'), '/path/to/いろはにほへと.txt')
end

なお、これはMacでは文字化けします(たぶん)。Macでも文字化けしないようです。

あらかじめZIPファイルを渡す相手の環境がわかっているのであれば、それに合わせて変更するのもいいでしょう。

Webアプリであれば User-Agent を見て動きを変えるのもいいかも知れませんね。

ZIP中のファイル名の文字化け

こんな記事がありました。

gihyo.jp

これはMacユーザー用の書籍の宣伝記事らしいのですが、「Windowsを使ってる人のためにMac側がひと手間かけてあげよう」なんて殊勝なことをマカーが言うとは時代も変わったもんです。([追記] はてブのコメントを見たらさすがマカーという意見が並んでて安心しました)

まあ私はWindowsユーザーでもMacユーザーでもないのでどうでもいいのですが、文字化けなネタなので食いついてみます。

記事中に、「付物出稿.zip」というファイルを開いた時の画像が載ってます。

文字の並びからして、UTF-8文字列をシフトJIS(CP932)とみなして表示してしまった文字列でしょう(「繧ォ繝上y繝シ繝輔か繝ォ繧ソ繧・」の元の文字は「カバーフォルダ」で、「蟶ッ繝輔か繝ォ繧ソ繧・」は「帯フォルダ」)。

つまり、Macはファイル名をUTF-8でZIPに書き込み、WindowsはそれをシフトJIS(CP932)と思って開いているということです。

ZIPフォーマット中のファイル名は文字コード情報を持っていません。なので、MacとWindowsのようにファイル名の文字コードが異なるシステム同士でZIPをやりとりすると、このように文字化けしてしまうのです。

手元にMacが無いので確かめてはいないのですが、どうやらWindowsで作ったZIPファイルをMacで開く分には問題ないらしいです。 Macのファイル名の文字コードは昔はシフトJISだったようなので、互換性を考慮してUTF-8で文字化けする場合はシフトJISとみなして開いてくれるのかもしれません。

2007年にZIPフォーマットは拡張されてファイル名がUTF-8であることを示すフラグが追加されました。 ということは、現代においてはZIP作成時にファイル名の文字コードをUTF-8にしてこのフラグを立てれば文字化けはしないはずなのです。

実はWindowsはZIPを作成するときにはCP932でファイル名を書くのですが、ZIPを開くときにはちゃんとこのフラグを見てくれます(Windows 8 あたりから)。 なのにMacで作られたZIPのUTF-8のファイル名が文字化けするということは、MacのZIPはこのフラグを立ててないということなのでしょう。

WindowsがZIP作成時にUTF-8にしないのはおそらく過去の互換性を重視しているからでしょう。 MacはせっかくUTF-8なんだからフラグを立ててくれればいいんですけどね。惜しい。

まあ、それぞれ自分のロケールに従った文字コードでZIPを書いてるという点ではお互い様です。

ちなみにUbuntuはZIP作成時にちゃんとUTF-8フラグを立ててUTF-8で作ります。なのでWindowsでも問題なく開けますし、Macはこのフラグを見てるかどうかは知りませんが、文字コードがUTF-8だからおそらく問題なく開けるでしょう。

UbuntuでのZIP展開時には、UTF-8フラグが立っていたらそのままUTF-8のファイル名として扱い、フラグが立っていない場合は、現在のロケール(LC_ALL, LANG環境変数等)を見て文字コード変換を行います。 jaロケールの場合はCP932からUTF-8への変換を行います。

ですので、UbuntuとWindowsの間では問題になることはないでしょう。

ということでMacをやめてUbuntu使えば解決ですね!

大江戸Ruby会議06

大江戸Ruby会議06 #oedo06 に行ってきたので雑感など。

会場

@sora_h さん生誕20周年記念ということで、会場も「ソラシティカンファレンスセンター」という場所でした。

Docker時代の分散RSpec環境の作り方

speakerdeck.com

RSpecの実行に時間が掛かってたのをAmazon ECSを使って分散実行することで時間とコストが削減されたという話。

RSpec の結果を任意のストレージに出力できる rspec-storage gem は便利そう。

自分の今のプロジェクトも自動テストの時間がどんどん伸びているのでどうにかしたいと思ってます。

しかし、開発にAWSを使える環境はうらやましい。

Text Editing in Ruby

github.com

Ruby製のEmacsライクなテキストエディタを作ったという話でした。名前が中二病っぽい。

冒頭で何のエディタを使っているのか挙手によるアンケートを取ったのですが、会場のほとんどの人がEmacs使いでした(ただし @shugomaeda さんの観測による)。

プレゼン自体このエディタ上のツール(textbringer-presentation)で動いていたとのことでエディタの完成度が伺えます。

Ruby考古学II 1993-1997

まつもとさんがRubyを作るきっかけになった @keiju_ishitsukaさんが、メールとかメモとか記憶を頼りに1993年から1997年当時のRubyについて語りました。

自分がRubyを使い始めたのがちょうど1997年 Ruby 1.1b の頃だったので、それまでの歴史がわかったのは良かったです。

まつもとさんがRubyを作り始めたころはPerlを使ってなかったとか、rescue のスペルミスがかなり長い間残っていたとか、面白かったです。

多相型、推論、Ruby

speakerdeck.com

型を書かずにうまくやろうとすると、現在のRubyをいろいろいじらないといけなくて、それは許容できないだろう。最終的には「型は絶対書きたくない」と言っているまつもとさんをどうにかしないといけないのでは。みたいな話でした。

フルタイムコミッター大戦

コミッター4人が登壇して問題を出し合う早押しクイズ。ぐだぐだでした。

それよりも早押しシステムの方が気になりました。Nintendo Switch のコントローラが Bluetooth なのは知ってたのですが、それをどうやってウェブで制御しているのかと。

懇親会でシステム担当の @yancya さんに聞いてみたら、HTML5 に Gamepad API があって結構簡単にできるとのことでした。Gamepad API の存在は知らなかったので、今度調べてみます。

最初に少しトラブってたのは、会場に Bluetooth デバイスがたくさんあったせいでコントローラとのペアリングがうまくいかなかったそうで、会場の外でペアリングしてうまくいったとのことでした。

upec.jp

如何にして若き天才コミッタは生まれるのか

@rosylillyさんによる、@sora_hさんの本人も覚えてないような過去の話と、後進をどのように育成するかという話。 私もそうなんですが、自分がそうだっただけに若者は勝手に育つと思ってるんだけど、それじゃあダメだと。

Keynote

speakerdeck.com

@sora_hさんのスキルセットがものすごい。うちの上の子と同じ年(誕生日もほぼ同じ)なんだけど、技術的には自分はとっくに追い越されてる。

「インターネットやりたい」って何を言ってるのかと思ったら、ISPのようなことをやりたいとのことで、もうAS番号も取ったとのこと。

「めざせフルスタック」と言ってましたが、もう十分フルスタックエンジニア。

その他

  • Asakusa.rbには行きたいと思いつつ行けてません。ずっと前に一度だけ行ったことありますが、またいつか行ってみたいです。

  • パンダさんかわいかった。

  • 金をケチって高速バスを使ったんだけど、日帰りで往復とも高速バスはさすがに疲れました。もう若くないし…。

Ruby製のEmacsライクなテキストエディタTextbringer

大江戸Ruby会議06で前田さんがRubyでEmacsライクなエディタTextbringerを作ったという発表をしていました。

最初はEmacs上の何かでプレゼンしてたと思ってたのですが、なんとTextbringerでプレゼンしてたとのこと。完成度高い。

Textbringer上で動くMUAを作るのが目標とのことで、Emacs使いで、MUAを作ることがRubyを始めたきっかけの私としては非常に共感しました。

ということで試してみました。

インストール

% gem install textbringer

起動

% textbringer

ファイル読み込みとか保存とかカーソル移動とか基本的な操作は普通にEmacsのキーバインドで使えます。 機能的にはまだまだ足りないようですが、Rubyで拡張できるということで夢が膨らみますね。

おまけ

最初は日本語が入力できなかったのですが、libncursesw5-dev deb をインストールしたらうまくいきました(Ubuntuの場合)。

Sequelのトランザクション内でタイムアウトするとCOMMITされてしまう

ちょっと前にハマったのでメモ。

Sequelでトランザクションを使う時は次のように transaction メソッドにブロックを渡します。

require 'sequel'
require 'logger'

db = Sequel.connect('mysql2://user:passwd@localhost/test')
db.loggers = [Logger.new($stdout)]
db.transaction do
  db[:test].insert(id: 123)
end
I, [2017-03-12T22:34:51.946849 #27932]  INFO -- : (0.000119s) SET @@wait_timeout = 2147483
I, [2017-03-12T22:34:51.947047 #27932]  INFO -- : (0.000133s) SET SQL_AUTO_IS_NULL=0
I, [2017-03-12T22:34:51.947182 #27932]  INFO -- : (0.000039s) BEGIN
I, [2017-03-12T22:34:51.947517 #27932]  INFO -- : (0.000149s) INSERT INTO `test` (`id`) VALUES (123)
I, [2017-03-12T22:34:51.955289 #27932]  INFO -- : (0.007675s) COMMIT

ブロックを抜ける時に自動的にCOMMITされます。

ブロック内で例外が発生した場合は、ROLLBACKされます。

require 'sequel'
require 'logger'

db = Sequel.connect('mysql2://user:passwd@localhost/test')
db.loggers = [Logger.new($stdout)]
db.transaction do
  db[:test].insert(id: 123)
  raise 'hoge'
end
I, [2017-03-12T22:36:10.121107 #27942]  INFO -- : (0.000191s) SET @@wait_timeout = 2147483
I, [2017-03-12T22:36:10.121317 #27942]  INFO -- : (0.000110s) SET SQL_AUTO_IS_NULL=0
I, [2017-03-12T22:36:10.121454 #27942]  INFO -- : (0.000043s) BEGIN
I, [2017-03-12T22:36:10.121834 #27942]  INFO -- : (0.000149s) INSERT INTO `test` (`id`) VALUES (123)
I, [2017-03-12T22:36:10.152997 #27942]  INFO -- : (0.031052s) ROLLBACK
test-2.rb:8:in `block in <main>': hoge (RuntimeError)

ですが、Timeout を使っていてトランザクション中でタイムアウトが発生した場合は

require 'timeout'
require 'sequel'
require 'logger'

db = Sequel.connect('mysql2://user:passwd@localhost/test')
db.loggers = [Logger.new($stdout)]
Timeout.timeout(1) do
  db.transaction do
    db[:test].insert(id: 123)
    sleep 2
  end
end
I, [2017-03-12T22:37:57.026693 #27953]  INFO -- : (0.000084s) SET @@wait_timeout = 2147483
I, [2017-03-12T22:37:57.026803 #27953]  INFO -- : (0.000047s) SET SQL_AUTO_IS_NULL=0
I, [2017-03-12T22:37:57.026924 #27953]  INFO -- : (0.000041s) BEGIN
I, [2017-03-12T22:37:57.027223 #27953]  INFO -- : (0.000119s) INSERT INTO `test` (`id`) VALUES (123)
I, [2017-03-12T22:37:58.034292 #27953]  INFO -- : (0.007787s) COMMIT
test-3.rb:10:in `sleep': execution expired (Timeout::Error)

なんとCOMMITされてしまいます!こわい!

ちなみに、タイムアウト時に発生する例外を指定した場合はちゃんとROLLBACKされます。

require 'timeout'
require 'sequel'
require 'logger'

db = Sequel.connect('mysql2://user:passwd@localhost/test')
db.loggers = [Logger.new($stdout)]
Timeout.timeout(1, Timeout::Error) do
  db.transaction do
    db[:test].insert(id: 123)
    sleep 2
  end
end
I, [2017-03-12T22:40:50.531245 #27979]  INFO -- : (0.000166s) SET @@wait_timeout = 2147483
I, [2017-03-12T22:40:50.531455 #27979]  INFO -- : (0.000106s) SET SQL_AUTO_IS_NULL=0
I, [2017-03-12T22:40:50.531820 #27979]  INFO -- : (0.000222s) BEGIN
I, [2017-03-12T22:40:50.532302 #27979]  INFO -- : (0.000240s) INSERT INTO `test` (`id`) VALUES (123)
I, [2017-03-12T22:40:51.540196 #27979]  INFO -- : (0.009461s) ROLLBACK
test-4.rb:10:in `sleep': execution expired (Timeout::Error)

さっきの例と発生する例外も発生箇所も同じなのに、Sequelの振る舞いが異なっています。

これは、Timeout.timeout の引数で例外を指定しない場合は、実際には例外ではなく throw - catch が使われているためです。 この辺の仕組みは前に書きました。

tmtms.hatenablog.com

Sequel の transaction はブロックを例外で抜けた時はROLLBACKし、そうでないとCOMMITするようになっているため、例外ではなく throw で抜けた場合には COMMIT されてしまうのでした。

例外だけでなく throw でもブロックを抜け得ることを考慮していない Sequel の問題なような気もするのですが、とりあえず Sequel を使用している場合は Timeout を使用しない方が無難なようです。

追記

Sequelの開発者的には、SequelのバグではなくてTimeoutのバグって見解のようです。

github.com

ThinkPad T460s Ubuntu でトラックパッドを無効化

5ヶ月ほど ThinkPad T460s を Ubuntu(Xubuntu) で使ってます。

ちょっと前に さよならMac | めがねをかけるんだ という ThinkPad をdisった記事が話題になりましたが、自分はあんまり不満はありません。 ノートPCでLinuxを使う時の鬼門だった無線LANやサスペンドもまったく問題ありません。 当然トラックパッドは無効にしています。

唯一の不満がこのトラックパッド無効化です。

トラックパッドを無効にしようとBIOSで無効に設定しても無効になりません。

しょうがないので、Xubuntu の設定の「マウスとタッチパッド」でタッチパッドの設定を無効化します。

f:id:tmtms:20170218135347p:plain

これでトラックパッドが無効になってめでたしめでたし。

…と思ったのですが、しばらく使ってると、時折トラックポイントのボタンをクリックしても効かないことがあります。

どうやらトラックパッドに触れているとボタンのクリックイベントを取りこぼすようでした。 トラックパッドを無効にしていたので、それが原因だとなかなか気がつきませんでした。

トラックパッドに二本以上の指が触れているとかなりの確率でボタンのクリックイベントを取りこぼしてしまいます。

どうやら psmouse ドライバのインストール時に proto=bare を指定するとトラックパッドを完全に無効化できるようです。 というより proto=bare ではトラックパッドを扱えないというだけな気がします。

# modprobe -r psmouse
# modprobe psmouse proto=bare

これでトラックパッドに触れていてもボタンクリックを取りこぼすことはなくなりました。

…が、もうひとつ問題が。

proto=bare ではトラックポイントの感度と速度の調整ができません。

proto=bare を指定しない時は、次のようにして感度と速度を調整できました。

# echo 200 > /sys/devices/platform/i8042/serio*/serio*/sensitivity
# echo 200 > /sys/devices/platform/i8042/serio*/serio*/speed

proto=bare で psmouse をインストールした時は、この sensitivity, speed ファイル自体ありません。

色々試行錯誤したところ、proto=bare をつけずに psmouse をインストールして、感度と速度を調整した後に proto=bare をつけて psmouse を再インストールすると感度と速度が保持されたままになることがわかりました。

とりあえず、簡単なシェルスクリプトを作って、電源ON時やサスペンドからの復帰時に走らせてます。

#!/bin/bash
DIR=/sys/devices/platform/i8042
SPEED=200
SENSITIVITY=200

# echo が刺さることがあるので1秒でタイムアウトさせる
sub() {
    (echo $1 > $2) &
    pid=$!
    (sleep 1; kill $pid) &
    pid2=$!
    wait $pid
    kill $pid2
}

modprobe -r psmouse
modprobe psmouse proto=any
while :; do
    sleep 0.3
    speed=$DIR/serio*/serio*/speed
    test -f $speed || continue
    sens=$(dirname $speed)/sensitivity
    test -f $sens || continue
    sub $SPEED $speed
    test "$(cat $speed)" -eq $SPEED || continue
    sub $SENSITIVITY $sens
    test "$(cat $sens)" -eq $SENSITIVITY || continue
    break
done
modprobe -r psmouse
modprobe psmouse proto=bare

Unicode Collation Algorithm

文字コードは面白いね! わーい! たのしー!

🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾🐾

MySQL で utf8mb4_unicode_ci コレーションを使用した時に「🍣」=「🍺」や「ハ」=「パ」になる問題があります。

この utf8mb4_unicode_ci ってなんぞや?と思ってマニュアルを見てみると、

MySQL は、http://www.unicode.org/reports/tr10/ で説明している Unicode 照合順序アルゴリズム (UCA) に従って xxx_unicode_ci 照合順序を実装します。照合順序は、バージョン 4.0.0 UCA 重みキー (http://www.unicode.org/Public/UCA/4.0.0/allkeys-4.0.0.txt) を使用します。

https://dev.mysql.com/doc/refman/5.6/ja/charset-unicode-sets.html

とあります。

Unicode には Unicode Collation Algorithm (UCA) という標準があり、MySQL の utf8mb4_unicode_ci は UCA のバージョン 4.0.0 を使用しています。

UCAのドキュメントをちゃんと読んだわけではないので以下の説明はテキトーです。

各文字の比較レベルを定義したテーブルは Default Unicode Collation Element Table (DUCET)と呼ばれて UCA のバージョン毎に提供されています。

UCA 4.0.0 の DUCET の中味はこんな感じです。

0061  ; [.0E33.0020.0002.0061] # LATIN SMALL LETTER A
FF41  ; [.0E33.0020.0003.FF41] # FULLWIDTH LATIN SMALL LETTER A; QQK
0363  ; [.0E33.0020.0004.0363] # COMBINING LATIN SMALL LETTER A; QQK
249C  ; [*0288.0020.0004.249C][.0E33.0020.0004.249C][*0289.0020.001F.249C] # PARENTHESIZED LATIN SMALL LETTER A; QQKN
1D41A ; [.0E33.0020.0005.1D41A] # MATHEMATICAL BOLD SMALL A; QQK

左端の16進数はUnicodeのコードポイントを表し、その次の [ ] で括られた4つの16進数は文字の比較レベルを表します。

レベルは左から順に次のようになっています。

L1 Base characters 基本文字
L2 Accents アクセント
L3 Case/Variants 大文字小文字/異体字
L4 Punctuation 句読点(?)

いくつか抜粋してみます。左に文字をつけました。

a 0061 ; [.0E33.0020.0002.0061] # LATIN SMALL LETTER A
FF41 ; [.0E33.0020.0003.FF41] # FULLWIDTH LATIN SMALL LETTER A; QQK
24D0 ; [.0E33.0020.0006.24D0] # CIRCLED LATIN SMALL LETTER A; QQK
A 0041 ; [.0E33.0020.0008.0041] # LATIN CAPITAL LETTER A
FF21 ; [.0E33.0020.0009.FF21] # FULLWIDTH LATIN CAPITAL LETTER A; QQK
å 00E5 ; [.0E33.0020.0002.0061][.0000.0043.0002.030A] # LATIN SMALL LETTER A WITH RING ABOVE; QQCM
Å 00C5 ; [.0E33.0020.0008.0041][.0000.0043.0002.030A] # LATIN CAPITAL LETTER A WITH RING ABOVE; QQCM
b 0062 ; [.0E4A.0020.0002.0062] # LATIN SMALL LETTER B
FF42 ; [.0E4A.0020.0003.FF42] # FULLWIDTH LATIN SMALL LETTER B; QQK
B 0042 ; [.0E4A.0020.0008.0042] # LATIN CAPITAL LETTER B

「a」っぽい文字は L1=0E33 で「b」っぽい文字は L1=0E4a になっています。

Å」は複数のレベルを持ち、1個目のレベルは「A」とまったく同じで、2個目のレベルは合成文字用の「˚」です。 NFD正規化された状態(?)でレベルが表されます。

L1 や L1+L2 で比較すると「a」「」「A」「」は同じ文字として扱われます。 L1+L2+L3 で比較すると異なる文字として扱われます。

文字の比較にどのレベルまで使用するかはアプリ次第で、MySQL の utf8mb4_unicode_ci では L1 しか使用していません。 そのため、英字は大文字/小文字/全角/半角は区別されません。

は=ぱ=ば=ハ=パ=バ

で、問題の「は」「ぱ」「ば」「ハ」「パ」「バ」ですが、次のようになっています。 濁点/半濁点つきの文字は正規化されて、清音文字+濁点文字の2つのレベルの組み合わせで表されてます。

306F ; [.1E6B.0020.000E.306F] # HIRAGANA LETTER HA
3071 ; [.1E6B.0020.000E.306F][.0000.0141.0002.309A] # HIRAGANA LETTER PA; QQCM
3070 ; [.1E6B.0020.000E.306F][.0000.0140.0002.3099] # HIRAGANA LETTER BA; QQCM
30CF ; [.1E6B.0020.0011.30CF] # KATAKANA LETTER HA
30D1 ; [.1E6B.0020.0011.30CF][.0000.0141.0002.309A] # KATAKANA LETTER PA; QQCM
30D0 ; [.1E6B.0020.0011.30CF][.0000.0140.0002.3099] # KATAKANA LETTER BA; QQCM

これらの文字は L1 レベルでは同じレベルなので、L1 でしか使用しない MySQL の utf8mb4_unicode_ci では区別されないことになります。

「は」「ぱ」「ば」だけでなく「か」「が」や「さ」「ざ」も区別されません。

日本語としては、清音、濁音、半濁音をそれぞれ区別するのが自然ですが、Unicode の標準の規則にしたがった Case insensitive だと区別できません。

utf8mb4_japanese_ci の登場に期待したいところです。

🍣=🍺

絵文字の比較はまた事情が異なります。DUCET には絵文字は定義されていないのです。実は漢字も定義されていません。

UCA では DUCET に定義されていない文字の扱い方も定めています。(7.1.3)

AAAA = BASE + (CP >> 15);
BBBB = (CP & 0x7FFF) | 0x8000;
CP => [.AAAA.0020.0002.][.BBBB.0000.0000.]

BASE:
FB40 CJK Ideograph
FB80 CJK Ideograph Extension A/B
FBC0 Any other code point

「漢」という文字のCP(Code point)はU+6F22なので、[.FB40.0020.0002.][.EF22.0000.0000] となります。 この2つのレベルを組みわせて使用します。

mysql> SELECT HEX(WEIGHT_STRING('漢'));
+---------------------------+
| HEX(WEIGHT_STRING('漢'))  |
+---------------------------+
| FB40EF22                  |
+---------------------------+

同じように「🍣」と「🍺」の値を求めると 「🍣」(U+1F363)は FBC3F363となり、「🍺」(U+1F37A)はFBC3F37Aとなるので、区別できるはずです。

ところが MySQL の utf8mb4_unicode_ci では、絵文字についてはそれに従わず、FFFD にしてしまっています。

一般的な照合順序の補助文字の場合、重みは 0xfffd REPLACEMENT CHARACTER の重みです。UCA 4.0.0 照合順序の補助文字の場合、照合重みは 0xfffd です。

https://dev.mysql.com/doc/refman/5.6/ja/charset-unicode-sets.html

mysql> SELECT HEX(WEIGHT_STRING('🍣'));
+-------------------------+
| HEX(WEIGHT_STRING('?')) |
+-------------------------+
| FFFD                    |
+-------------------------+

つまり、utf8mb4_unicode_ci で 🍣=🍺 となるのは Unicode のせいではなく、MySQL の問題です。

なお、utf8mb4_unicode_520_ci ではちゃんと計算された値を使用しています。

mysql> SET NAMES utf8mb4 COLLATE utf8mb4_unicode_520_ci;
mysql> SELECT HEX(WEIGHT_STRING('🍣'));
+-------------------------+
| HEX(WEIGHT_STRING('?')) |
+-------------------------+
| FBC3F363                |
+-------------------------+
mysql> SELECT HEX(WEIGHT_STRING('🍺'));
+-------------------------+
| HEX(WEIGHT_STRING('?')) |
+-------------------------+
| FBC3F37A                |
+-------------------------+