Hatena::ブログ(Diary)

あらびき日記 Twitter

2016-08-29

Redshift の UDF では非 ASCII 文字を含む unicode 型を返すとエラーになる

次のように unicode 型を返す UDF を定義すると、返り値に非 ASCII 文字を含む場合にエラーになります。

CREATE OR REPLACE FUNCTION f_decode_utf8 (s varchar(max)) RETURNS varchar(max) IMMUTABLE AS $$
  return s.decode('utf-8')
$$ LANGUAGE plpythonu;

> select f_decode_utf8('a');
 f_decode_utf8
---------------
 a
(1 row)

> select f_decode_utf8('あ');
ERROR:  UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal not in range(128). Please look at svl_udf_log for more information
DETAIL:
  -----------------------------------------------
  error:  UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal not in range(128). Please look at svl_udf_log for more information
  code:      10000
  context:   UDF
  query:     0
  location:  udf_client.cpp:350
  process:   padbmaster [pid=29161]
  -----------------------------------------------

わざわざ decode('utf-8') する人はいないと思いますが、例えば JSON を渡して json.loads すると unicode 型になるので、意味不明なエラーに悩まされることになります。

-- json_extract_array_element_text を UDF で定義してみる
CREATE OR REPLACE FUNCTION f_json_extract_array_element_text (json_string varchar(max), pos integer) RETURNS varchar(max) IMMUTABLE AS $$
  import json
  return json.loads(json_string)[pos]
$$ LANGUAGE plpythonu;

> select f_json_extract_array_element_text('["あ"]', 0);
ERROR:  UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal not in range(128). Please look at svl_udf_log for more information
DETAIL:
  -----------------------------------------------
  error:  UnicodeEncodeError: 'ascii' codec can't encode character u'\u3042' in position 0: ordinal not in range(128). Please look at svl_udf_log for more information
  code:      10000
  context:   UDF
  query:     0
  location:  udf_client.cpp:350
  process:   padbmaster [pid=29161]
  -----------------------------------------------

次のように最後に encode('utf-8') すればエラーになりません。

CREATE OR REPLACE FUNCTION f_json_extract_array_element_text (json_string varchar(max), pos integer) RETURNS varchar(max) IMMUTABLE AS $$
  import json
  return json.loads(json_string)[pos].encode('utf-8')
$$ LANGUAGE plpythonu;

> select f_json_extract_array_element_text('["あ"]', 0);
 f_json_extract_array_element_text
-----------------------------------
 あ
(1 row)

2016-06-27

Rails の send_data で Windows 用の zip ファイルを送る

Windows 用にファイル名の文字コードCP932 にしたいわけですが、ハマりどころが多かったのでメモです。

次のようなコードで send_data (not send_file) に指定する zip データを生成可能です。

require 'zip'
require 'zip/version'
require 'open-uri'

def image_data
  @image_data ||= open('http://abicky.net/img/favicon.png').read
end

puts "rubyzip version: #{Zip::VERSION}"

if Zip::VERSION < '1.2.0'
  module Zip
    class Deflater
      module EncodingSetter
        def initialize(*args)
          super
          @buffer_stream.set_encoding(@output_stream.external_encoding)
        end
      end
      prepend EncodingSetter
    end
  end
end

def zip_data_using_write_buffer
  io = StringIO.new
  io.set_encoding(Encoding::CP932)
  Zip::OutputStream.write_buffer(io) { |out|
    out.put_next_entry('テスト.png'.encode(Encoding::CP932))
    out.write(image_data)
  }.string
end

解説

Zip::OutputStream.write_buffer には Encoding::CP932 の StringIO を渡す

そうしないと "local header size changed (43 -> 40) (Zip::Error)" のようなエラーが出ます。

これは、StringIO の文字コードUTF-8 であり、CP932 のファイル名を指定しても勝手に UTF-8 に変換されてしまうことが原因です。それによって、StringIO の情報から出したヘッダーサイズと、与えられたファイル名(CP932 のファイル名)のバイト数から出したヘッダーサイズが異なるようになり、エラーになります。

cf. https://github.com/rubyzip/rubyzip/blob/v1.2.0/lib/zip/entry.rb#L128

[1] pry(main)> io = StringIO.new; io << ''; io.tell
=> 3
[2] pry(main)> io = StringIO.new; io.set_encoding(Encoding::CP932); io << ''; io.tell
=> 2

rubyzip のバージョンが 1.2.0 より前の場合は Zip::Deflater にパッチを当てる

そうしないと "incompatible character encodings: Windows-31J and UTF-8 (Encoding::CompatibilityError)" のようなエラーが出ます。

