Edited at
RubyDay 18

Ruby で PDF と戯れるの巻

More than 1 year has passed since last update.

とちぎ Ruby 会議 05 の懇親会の LT で発表したネタですが、一部の方から関心を持って頂けたようなので記事にします。


関心事の全体

まず、下記のような流れをサーバーで定期的に自動で行いたいという目的があるとします。


  1. WEB で公開されてる PDF を取得

  2. テキストを抽出

  3. テキスト整形

  4. データベースへ格納


今回やること

今回は PDF の取得やテキスト整形やデータベースへの格納は割愛します。

PDF からのテキスト抽出についての紹介だけをします。


今回の題材

今回は適当な go.jp サイトから文化庁の「敬語の指針」を選びました。この PDF を適当に開くと、1ページ目に「敬語の指針」「平成19年2月2日」「文化審議会答申」と書いてあるのが分かると思います。このテキストを Ruby で抽出してみましょう。


なんかライブラリに喰わせればテキストがポンと出てくるでしょ

色々調べた中で、PDF を読むのに一番使いやすかったのは Origami という gem でした。

gem i origami でインストール出来ます。

インストール出来ましたね。この Origami を使えば「敬語の指針」が取り出せそうですね。やってみましょう。


puts_first_page.rb

require 'origami'

puts Origami::PDF.read("keigo_tousin.pdf").pages.first.Contents.data

これで最初のページのコンテンツが標準出力に表示されるはずです。

BT

/Part <</MCID 0 >>BDC
/CS0 cs 0 0 0 scn
/GS0 gs
/C2_0 1 Tf
0.5103 Tc 31.98 0 0 31.98 184.98 623.9603 Tm
<18982E8607E91730328E>Tj
0.0055 Tc 15 0 0 15 229.8 176.7203 Tm
<1414169139DD39E5141539DE19AC39DE18E3>Tj
22.02 0 0 22.02 220.2 116.4803 Tm
<18A50D7B12AD2F0F0AB3257521B3>Tj
EMC
ET

はぁ。「なんかライブラリに喰わせればテキストがポンと出てくるでしょ」

そういうライブラリをご存じの方、是非教えて下さい...

私はそういう便利なものに辿り着けませんでした。


PDF の構造について

とりあえず、自分の甘さを痛感したところで、敵を知る取り組みに移ります。

右も左も分からないときには文献に当たるのが定石です。PDF構造解説という本を読んでみます。


テキストセクションは、BT(Begin Text)と ET(End Text)というオペレータで 囲むことによって作り出すことができます (p.97)


ふむふむなるほど。確かにさっきの文字列は最初が BT で 終わりが ET だった。


ここでは、Tf オペレータにフォント名とサイズを指定してフォントを選択した後、 Tj オペレータを用いてテキスト文字列を描画しています。(p.98)


はーいはいはい。Tf も Tj もありますわー。段々構造が分かってきたー。

要するに Tj オペレーターが print 命令みたいなもんなんですね。と、言うことは、さっき出てきた <18982E8607E91730328E>Tj って部分の16進数っぽい部分が文字情報になっていると見て良さそうですね。


最初の例では、さまざまなオペレータを用いてテキストを数行描画しています。

(Text and graphics) Tj T* (p.100-101)


えっ?ASCII だと文字列をそのまんま指定できるの?じゃあマルチバイトは?ねぇねぇ、マルチバイトは?

さらに読み進めると、こんな記述があります。


6.5章 日本語の取り扱い

ここでは本文で取り扱っていない、日本語の表示方法について解説しています。 CID フォントを使用して日本語表示を行うには、少し複雑なオブジェクト構造を記 述する必要があります。(p.115)


すごいドツボの臭いしますね...


CID フォント

PDF って、誤解を恐れずざっくりと言えば電子的な写植情報が詰まったドキュメントなんですよ。「どういう文字列なのか」というよりも「どのように印字するか」というデータの集まりなんです。その中に組み込まれている CID フォントというのは「どのように印字するか」を表現するためのもので、写植における文字盤みたいなものなんだと思います。

何言ってるんだか、全然わかりませんよね?わしも良く分からん。

Adobe-Japan1-6 Character Collection for CID-Keyed Fonts このドキュメントを見て下さい。沢山の文字が載ってます。これが Adobe-Japan1-6 という規格の CID フォントの仕様書です。

試しに、この仕様書と、さっき出力された <18982E8607E91730328E>Tj って部分とを照らし合わせてみましょう。まずは4桁ずつの16進数に分離して、10進数に変換します。

"18982E8607E91730328E".split("").each_slice(4).map(&:join).map{|o| o.hex}

