golden-luckyの日記

ツイッターより長くなるやつ

XMLをつぶす機械を作る機械を作る

昨日は、ドキュメントの構造をプログラムのように実行できるというアイデアの話をしました。 具体的には、「ドキュメントの構造をS式で表現し(SXML)、そのタグをLispの関数と見立て、それを要素に関数適用する」というアプローチです。 たとえば、XMLで表したときに段落を意味する<para>のようなタグに対する変換処理は、こんな感じのLispの関数として定義できます。

(define (para arg)
  (print arg "\n\n"))

今日は、これをもうちょっと真面目に定義する部分と、これを評価する部分、それに実用的に使うためのフレームワークについて書きます。 以降、Lispの処理系としては、GaucheというSchemeの実装を使います。

ドキュメントをS式で書くの?

まず、そもそもドキュメントを書くときにS式で書くのか、という点に答えておきます。 べつにS式で書きたければ書いてもいいんですが、実際にはXMLをSXMLに変換して使うほうが便利でしょう。

というのは、いま本当にやりたいのは、「何らかの方法で手に入れた、スキーマが明らかとは限らないXMLなドキュメントを、LaTeXなどの他のマークアップへと変換する」ことです。 SXMLでドキュメントを書いていくためのエコシステムが欲しいわけではありません。

いまは「記法」の話をしていないことを思い出してください。 プログラミング言語でいえば、抽象構文木を評価して最終的な実行可能ファイルを得ることを考えています。 記法と構造の関係については、いまのところ別のシリーズで言及するつもりです。

なので、XMLをSXMLに変換して使います。 そのための方法は、いわゆるSAXパーサのSXML版の標準的な実装を使います。

これはGaucheにも ssax:xml->sxml という関数で組み込まれています。

これをありがたく使うだけで、基本的には(整形式の)XMLからSXMLが得られます。

XMLのタグ」を関数適用できるようにする

いくらLisp族の言語といえども、リテラルなS式を自由自在に評価させてくれるわけではありません。 そういうLispの実装を自分で作るか、そのためのバックドアがある実装を使う必要があります。

幸い、GaucheというSchemeの実装には、型を指定して好きなモノを適用可能にできるobject-applyという汎関数があります。

このobject-applyを使って、こんなふうにすると、 (<XMLのタグ> <XMLの要素>) という形をしたリストを評価できるようになります。

