某雑誌の総集編から特定の連載記事だけ抜き取りたい (その1)
スポンサーリンク
PDFの目次?部分をパースすればいいかと思ったらまたしても単純ではない……。
やりたいこと
WEB+DB PRESS総集編[Vol.1~102] (WEB+DB PRESS plusシリーズ)
- 作者: 川合史朗,秋葉拓哉,中嶋謙互,木村廉,酒井政裕,ninjinkun,渡辺訓章,WEB+DB PRESS編集部
- 出版社/メーカー: 技術評論社
- 発売日: 2018/04/26
- メディア: 単行本
- この商品を含むブログを見る
WEB+DB PRESSの総集編のPDFの一部のページを取り出したい。目視でページ番号を割り出してPDF分割コマンドにしてするのはちょっと面白みがない。
そこで、目次部分のページ番号を参照して必要な箇所だけ取り出したい。
まずはPDFの「しおり」とか「ブックマーク」とか呼ばれているデータを取り出したい。正式名称は「ドキュメントアウトライン(document outline)」。
フォントのアウトライン化の話と用語が似ていてややこしいし、ググりにくい。
文章の概観というか外観。「目次としても」使われるデータ、だそうな。
方針および手順
言うまでもないことだが、対象のPDFを入手する必要がある。暗号化などのPDFの状態を考慮して使うツールを決める。
- 総集編の目録PDFまたは検索用のPDFで読みたい連載を選び出す
- PDF群をパースして、対象の連載の掲載されているページ範囲を特定
- 各号のPDFからページを切り出して再結合
可能な限り勉強を兼ねてPythonでやりたかったが、上記の2. に関してはちょうどいいライブラリがないのでpdftk
を使うことに。
手元のマシンにはDVDドライブがないのでzip形式のデータをGihyo Digital Publishingのマイページから入手。
なお、PDFのファイル構造の関係上、目次(Outline? bookmark?)データに直接ページ番号は記録されておらず、ページと結びつけるためのリソースIDが記録されているので対応関係を割り出す必要がある。
課題と方式検討。
まず、この総集編のPDFのバージョンは1.6。
そしてこのPDF、セキュリティ保護がかかっている......。そしてその暗号化アルゴリズムにPyPDF2は対応していない……。
結局、暗号化を外せたからいいけど。
以下、各種Pythonライブラリと留意点。
- PyPDF2: 一部の暗号化方式に未対応。また、目次情報のエントリに参照情報がないとエラーになる
- pdfminer.six: できないことはないけどページの構造を解析する関係で激遅
- pikepdf: 目次部分の抽出はサポートしてないっぽい(Popplerのラッパーライブラリ)
- [reportlab(https://www.reportlab.com/) : 出力用
- python-poppler-qt5: macOSでビルドできず(Popplerのラッパーライブラリ)
- pdflib: 本文の取り出しと画像の抽出ぐらいしかサポートしてなさそう(Popplerのラッパーライブラリ)
- pdfrw: 試してないが目次情報に関するAPIはなさそう
- PyMuPDF: 試してない。実は結構いいかも?
以下、参考情報。
$ qpdf --show-encryption webdb_sp_001-102/webdb_pdf/webdb_vol102.pdf R = 4 P = -1324 User password = extract for accessibility: allowed extract for any purpose: allowed print low resolution: allowed print high resolution: allowed modify document assembly: not allowed modify forms: not allowed modify annotations: not allowed modify other: not allowed modify anything: not allowed stream encryption method: AESv2 string encryption method: AESv2 file encryption method: AESv2
制限はかなり緩いけど、暗号化されているのは事実。発行者情報を改ざんされたくないってことか。 何はともあれ、PyPDF2ではアウトライン情報を読み取れなかった。参照先のページ情報のない目次エントリがあるとエラーになってしまう。
あとはqpdfのサンプルソースを改造するという手もあるけど最近C++のコードは書いていないのでちょっと無理。
準備
暗号化の解除
このPDF、普通に暗号化を解除できる。解除できるならする必要があったのか疑問だけど、まあいいでしょう。
qpdf
はHomebrewならコマンド一発でインストールできる。
$ brew install qpdf
PDFファイルの暗号化は以下のように実行すると解除できる。
$ qpdf --decrypt 暗号化を外したいファイル名 出力ファイル名
一括処理スクリプト。zipを展開したディレクトリ*1に移動して、適当なファイル名で下記のコードを保存してシェルスクリプトとして実行して一括変換。
#! /bin/bash mkdir -p decrypt/webdb_pdf for fname in `ls webdb_sp_001-102/webdb_pdf/*.pdf`; do #echo ${fname} decrypt/webdb_pdf/$(basename $fname) qpdf --decrypt ${fname} decrypt/webdb_pdf/$(basename $fname) done
ものすごく、やっつけ仕事……。
ページ番号の取り出し
pdftk のセットアップ
Homebrew ではインストール出来ないのでTex Wikiを参考にして入手、インストールする。
確かソースからビルドした気がする。 下記のリンク先の、一番上の回答に記載のリンク先からダウンロード。
macos - PDFtk Server on OS X 10.11 - Stack Overflow
参考情報:pdftk on OSX10.11(El capitan) - Qiita
$ pdftk --version pdftk 2.02 a Handy Tool for Manipulating PDF Documents Copyright (c) 2003-13 Steward and Lee, LLC - Please Visit: www.pdftk.com This is free software; see the source code for copying conditions. There is NO warranty, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
使用方法
暗号化されたPDFの場合は復号用のパスワードが必要なはずですが、qpdf
で復号化済みなので試していない。
$ pdftk 対象のPDFファイル名 dump_data_utf8 output 出力ファイル名
具体例としては以下のようにして実行。
$ pdftk decrypt/webdb_pdf/webdb_vol102.pdf dump_data_utf8 output test_vol102.txt
出力ファイルに下記のようにメタデータが諸々出力される。各ファイルについてBookmarkTitle
とBookmarkPageNumber
さえあればOK。
InfoBegin InfoKey: ModDate InfoValue: D:20180220144306+09'00' InfoBegin InfoKey: CreationDate InfoValue: D:20171213141952+09'00' InfoBegin InfoKey: Author InfoValue: (株)技術評論社 InfoBegin InfoKey: Title InfoValue: WEB+DB PRESS Vol.102 InfoBegin InfoKey: Creator InfoValue: Acrobat 11.0.22 InfoBegin InfoKey: Producer InfoValue: Acrobat Distiller 11.0 (Macintosh); modified using iText 2.1.7 by 1T3XT PdfID0: 3c25c6900f3f6c5a51982f03fa38b8f8 PdfID1: ffb6b04b6d4411a1543b5883ef727550 NumberOfPages: 179 BookmarkBegin BookmarkTitle: 表紙 BookmarkLevel: 1 BookmarkPageNumber: 1 BookmarkBegin BookmarkTitle: 目次 BookmarkLevel: 1 BookmarkPageNumber: 3 (以下省略)
連載記事名とページ番号の取り出し
pdftkの出力をパースして、タブ区切りのテキストで出力する。
ファイル名決め打ちなのでそこをいじって一括処理用にする予定。
#! /usr/local/bin/python3 # coding: utf-8 import re filename = 'data/output_log/dump_webdb_vol102.txt' fp = open(filename, 'r') pdf_filename = re.search('dump_(.+).txt', filename)[1] + '.pdf' bk_list = [] entry = { 'title': "", 'page_no': None} last_page = 0 for line in fp: if line.startswith("NumberOfPages"): last_page = int(line.rstrip("\n").split(': ')[1]) if line.startswith("PageMediaBegin"): bk_list.append(entry) break if not line.startswith("Bookmark") : continue else: if line.startswith("BookmarkTitle"): entry['title'] = line.rstrip("\n").split(': ')[1] if line.startswith("BookmarkPageNumber") : entry['page_no'] = int(line.rstrip("\n").split(': ')[1]) if line.startswith("BookmarkBegin") : if len(entry['title']) > 0: bk_list.append(entry) entry = { 'title': "", 'page_no': None} if line.startswith("PageMediaBegin"): bk_list.append(entry) break fp.close() filtered_list = [ e for e in bk_list if e['page_no'] > 0 ] sorted_list = filtered_list sorted_list.sort(key=lambda x: x['page_no']) for i, e in enumerate(sorted_list): end_page = last_page try: # print(i) n = sorted_list[i+1] next_article_page = n['page_no'] end_page = next_article_page - 1 except: pass print(pdf_filename, e['title'], e['page_no'],end_page, sep="\t")
元のデータでは目次が階層構造になっており、その情報を使えばどれが特集と連載記事をカテゴリ分けすることはできる。ページの並びと一部の記事の目次データ上の並びが違う関係で面倒なので考慮していない。
もっとスマートに書けるはずだけど、面倒だから頃の辺で。
出力例
こんな感じ。他のファイルも同じ要領でTSVで吐き出して、grepとpdftkなりqpdfで切り出して、あとは目次情報を追加すれば完成。手作業でもいいかも。
WEB+DB PRESSのPDFを分割するためのページ番号対応表(vol. 102)
まとめ
手作業でやるほうがラクな気がしてきた。それとこれ一個でなんでもできるよって感じのPythonのPDF用ライブラリがない......。
それはさておき、この雑誌、Pythonの連載はないのか。それにRails 5の特集も組まれてないし。
以外なことにLaravelも特集のテーマになってない。不思議。
Railsのイメージが強かったんだけど、最近はテストとかモバイルアプリ(特にiOS)に注力しているのか。
- 作者: Software Design編集部
- 出版社/メーカー: 技術評論社
- 発売日: 2018/05/26
- メディア: 単行本
- この商品を含むブログを見る
退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング
- 作者: Al Sweigart,相川愛三
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/06/03
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (5件) を見る
- 作者: 小林徳滋
- 出版社/メーカー: アンテナハウスCAS電子出版
- 発売日: 2017/03/07
- メディア: オンデマンド (ペーパーバック)
- この商品を含むブログを見る
*1:またはDVDからPDFをごっそりコピーした先。パスは適宜書き換え