これは、Zip::Deflater が Zlib::Deflate#deflate の結果を独自に用意した StringIO に格納しているせいで、UTF-8 に変換されてしまうことが原因です。1.2.0 は実装が変わっているのでこの問題は起きません。

cf. https://github.com/rubyzip/rubyzip/blob/v1.1.7/lib/zip/deflater.rb


もっとシンプルにやればいいんじゃないの?

パフォーマンスが求められないのであれば、次のような書き方が可能です。この場合、zip ファイルが作成されるので、send_data ではなく send_file を使うと思いますが。

def zip_data_using_open
  Tempfile.open('') do |tmp|
    Zip::OutputStream.open(tmp) do |out|
      out.put_next_entry('テスト.png'.encode(Encoding::CP932))
      out.write(image_data)
    end
    File.read(tmp)
  end
end

2016-05-16

MeCab ソースコードリーディング私的メモ(形態素解析編)

先日、次のエントリーを書きました。

日本語形態素解析の裏側を覗く!MeCab はどのように形態素解析しているか - クックパッド開発者ブログ


このエントリーを書く際に MeCabソースコードをそれなりに読んだので、記憶が薄れないうちにメモっておきます。とりあえず形態素解析部分です。コスト算出部分は気が向いたら書きます・・・。

勘違いしている箇所もあるかと思うので、気付いたら指摘してもらえると嬉しいです!


形態素解析時の主要クラス

形態素解析時に関連するクラスとして特に意識しないといけないのは以下のクラスかと思います。メソッドも主要なものしか表示していません。

f:id:a_bicky:20160516052557p:image

↑ のクラス図を出力した PlantUML のコード

@startuml
skinparam classAttributeIconSize 0

class Model {
  +viterbi()
}

class Tagger {
  +parse()
}

class Tokenizer {
  +getBOSNode()
  +getEOSNode()
  +lookup()
}
note left: 文の指定された位置に対応するノード(単語)\nを返したりする。未知語処理も行う。

class Viterbi {
  +analyze()
  -viterbi()
  -buildBestLattice()
}
note left: 与えられた文に対して viterbi アルゴリズムにより\n最適なパスを求める。

class CharProperty {
  +seekToOtherType()
  +getCharInfo()
}
note bottom: char.bin の情報から文字種情報\n(SPACE, KANJI etc.)に関する処理を行う

class Dictionary {
  +commonPrefixSearch()
}
note bottom: 辞書(sys.dic etc.)の情報を保持する

class Lattice {
  +toString()
}
note right: 解析対象の文を保持したり、\nViterbi#analyze() の結果を格納したりする

class Connector {
  +cost()
}
note bottom: matrix.bin の情報から\n2ノード(単語)間の連接コストを返したりする

Model *--> Viterbi : -viterbi_
Tagger *--> Model : -current_model_
Tagger *--> Lattice : -lattice_

Viterbi *--> Tokenizer : -tokenizer_
Viterbi *--> Connector : -connector_

Tokenizer *--> Dictionary : -unkdic, -dic_
Tokenizer *--> CharProperty : -property_

@enduml

形態素解析時のシーケンス

主要クラスを把握したら、次は解析の流れです。クラス図のとおり model が viterbi を所有していますが、model()->viterbi()->analyze(lattice)という形で tagger から model 経由で viterbi の analyze メソッドを呼んで lattice に解をセットしています。

f:id:a_bicky:20160516052558p:image

↑ のシーケンス図を出力した PlantUML のコード

@startuml

actor user
participant main
participant model
participant tagger
participant viterbi
participant tokenizer

user -> main : mecab
  main -> main : mecab_do(argc, argv)
  activate main
  create model
  main -> model : open(param)
    create viterbi
      model -> viterbi : open(param)
        create tokenizer
          viterbi -> tokenizer : open(param)
            tokenizer -> tokenizer : Open unk.dic, char.bin,\nsys.dic, and user dics
          tokenizer --> viterbi
        create connector
          viterbi -> connector : open(param)
            connector -> connector : Open matrix.bin
          connector --> viterbi
      viterbi --> model
  model --> main

  main -> model : createTagger()
    create tagger
      model -> tagger : open(*this)
      tagger --> model
  model --> main

  loop
    user -> main : Input sentence
    main -> main : Read input and set it to ibuf
    main -> tagger : parse(ibuf)
    tagger -> tagger : parse(lattice)
    tagger -> viterbi : analyze(lattice)
    viterbi -> viterbi : viterbi(lattice)
    viterbi -> viterbi : buildBestLattice(lattice)
    viterbi --> tagger
    tagger --> main : lattice->toString()
    main --> user : Show the result
  end
  deactivate main
@enduml

