« ^ »

Python製PDF解析ライブラリslateをPython3対応する

所要時間: 約 5分

slateはPython3では使えない

slateはPython2系の頃によく使われていたPDF解析用のライブラリです。しかしPython2.6系しかサポートしていません https://github.com/timClicks/slate/blob/master/setup.py#L32 。 ソースを見るとslateのPython3対応が少しは入っていますが不十分な修正となっています。[Python3.4+のPullRequest](https://github.com/timClicks/slate/pull/32)も作成されていますが放置されているようです。さらにdistributeを中で使っているためインストールに失敗します。

pip install slate
Python3系ではslateはインストールできない
Collecting slate
  Using cached slate-0.3.zip
Collecting distribute (from slate)
  Using cached distribute-0.7.3.zip
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-57cpbkji/distribute/setuptools/__init__.py", line 2, in <module>
        from setuptools.extension import Extension, Library
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-57cpbkji/distribute/setuptools/extension.py", line 5, in <module>
        from setuptools.dist import _get_unpatched
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-57cpbkji/distribute/setuptools/dist.py", line 7, in <module>
        from setuptools.command.install import install
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-57cpbkji/distribute/setuptools/command/__init__.py", line 8, in <module>
        from setuptools.command import install_scripts
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-57cpbkji/distribute/setuptools/command/install_scripts.py", line 3, in <module>
        from pkg_resources import Distribution, PathMetadata, ensure_directory
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-57cpbkji/distribute/pkg_resources.py", line 1518, in <module>
        register_loader_type(importlib_bootstrap.SourceFileLoader, DefaultProvider)
    AttributeError: module 'importlib._bootstrap' has no attribute 'SourceFileLoader'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-57cpbkji/distribute/
pip install slate --pre
Collecting slate
  Using cached slate-0.3.zip
Collecting distribute (from slate)
  Using cached distribute-0.7.3.zip
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-2x0di9p3/distribute/setuptools/__init__.py", line 2, in <module>
        from setuptools.extension import Extension, Library
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-2x0di9p3/distribute/setuptools/extension.py", line 5, in <module>
        from setuptools.dist import _get_unpatched
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-2x0di9p3/distribute/setuptools/dist.py", line 7, in <module>
        from setuptools.command.install import install
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-2x0di9p3/distribute/setuptools/command/__init__.py", line 8, in <module>
        from setuptools.command import install_scripts
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-2x0di9p3/distribute/setuptools/command/install_scripts.py", line 3, in <module>
        from pkg_resources import Distribution, PathMetadata, ensure_directory
      File "/private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-2x0di9p3/distribute/pkg_resources.py", line 1518, in <module>
        register_loader_type(importlib_bootstrap.SourceFileLoader, DefaultProvider)
    AttributeError: module 'importlib._bootstrap' has no attribute 'SourceFileLoader'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /private/var/folders/hx/xp4thw0x7rj15r_2w57_wvfh0000gn/T/pip-build-2x0di9p3/distribute/

今回はこのslateを復活させPDFを解析します。

まずfork

本家slateは https://github.com/timClicks/slate/ にあります。色々コードを変更しないといけないのでforkしてしまいます。本家のリポジトリの右上の`Fork`ボタンをクリックするとオーガニゼーション選択画面が出ます。自分とアカウントと所属するオーガニゼーション(権限を持っている必要があります)が選択できます。今回は自分のアカウントを選択します。

https://res.cloudinary.com/symdon/image/upload/v1642251539/blog.symdon.info/fork_cdutdb.png

アカウントを選択するとforkが開始されます。

https://res.cloudinary.com/symdon/image/upload/v1642251538/blog.symdon.info/forking_btr26x.png

forkが終了すると新しくリポジトリのページが作成されます。今回はパッケージ名も変えてしまうつもりなので、プロジェクト名も変更します。`Settings` のページを開きます。

https://res.cloudinary.com/symdon/image/upload/v1642251538/blog.symdon.info/settings_imns6q.png