(define-method object-apply 
    ((tag <symbol>) (body <list>))
  ((global-variable-ref 'user tag) body))

あとは、個々のタグをどういうふうに変換したいか、関数として定義していくだけです。

たとえば para タグをLaTeXに変換する処理であれば、まず改行を出力して、それから中の子要素を再帰的に処理して、最後に改行を2つ出力したいので、こんな感じに定義します。

(define (para body)
  (begin
    (print "\n")
    (map (lambda (b)
           (cond ((bがリストの場合) ((car b) (cdr b)))
                 ((bが文字列の場合) (print b))
                 (else '())))
         body)
    (print "\n\n")))

bタグだったらこんな感じ。

(define (para body)
  (begin
    (print "\\textbf{")
    (map (lambda (b)
           (cond ((bがリストの場合) ((car b) (cdr b)))
                 ((bが文字列の場合) (print b))
                 (else '())))
         body)
    (print "}")))

「「XMLのタグ」を関数として定義する」を抽象化する

こんなノリでLaTeXへのコンバーターを何回か作ってみると気づきますが、やることは毎回だいたい同じです。

  1. 先頭にLaTeXのコマンド名とか開き波カッコとか改行とかを配置
  2. 中身がリストだったら再帰的に処理、文字列だったら必要な加工をして配置
  3. 後ろに閉じ波カッコとか改行とかを配置

要するに、最初にやること、間にやること、最後にやること、の3つをタグごとに決めるだけです。 実際、ほとんどの変換処理は、「この3つをルールとしてタグに与える」で書けます。

いま、「この3つをルールとして定義する」と「それをタグに関連付ける」を、それぞれdefine-ruleおよびdefine-tagとして用意できたとしましょう。 これらを使うことで、たとえば「段落タグpara用の処理ルール」をこんな感じに書こうという算段です。

(define-tag para
  (define-rule
    "\n"
    エスケープ処理用の関数
    "\n\n")

「太字タグb用の処理ルール」であればこんな感じ。

(define-tag para
  (define-rule
    "\\textbf{"
    エスケープ処理用の関数
    "}")

ちなみに、define-ruleおよびdefine-tagを分離しているのは、ルールにはタグ間で共通するものも多いからです。 たとえば、bタグ用のルールをemphタグでも共通して使う、みたいなことをしたいと思ったら、ルール部分だけlatex-bf-cmdとかの名前で定義しておいて、タグだけ関連づけれることが考えられます。

(define-tag b latex-bf-cmd)
(define-tag emph latex-bf-cmd)

このような抽象化は、define-ruleおよびdefine-tagLispのマクロとして定義することで簡単に実装できます。

さらに、Lispマクロのパワーで、「タグの名前からルールを自動生成する」とか「XMLの属性をルールの中で定数として扱えるようにする」といったこともできます。 特に後者はOnLispに出てくるアナフォリックマクロの応用で、それ自体がだいぶ面白いと思うんですが、Lispの話は無限にできてしまう割に読者をさらに選ぶので、このへんでやめておきます。

車輪は自分のために再発明する

ここまでに説明した仕掛けは、xml2texという名前で公開しています。

詳しくは、TeX界のTUGboatというジャーナルに投稿した記事もあるので、気になる人は読んでみて。

xml2texという名前を見ると、「XMLからTeXへの変換器」を想像するかもしれません。 実際、最初はTeXへの変換器を作るバックエンドとして開発したのですが、作っているうちに、xml2texの実際の機能は「「XMLシリアライズして他のマークアップ原稿へと変換するコンバーター」を作るフレームワーク」であることに気付きました。 過去には、FrameMakerというアプリケーションのSGMLXMLの親分みたいなやつ)からRe:VIEW原稿を作ったりするのにも使っています。

ところで、SXMLとXMLシンタックスが違うだけなので、ふつうのXMLでも同様のことが実現できないはずがないんですよね。 どういうことかというと、山かっこのXMLに対する変換処理を山かっこのシンタックスで書き、山かっこのXMLを実行するようなプログラミング言語が作れるだろう、ということです。

で、昨日の記事を書いていて初めて気が付いたんですが、XSLTという関数型言語がまさにそれに相当するような気がしました。 ドキュメント全体をS式として表現したことで「ドキュメントの構造をそのまま実行する」という策が見えてきた気がしていたけれど、その先にあったのはやはり車輪の再発明でしかなかったというわけです。

でも、車輪の再発明、常套だと思っています。 XSLTは、XMLの世界観では完成度が高い仕組みだけれど、やはりふつうのプログラミング言語と同じ感覚では書きにくいし、処理系の機能も限られます。 そこいくとLispはやはり自由度が高い。 自分の直観にあう方法で変換器をぽんぽん生成できる仕組みを作ったことは個人的にはとても気に入っています。

明日は、このxml2texを使うことで、DocBookの原書データを活用した翻訳版の本をLaTeX経由で作っている話をします。

XMLをLisp評価器で実行する

昨日は、ドキュメントとは木であり、その木はXML、さらにいうとXMLアプリケーションとして形作られる、という話をしました。 一般にドキュメントは、生のままの構造として読み手に与えられるものではありません。 ドキュメントの構造が何らかのXMLアプリケーションであれば、本来はそのスキーマに従って解釈し、しかるべきスタイルを適用することになります。

ここで、しばしば厄介になるのは、XMLアプリケーションのスキーマを正しく扱うのはなかなか大変だということです。 そもそも、プロプラなXMLアプリケーションだと、スキーマの定義が手に入らない場合もあります。 そういった「野良XMLをどうやって扱うか」が今日からの話題です。

でもその前に、昨日の記事の最後で紹介したSXMLについて雑に補足しておきます。

XMLをSXMLに引き写すことで見える景色

さて、昨日はSXMLという技術を紹介しました。 SXMLのSはS式のSです。 簡単に言うと、こういうXMLで構造が表されている書籍は…、

<book>
  <info>
    <author>著者</author>
    <title>本タイトル</title>
  </info>
  <chapter>
    <title>章タイトル</title>
    <para>段落の本文</para>
  </chapter>
</book>

こういうS式で表せます。

(book
  (info
    (author "著者")
    (title "本タイトル"))
  (chapter
    (title "章タイトル")
    (para "段落の本文" (b "ここは太字") )))

ところで、みなさんご存じのように、S式はLispというプログラミング言語で(現代では)主に採用されています。 Lispというプログラミング言語におけるS式は、Lispシンタックスであると同時に、抽象構文木でもあります。 Lispでは、S式で表された抽象構文木をそのまま評価器で実行でき、しかもマクロでDSLを作れます。

さて、ここで、一昨日の記事を思い出してください。 そこでは、「ドキュメントの構造はプログラミング言語における抽象構文木に対応している」と述べました。 そしてSXMLでは、ドキュメントの構造がS式で表現されています。

ということは、ドキュメントの構造をそのままLispで実行できそうに思えませんか?

XMLをわざわざS式で表現することには、見た目の雰囲気を変えただけでなく、「ドキュメントの構造をプログラミング言語の抽象構文木と同じように評価できる」ことを思い出させてくれる効能があったのです(ただしLispプログラマーにとって)。 スキーマ言語XMLアプリケーションを作るのと事実上同じことが、SXMLであればLispのマクロとして、より直観的に実現できると言ってもいいでしょう。

SXMLをLispで「評価」する

野良XMLLispマクロで手なずける前に、「LispでSXMLを評価する」ことについて、Lispを少ししか知らない人向けに補足しておきます。

先ほど例として示した書籍の構造を表すSXMLの一部には、次のようなS式が含まれていました。

(para "段落の本文")

このSXMLは「paraという関数に引数を適用する」と見なせます。 この場合の引数は"段落の本文"という文字列です。

山かっこのXMLだと「段落要素を表す<para>タグ」にしか見えなかったものが、SXMLだと関数適用に見えるのがわかるでしょうか。 たとえば、(para ...)というSXMLをLaTeXの段落として出力したければ、関数paraを「引数の中身をLaTeXの段落として出力する」というふうに定義すればよいでしょう。

(define (para arg)
  (print arg "\n\n"))

同様に、関数bを「引数の中身をLaTeXの太字として出力する」というふうに定義すれば、(b ...)というSXMLの項を評価した結果はLaTeXの太字になります。

(define (b arg)
  (print "\\textbf{" arg "}"))

こんな感じで元のXMLに出現するタグと同じ名前の関数にしかるべき定義を与えてあげれば、あとはSXMLの全体をLispの評価器で実行するだけで、「元のXMLアプリケーションを処理した結果」が得られるという寸法です。 画期的ですね。

(define (book arg)
  ("\\documentclass{jsbook}\n\\begin{document}" arg "\\end{document}"))

ただ、これだけだと、argの中身が再帰的に評価されないのでうまくいきません。 そのへん、もうちょっとちゃんと定義してあげる必要があります。

また、XML名前空間や属性の扱いなども、それなりに対処してあげる必要があります。 SXMLでこれらを扱うのは、XMLスキーマを書くときと同様に、わりと大変です。 そもそも、素性のわからない野良XMLの中身を見ながら「元のXMLに出現するタグと同じ名前の関数にしかるべき定義を与えてあげる」という作業はかなりうっとうしい。

SXMLを評価することで野良XMLに意味を与える、というアイデアを実用的なものにするには、これらの細かい部分を抽象化してくれるフレームワークが欲しくなります。

明日は、そういうフレームワークを実装する話をします。

なんでドキュメントといったらXMLが出てくるのか

昨日は、ドキュメントにおける構造というのはセマンティックな構造である、という話をしました。 今日は、そのセマンティックな構造をどう扱うか、という話です。

ドキュメントの構造は一般にXMLを使って表されている

結論から言うと、ドキュメントの構造は、XMLで扱うのが一般的です。 ドキュメントの構造を表すのにXMLがよく使われているのには理由があって、それは、ドキュメントが木構造だからです。

本当はここで「XMLとは何か」みたいな話をする必要があると思うんですが、ここではXMLというのは「木構造のデータを表現するときの標準的な構文」くらいの意味で使います。 つまり、表現する「木構造のデータが具体的にどんなか」については別の問題ということにして、木構造で表せるようなデータにとって共通で必要そうな構文だけを定めたものが、(ここでいう)XMLです。

ちなみに、「木構造のデータが具体的にどんなか」のほうは、「XMLアプリケーション」と呼ばれます。 XMLアプリケーションとしては、HTML(正確にはXHTML)とかMathMLとかDocBookとかがあります。 XMLアプリケーションを定義するには、一般にはDTDとかXML SchemaとかRELAX NGとかいった「スキーマ」を使います。 上の段落で言いたいことは、そういったスキーマを使って定義されたXMLアプリケーションが何であるかを本記事では気にしない、ということです。

ドキュメントの木

よくわからないと思うので、ドキュメントの木構造を例に説明します。 ドキュメントの木構造といって想像するのは、たぶんこんなのでしょう(適当に描いたのでツッコミはなしで)。

f:id:golden-lucky:20191208115241p:plain

緑色の葉に相当するのがインライン要素、そのインラインの要素が集まったものが茶色の節に相当するブロック要素、ブロック要素が集まってドキュメントになる、という感じです。

ただ、この単純なイメージだと、「インライン要素にどんなものがあるか」、「ブロック要素にどんなものがあるか」、「それらをどう組み合わせていいか」といった具体的な木の形状までは説明できてません。 実際に必要な要素が何であるか、要素の組み合わせとして何を認めるかは、ドキュメントの用途や種類によってまちまちです。

とはいえある程度までは、ドキュメントが必要とされる分野ごとに「汎用性のある要素の組み合わせ」みたいなものは考えられるでしょう。 そういう「汎用性のある要素の組み合わせ」を考えるということは、木構造を制限するということです。 その手段がXMLアプリケーションです。

言い換えると、ドキュメントがどんな形状の木になりうるかをXMLアプリケーションとして制限する、という世界観です。 たとえば、技術書のために必要な制限を課したXMLアプリケーションとしては、DocBookがあります。

木構造ではないドキュメントもありうるとは思うんですが、それはここではドキュメントではないものとします。どう考えても木構造とみなすのが適切でなさそうなドキュメントがあったとして、それをコンピューターでどう扱えばいいかという話も面白そうだけど、そういう話はどこかにあるのかなあ。)

XMLは山かっこでなくてもいい

ここまでの話を整理すると、こうなります。

  • ドキュメントは木構造
  • どんなドキュメントかに応じて木構造を制限したい
  • それに都合がいい仕組みとしてXMLがある

どうでもいい話に聞こえるかもしれませんね。 なんでわざわざこんなことをくどくど書いてるかというと、これらの話にはXMLの象徴である「山かっこの記法」が出てこないことを強調したいからです。 実際、こういう枠組みを実現するのに、記法が山かっこタグである必要はありません。

XMLで表せる木構造は、文字通り「構造」であり、記法とはレイヤが違います。 XMLの世界観だと、「XMLアプリケーションごとにスキーマで定義した構文」のほうが記法に相当します。

もっとも、この「XMLアプリケーションごとにスキーマで定義された構文」もXMLと見た目は同じ、つまり、通常は山かっこタグになります。 記法についてはドキュメントの構造とは別に考えるべきだけど、XMLでは両方とも同じ記法を採用している、みたいな感じです。 ドキュメント屋さんとして、XMLを使うときはこの辺りの事実からは逃れられない感じです。

ただ、ぶっちゃけ木構造を表すなら、山かっこタグよりも優れたシンタックスがあります。そう、S式です。 S式をシンタックスとするXMLをSXMLといいます。

というわけで、明日はLispの時間です。

ドキュメント技術とプログラミング言語の相似について

よく知られているように、ドキュメントには「構造」があります。 WebページではHTMLとCSSにより構造とスタイルを分離するべきとか、Wordでは書式設定をスタイルとして定義して使うことで構造とスタイルを分離するべきとか、ドキュメントの「べき」論で必ず言及される「構造とスタイルの分離」における「構造」です。

昨日までの話ではPDFにもドキュメント構造というのが出てきました。あれは、この「構造とスタイルの分離」というときの「構造」とは別物なので注意してください。 たぶん、PDFのドキュメント構造には、「ドキュメントを表すデータ構造」くらいの意味合いくらいしかありません。

一方、ドキュメントの話において「構造とスタイルの分離」というときの「構造」は、もうちょっとこうなんていうか、セマンティックな話です。 データをどう構成するかではなく、ドキュメントで表したい意味をどう構成するか、という話。

したがって、ドキュメントの話をするときは、「ドキュメントで表したい内容」を「ドキュメントの最終的な見た目」みたいなフワフワから切り離すことで、前者の可搬性を高めていくことが目指されます。

話はちょっとずれるんですが、コンピューターで扱うドキュメントの話をするときって、「可搬性を持たせたい部分がどこか」という点がわりと曖昧なままになってることがあるので、その点に注意して聞くといいと思います。 PDFは、見た目の可搬性から出発していました。 「構造とスタイルの分離」の話では、構造の可搬性を目標にします。まあ、あんまりこっちの話をするときは「可搬性」という用語は使わず、再利用可能性とかいうことが多いので、混乱はないと思うけれど。

構造とスタイルと記法

コンピューターでドキュメントを扱う話をするときは、「構造とスタイルの分離」がどれくらいかっちりできてるかで、技術の素性の良さみたいなのが語られがちです。 つまり、構造とスタイルの分離っていうのは、ドキュメント技術においてはある種の金科玉条です。

まあ最近だと、「金科玉条でした」と過去形で書くほうが正解なのかもしれませんね。 いまの流行りは構造とスタイルの分離でなく、書式とか記法、つまりシンタックスに移っているようなので。

しかしドキュメントにおけるシンタックスって、結局のところ、ドキュメントのセマンティックな構造を暗に埋め込むためにどんな表現を使うか、という話なんですよね。

ドキュメントの構造を暗に表現するシンタックスの話については、このアドベントカレンダーでいつか話す予定です。 いまは何が言いたいかというと、構造とスタイルの分離を重視する立場だと、「人間の直観を裏切らない記法」みたいな観点はそれほど重視されません。 一方で、人間が編集しやすい記法が何かという話に注力してしまうと、それはそれでドキュメントにおける構造とスタイルの分離の伝統とは違う話をしてしまう可能性が高い。

なにが言いたいかというと、これからのドキュメント技術について語るときの前提は、「記法」「構造」「見た目」の3つのレイヤを意識したモデルに依拠するといいのかな、ということです。

ドキュメント技術の3階層モデル

だんだん与太話っぽくなってきましたが、「シンタックス(記法)」→「セマンティクス(構造)」→「スタイル(見た目)」という3つの階層でいろんなドキュメント技術を考えるのは、わりと建設的なモデルなんじゃないかなと個人的には考えています。

個人的にこのモデルが特に気に入ってるのは、プログラミング言語における「ソースコード」→「抽象構文木」→「実行ファイル」の関係によく似ている点です。 プログラミング言語に似ているということは、コンピューターでドキュメントを扱う方法について考察するときに都合がいい。

たとえば、プログラミング言語だと、この階層の矢印を逆方向にたどるのが無理筋だということをみんながよく知っています。 ふつうの人が直接触れるのは最下層のみだけど、階層を下にいくにつれて、その中身は人間が理解しにくいモノになっていく。 これがドキュメント技術の話になると、どの階層を見ても人間が読む文字が見えるので、似たような階層があることに気が付きにくい。 意識的に層の違いを強調してあげる必要があると思うんですが、そこで「プログラミング言語における「ソースコード」→「抽象構文木」→「バイナリ」の階層みたいな感じ」といえば、なんとなく伝わりそうな気がします。

階層を下に降りるほど編集の自由度がなくなるという点も、ドキュメントの3階層がプログラミング言語に似ているところだと思います。 PDFを直接いじることが煩雑かつ(ドキュメントの可搬性にとって)危険であることは何となく昨日までの記事で伝わってると思うんですが、これはドキュメントの「シンタックス(記法)」→「セマンティクス(構造)」→「スタイル(見た目)」という階層で考えれば「プログラムの実行ファイルを直接いじる」のと同じ話なわけで、なんとなく当然の成り行きであることが伝わりやすい気がします。 大変かつ危険だけど、条件によっては安全に実行するすべがないわけでもないので、その必要がある場合にはそのためのPDF編集ツールを導入してください、という話がしやすいでしょう。

編集者の仕事は各階層をそれぞれがんばること

この見方でポジショントーク的に説明しやすいことがもう一つあって、それは、プログラミング言語で実行環境に相当する部分はドキュメント技術では人間の脳である、という点です。 そう考えると、編集者の仕事っていうのは、「人間の脳におけるドキュメントの実行を最適化するために各階層で手を尽くすこと」に見えてこないでしょうか?

プログラミングにおいては、シンタックス、セマンティクス、スタイルの各階層だけでなく、その上、つまりアルゴリズムの改良とかデータ構造の工夫も重要です。 たぶん、ドキュメントの仕事でそれに近いのは、文章のリライトとか、いわゆるトンマナの調整、それに校正なんかなんでしょう。

さらに上の階層、そもそも現実の問題をどんなアプリケーションとして作ればいいのかを含めた設計に相当する部分は、ドキュメントでいうと企画ですね。

最後はちょっと強引に編集者の仕事論っぽい話をしてみました。 明日は構造化文書の本丸、XMLの話です。

一人でアドベントカレンダーを書いている

去年に引き続き会社の近況報告をしようと思ってpyspaアドベントカレンダーにエントリしたけれど、今年は会社の話はやめて、メタアドベントカレンダーを書きます。

今年は一人でアドベントカレンダーをやることになり、とりあえず6日間、必至で書き続けました。毎日、2時間くらいは溶けています。著者の人はこういう気持ちなのかなと思いました。

もっとも、この一週間はたまたま本業のほうでも執筆仕事を2つ抱えていて、アドベントカレンダーの執筆はそのための格好の素振りになっていた気がします。 みなさんも経験があると思いますが、文章を書くときには、とにかく書くしかありません。 書かない時間が少しでもあると脳が止まってしまう。 しかし、同じ内容についてずっと考えているのは無理なので、なんでもいいから書き始めて、なんでもいいから書き続けるしかありません。 この状況をぼくは「素振り」などと呼んでいます。

今週は、アドベントカレンダーを素振りにすることで、執筆のお仕事を乗り切れました。それがさっき終わったところ。

来週の本業は編集制作の佳境なので、たぶんアドベントカレンダーの執筆とは両立できません。 今週末にどれくらい書きためられるかで、この一人アドベントカレンダーの成否は決まりそうです。

さて、そもそもなんで一人アドベントカレンダーをやることになったかというと、渋川さんが「会社アドベントカレンダーが一瞬で埋まった」という話をしているを聞いて、Qiitaのアドベントカレンダーには企業という概念があることを知り、「それなら当社も一瞬で埋められるのでは?」と変な気を起こしたからなのです。

ただ、これは理由の半分でしかありません。 さらにもとをたどると、「ラムダノートは良い本を作ってはいるけれど、本の売り上げだと経済的な基盤が危ういので、もうちょっと手堅い仕事ができるってことをアピールしろよ」という大株主の意向があったのです。 この意向が下されたのは夏過ぎに開催された臨時株主総会(という名のランチ奢られ)の日のことでした。

実際、年度末になって会社の現金が本気でまずい状況になりそうな状況が発覚してしまい、まだ予断を許さないのだけど、読者の皆様のおかげで少しだけ息を吹き返しました。 いやほんと正直なところ夏ぐらいのぼくの皮算用だと前年よりも順調に推移している感触だったのですが、今年の4月に創刊した『n月刊ラムダノート』に全力投入しすぎて受託のお仕事を取らなかったからか、あるいは単純に5年目の危機というやつなのか、大株主の予感どおりになり、大株主やっぱすごいなと思いました。

で、いわゆる「手堅い」お仕事を増やせばいいかというと、それはそれでまずくて、なぜなら出版業界は単価の相場が低いからです。 低めの単価でたくさん仕事を受けてしまうと、自社の出版活動が本気で滞ってしまう。

つまり、当社(というかぼく)が実際に請け負える仕事はというと、ドキュメントに関する「人海戦術が効かないような力仕事」ということになります。 適当なツールや知見が存在しないので腕力がいるけれど、業務フローに落とし込めるわけでもないので人手を増やしても解決できないような仕事です。

で、株主の人は「そういう仕事やれよ」といいました。 加えて、そういうお仕事を営業で取りに行く余裕がないのも知っているので(なぜなら『n月刊ラムダノート』の編集に忙しいから)、「ブログとか書いてアピールしろよ」みたいに言いました。

個人的にも、自分が企画する本ばかり作っているとスキル(それがあったとして)がたこつぼ化するので、特殊な挑戦が必要になるような仕事を積極的にやってみたいという気持ちがあります。

そんなところで渋川さんの会社の話を聞いたのがトリガーになり、Qiitaの企業カテゴリーでアドベントカレンダーを立ち上げて一瞬で埋めるに至りました。 今日までは低レイヤの話をしたので、明日からはもうちょっと高レイヤでドキュメント技術の話を続けます。 ラムダノートの技術 アドベントカレンダーを見て、なんか相談できるかなと思った方は、ご相談ください。

明日のPyspaアドベントカレンダーあきすて氏です。

PDFから「使える」テキストを取り出す(第6回)

今日まで延々と「PDFからテキストデータを取り出すのは大変」という話を続けてきましたが、その構造を見るにあたっては、 hpdft という自作のツールを使ってきました。 大変とはいっても、まあ実現困難な話ではなく、この程度のPDFパーザであれば趣味プログラミングで自作できる範囲です。

しかし、べつにわざわざ自作しなくても、「PDFからテキストデータを取り出す」ためのツールなら世の中にはすでにいくつもあります。 特に有名で昔からよく使われているのは、Xpdf由来のpdftotextでしょう。

XpdfからはPopplerが分派しているので、Poppler版のpdftotextもあります。

また、pdfminerというツールもあります。

pdfminerが面白いのは、行間などを細かく指定して読み方をユーザがある程度まで制御できるところです。ちなみにhpdftはpdfminerにちょっとだけ影響を受けています。

有償の製品まで幅を広げるともっといろいろな選択肢があります。 いずれも使ったことはないですが、個人的にいちばん気になるのは、アンテナハウスのPDFXMLです。 これ、たぶん同社のPDF編集アプリの応用で、あのアプリを実現するためにPDFのテキストを認識する部分だけを切り出した製品であるような気がするんですが、だとするとかなり構造化されたデータが取れたりしそう。

Acrobatのようなビューワーからテキストを範囲選択してコピペすることもできます。「テキストとして書き出し」みたいなオプションがある場合もあるでしょう。

PDFからHTMLを作る

hpdftを作り始めたのは、もともとは「PDFの中身を直接読みたい」という単純な好奇心からでした。 しかしながら、既存のツールに頼らず自作したことで実際に仕事に役立った事例があるので、最終回はそれを紹介だけして締めたいと思います(その後でちょっとだけエモい話もするよ)。

さて、ここに当社で発行している『定理証明手習い』という本があります。

『定理証明手習い』www.lambdanote.com f:id:golden-lucky:20191205214620p:plain

この本、翻訳書なんですが、使える原書のデータがPDFしかありませんでした。 翻訳の版権を購入すると、原書のDTPデータやTeXソースを原書出版社から提供してもらえることが多いんですが、どういうわけか本書については印刷所に入稿されたPDFしかもらえず、それをベースに翻訳作業を開始することになりました。

しかしこの本、そこそこ特殊で微妙な色分けなどが施されていて、既存のPDFテキスト抽出ツールでは翻訳に使えるようなデータが得られません。 というか、膨大な目視による手作業が必要になる有様でした。 そんなのはいやだ。

そこでどうしたかというと、hpdftを本書専用に改造し、それでテキストだけでなくグラフィックスに関する情報をなんとなく読み取ってHTMLを吐き出すようにしました。 たとえばPDFのコンテンツストリームにはCSSCというオペレータがあるのですが、これらでPDF上の色が決まります。 /PANTONE285PC CSとか/PANTONE153PC CSで色空間を決めて、数値 SCでその強度を設定するという感じです。 ここから取得した値を使って、<font color="">...</font> を組み立てるようにします。

また、PDFを生成したツールが一冊の書籍中でまちまちということはないので、テキストに関するオペレータからインデントの分量もある程度までは読み取れます。 もちろん人間による確認と細かい調整は必要ですが、それは自分で原書を見つつ一次翻訳をするので、その過程でついでにやりました。

最終的に、こんな感じのオリジナルのPDFから…

f:id:golden-lucky:20191205214122p:plain

こんな感じにレンダリングされるようにCSSをあてがったHTMLを半自動で出力しています。 (ちなみに、このHTMLでは英日を対訳で表示できるようになっていますが、これはhpdftの出力とは別に後処理でやりました。)

f:id:golden-lucky:20191205214325p:plain

このHTMLの状態で監訳の中野さんにみっちり直してもらい、そのHTMLを最終原稿としてそこからLaTeXを介してPDFにすることで、翻訳版の書籍が次のようなページとして完成しています。

f:id:golden-lucky:20191205214344p:plain

つらいのはPDFではなく人間

今日まで6回に分けて、「PDFファイルからのテキスト抜き出しは単純な作業ではないけれど、条件さえ合えば、PDFからうまくテキストを取り出して再利用できないこともない」という話をしてきました。 結局、「PDFファイルからのテキスト抜き出しは単純な作業ではない」の部分だけが強調されてしまった気もしますが、これはPDFの出自にさかのぼった第1回の記事のことを考えると、ある意味では「予想された結末」だったような気もします。

そもそもPDFは、紙に印刷された状態を再現するためのデータ形式であり、そこに埋め込まれたテキストを再利用できる条件は限られています。 本文では紹介しませんでしたが、/ActualTextというエントリに「実際の文字列」を含めるようなこともPDFの仕様上は可能です。 さらに進むと、タグ付きPDFといって、ページ上のテキストに対する構造を伴ったPDFも仕様化されています。 そういったPDFであれば、データの再利用を目的とした用途にも有効でしょう。 最終回の記事で紹介した事例のように、書籍のような「それなりに同じパターンの見た目のページが数百ページくらい繰り返す」ドキュメントであれば、そのパターンに合わせてコンテンツストリームからテキストを取り出す処理を書くこともまったくのおとぎ話というわけでもありませんでした。

ようするにPDFのつらさとは、その仕様からくるテキスト抜き出しの煩雑さではなく、「ドキュメントの見た目」と「データとしてのドキュメント」をごっちゃにしている人間の残念さだといえます。

ドキュメントの見た目という観点では、人間向けにプラットフォームをまたいだ見た目の再現に関してPDFは本当に考え抜かれたフォーマットなので、当社の商品である書籍のようなメディアにはうってつけです。 一方で、データの再利用を前提としたドキュメント、たとえば行政機関による統計データの配布などにはまったく不向きです。 後者にPDFを使うのはやめましょう。

さて、PDF編はこれでおしまい。 明日からは「HTMLからLaTeXを介してPDFにする」部分を何回かに分けてお話する予定です。

(そう考えると、『定理証明手習い』の事例は、PDF→HTML→LaTeX→PDFという円環を閉じる物語だったとも言えそうですね。)

PDFから「使える」テキストを取り出す(第5回)

昨日の記事では、PDFのコンテンツストリームから文字を読めたことにして、その文字をテキストとして再構築する話をしました。 今日は昨日までの話の締めくくりとして、「PDFごとにカスタムなテキスト取り出し」の話をするつもりだったのですが、その前に文字とコンテンツストリームについて落穂拾いをしておくことにしました。

というのは、昨日までの記事への反応を見ていて、この本のことをちょっと思い出したからです。

この本、PDFのドキュメント構造を知りたい人が最初に読むにはぴったりだと思います。 自分で簡単なPDFを手書きしながら「PDFの中身がどうなっているのか」を学べるように書かれているので、ドキュメント構造やコンテンツストリームの雰囲気を手軽に体験できる良書です。

しかし、この「自分で簡単なPDFを手書きしながら」って部分に、実用上はどうしても難があるんですよね。 特に、テキストに関していうと、英文の例でも和文の例でも「コンテンツストリームにリテラル文字列を書く」というアプローチをとっているので、これが誤解を与えやすいと思います。 この本のサンプルコードだけでPDFを理解した気になってしまうと、「世の中のPDFには文字データがそのまま含まれている」ような錯覚にとらわれてしまい、この連載で1回目に強調した「PDFの文字は文字ではない」ということが意識できなくなる気がします。

PDFファイルを開いてドキュメント構造をパースし、そこで得られたコンテンツストリームを通常のテキストデータと同じ感覚で読んでも、期待される「文字」は手に入らないのです。 CID/GIDを表現する値からUnicodeの文字を変換して手に入れる必要があります。

そして、実を言うと、罠はこれだけではありません。 PDFのコンテンツストリームにおいて「CID/GIDを表現する値」がそもそも単純ではなく、PDF生成エンジンの実装に依存しているのです。

PDFのコンテンツストリームでテキストを表す方法は生成エンジンごとに違う

たぶん、この記事を読んでいるような人は、PDFのコンテンツストリームを自分で開いてみたことがあるように思います。 そこで目にしたのは、ほとんどの場合、昨日までの例に出てきたのと同じような16進表記のCIDの値の列だと思います。

BT
  /F1
  12.4811 Tf
  125.585 -462.55 Td
  [(#1)] TJ
  /F2
  13.2657 Tf
  19.932 0 Td
  [<0b450a3a0c2403c3029403bb0715037103cd03bb029403ef03da03bf03bd0377062c0ac5>] TJ
ET

この例の0b450a3a0c24...というやつですね。このような方法でコンテンツストリームのテキストが表現されたPDFは「もっともよく見かける」タイプです。 言い換えると、コンテンツストリームにおけるテキストの表現方法はこれだけではありません。

ぼくが今まで遭遇したものだけで、世間には少なくとも次の3種類のテキストの表現方法をもったPDFが存在しています。

  • PDFのコンテンツストリームでは、16進表記で文字列を表現することになっています。なので、この方法でテキストを表現する実装が一般的です。
  • PDFのコンテンツストリームではリテラルを使えるという話をしましたが、このリテラルの一種で「8進表記の3桁の数字で任意の文字を表せる」というチートがあります。このチートを使ってテキスト中の文字を表現してくる実装があります。
  • PDFでは、スラッシュで始まる文字列を「名前」として使うという話を昨日か一昨日かにしたのを覚えているでしょうか。/Nameみたいなやつです。この表現を使ってコンテンツストリームのテキスト中でUnicode文字を表現してくる実装があります。利用する名前についてはどこかで決められていた気がしますが、忘れました。名前がない文字も、XXXXUnicodeのコードポイントとして/uniXXXXというふうに表現されます。

個人的にはじめて見たときに特に驚いたのは、このうち2つめの「ASCIIの印字可能文字と「エスケープ+8進3桁」によりCID/GIDをテキストとして指定してくるPDF」の存在でした。 わりと歴史が古いPDF生成エンジンの中には、そういう実装になっているものがあるようです。 まあ、どのみち/CIDSystemInfoを確認してから文字列を見くので、リテラル文字の扱いを仕様どおりに実装していれば混乱はないのですが、ぼくは最初にhpdftを作り始めたときリテラル文字をそのままASCIIとして読むという怠惰なことをしていたので、この手のPDFを始めて見たときには混乱しました。

なんにしても、まず「文字」の原料となる値の取り出しだけで、少なくともこれだけの方法に対応したパーサを書く必要があります。

CMapをどこで手に入れるか

次に、適切なCMapを見つけるという問題があります。

/CIDSystemInfoで具体的なCMapの種類が指定されている場合には、そのCMapをどこかで入手して参照できるようにしておかなければなりません。 Adobe Japan-1などであれば、Adobeが公開しているものが使えるので、この辺から手に入れておきます。

もし、こういう形で入手できない独自のcmapを利用しているTrue TypeフォントがPDFの中で使われていたりしたら、一体どうすればいいでしょうか? わかりません。たぶん、どうしようもない気がします。

さらにやばいのはType 3フォントです。Type 3、まじやばい。 これは「グリフの形状を表現した完全なPostScript命令」がそのままフォントとして使えるという仕組みで、つまり出自からしてグリフしか存在せず、「文字」がない。 TeX界隈だと、DVIPSというソフト経由で生成されたPDFではType 3フォントが使われることになっているので(DVIPSの仕様のはず)、この理由で文字が取れないPDFファイルはわりと目にします。

一方、PDFに/ToUnicodeが埋め込まれている場合にはむしろ楽で、そこで指示されている材料からCMapを自分で復元するだけです。 まあ、自分でCMapを復元しないといけないので、PDFの中を眺めるだけだと限界がありますが。

ただ、ここにも罠があって、/ToUnicodeエントリから復元したCMapではコンテンツストリームのテキストから文字を取れないことがあります。 たとえば、昨日の例でうまく取れなかった「κ」の文字。 コンテンツストリームではこんなふうに表現されています。

BT /F4 10.5206 Tf 395.991 -462.55 Td[(\024)]TJ...

フォントリソース/F4を使って、\024という8進表記の値から、Unicodeの文字を解決すればよさそうですよね(余談ですが、こんな感じでカジュアルに「8進表記の数字3桁によるリテラル」は身近なPDFに登場します)。 そこで、/F4を表すオブジェクトを見てみると、こんな感じになっています。

$ hpdft -r 59 NML-book.pdf
[
/Type: /Font
/Subtype: /Type1
/ToUnicode: 2309
/Widths: 2310
/FirstChar: 20.0
/LastChar: 110.0
/BaseFont: /SRDEOM+LucidaNewMath-AltDemiItalic
/FontDescriptor: 2353]

2309/ToUnicodeがあるらしいので、当然、これを見てUnicodeの値を解決できそうな気がします。

$ hpdft -r 2309 NML-book.pdf
[
/Filter: /FlateDecode
/Length: 247.0,
  /CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CMapName /SRDEOM+LucidaNewMath-AltDemiItalic-UTF16 def
/CMapType 2 def
/CIDSystemInfo <<
  /Registry (Adobe)
  /Ordering (UCS)
  /Supplement 0
>> def
1 begincodespacerange
<00> <FF>
endcodespacerange
1 beginbfchar
<5D> <266F>
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
]

これがCMapの材料です。これから、CMapの仕様にしたがってCMapを再構築すると、こういう対応関係が1つだけ得られます。

(93,"\9839")

これは、「10進表記で93のテキストはUTF-169839として読めばよい」という意味です。 UTF-169839というのは、「」のことで、実際、このPDFの別の場所のコンテンツストリームには10進表記で93に相当する8進表記を含むテキストが出てきて、それは文字「♯」として読むことになります。

しかし、このCMapには、\024つまり10進表記の20に対応する変換表がありません。 そのため、たとえばAcrobatで開いてこの「κ」をコピペしても「κ」は得られず、10進表記の20に対応する文字(ASCIIの制御文字のひとつ)しか得られません。

こうなると、なんでpdftotextでこれを文字「κ」として読めるのか、のほうが謎に見えてきます。 いまのぼくにはほんとに謎なんですが、XPdf系の実装は何か特殊なことをしているのかもしれない。 でも、Sumatraでも「κ」が取れるので、ぼくの理解(とAcrobatの実装)がダメなだけかもしれない。

追記:たぶんぼくが手を抜いてるからで、埋め込まれているTrueTypeフォントの中に入ってるcmapを見れ、との情報をいただきました。ありがとうございます。

そもそもPDFのコンテンツストリームの表現方法に生成エンジンの実装による癖がある

昨日の記事では、「どこに文字が配置されるか」を表現するのにTdオペレータとTJオペレータを使っているPDFの例を見ました。 あのPDFはdvipdfmxというPDF生成エンジンで生成したものなんですが、dvipdfmxはこのような比較的単純なコンテンツストリームの表現でPDFを出してくるようです。

これがmacOS標準のQuartzというエンジンだと、比較的単純なドキュメントであってもTmというオペレータを多用してくるように見えます。 なのでちょっと「いい感じの改行とスペーシングでテキストを取り出す」のがうまくいかないことがあります。

今日の記事で最初のほうに説明したテキストの表現やCMapの埋め込み方がまちまちなのも、生成エンジンのクセみたいな感じで、いろんなPDFの中身を見ているとなんだかPDFソムリエ的な気分になってきます。

明日はPDFからHTMLを作って本の原稿にした話です

というわけで、今日は「気が向いたら明日の記事で話すつもりだった話」をしました。 明日こそは、ほんとうは今日話すつもりだった、PDFからHTMLを作った話をしたいと思います。