比較的複雑な主要メソッド

bool Viterbi::viterbi(Lattice *lattice)

ラティスの構築を行うメソッド

template <bool IsAllPath> bool connect(size_t pos, Node *rnode, ...)

viterbi メソッドの中で呼ばれるメソッド。viterbi メソッドで取得した現在位置に対する全ノード (right_node) に対して、一つ手前の全ノードとの連接コストを matrix.bin から取得し累積コストが最小になる左側のノード (best_node) を rnode->prev に格納しておく。lnode->cost には BOS からの最小累積コストが格納されている。単語の長さに応じて end_node_list に各ノードを格納する

bool Viterbi::buildBestLattice(Lattice *lattice)

viterbi メソッドで構築したラティスの EOS ノードから prev を辿っていくことで最適パスを求める。prev は connect 関数でセットされた累積コストを最小とする一つ手前のノード


N *Tokenizer<N, P>::lookup(const char *begin, const char *end, ...)

指定された位置に対して候補となる全ての単語を sys.dic とユーザ辞書から取得する。

未知語処理もこのメソッドで行う。未知語のコストは unk.dic に登録されている値を使う。


おまけ 〜 Doxygen によるドキュメント生成〜

EXTRACT_PRIVATE = YES にすると、かなり詳細なクラス図が生成されるのでオススメです。

% brew install doxygen graphviz
% cd /path/to/mecab/mecab/src/
% doxygen -g
% cat <<EOF >> Doxyfile
heredoc> EXTRACT_ALL            = YES
heredoc> HAVE_DOT               = YES
heredoc> UML_LOOK               = YES
heredoc> EXTRACT_PRIVATE        = YES
heredoc> EOF
% doxygen
% open html/index.html

日本語を含む Keynote を slideshare にアップロードする

次のエントリーにあるように、最近 KeynotePDF に変換して slideshareアップロードすると、Osaka 等一部のフォントを除いて日本語が表示されなくなりました。

KeynoteのスライドをSlideShareにアップロードすると日本語が表示されない問題 - Qiita


ワークアラウンドとして、次のコマンドで PDF を変換した上でアップロードすると正常に表示されるようになるかもしれません。

$ LANG=C LC_ALL=C sed -i '' s'|/Registry (Adobe) /Ordering (Japan1) /Supplement [0-9]|/Registry(Adobe) /Ordering(Identity) /Supplement 0|g' /path/to/pdf

何故日本語が表示されるようになるか?

PDF の構造に詳しくないので間違っているかもしれませんが、ググッて自分なりに解釈した内容を以下にまとめます。


フォントを表示するには次のような変換が行われます。


文字コード → CID (Character ID) → GID (Glyph ID) → フォント(グリフ)


次のページの図が非常にわかりやすいです。

CMap・cmap(Character Map) | フォント用語集 | 文字の手帖 | 株式会社モリサワ


っで、文字コードと CID の対応関係は CMap が管理しており、CID の集合は Adobe-Japan1-6, Adobe-Identity-0 等いくつもの種類が存在します。

PDF の CIDSystemInfo には Registry, Ordering, Supplement (頭文字をとって ROS) の情報が記述されており、Adobe-Japan1-6 は Registry: Adobe, Ordering: Japan1, Supplement 6 です。

ROS ごとに CMap テーブルが存在するわけですが、slideshare では Adobe-Japan1-6 の CMap テーブルを持っていないんだと思います。なので、文字コードに対応するフォントを表示できないんじゃないかと。

一方、Adobe-Identity-0 の CMap テーブルは持っているようなので、ROS を Adobe-Japan1-6 から Adobe-Identity-0 に変換することでフォントが表示できるというということかと思います。

Adobe-Identity-0 の CMap テーブルの内容は Adobe-Japan1-6 の CMap テーブルの内容を包含しているようなので、変換も正しく行われるようです。


----

5/19 追記


SlideShare に問い合わせてみました。次のような内容をめちゃくちゃ丁寧に送ったつもりです。

  • Keynote で生成した PDF の大半は日本語フォントが表示されない。日本人にとってクリティカルな問題
  • Adobe-Japan1-6 を Adobe-Identity-0 にしたら表示された(デモ用の PDFslideshare のリンク付き)
  • Adobe-Japan1-6 の CMap がないだけだと思うから、簡単に直せると思う

それに対する回答は次のとおりでした。

Thanks for contacting us about this. What you've encountered is a known issue and I'm very sorry for the inconvenience. Our engineering team is working on it but there's no estimate as to how long that might take. We'll do our best to keep you posted.

要約すると、「対応中だけど解決の目処が立ってない」ってところでしょうか。