プロジェクト名を変更します。 今回は `slate3k` とします。`3k` というのは3000という意味で、Python3系がリリースされる以前に次期メジャーバージョンをリリースするにはもう1000年かかるというジョークでpy3kなどと呼ばれていました。Python3系がリリースされてからライブラリがPython3対応されるようになりましたが、保守されていないライブラリなどはPyhton3対応されませんでした。それらはforkされて `***3k` などと名付けられることがしばしばあります。ライブラリ名に `3k` と付くものはPython3対応したライブラリで元ネタが他にあることが多いです。

https://res.cloudinary.com/symdon/image/upload/v1642251538/blog.symdon.info/rename-project_yjwgby.png

Python3対応する

インストールする

まずはcloseします。

git clone [email protected]:TakesxiSximada/slate3k.git
clone
Cloning into 'slate3k'...
remote: Counting objects: 195, done.
remote: Total 195 (delta 0), reused 0 (delta 0), pack-reused 195
Receiving objects: 100% (195/195), 66.84 KiB | 0 bytes/s, done.
Resolving deltas: 100% (93/93), done.
Checking connectivity... done.

py3kブランチを作成します。

git checkout -b py3k
py3kとしてブランチ作成
Switched to a new branch 'py3k'

作成したブランチをリモートにpushします。

git push --set-upstream origin py3k
ブランチをリモートにpushしておく

Total 0 (delta 0), reused 0 (delta 0)
To [email protected]:TakesxiSximada/slate3k.git
 * [new branch]      py3k -> py3k
Branch py3k set up to track remote branch py3k from origin.

https://github.com/timClicks/slate/pull/32 のパッチは欲しいのでこれを取り込みます。 Pull Requestは`hub am`コマンドを使うことでパッチを当てられます。

hub am -3 https://github.com/timClicks/slate/pull/32
Applying: Fix tests on python 3.5
Applying: add a .gitignore file
Applying: Convert README to reStructuredText so it looks better on github
Applying: Fix unit tests
Applying: Fix demo code
Applying: Fix setup.py so it uses README.rst as readme file
(py3.6.0) $ git push
Counting objects: 24, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (22/22), done.
Writing objects: 100% (24/24), 3.36 KiB | 0 bytes/s, done.
Total 24 (delta 13), reused 0 (delta 0)
remote: Resolving deltas: 100% (13/13), completed with 6 local objects.
To [email protected]:TakesxiSximada/slate3k.git
   3198bfd..c31dbb7  py3k -> py3k

これでPull Requstを取り込めました。slateと名前が衝突するのは面倒なのでslate3kに変更します。まずはソースディレクトリをsrc/slateからsrc/slate3kに変更しました。 その他、修正箇所は以下になります。

git show d5f982e753f6fc22e3c5b08a052d226a53821529
setup.pyに記述されているパッケージ名をslate3kに変更
commit d5f982e753f6fc22e3c5b08a052d226a53821529
Author: TakesxiSximada <[email protected]>
Date:   Mon Jan 23 21:02:06 2017 +0900

    rename packaeg name to slate3k

diff --git a/setup.py b/setup.py
index 2a89881..0ec5617 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ else:
 with open('README.rst') as f:
     long_description = f.read()