#=> [6296, 11910, 2025, 5936, 12942]

なになに、「舁テラ貢窩ə」かー。なるほどー。化けてますね。

Adobe-Japan1-6 を使ってる PDF はメタ情報の中にそれを使ってる宣言をするらしいんですが、この PDF にはどこにもその宣言が無いですね。ということは、別のコード表を探さないといけません。というか、そもそも Adobe-Japan1-6 Character Collection for CID-Keyed Fontsは CID コードと Unicode の対応表になっていませんよね。単にどの CID がどのように印字されるかを記したものに過ぎないのでした。


フォント情報

じゃあ、フォントのメタ情報をまず見てみましょう。


show_font_info.rb

require 'origami'

puts Origami::PDF.read("keigo_tousin.pdf").pages.first.Resources.Font.C2_0

403 0 obj

<<
/Subtype /Type0
/DescendantFonts [ 409 0 R ]
/BaseFont /OCHGOO+MS-Mincho
/ToUnicode 404 0 R
/Encoding /Identity-H
/Type /Font
>>
endobj

Adobe-Japan1-6 を使っているならあるはずの /CIDSystemInfo が無い。

代わりに /ToUnicode 404 0 R という要素が何らかのコンテンツへの参照を持っているようです。

ToUnicode とはなんだろうか。PDF構造解説に解説があります。


6.5 ドキュメントからテキストを抽出する

(略)

こういったことを行うメカニズムとして、フォント中の /Encoding エントリ(こ れは文字コードと /bullet のような Adobe Glyph List エントリを対応付けるもので す)と、より近代的なメカニズムである /ToUnicode エントリ(これは文字コードと Unicode エントリを直接対応付けるための、Adobe が定義した言語によるプログラ ムを指定するためのものです)の 2 つが提供されています。


いまいち何を言ってるか分かりませんが、ToUnicode という名前からして「CID を Unicode のコードポイントに変換するための何か」って感じがしませんか。


ToUnicode

まずは、ToUnicode の実体を見てみようー


show_to_unicode.rb

require 'origami'

puts Origami::PDF.read("keigo_tousin.pdf").pages.first.Resources.Font.C2_0.ToUnicode.data

/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo <<

/Registry (TT1+0) /Ordering (T42UV) /Supplement 0 >> def
/CMapName /TT1+0 def
/CMapType 2 def
1 begincodespacerange <000a> <3a2a> endcodespacerange
100 beginbfchar
<0098> <00D7>
<0316> <2026>
<032a> <203B>
<03c6> <2190>
(省略)
<34cf> <34d0> <968E>
<350c> <350d> <96E2>
<35c4> <35c5> <9805>
<39d4> <39e6> <FF08>
<39e8> <39ea> <FF1C>
endbfrange
1 beginbfrange
<39f4> <39f7> <FF28>
endbfrange
endcmap CMapName currentdict /CMap defineresource pop end end

おーおー。なるほどー。

<0098> <00D7> ってのは「0098 ってのが出てきたら、Unicode 00D7 の事だよ」っていう意味で、<34cf> <34d0> <968E> ってのは「 34cf から 34d0 までは 968E で始まる Unicode だよ」かな。

"18982E8607E91730328E".split("").each_slice(4).map(&:join)

#=> ["1898", "2E86", "07E9", "1730", "328E"]

改めて、対応する部分を探してみよう。

"1898" は <1898> <656C> で "656C"

"2E86" は <2e86> <8A9E> で "8A9E"

"07E9" は <07e8> <07eb> <306D> で "306E"

"1730" は <1730> <6307> で "6307"

"328E" は <328e> <91DD> で "91DD"

よさそう。では、まとめてデコードしてみよう!

 ["656C", "8A9E", "306E", "6307", "91DD"].map(&:hex).pack("U*")

#=> "敬語の指針"

キター!


疲れる

今回は /ToUnicode があったので、それを使ったけど、必ずしもこれが埋め込まれているわけでもない。

外から CID Map を持ってきて使わないといけなかったり、Unicode の Code Point がそのまま Tj に渡されているケースもあるので辛い。見たことはないけど、SJIS だったりすることもあるらしい。

色んなケースを網羅して、サっとテキスト抽出してくれるようなフリーのライブラリがあれば嬉しいんだけど、わしには見つけられなかった。情報求む。

今は対象の PDF の性質毎に特化したプログラムを書いてテキスト抽出をしています。正直しんどい。


まとめ

行政は情報を PDF で配信するのをやめよう。


あとがき

こうしてまとめてみると、5分で発表するような内容じゃなかったですね。


ついでに

今日誕生日なんです!ありがとうございます!