エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

pdftotextをRubyの関数にする

pdftotext

Poppler のコマンドラインツールの一つである pdftotext は PDF からテキストを抽出することができます。

en.wikipedia.org

通常は入力に PDF ファイルを指定するのですが、マニュアルに "If PDF-file is ´-', it reads the PDF file from stdin." と書かれているように標準入力から PDF データを流し込むことも可能です。

$ man pdftotext

pdftotext(1)                                                                                General Commands Manual                                                                                pdftotext(1)

NAME
       pdftotext - Portable Document Format (PDF) to text converter (version 3.03)

SYNOPSIS
       pdftotext [options] PDF-file [text-file]

DESCRIPTION
       Pdftotext converts Portable Document Format (PDF) files to plain text.

       Pdftotext reads the PDF file, PDF-file, and writes a text file, text-file.  If text-file is not specified, pdftotext converts file.pdf to file.txt.  If text-file is ´-', the text is sent to stdout.
       If PDF-file is ´-', it reads the PDF file from stdin.

...

と、いうわけで。 Ruby の Open3 を使って pdftotext を起動し、標準入力で PDF ファイルを流し込んで標準出力からテキストを取り出してみました。

Open3

docs.ruby-lang.org

Open3.capture3 は、引数に指定したコマンドを実行し、標準出力、標準エラー、終了ステータスの三つ組を返してくれます。 またオプション :stdin_data を指定すると、その内容を標準入力へ流し込んでくれます。

これらを踏まえて。 実際に PDF をテキストに変換してみます。

ここではサンプルとして、誰でも入手できてかつ無難な内容ということで、気象庁のサイトに掲載されている PDF ファイルを使いました。

PDF データの取得は Net::HTTP.get を使います。

class Net::HTTP (Ruby 3.4 リファレンスマニュアル)

require 'open3'
require 'net/http'

uri = URI.parse('https://www.jma.go.jp/jma/kishou/know/jishin/joho/pdf/jishin.pdf')
body = Net::HTTP.get(uri)
out, err, status = Open3.capture3('pdftotext - -', stdin_data: body)
pp out
#=> "地震情報\n" + ...

pp err
#=> ""

pp status
#=> #<Process::Status: pid 20786 exit 0>

Process::Status

docs.ruby-lang.org

三つ目のオブジェクトは Process::Status のオブジェクトなので、状態を知るために #success? などのメソッドを利用することができます。

status.success?
#=> true

これらをメソッド定義に収め、終了ステータスで判定して例外を生成するようにすると取り回しも楽になりそうです。

module PdfToText

require 'open3'

module PdfToText
  def self.call(pdf)
    out, err, status = Open3.capture3('pdftotext - -', stdin_data: pdf)

    raise err unless status.success?

    out
  end
end
uri = URI.parse('https://www.jma.go.jp/jma/kishou/know/jishin/joho/pdf/jishin.pdf')
PdfToText.call(Net::HTTP.get(uri))
#=> "地震情報\n" ...

不適切なデータを渡すと例外を例外を生成します。

PdfToText.call('wrong data')
#=> #<RuntimeError:"Syntax Warning: May not be a PDF file (continuing anyway)\n ... >

ここでは横着して RuntimeError に標準エラーの内容を丸ごと載せてだけで済ませていますが、例外の種類を自分で定義することでコマンドの実行に失敗したのかコマンドが失敗を返したのかをきちんと識別できるようにできます。

いつか読むはずっと読まない:脳と心