-setup(name='slate',
+setup(name='slate3k',
       version='0.5.2',
       description='Extract text from PDF documents easily.',
       author='Tim McNamara',
git show a9e79a2d222c9de705ebc244bec1edf1b28e9ea5...
パッケージ内でslateという名前からimportしている箇所を片っ端から相対importに変更
commit c1259d4cbccf370492f19727d95d27022a8a46b5
Author: TakesxiSximada <[email protected]>
Date:   Mon Jan 23 21:06:37 2017 +0900

    rename slate to slate3k

diff --git a/src/slate3k/classes.py b/src/slate3k/classes.py
index 27be417..af32cd0 100644
--- a/src/slate3k/classes.py
+++ b/src/slate3k/classes.py
@@ -23,7 +23,7 @@ try:
 except ImportError:
     from pdfminer.pdfpage import PDFPage
 if PYTHON_3:
-    import slate.utils as utils
+    import .utils as utils
 else:
     import utils

diff --git a/src/slate3k/test_slate.py b/src/slate3k/test_slate.py
index 8416da2..6622e31 100644
--- a/src/slate3k/test_slate.py
+++ b/src/slate3k/test_slate.py
@@ -12,7 +12,7 @@ PYTHON_3 = sys.version_info[0] == 3
 import os

 if PYTHON_3:
-    from slate.classes import PDF
+    from .classes import PDF
 else:
     from classes import PDF

@@ -44,5 +44,3 @@ def get_pdf_path(pdf_file):
     return os.path.join(
         os.path.dirname(__file__),
         pdf_file)
-
-
diff --git a/src/slate3k/unittests.py b/src/slate3k/unittests.py
index 29c2b6b..78dcd68 100644
--- a/src/slate3k/unittests.py
+++ b/src/slate3k/unittests.py
@@ -1,7 +1,7 @@
 import unittest

 import os
-from slate import PDF
+from . import PDF

 class TestSlate(unittest.TestCase):
     def setUp(self):
git show
distributeはsetuptoolsに取り込まれたので今は使われない
commit 3960b3eb3f5182278995030ba4de545f41f9b4b7
Author: TakesxiSximada <[email protected]>
Date:   Mon Jan 23 21:15:46 2017 +0900

    Can not use distribute

diff --git a/setup.py b/setup.py
index 0ec5617..2baa9ff 100644
--- a/setup.py
+++ b/setup.py
@@ -23,7 +23,7 @@ setup(name='slate3k',
       packages=find_packages('src'),
       package_dir={'': 'src'},
       requires=[pdfminer],
-      install_requires=['distribute', pdfminer],
+      install_requires=[pdfminer],
       classifiers= [
         'Development Status :: 4 - Beta',
         'Intended Audience :: Developers',

ではediable installします。editable installは開発用のインストール機能です。インストールしたライブラリのコードはsite-packagesディレクトリにコピーされるのですが、editable installの場合はコードはコピーされず、site-packages配下にはegg-linkが作成されます。ソースコード自体は元のコードが使われるのでライブラリとして使いながら開発できるようになります。

pip install -e .
Obtaining file:///Users/sximada/ng2/src/github.com/TakesxiSximada/slate3k
Collecting pdfminer3k (from slate3k==0.5.2)
  Using cached pdfminer3k-1.3.1.tar.gz
Requirement already satisfied: pytest>=2.0 in /Users/sximada/ng2/var/lib/miniconda3/envs/py3.6.0/lib/python3.6/site-packages (from pdfminer3k->slate3k==0.5.2)
Requirement already satisfied: ply>=3.4 in /Users/sximada/ng2/var/lib/miniconda3/envs/py3.6.0/lib/python3.6/site-packages (from pdfminer3k->slate3k==0.5.2)
Requirement already satisfied: py>=1.4.29 in /Users/sximada/ng2/var/lib/miniconda3/envs/py3.6.0/lib/python3.6/site-packages (from pytest>=2.0->pdfminer3k->slate3k==0.5.2)
Building wheels for collected packages: pdfminer3k
  Running setup.py bdist_wheel for pdfminer3k ... done
  Stored in directory: /Users/sximada/Library/Caches/pip/wheels/cd/84/67/3eb20c984d51d38db1ca65ecba0d866407f46d8b3a9e72f7b2
Successfully built pdfminer3k
Installing collected packages: pdfminer3k, slate3k
  Running setup.py develop for slate3k
Successfully installed pdfminer3k-1.3.1 slate3k

インストールできました。

使ってみる

Pythonインタラクティブシェルを起動してslate3kをインポートします。

>>> import slate3k
>>>

良さそうです。

PDFをパース

from unittest import TestCase


class PDFPageParseTest(TestCase):
    def test_parse_page(self):
        # <PARSE_TEST>
        import slate3k as slate

        pdf = 'test.pdf'

        with open(pdf, 'rb') as f:
            doc = slate.PDF(f)

        for page in doc[:2]:
            print(page)
        # </PARSE_TEST>