うーん・・・いつになったら対応されるのやら・・・

2016-04-02

MobileSafari でプログレスバーを出さないように画像を読み込む

MobileSafari では、どうやら window load のタイミングで画像の読み込みを開始したとしても、その画像の読み込みが完了するまでプログレスバーが表示され続けます。setTimeout などで読み込み開始時間をずらせば回避できますが、プログレスバーが消えたタイミングを取得する術はありません。

何が困るって、バックグラウンドで画像を読み込みたいのに、プログレスバーの表示が終わる前に読み込みを開始するとめちゃくちゃ重いページみたいな印象を与えてしまうわけです。


どうやら、XHR であれば通信中であってもプログレスバーが消えるようなので、次のサンプルコードの loadImage のようにして画像を取得すれば良さそうです。

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=320,user-scalable=no">
</head>
<body style="width:100%; height:1000px;">
<img id="img" width="100" height="75">
<script>
function loadImage(img, url, callback) {
  if (location.search.indexOf('lazy=1') !== -1 && window.URL) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.responseType = 'blob';

    xhr.addEventListener('load', function(evt) {
      if (this.status === 200) {
        var blob = this.response;

        img.addEventListener('load', function(evt) {
          window.URL.revokeObjectURL(img.src);
          if (callback) {
            callback(evt);
          }
        });
        img.src = window.URL.createObjectURL(blob);
      }
    });

    xhr.send();
  } else {
    if (callback) {
      img.addEventListener('load', callback);
    }
    img.src = url;
  }
}

var imageUrl = 'http://abicky.net/hatena/pack_image/wood-fence-texture-04.png?s=' + Date.now();

window.addEventListener('load', function(evt) {
  loadImage(document.getElementById('img'), imageUrl);
});
</script>
</body>
</html>

注意点

  • blob がサポートされてないとけっこう辛い処理が必要
  • 画像のドメインが異なる場合はヘッダに Access-Control-Allow-Origin を付与する必要がある


大きめの画像を読み込む時に試してみると、体感速くなったように感じてもらえるかもしれないですね!

2016-01-12

Ruby で upcasting 的なことがしたくて upcastable という gem を作った

Ruby の良さを殺してると言われそうですが、upcasting っぽいことができる gem を作りました。初めて公開した gem なので至らないところもありますが…

https://github.com/abicky/upcastable


次のコマンドでインストールできます。

gem install upcastable

サンプルコード

次のような Animal module と Cat class があるとします。

module Animal
  def talk; end
end

class Cat
  include Animal

  def talk
    'Meow!'
  end

  def run
    'Running...'
  end
end

次のように upcast_to で Animal に upcasting することで、Animal に定義されているメソッドしか使えなくすることができます。

cat = Cat.new
animal = cat.upcast_to(Animal)
animal.class #=> Cat
animal.talk  #=> "Meow!"
animal.run   #=> NoMethodError: `run' is not defined in Animal

詳細は README を見てください。


仕組み

upcast_to の返り値は Upcastable::UpcastedObject なんですが、そいつが次のように upcast した class または module にインスタンスメソッドが定義されているかチェックして、存在すれば delegate しているだけです。

    def method_missing(m, *args, &block)
      unless @ancestor.method_defined?(m)
        raise NoMethodError, "`#{m}' is not defined in #{@ancestor}"
      end
      @object.send(m, *args, &block)
    end

メソッドが存在しない場合だけでなく、==, eql? 等 Object に定義されているインスタンスメソッドdelegate しているので、現在の実装だと a == a.upcast_to(SomeClass) と a.upcast_to(SomeClass) == a で結果が変わるという鬼畜なことになっています。どちらも false を返すようにするかもです。


メソッドの存在チェックと delegate をしているので当然遅くなりますが、https://github.com/abicky/upcastable/blob/v0.1.0/benchmark/upcasting.rb を実行してみる感じだと 1 μs/call 程度の差なのかなと思います。


何故作ったか?

例えば class A, class B, class C どのインスタンスでも同じように処理しているコードがあったとして、class A のインスタンスでしかテストしていなかったとします。ところが、そのコードで class A にしか定義されていないメソッドを呼んでいると、テストは通るのに class B や class C のインスタンスを処理する場合にエラーになってしまいます。

class A, class B, class C が共通メソッドを持つことはテストで簡単に保証することができますが、ある一連の処理の中で共通メソッドしか呼ばれないことを保証するのは少し大変です。


共通メソッドを super class や module に定義した上で upcasting すれば、class A, class B, class C 全てのケースでテストしなくても、いずれかのクラスでテストが通れば他のクラスでもテストが通ることになり、より本質的なロジックのテストに集中できます。