75 Commits

Author SHA1 Message Date
Vinayak Mehta 36006cadc5 Bump version and update HISTORY.md 2018-11-02 23:25:07 +05:30
Vinayak Mehta db3f8c6897 [MRG] Make matplotlib optional (#190)
* Rename png files

* Convert plot to PlotMethods class and update docs

* Update test

* Update setup.py and docs

* Refactor PlotMethods

* Make matplotlib optional

* Raise ImportError in cli
2018-11-02 23:16:03 +05:30
Suyash Behera c0e9235164 [MRG + 1] Create a new figure and test each plot type #127 (#179)
* [MRG] Create a new figure and test each plot type #127

 - move `plot()` to `plotting.py` as `plot_pdf()`
 - modify plotting functions to return matplotlib figures
 - add `test_plotting.py` and baseline images
 - import `plot_pdf()` in `__init__`
 - update `cli.py` to use `plot_pdf()`
 - update advanced usage docs to reflect changes

* Change matplotlib backend for image comparison tests

* Update plotting and tests
 - use matplotlib rectangle instead of `cv2.rectangle` in
`plot_contour()`
 - set matplotlib backend in `tests/__init__`
 - update contour plot baseline image
 - update `test_plotting` with more checks

* Update plot tests and config
 - remove unnecessary asserts
 - update setup.cfg and makefile with `--mpl`

* Add  to

* Add tolerance

* remove text from baseline plots
update plot tests with `remove_text`

* Change method name, update docs and add pep8

* Update docs
2018-11-02 20:57:02 +05:30
Vinayak Mehta 79db6e3d1b Add gitter badge 2018-10-31 17:33:40 +05:30
Vinayak Mehta 29f22ad1a6 Update conda definition 2018-10-30 23:48:56 +05:30
Vinayak Mehta e8af4c2c1c Update conda install instructions 2018-10-30 23:36:31 +05:30
Vinayak Mehta 220d6ad29c Fix cli doc 2018-10-29 01:03:36 +05:30
Vinayak Mehta f73062c1c4 Bump version
Update HISTORY.md
2018-10-28 22:37:33 +05:30
Vinayak Mehta 9cffe0adbe Update HISTORY.md
Update HISTORY.md

Update HISTORY.md again
2018-10-28 22:12:47 +05:30
rbares 429640feea [MRG + 1] Add basic support for encrypted PDF files (#180)
* [MRG] Add basic support for encrypted PDF files

Update API and CLI to accept ASCII passwords to decrypt PDFs
encrypted by algorithm code 1 or 2 (limited by support from PyPDF2).
Update documentation and unit tests accordingly.

Example document health_protected.pdf generated as follows:
qpdf --encrypt userpass ownerpass 128 -- health.pdf health_protected.pdf

Issue #162

* Support encrypted PDF files in python3

Issue #162

* Address review comments

Explicitly check passwords for None rather than falsey.
Correct read_pdf documentation for Owner/User password.

Issue #162

* Correct API documentation changes for consistency

Issue #162

* Move error tests from test_common to test_errors

Issue #162

* Add qpdf example

* Remove password is not None check

* Fix merge conflict

* Fix pages example
2018-10-28 22:01:10 +05:30
gison93 4366313484 Clarify example for argument pages in read_pdf (#177) 2018-10-28 14:41:04 +05:30
Vinayak Mehta 2830ed9418 Update HISTORY.md 2018-10-25 00:07:16 +05:30
Vinicius Mesel 39cf65ffef [MRG + 1] Convert filename to lowercase to check for extension (#169)
* Creates a new variable that stores a lowercase version of the filename

* Remove variable
2018-10-24 23:53:54 +05:30
Parth P Panchal 32df09ad1c Renames the keyword table_area to table_areas (#171)
`table_areas` sounds more apt since it is a list and there can be
multiple table areas on a page.

Closes #165
2018-10-24 23:06:53 +05:30
Vinayak Mehta 8205e0e9ab Update HISTORY.md 2018-10-23 21:16:18 +05:30
Vinayak Mehta a78ef7f841 [MRG] Use find_executable for gs and raise error if not found (#166)
* Use find_executable for gs and raise error if not found

* Remove unused variable

* Add test

* Use pytest monkeypatch
2018-10-23 21:12:43 +05:30
Vinayak Mehta f734af3a0b Update HISTORY.md 2018-10-23 15:04:54 +05:30
Parth P Panchal 61963aabb6 [MRG + 1] Add __main__ (#159)
* Renames camelot.cli to camelot.__main__

Closes #154

* Keep __main__ and cli separate

* Monkey patch click HelpFormatter
2018-10-23 15:01:20 +05:30
Jonathan Lloyd 60c1270745 Fix typo in test name (#160)
test_no_tables_found_warnings_supressed -> test_no_tables_found_warnings_suppressed
2018-10-23 04:54:57 +05:30
Vinayak Mehta 72481bc1b5 Replace table_areas with table_area 2018-10-23 04:00:17 +05:30
Vinayak Mehta c5c85a2dc8 Fix index.rst 2018-10-22 21:58:51 +05:30
Vinayak Mehta 9c6ec49652 Update index.rst 2018-10-22 21:53:38 +05:30
Vinayak Mehta 2a60d1fd54 Update README 2018-10-22 21:52:49 +05:30
Vinayak Mehta 2022a8abc9 Update HISTORY.md 2018-10-19 17:00:20 +05:30
Jonathan Lloyd 3def4a5aea [MRG + 1] Add suppress_warnings flag (#155)
* Add suppress_warnings flag

* Add --quiet flag to cli (to suppress warnings)

* Remove TODO and update comment
2018-10-19 16:55:00 +05:30
Vinayak Mehta 1d064adc3e Update .editorconfig and HISTORY.md 2018-10-19 16:23:15 +05:30
KOLANICH 7baea06bca Add .editorconfig (#151) 2018-10-19 16:19:06 +05:30
Vinayak Mehta 5645ef5b62 Update setup.py 2018-10-15 04:31:54 +05:30
Krishna Sumanth 7a3b76cb76 Update conf.py 2018-10-12 21:39:38 +05:30
Krishna Sumanth 970f906435 Update conf.py 2018-10-12 21:37:53 +05:30
Krishna Sumanth 297888b18c Update conf.py 2018-10-12 20:22:02 +05:30
Vinayak Mehta 9362175a82 Update advanced.rst 2018-10-12 16:46:09 +05:30
Vinayak Mehta 9e6474e5a6 Update HISTORY.md 2018-10-11 23:51:05 +05:30
Vaibhav Mule 1ba0cfc7bc [MRG + 1] Run codecov only once (#132)
* Run codecov only once

* Update .travis.yml

* Update .travis.yml

* Add os based install to Makefile

* Add requests like .travis.yml and Makefile

* Add 'sudo: required' to .travis.yml

* Add before_install

* Make separate command
2018-10-11 23:36:36 +05:30
Vinayak Mehta 8d38907832 Update conda installation instructions
Update conda installation instructions again
2018-10-11 13:00:41 +05:30
Vinayak Mehta ac2d40aa44 Update HISTORY.md
Update HISTORY.md again
2018-10-10 00:37:25 +05:30
Vinayak Mehta c33bf9c168 Add docs badge 2018-10-09 21:33:51 +05:30
Vinayak Mehta d628e9b5df Update requirements.txt 2018-10-09 21:23:10 +05:30
Vinayak Mehta 750f955f9c Add requirements.txt for rtd 2018-10-09 21:21:50 +05:30
Vinayak Mehta 898646b73b Add conda installation instructions 2018-10-09 20:22:07 +05:30
Vinayak Mehta 45e7f7570e Bump version 2018-10-08 03:54:21 +05:30
Vinayak Mehta 296be21d9d Update requirement versions 2018-10-08 01:44:20 +05:30
Vinayak Mehta 1a358f603a Update MANIFEST.in
Update HISTORY.md
2018-10-08 01:18:19 +05:30
Vinayak Mehta fe68328ef2 Move opencv-python to extra_requires (#134) 2018-10-08 01:10:48 +05:30
Vinayak Mehta 9b2fc53e58 Bump version 2018-10-05 20:22:46 +05:30
Vinayak Mehta 80f6870117 Update HISTORY.md 2018-10-05 20:21:02 +05:30
Vaibhav Mule c53ea795fd [MRG + 1] Add tests for repr (#128)
* add tests for repr

* remove repr for Cell

* add round for repr of Cell

* change decimal places to 2

* change tests for 2 decimal places
2018-10-05 20:19:24 +05:30
Vinayak Mehta f13337d50a Update README 2018-10-05 19:46:38 +05:30
Vinayak Mehta 192f12a710 Add HISTORY.md 2018-10-05 19:42:19 +05:30
Oshawk 90aaba6eec [MRG + 1] Make pep8 (#125)
* Make setup.py pep8

Add new line at end of file, fix bare except, remove unused import.

* Make tests/*.py pep8

Add some newlines at and of files and a visual indent.

* Make docs/*.py pep8

Fix block comments and add new lines at end of files.

* Make camelot/*.py pep8

Fixed unused import, a few weirdly ordered imports, a docstring typo and  many new lines at the end of lines.

* Fix imports

Fix import order and remove a couple more unused imports.

* Fix indents

Fix indentation (no opening delimiter alignment).

* Add newlines
2018-10-05 16:55:43 +05:30
Vinayak Mehta 6e8079df84 [MRG] Add tests for output formats and parser kwargs (#126)
* Remove unused image processing code

* Add opencv back-compat comment

* Add tests for parser special cases

* Fix lattice table area test

* Add tests for output format

* Add openpyxl dep
2018-10-05 16:15:30 +05:30
Vinayak Mehta cf7823f33c [MRG] Add ghostscript fix for windows (#124)
* Add ghostscript fix for windows

* Add python2 fix

* Update install.rst
2018-10-05 02:06:37 +05:30
Vinayak Mehta f7e69bbbfe [MRG] Add python versions (#119)
* Add python versions

* Add MANIFEST.in

* Bump numpy version
2018-10-04 23:43:52 +05:30
Vaibhav Mule 58eddd0804 [MRG + 1] Test UsageError for CLI (#122)
* add .vscode

* add tests for UsageError

* fix pep8
2018-10-04 22:01:20 +05:30
Vinayak Mehta 9d00937ec7 Fix GH issues link 2018-10-03 19:36:29 +05:30
Vinayak Mehta 9ff61c70d3 Update README
Update docs
2018-10-03 13:12:32 +05:30
Vinayak Mehta 2a1d21af32 Add CLI tests (#117) 2018-10-03 01:09:08 +05:30
Johnny Metz 2b8ae5fa4d [MRG + 1] Update contributor's guide with new labels (#116)
* Update contributor's guide with new labels

* Update labels in docs
2018-10-03 00:48:13 +05:30
christinegarcia ea79cf2fb7 [MRG + 1] Copyedit all documentation for Camelot (#112)
* Copyedit index.rst

* Copyedit intro.rst

* Copyedit install.rst

* Copyedit how-it-works.rst

* Fix subheading capitalization on install.rst

* Copyedit quickstart.rst

* Copyedit advanced.rst

* Copyedit cli.rst

* Copyedit contributing.rst

* Make more heading & sub-heading capitalization fixes (for consistency)

* Fix README and docs
2018-10-02 23:37:49 +05:30
Vinayak Mehta 9537143fe0 Add pytest-cov
Add fix for coverage

Add source and omit to coveragerc

Update coveragerc

Update coveragerc

Add source to coveragerc

Update coveragerc source

Add init to tests

Fix ImportError

Fix ImportError again
2018-10-02 22:37:38 +05:30
Vinayak Mehta c5bde5e2ad [MRG] Add error/warning tests (#113)
* Add unknown flavor test

* Add input kwargs test

* Remove unused utils

* Add unsupported format test

* Add stream unequal tables-columns length test

* Add python3 compat

* Add no tables found test

* Convert util info log to warning
2018-10-02 19:28:42 +05:30
Vinayak Mehta f1bf4309ec Update .coveragerc
Update requirements.txt

Revert .coveragerc

Update pytest command

Revert coverage
2018-10-02 17:58:50 +05:30
Vinayak Mehta 09ed772b6a Fix doc typo 2018-09-30 14:09:22 +05:30
Vinayak Mehta fc0542bd3c Add Python 3 compatibility (#109)
* Add python3 compat

* Update .gitignore

* Update .gitignore again

* Remove debugging return

* Add unicode_literals import

* Bump version

* Add python3-tk note
2018-09-28 21:58:29 +05:30
Vinayak Mehta 82463e10b4 Fix link 2018-09-28 14:58:11 +05:30
Vinayak Mehta b9648f3b39 Update intro doc 2018-09-28 14:49:07 +05:30
Vinayak Mehta dfb0d4fb4c Fix TableList repr 2018-09-27 04:42:23 +05:30
Vinayak Mehta 00b7b2aeba Add note 2018-09-27 00:31:51 +05:30
Vinayak Mehta 759e635a3c Bump version 2018-09-25 12:32:01 +05:30
Vinayak Mehta 6298f3a0f6 Add codecov to Travis 2018-09-25 12:03:55 +05:30
Vinayak Mehta 08f638243e Fix codecov link 2018-09-25 11:45:57 +05:30
Vinayak Mehta 6703f2f254 Add codecov 2018-09-25 11:42:45 +05:30
Vinayak Mehta 13f2b199c9 Add Travis 2018-09-25 11:01:42 +05:30
Vinayak Mehta 7731497a5b Fix relative links
Fix broken links
2018-09-24 22:15:43 +05:30
Vinayak Mehta 9c4e7bf3f6 Add long description content type 2018-09-24 21:08:10 +05:30
61 changed files with 1697 additions and 906 deletions
+1 -5
View File
@@ -1,6 +1,2 @@
[run]
branch = True
source = camelot
include = */camelot/*
omit =
*/setup.py
branch = True
+10
View File
@@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.py]
charset = utf-8
indent_style = space
indent_size = 4
+5
View File
@@ -5,7 +5,12 @@ __pycache__/
build/
dist/
*.egg-info/
.eggs/
.coverage
coverage.xml
.pytest_cache/
_build/
# vscode
.vscode
Executable
+32
View File
@@ -0,0 +1,32 @@
sudo: true
language: python
cache: pip
addons:
apt:
update: true
install:
- make install
jobs:
include:
- stage: test
script:
- make test
python: '2.7'
- stage: test
script:
- make test
python: '3.5'
- stage: test
script:
- make test
python: '3.6'
- stage: test
script:
- make test
python: '3.7'
dist: xenial
- stage: coverage
python: '3.6'
script:
- make test
- codecov --verbose
+12 -14
View File
@@ -2,7 +2,7 @@
If you're reading this, you're probably looking to contributing to Camelot. *Time is the only real currency*, and the fact that you're considering spending some here is *very* generous of you. Thank you very much!
This document will help you get started with contributing documentation, code, testing and filing issues. If you have any questions, feel free to reach out to [Vinayak Mehta](http://vinayak-mehta.github.io), the author and maintainer.
This document will help you get started with contributing documentation, code, testing and filing issues. If you have any questions, feel free to reach out to [Vinayak Mehta](https://vinayak-mehta.github.io), the author and maintainer.
## Code Of Conduct
@@ -14,9 +14,9 @@ Kenneth Reitz has also written an [essay](https://www.kennethreitz.org/essays/be
As the [Requests Code Of Conduct](http://docs.python-requests.org/en/master/dev/contributing/#be-cordial) states, **all contributions are welcome**, as long as everyone involved is treated with respect.
## Your First Contribution
## Your first contribution
A great way to start contributing to Camelot is to pick an issue tagged with the [Contributor Friendly](https://github.com/socialcopsdev/camelot/labels/Contributor%20Friendly) tag or the [Level: Easy](https://github.com/socialcopsdev/camelot/labels/Level%3A%20Easy) tag. If you're unable to find a good first issue, feel free to contact the maintainer.
A great way to start contributing to Camelot is to pick an issue tagged with the [help wanted](https://github.com/socialcopsdev/camelot/labels/help%20wanted) tag or the [good first issue](https://github.com/socialcopsdev/camelot/labels/good%20first%20issue) tag. If you're unable to find a good first issue, feel free to contact the maintainer.
## Setting up a development environment
@@ -26,19 +26,17 @@ To install the dependencies needed for development, you can use pip:
$ pip install camelot-py[dev]
</pre>
### Alternatively
You can clone the project repository, and install using pip:
Alternatively, you can clone the project repository, and install using pip:
<pre>
$ pip install .[dev]
$ pip install ".[dev]"
</pre>
## Pull Requests
### Submit a Pull Request
### Submit a pull request
The preferred workflow for contributing to Camelot is to fork the [project repository](https://github.com/socialcopsdev/camelot) on GitHub, clone, develop on a branch and then finally submit a pull request. Steps:
The preferred workflow for contributing to Camelot is to fork the [project repository](https://github.com/socialcopsdev/camelot) on GitHub, clone, develop on a branch and then finally submit a pull request. Here are the steps:
1. Fork the project repository. Click on the Fork button near the top of the page. This creates a copy of the code under your account on the GitHub.
@@ -73,7 +71,7 @@ $ git push -u origin my-feature
Now it's time to go to the your fork of Camelot and create a pull request! You can [follow these instructions](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) to do this.
### Work on your Pull Request
### Work on your pull request
We recommend that your pull request complies with the following rules:
@@ -81,7 +79,7 @@ We recommend that your pull request complies with the following rules:
- In case your pull request contains function docstrings, make sure you follow the [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) format. All function docstrings in Camelot follow this format. Moreover, following the format will make sure that the API documentation is generated flawlessly.
- Make sure your commit messages follow [the seven rules of a great git commit message](https://chris.beams.io/posts/git-commit/).
- Make sure your commit messages follow [the seven rules of a great git commit message](https://chris.beams.io/posts/git-commit/):
- Separate subject from body with a blank line
- Limit the subject line to 50 characters
- Capitalize the subject line
@@ -104,15 +102,15 @@ Writing documentation, function docstrings, examples and tutorials is a great wa
It is written in [reStructuredText](https://en.wikipedia.org/wiki/ReStructuredText), with [Sphinx](http://www.sphinx-doc.org/en/master/) used to generate these lovely HTML files that you're currently reading (unless you're reading this on GitHub). You can edit the documentation using any text editor and then generate the HTML output by running `make html` in the `docs/` directory.
The function docstrings are written using the [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) extension for Sphinx. Make sure you check out its format guidelines, before you start writing one.
The function docstrings are written using the [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) extension for Sphinx. Make sure you check out its format guidelines before you start writing one.
## Filing Issues
We use [GitHub issues](https://docs.pytest.org/en/latest/) to keep track of all issues and pull requests. Before opening an issue (which asks a question or reports a bug), it is advisable to use GitHub search to look for existing issues (both open and closed) that may be similar.
We use [GitHub issues](https://github.com/socialcopsdev/camelot/issues) to keep track of all issues and pull requests. Before opening an issue (which asks a question or reports a bug), please use GitHub search to look for existing issues (both open and closed) that may be similar.
### Questions
Please don't use GitHub issues for support questions, a better place for them would be [Stack Overflow](http://stackoverflow.com). Make sure you tag them using the `python-camelot` tag.
Please don't use GitHub issues for support questions. A better place for them would be [Stack Overflow](http://stackoverflow.com). Make sure you tag them using the `python-camelot` tag.
### Bug Reports
Executable
+107
View File
@@ -0,0 +1,107 @@
Release History
===============
master
------
0.3.1 (2018-11-02)
------------------
**Improvements**
* Matplotlib is now an optional requirement. [#190](https://github.com/socialcopsdev/camelot/pull/190) by Vinayak Mehta.
* You can install it using `$ pip install camelot-py[plot]`.
* [#127](https://github.com/socialcopsdev/camelot/issues/127) Add tests for plotting. Coverage is now at 87%! [#179](https://github.com/socialcopsdev/camelot/pull/179) by [Suyash Behera](https://github.com/Suyash458).
0.3.0 (2018-10-28)
------------------
**Improvements**
* [#162](https://github.com/socialcopsdev/camelot/issues/162) Add password keyword argument. [#180](https://github.com/socialcopsdev/camelot/pull/180) by [rbares](https://github.com/rbares).
* An encrypted PDF can now be decrypted by passing `password='<PASSWORD>'` to `read_pdf` or `--password <PASSWORD>` to the command-line interface. (Limited encryption algorithm support from PyPDF2.)
* [#139](https://github.com/socialcopsdev/camelot/issues/139) Add suppress_warnings keyword argument. [#155](https://github.com/socialcopsdev/camelot/pull/155) by [Jonathan Lloyd](https://github.com/jonathanlloyd).
* Warnings raised by Camelot can now be suppressed by passing `suppress_warnings=True` to `read_pdf` or `--quiet` to the command-line interface.
* [#154](https://github.com/socialcopsdev/camelot/issues/154) The CLI can now be run using `python -m`. Try `python -m camelot --help`. [#159](https://github.com/socialcopsdev/camelot/pull/159) by [Parth P Panchal](https://github.com/pqrth).
* [#165](https://github.com/socialcopsdev/camelot/issues/165) Rename `table_area` to `table_areas`. [#171](https://github.com/socialcopsdev/camelot/pull/171) by [Parth P Panchal](https://github.com/pqrth).
**Bugfixes**
* Raise error if the ghostscript executable is not on the PATH variable. [#166](https://github.com/socialcopsdev/camelot/pull/166) by Vinayak Mehta.
* Convert filename to lowercase to check for PDF extension. [#169](https://github.com/socialcopsdev/camelot/pull/169) by [Vinicius Mesel](https://github.com/vmesel).
**Files**
* [#114](https://github.com/socialcopsdev/camelot/issues/114) Add Makefile and make codecov run only once. [#132](https://github.com/socialcopsdev/camelot/pull/132) by [Vaibhav Mule](https://github.com/vaibhavmule).
* Add .editorconfig. [#151](https://github.com/socialcopsdev/camelot/pull/151) by [KOLANICH](https://github.com/KOLANICH).
* Downgrade numpy version from 1.15.2 to 1.13.3.
* Add requirements.txt for readthedocs.
**Documentation**
* Add "Using conda" section to installation instructions.
* Add readthedocs badge.
0.2.3 (2018-10-08)
------------------
* Remove hard dependencies on requirements versions.
0.2.2 (2018-10-08)
------------------
**Bugfixes**
* Move opencv-python to extra\_requires. [#134](https://github.com/socialcopsdev/camelot/pull/134) by Vinayak Mehta.
0.2.1 (2018-10-05)
------------------
**Bugfixes**
* [#121](https://github.com/socialcopsdev/camelot/issues/121) Fix ghostscript subprocess call for Windows. [#124](https://github.com/socialcopsdev/camelot/pull/124) by Vinayak Mehta.
**Improvements**
* [#123](https://github.com/socialcopsdev/camelot/issues/123) Make PEP8 compatible. [#125](https://github.com/socialcopsdev/camelot/pull/125) by [Oshawk](https://github.com/Oshawk).
* [#110](https://github.com/socialcopsdev/camelot/issues/110) Add more tests. Coverage is now at 84%!
* Add tests for `__repr__`. [#128](https://github.com/socialcopsdev/camelot/pull/128) by [Vaibhav Mule](https://github.com/vaibhavmule).
* Add tests for CLI. [#122](https://github.com/socialcopsdev/camelot/pull/122) by [Vaibhav Mule](https://github.com/vaibhavmule) and [#117](https://github.com/socialcopsdev/camelot/pull/117) by Vinayak Mehta.
* Add tests for errors/warnings. [#113](https://github.com/socialcopsdev/camelot/pull/113) by Vinayak Mehta.
* Add tests for output formats and parser kwargs. [#126](https://github.com/socialcopsdev/camelot/pull/126) by Vinayak Mehta.
* Add Python 3.5 and 3.7 support. [#119](https://github.com/socialcopsdev/camelot/pull/119) by Vinayak Mehta.
* Add logging and warnings.
**Documentation**
* Copyedit all documentation. [#112](https://github.com/socialcopsdev/camelot/pull/112) by [Christine Garcia](https://github.com/christinegarcia).
* [#115](https://github.com/socialcopsdev/camelot/issues/115) Update issue labels in contributor's guide. [#116](https://github.com/socialcopsdev/camelot/pull/116) by [Johnny Metz](https://github.com/johnnymetz).
* Update installation instructions for Windows. [#124](https://github.com/socialcopsdev/camelot/pull/124) by Vinayak Mehta.
**Note**: This release also bumps the version for numpy from 1.13.3 to 1.15.2 and adds a MANIFEST.in. Also, openpyxl==2.5.8 is a new requirement and pytest-cov==2.6.0 is a new dev requirement.
0.2.0 (2018-09-28)
------------------
**Improvements**
* [#81](https://github.com/socialcopsdev/camelot/issues/81) Add Python 3.6 support. [#109](https://github.com/socialcopsdev/camelot/pull/109) by Vinayak Mehta.
0.1.2 (2018-09-25)
------------------
**Improvements**
* [#85](https://github.com/socialcopsdev/camelot/issues/85) Add Travis and Codecov.
0.1.1 (2018-09-24)
------------------
**Documentation**
* Add documentation fixes.
0.1.0 (2018-09-24)
------------------
* Rebirth!
+1
View File
@@ -0,0 +1 @@
include MANIFEST.in README.md HISTORY.md LICENSE setup.py setup.cfg
+28
View File
@@ -0,0 +1,28 @@
.PHONY: docs
INSTALL :=
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
INSTALL := @sudo apt install python-tk python3-tk ghostscript
else ifeq ($(UNAME_S),Darwin)
INSTALL := @brew install tcl-tk ghostscript
else
INSTALL := @echo "Please install tk and ghostscript"
endif
install:
$(INSTALL)
pip install --upgrade pip
pip install ".[dev]"
test:
pytest --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl tests
docs:
cd docs && make html
@echo "\033[95m\n\nBuild successful! View the docs homepage at docs/_build/html/index.html.\n\033[0m"
publish:
pip install twine
python setup.py sdist
twine upload dist/*
rm -fr build dist .egg camelot_py.egg-info
+36 -20
View File
@@ -1,20 +1,26 @@
<p align="center">
<img src="https://raw.githubusercontent.com/socialcopsdev/camelot/master/docs/_static/camelot.png" width="200">
</p>
# Camelot: PDF Table Extraction for Humans
![license](https://img.shields.io/badge/license-MIT-lightgrey.svg) ![python-version](https://img.shields.io/badge/python-2.7-blue.svg)
[![Build Status](https://travis-ci.org/socialcopsdev/camelot.svg?branch=master)](https://travis-ci.org/socialcopsdev/camelot) [![Documentation Status](https://readthedocs.org/projects/camelot-py/badge/?version=master)](https://camelot-py.readthedocs.io/en/master/)
[![codecov.io](https://codecov.io/github/socialcopsdev/camelot/badge.svg?branch=master&service=github)](https://codecov.io/github/socialcopsdev/camelot?branch=master)
[![image](https://img.shields.io/pypi/v/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/l/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![image](https://img.shields.io/pypi/pyversions/camelot-py.svg)](https://pypi.org/project/camelot-py/) [![Gitter chat](https://badges.gitter.im/camelot-dev/Lobby.png)](https://gitter.im/camelot-dev/Lobby)
**Camelot** is a Python library which makes it easy for *anyone* to extract tables from PDF files!
**Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files!
![camelot-logo](docs/_static/png/camelot-logo.png)
**Note:** You can also check out [Excalibur](https://github.com/camelot-dev/excalibur), which is a web interface for Camelot!
---
**Here's how you can extract tables from PDF files.** Check out the PDF used in this example, [here](docs/_static/pdf/foo.pdf).
**Here's how you can extract tables from PDF files.** Check out the PDF used in this example [here](https://github.com/socialcopsdev/camelot/blob/master/docs/_static/pdf/foo.pdf).
<pre>
>>> import camelot
>>> tables = camelot.read_pdf('foo.pdf')
>>> tables
&lt;TableList tables=1&gt;
&lt;TableList n=1&gt;
>>> tables.export('foo.csv', f='csv', compress=True) # json, excel, html
>>> tables[0]
&lt;Table shape=(7, 7)&gt;
@@ -38,28 +44,40 @@
| 2032_2 | 0.17 | 57.8 | 21.7% | 0.3% | 2.7% | 1.2% |
| 4171_1 | 0.07 | 173.9 | 58.1% | 1.6% | 2.1% | 0.5% |
There's a [command-line interface](http://camelot-py.readthedocs.io/en/master/user/cli.html) too!
There's a [command-line interface](https://camelot-py.readthedocs.io/en/master/user/cli.html) too!
**Note:** Camelot only works with text-based PDFs and not scanned documents. (As Tabula [explains](https://github.com/tabulapdf/tabula#why-tabula), "If you can click and drag to select text in your table in a PDF viewer, then your PDF is text-based".)
## Why Camelot?
- **You are in control**: Unlike other libraries and tools which either give a nice output or fail miserably (with no in-between), Camelot gives you the power to tweak table extraction. (Since everything in the real world, including PDF table extraction, is fuzzy.)
- **Metrics**: *Bad* tables can be discarded based on metrics like accuracy and whitespace, without ever having to manually look at each table.
- Each table is a **pandas DataFrame**, which enables seamless integration into [ETL and data analysis workflows](https://gist.github.com/vinayak-mehta/e5949f7c2410a0e12f25d3682dc9e873).
- **Export** to multiple formats, including json, excel and html.
- **You are in control.**: Unlike other libraries and tools which either give a nice output or fail miserably (with no in-between), Camelot gives you the power to tweak table extraction. (This is important since everything in the real world, including PDF table extraction, is fuzzy.)
- *Bad* tables can be discarded based on **metrics** like accuracy and whitespace, without ever having to manually look at each table.
- Each table is a **pandas DataFrame**, which seamlessly integrates into [ETL and data analysis workflows](https://gist.github.com/vinayak-mehta/e5949f7c2410a0e12f25d3682dc9e873).
- **Export** to multiple formats, including JSON, Excel and HTML.
See [comparison with other PDF table extraction libraries and tools](https://github.com/socialcopsdev/camelot/wiki/Comparison-with-other-PDF-Table-Extraction-libraries-and-tools).
## Installation
After [installing the dependencies](http://camelot-py.readthedocs.io/en/master/user/install.html), [tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/), you can simply use pip to install Camelot:
### Using conda
The easiest way to install Camelot is to install it with [conda](https://conda.io/docs/), which is a package manager and environment management system for the [Anaconda](http://docs.continuum.io/anaconda/) distribution.
<pre>
$ pip install camelot-py
$ conda install -c conda-forge camelot-py
</pre>
### Alternatively
### Using pip
After [installing the dependencies](http://camelot-py.readthedocs.io/en/master/user/install.html), clone the repo using:
After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip) ([tk](https://packages.ubuntu.com/trusty/python-tk) and [ghostscript](https://www.ghostscript.com/)), you can simply use pip to install Camelot:
<pre>
$ pip install camelot-py[cv]
</pre>
### From the source code
After [installing the dependencies](https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip), clone the repo using:
<pre>
$ git clone https://www.github.com/socialcopsdev/camelot
@@ -69,18 +87,16 @@ and install Camelot using pip:
<pre>
$ cd camelot
$ pip install .
$ pip install ".[cv]"
</pre>
Note: Use a [virtualenv](https://virtualenv.pypa.io/en/stable/) if you don't want to affect your global Python installation.
## Documentation
Great documentation is available at [http://camelot-py.readthedocs.io/](http://camelot-py.readthedocs.io/).
## Development
The [Contributor's Guide](CONTRIBUTING.md) has detailed information about contributing code, documentation, tests and more. We've included some basic information in this README.
The [Contributor's Guide](https://camelot-py.readthedocs.io/en/master/dev/contributing.html) has detailed information about contributing code, documentation, tests and more. We've included some basic information in this README.
### Source code
@@ -108,8 +124,8 @@ $ python setup.py test
## Versioning
Camelot uses [Semantic Versioning](https://semver.org/). For the available versions, see the tags on this repository.
Camelot uses [Semantic Versioning](https://semver.org/). For the available versions, see the tags on this repository. For the changelog, you can check out [HISTORY.md](https://github.com/socialcopsdev/camelot/blob/master/HISTORY.md).
## License
This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
This project is licensed under the MIT License, see the [LICENSE](https://github.com/socialcopsdev/camelot/blob/master/LICENSE) file for details.
+28 -2
View File
@@ -1,5 +1,31 @@
# -*- coding: utf-8 -*-
from .__version__ import __version__
import logging
from .io import read_pdf
from click import HelpFormatter
from .__version__ import __version__
from .io import read_pdf
from .plotting import PlotMethods
def _write_usage(self, prog, args='', prefix='Usage: '):
return self._write_usage('camelot', args, prefix=prefix)
# monkey patch click.HelpFormatter
HelpFormatter._write_usage = HelpFormatter.write_usage
HelpFormatter.write_usage = _write_usage
# set up logging
logger = logging.getLogger('camelot')
format_string = '%(asctime)s - %(levelname)s - %(message)s'
formatter = logging.Formatter(format_string, datefmt='%Y-%m-%dT%H:%M:%S')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
# instantiate plot method
plot = PlotMethods()
+16
View File
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
__all__ = ('main',)
def main():
from camelot.cli import cli
cli()
if __name__ == "__main__":
main()
+2 -2
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
VERSION = (0, 1, 0)
VERSION = (0, 3, 1)
__title__ = 'camelot-py'
__description__ = 'PDF Table Extraction for Humans.'
@@ -8,4 +8,4 @@ __url__ = 'http://camelot-py.readthedocs.io/'
__version__ = '.'.join(map(str, VERSION))
__author__ = 'Vinayak Mehta'
__author_email__ = 'vmehta94@gmail.com'
__license__ = 'MIT License'
__license__ = 'MIT License'
+51 -24
View File
@@ -1,15 +1,24 @@
# -*- coding: utf-8 -*-
from pprint import pprint
import logging
import click
try:
import matplotlib.pyplot as plt
except ImportError:
_HAS_MPL = False
else:
_HAS_MPL = True
from . import __version__
from .io import read_pdf
from . import __version__, read_pdf, plot
logger = logging.getLogger('camelot')
logger.setLevel(logging.INFO)
class Config(object):
def __init__(self):
def __init__(self):
self.config = {}
def set_config(self, key, value):
@@ -23,6 +32,7 @@ pass_config = click.make_pass_decorator(Config)
@click.version_option(version=__version__)
@click.option('-p', '--pages', default='1', help='Comma-separated page numbers.'
' Example: 1,3,4 or 1,4-end.')
@click.option('-pw', '--password', help='Password for decryption.')
@click.option('-o', '--output', help='Output file path.')
@click.option('-f', '--format',
type=click.Choice(['csv', 'json', 'excel', 'html']),
@@ -34,16 +44,17 @@ pass_config = click.make_pass_decorator(Config)
' font size. Useful to detect super/subscripts.')
@click.option('-M', '--margins', nargs=3, default=(1.0, 0.5, 0.1),
help='PDFMiner char_margin, line_margin and word_margin.')
@click.option('-q', '--quiet', is_flag=True, help='Suppress warnings.')
@click.pass_context
def cli(ctx, *args, **kwargs):
"""Camelot: PDF Table Extraction for Humans"""
ctx.obj = Config()
for key, value in kwargs.iteritems():
for key, value in kwargs.items():
ctx.obj.set_config(key, value)
@cli.command('lattice')
@click.option('-T', '--table_area', default=[], multiple=True,
@click.option('-T', '--table_areas', default=[], multiple=True,
help='Table areas to process. Example: x1,y1,x2,y2'
' where x1, y1 -> left-top and x2, y2 -> right-bottom.')
@click.option('-back', '--process_background', is_flag=True,
@@ -74,8 +85,8 @@ def cli(ctx, *args, **kwargs):
@click.option('-I', '--iterations', default=0,
help='Number of times for erosion/dilation will be applied.')
@click.option('-plot', '--plot_type',
type=click.Choice(['text', 'table', 'contour', 'joint', 'line']),
help='Plot geometry found on PDF page, for debugging.')
type=click.Choice(['text', 'grid', 'contour', 'joint', 'line']),
help='Plot elements found on PDF page for visual debugging.')
@click.argument('filepath', type=click.Path(exists=True))
@pass_config
def lattice(c, *args, **kwargs):
@@ -85,31 +96,39 @@ def lattice(c, *args, **kwargs):
output = conf.pop('output')
f = conf.pop('format')
compress = conf.pop('zip')
suppress_warnings = conf.pop('quiet')
plot_type = kwargs.pop('plot_type')
filepath = kwargs.pop('filepath')
kwargs.update(conf)
table_area = list(kwargs['table_area'])
kwargs['table_area'] = None if not table_area else table_area
table_areas = list(kwargs['table_areas'])
kwargs['table_areas'] = None if not table_areas else table_areas
copy_text = list(kwargs['copy_text'])
kwargs['copy_text'] = None if not copy_text else copy_text
kwargs['shift_text'] = list(kwargs['shift_text'])
tables = read_pdf(filepath, pages=pages, flavor='lattice', **kwargs)
click.echo('Found {} tables'.format(tables.n))
if plot_type is not None:
for table in tables:
table.plot(plot_type)
if not _HAS_MPL:
raise ImportError('matplotlib is required for plotting.')
else:
if output is None:
raise click.UsageError('Please specify output file path using --output')
if f is None:
raise click.UsageError('Please specify output file format using --format')
tables = read_pdf(filepath, pages=pages, flavor='lattice',
suppress_warnings=suppress_warnings, **kwargs)
click.echo('Found {} tables'.format(tables.n))
if plot_type is not None:
for table in tables:
plot(table, kind=plot_type)
plt.show()
else:
tables.export(output, f=f, compress=compress)
@cli.command('stream')
@click.option('-T', '--table_area', default=[], multiple=True,
@click.option('-T', '--table_areas', default=[], multiple=True,
help='Table areas to process. Example: x1,y1,x2,y2'
' where x1, y1 -> left-top and x2, y2 -> right-bottom.')
@click.option('-C', '--columns', default=[], multiple=True,
@@ -119,8 +138,8 @@ def lattice(c, *args, **kwargs):
@click.option('-c', '--col_close_tol', default=0, help='Tolerance parameter'
' used to combine text horizontally, to generate columns.')
@click.option('-plot', '--plot_type',
type=click.Choice(['text', 'table']),
help='Plot geometry found on PDF page for debugging.')
type=click.Choice(['text', 'grid']),
help='Plot elements found on PDF page for visual debugging.')
@click.argument('filepath', type=click.Path(exists=True))
@pass_config
def stream(c, *args, **kwargs):
@@ -130,23 +149,31 @@ def stream(c, *args, **kwargs):
output = conf.pop('output')
f = conf.pop('format')
compress = conf.pop('zip')
suppress_warnings = conf.pop('quiet')
plot_type = kwargs.pop('plot_type')
filepath = kwargs.pop('filepath')
kwargs.update(conf)
table_area = list(kwargs['table_area'])
kwargs['table_area'] = None if not table_area else table_area
table_areas = list(kwargs['table_areas'])
kwargs['table_areas'] = None if not table_areas else table_areas
columns = list(kwargs['columns'])
kwargs['columns'] = None if not columns else columns
tables = read_pdf(filepath, pages=pages, flavor='stream', **kwargs)
click.echo('Found {} tables'.format(tables.n))
if plot_type is not None:
for table in tables:
table.plot(plot_type)
if not _HAS_MPL:
raise ImportError('matplotlib is required for plotting.')
else:
if output is None:
raise click.UsageError('Please specify output file path using --output')
if f is None:
raise click.UsageError('Please specify output file format using --format')
tables.export(output, f=f, compress=compress)
tables = read_pdf(filepath, pages=pages, flavor='stream',
suppress_warnings=suppress_warnings, **kwargs)
click.echo('Found {} tables'.format(tables.n))
if plot_type is not None:
for table in tables:
plot(table, kind=plot_type)
plt.show()
else:
tables.export(output, f=f, compress=compress)
+4 -46
View File
@@ -1,15 +1,12 @@
# -*- coding: utf-8 -*-
import os
import json
import zipfile
import tempfile
import numpy as np
import pandas as pd
from .plotting import *
class Cell(object):
"""Defines a cell in a table with coordinates relative to a
@@ -72,7 +69,7 @@ class Cell(object):
def __repr__(self):
return '<Cell x1={} y1={} x2={} y2={}>'.format(
self.x1, self.y1, self.x2, self.y2)
round(self.x1, 2), round(self.y1, 2), round(self.x2, 2), round(self.y2, 2))
@property
def text(self):
@@ -322,33 +319,6 @@ class Table(object):
cell.hspan = True
return self
def plot(self, geometry_type):
"""Plot geometry found on PDF page based on geometry_type
specified, useful for debugging and playing with different
parameters to get the best output.
Parameters
----------
geometry_type : str
The geometry type for which a plot should be generated.
Can be 'text', 'table', 'contour', 'joint', 'line'
"""
if self.flavor == 'stream' and geometry_type in ['contour', 'joint', 'line']:
raise NotImplementedError("{} cannot be plotted with flavor='stream'".format(
geometry_type))
if geometry_type == 'text':
plot_text(self._text)
elif geometry_type == 'table':
plot_table(self)
elif geometry_type == 'contour':
plot_contour(self._image)
elif geometry_type == 'joint':
plot_joint(self._image)
elif geometry_type == 'line':
plot_line(self._segments)
def to_csv(self, path, **kwargs):
"""Writes Table to a comma-separated values (csv) file.
@@ -438,8 +408,8 @@ class TableList(object):
self._tables = tables
def __repr__(self):
return '<{} tables={}>'.format(
self.__class__.__name__, len(self._tables))
return '<{} n={}>'.format(
self.__class__.__name__, self.n)
def __len__(self):
return len(self._tables)
@@ -447,18 +417,6 @@ class TableList(object):
def __getitem__(self, idx):
return self._tables[idx]
def __iter__(self):
self._n = 0
return self
def next(self):
if self._n < len(self):
r = self._tables[self._n]
self._n += 1
return r
else:
raise StopIteration
@staticmethod
def _format_func(table, f):
return getattr(table, 'to_{}'.format(f))
@@ -531,4 +489,4 @@ class TableList(object):
if compress:
zipname = os.path.join(os.path.dirname(path), root) + '.zip'
with zipfile.ZipFile(zipname, 'w', allowZip64=True) as z:
z.write(filepath, os.path.basename(filepath))
z.write(filepath, os.path.basename(filepath))
+18 -10
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
import sys
from PyPDF2 import PdfFileReader, PdfFileWriter
@@ -21,14 +22,22 @@ class PDFHandler(object):
Path to PDF file.
pages : str, optional (default: '1')
Comma-separated page numbers.
Example: 1,3,4 or 1,4-end.
Example: '1,3,4' or '1,4-end'.
password : str, optional (default: None)
Password for decryption.
"""
def __init__(self, filename, pages='1'):
def __init__(self, filename, pages='1', password=None):
self.filename = filename
if not self.filename.endswith('.pdf'):
raise TypeError("File format not supported.")
if not filename.lower().endswith('.pdf'):
raise NotImplementedError("File format not supported")
self.pages = self._get_pages(self.filename, pages)
if password is None:
self.password = ''
else:
self.password = password
if sys.version_info[0] < 3:
self.password = self.password.encode('ascii')
def _get_pages(self, filename, pages):
"""Converts pages string to list of ints.
@@ -52,6 +61,8 @@ class PDFHandler(object):
page_numbers.append({'start': 1, 'end': 1})
else:
infile = PdfFileReader(open(filename, 'rb'), strict=False)
if infile.isEncrypted:
infile.decrypt(self.password)
if pages == 'all':
page_numbers.append({'start': 1, 'end': infile.getNumPages()})
else:
@@ -84,7 +95,7 @@ class PDFHandler(object):
with open(filename, 'rb') as fileobj:
infile = PdfFileReader(fileobj, strict=False)
if infile.isEncrypted:
infile.decrypt('')
infile.decrypt(self.password)
fpath = os.path.join(temp, 'page-{0}.pdf'.format(page))
froot, fext = os.path.splitext(fpath)
p = infile.getPage(page - 1)
@@ -103,7 +114,7 @@ class PDFHandler(object):
os.rename(fpath, fpath_new)
infile = PdfFileReader(open(fpath_new, 'rb'), strict=False)
if infile.isEncrypted:
infile.decrypt('')
infile.decrypt(self.password)
outfile = PdfFileWriter()
p = infile.getPage(0)
if rotation == 'anticlockwise':
@@ -130,9 +141,6 @@ class PDFHandler(object):
-------
tables : camelot.core.TableList
List of tables found in PDF.
geometry : camelot.core.GeometryList
List of geometry objects (contours, lines, joints) found
in PDF.
"""
tables = []
@@ -145,4 +153,4 @@ class PDFHandler(object):
for p in pages:
t = parser.extract_tables(p)
tables.extend(t)
return TableList(tables)
return TableList(tables)
+7 -82
View File
@@ -1,14 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import division
from itertools import groupby
from operator import itemgetter
import cv2
import numpy as np
from .utils import merge_tuples
def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2):
"""Thresholds an image using OpenCV's adaptiveThreshold.
@@ -42,10 +38,12 @@ def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
if process_background:
threshold = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
threshold = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, blocksize, c)
else:
threshold = cv2.adaptiveThreshold(np.invert(gray), 255,
threshold = cv2.adaptiveThreshold(
np.invert(gray), 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blocksize, c)
return img, threshold
@@ -102,6 +100,7 @@ def find_lines(threshold, direction='horizontal', line_size_scaling=15, iteratio
_, contours, _ = cv2.findContours(
threshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
except ValueError:
# for opencv backward compatibility
contours, _ = cv2.findContours(
threshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
@@ -141,6 +140,7 @@ def find_table_contours(vertical, horizontal):
__, contours, __ = cv2.findContours(
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
except ValueError:
# for opencv backward compatibility
contours, __ = cv2.findContours(
mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10]
@@ -185,6 +185,7 @@ def find_table_joints(contours, vertical, horizontal):
__, jc, __ = cv2.findContours(
roi, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
except ValueError:
# for opencv backward compatibility
jc, __ = cv2.findContours(
roi, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
if len(jc) <= 4: # remove contours with less than 4 joints
@@ -197,79 +198,3 @@ def find_table_joints(contours, vertical, horizontal):
tables[(x, y + h, x + w, y)] = joint_coords
return tables
def remove_lines(threshold, line_size_scaling=15):
"""Removes lines from a thresholded image.
Parameters
----------
threshold : object
numpy.ndarray representing the thresholded image.
line_size_scaling : int, optional (default: 15)
Factor by which the page dimensions will be divided to get
smallest length of lines that should be detected.
The larger this value, smaller the detected lines. Making it
too large will lead to text being detected as lines.
Returns
-------
threshold : object
numpy.ndarray representing the thresholded image
with horizontal and vertical lines removed.
"""
size = threshold.shape[0] // line_size_scaling
vertical_erode_el = cv2.getStructuringElement(cv2.MORPH_RECT, (1, size))
horizontal_erode_el = cv2.getStructuringElement(cv2.MORPH_RECT, (size, 1))
dilate_el = cv2.getStructuringElement(cv2.MORPH_RECT, (10, 10))
vertical = cv2.erode(threshold, vertical_erode_el)
vertical = cv2.dilate(vertical, dilate_el)
horizontal = cv2.erode(threshold, horizontal_erode_el)
horizontal = cv2.dilate(horizontal, dilate_el)
threshold = np.bitwise_and(threshold, np.invert(vertical))
threshold = np.bitwise_and(threshold, np.invert(horizontal))
return threshold
def find_cuts(threshold, char_size_scaling=200):
"""Finds cuts made by text projections on y-axis.
Parameters
----------
threshold : object
numpy.ndarray representing the thresholded image.
line_size_scaling : int, optional (default: 200)
Factor by which the page dimensions will be divided to get
smallest length of lines that should be detected.
The larger this value, smaller the detected lines. Making it
too large will lead to text being detected as lines.
Returns
-------
y_cuts : list
List of cuts on y-axis.
"""
size = threshold.shape[0] // char_size_scaling
char_el = cv2.getStructuringElement(cv2.MORPH_RECT, (1, size))
threshold = cv2.erode(threshold, char_el)
threshold = cv2.dilate(threshold, char_el)
try:
__, contours, __ = cv2.findContours(threshold, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
except ValueError:
contours, __ = cv2.findContours(threshold, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
contours = [cv2.boundingRect(c) for c in contours]
y_cuts = [(c[1], c[1] + c[3]) for c in contours]
y_cuts = list(merge_tuples(sorted(y_cuts)))
y_cuts = [(y_cuts[i][0] + y_cuts[i - 1][1]) // 2 for i in range(1, len(y_cuts))]
return sorted(y_cuts, reverse=True)
+18 -8
View File
@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
import warnings
from .handlers import PDFHandler
from .utils import validate_input, remove_extra
def read_pdf(filepath, pages='1', flavor='lattice', **kwargs):
def read_pdf(filepath, pages='1', password=None, flavor='lattice',
suppress_warnings=False, **kwargs):
"""Read PDF and return extracted tables.
Note: kwargs annotated with ^ can only be used with flavor='stream'
@@ -16,11 +18,15 @@ def read_pdf(filepath, pages='1', flavor='lattice', **kwargs):
Path to PDF file.
pages : str, optional (default: '1')
Comma-separated page numbers.
Example: 1,3,4 or 1,4-end.
Example: '1,3,4' or '1,4-end'.
password : str, optional (default: None)
Password for decryption.
flavor : str (default: 'lattice')
The parsing method to use ('lattice' or 'stream').
Lattice is used by default.
table_area : list, optional (default: None)
suppress_warnings : bool, optional (default: False)
Prevent warnings from being emitted by Camelot.
table_areas : list, optional (default: None)
List of table area strings of the form x1,y1,x2,y2
where (x1, y1) -> left-top and (x2, y2) -> right-bottom
in PDF coordinate space.
@@ -85,8 +91,12 @@ def read_pdf(filepath, pages='1', flavor='lattice', **kwargs):
raise NotImplementedError("Unknown flavor specified."
" Use either 'lattice' or 'stream'")
validate_input(kwargs, flavor=flavor)
p = PDFHandler(filepath, pages)
kwargs = remove_extra(kwargs, flavor=flavor)
tables = p.parse(flavor=flavor, **kwargs)
return tables
with warnings.catch_warnings():
if suppress_warnings:
warnings.simplefilter("ignore")
validate_input(kwargs, flavor=flavor)
p = PDFHandler(filepath, pages=pages, password=password)
kwargs = remove_extra(kwargs, flavor=flavor)
tables = p.parse(flavor=flavor, **kwargs)
return tables
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from .stream import Stream
from .lattice import Lattice
from .lattice import Lattice
+1 -1
View File
@@ -18,4 +18,4 @@ class BaseParser(object):
self.horizontal_text = get_text_objects(self.layout, ltype="lh")
self.vertical_text = get_text_objects(self.layout, ltype="lv")
self.pdf_width, self.pdf_height = self.dimensions
self.rootname, __ = os.path.splitext(self.filename)
self.rootname, __ = os.path.splitext(self.filename)
+58 -19
View File
@@ -4,6 +4,7 @@ from __future__ import division
import os
import copy
import logging
import warnings
import subprocess
import numpy as np
@@ -13,12 +14,12 @@ from .base import BaseParser
from ..core import Table
from ..utils import (scale_image, scale_pdf, segments_in_bbox, text_in_bbox,
merge_close_lines, get_table_index, compute_accuracy,
compute_whitespace, setup_logging, encode_)
compute_whitespace)
from ..image_processing import (adaptive_threshold, find_lines,
find_table_contours, find_table_joints)
logger = setup_logging(__name__)
logger = logging.getLogger('camelot')
class Lattice(BaseParser):
@@ -27,7 +28,7 @@ class Lattice(BaseParser):
Parameters
----------
table_area : list, optional (default: None)
table_areas : list, optional (default: None)
List of table area strings of the form x1,y1,x2,y2
where (x1, y1) -> left-top and (x2, y2) -> right-bottom
in PDF coordinate space.
@@ -75,12 +76,12 @@ class Lattice(BaseParser):
For more information, refer `PDFMiner docs <https://euske.github.io/pdfminer/>`_.
"""
def __init__(self, table_area=None, process_background=False,
def __init__(self, table_areas=None, process_background=False,
line_size_scaling=15, copy_text=None, shift_text=['l', 't'],
split_text=False, flag_size=False, line_close_tol=2,
joint_close_tol=2, threshold_blocksize=15, threshold_constant=-2,
iterations=0, margins=(1.0, 0.5, 0.1), **kwargs):
self.table_area = table_area
self.table_areas = table_areas
self.process_background = process_background
self.line_size_scaling = line_size_scaling
self.copy_text = copy_text
@@ -173,15 +174,54 @@ class Lattice(BaseParser):
return t
def _generate_image(self):
# TODO: get rid of ghostscript #96
def get_executable():
import platform
from distutils.spawn import find_executable
class GhostscriptNotFound(Exception): pass
gs = None
system = platform.system().lower()
try:
if system == 'windows':
if find_executable('gswin32c.exe'):
gs = 'gswin32c.exe'
elif find_executable('gswin64c.exe'):
gs = 'gswin64c.exe'
else:
raise ValueError
else:
if find_executable('gs'):
gs = 'gs'
elif find_executable('gsc'):
gs = 'gsc'
else:
raise ValueError
if 'ghostscript' not in subprocess.check_output(
[gs, '-version']).decode('utf-8').lower():
raise ValueError
except ValueError:
raise GhostscriptNotFound(
'Please make sure that Ghostscript is installed'
' and available on the PATH environment variable')
return gs
self.imagename = ''.join([self.rootname, '.png'])
gs_call = [
"-q", "-sDEVICE=png16m", "-o", self.imagename, "-r600", self.filename
'-q',
'-sDEVICE=png16m',
'-o',
self.imagename,
'-r600',
self.filename
]
if "ghostscript" in subprocess.check_output(["gs", "-version"]).lower():
gs_call.insert(0, "gs")
else:
gs_call.insert(0, "gsc")
subprocess.call(gs_call, stdout=open(os.devnull, 'w'),
gs = get_executable()
gs_call.insert(0, gs)
subprocess.call(
gs_call, stdout=open(os.devnull, 'w'),
stderr=subprocess.STDOUT)
def _generate_table_bbox(self):
@@ -204,9 +244,9 @@ class Lattice(BaseParser):
self.threshold, direction='horizontal',
line_size_scaling=self.line_size_scaling, iterations=self.iterations)
if self.table_area is not None:
if self.table_areas is not None:
areas = []
for area in self.table_area:
for area in self.table_areas:
x1, y1, x2, y2 = area.split(",")
x1 = float(x1)
y1 = float(y1)
@@ -284,7 +324,6 @@ class Lattice(BaseParser):
table = Lattice._copy_spanning_text(table, copy_text=self.copy_text)
data = table.data
data = encode_(data)
table.df = pd.DataFrame(data)
table.shape = table.df.shape
@@ -306,11 +345,11 @@ class Lattice(BaseParser):
return table
def extract_tables(self, filename):
logger.info('Processing {}'.format(os.path.basename(filename)))
self._generate_layout(filename)
logger.info('Processing {}'.format(os.path.basename(self.rootname)))
if not self.horizontal_text:
logger.info("No tables found on {}".format(
warnings.warn("No tables found on {}".format(
os.path.basename(self.rootname)))
return []
@@ -319,10 +358,10 @@ class Lattice(BaseParser):
_tables = []
# sort tables based on y-coord
for table_idx, tk in enumerate(sorted(self.table_bbox.keys(),
key=lambda x: x[1], reverse=True)):
for table_idx, tk in enumerate(sorted(
self.table_bbox.keys(), key=lambda x: x[1], reverse=True)):
cols, rows, v_s, h_s = self._generate_columns_and_rows(table_idx, tk)
table = self._generate_table(table_idx, cols, rows, v_s=v_s, h_s=h_s)
_tables.append(table)
return _tables
return _tables
+19 -19
View File
@@ -3,6 +3,7 @@
from __future__ import division
import os
import logging
import warnings
import numpy as np
import pandas as pd
@@ -10,10 +11,10 @@ import pandas as pd
from .base import BaseParser
from ..core import Table
from ..utils import (text_in_bbox, get_table_index, compute_accuracy,
compute_whitespace, setup_logging, encode_)
compute_whitespace)
logger = setup_logging(__name__)
logger = logging.getLogger('camelot')
class Stream(BaseParser):
@@ -25,7 +26,7 @@ class Stream(BaseParser):
Parameters
----------
table_area : list, optional (default: None)
table_areas : list, optional (default: None)
List of table area strings of the form x1,y1,x2,y2
where (x1, y1) -> left-top and (x2, y2) -> right-bottom
in PDF coordinate space.
@@ -49,10 +50,10 @@ class Stream(BaseParser):
For more information, refer `PDFMiner docs <https://euske.github.io/pdfminer/>`_.
"""
def __init__(self, table_area=None, columns=None, split_text=False,
def __init__(self, table_areas=None, columns=None, split_text=False,
flag_size=False, row_close_tol=2, col_close_tol=0,
margins=(1.0, 0.5, 0.1), **kwargs):
self.table_area = table_area
self.table_areas = table_areas
self.columns = columns
self._validate_columns()
self.split_text = split_text
@@ -115,7 +116,7 @@ class Stream(BaseParser):
row_y = t.y0
temp.append(t)
rows.append(sorted(temp, key=lambda t: t.x0))
__ = rows.pop(0) # hacky
__ = rows.pop(0) # hacky
return rows
@staticmethod
@@ -210,7 +211,7 @@ class Stream(BaseParser):
text = Stream._group_rows(text, row_close_tol=row_close_tol)
elements = [len(r) for r in text]
new_cols = [(t.x0, t.x1)
for r in text if len(r) == max(elements) for t in r]
for r in text if len(r) == max(elements) for t in r]
cols.extend(Stream._merge_columns(sorted(new_cols)))
return cols
@@ -240,15 +241,15 @@ class Stream(BaseParser):
return cols
def _validate_columns(self):
if self.table_area is not None and self.columns is not None:
if len(self.table_area) != len(self.columns):
raise ValueError("Length of table_area and columns"
if self.table_areas is not None and self.columns is not None:
if len(self.table_areas) != len(self.columns):
raise ValueError("Length of table_areas and columns"
" should be equal")
def _generate_table_bbox(self):
if self.table_area is not None:
if self.table_areas is not None:
table_bbox = {}
for area in self.table_area:
for area in self.table_areas:
x1, y1, x2, y2 = area.split(",")
x1 = float(x1)
y1 = float(y1)
@@ -287,7 +288,7 @@ class Stream(BaseParser):
else:
ncols = max(set(elements), key=elements.count)
if ncols == 1:
logger.info("No tables found on {}".format(
warnings.warn("No tables found on {}".format(
os.path.basename(self.rootname)))
cols = [(t.x0, t.x1) for r in rows_grouped if len(r) == ncols for t in r]
cols = self._merge_columns(sorted(cols), col_close_tol=self.col_close_tol)
@@ -323,7 +324,6 @@ class Stream(BaseParser):
accuracy = compute_accuracy([[100, pos_errors]])
data = table.data
data = encode_(data)
table.df = pd.DataFrame(data)
table.shape = table.df.shape
@@ -345,11 +345,11 @@ class Stream(BaseParser):
return table
def extract_tables(self, filename):
logger.info('Processing {}'.format(os.path.basename(filename)))
self._generate_layout(filename)
logger.info('Processing {}'.format(os.path.basename(self.rootname)))
if not self.horizontal_text:
logger.info("No tables found on {}".format(
warnings.warn("No tables found on {}".format(
os.path.basename(self.rootname)))
return []
@@ -357,10 +357,10 @@ class Stream(BaseParser):
_tables = []
# sort tables based on y-coord
for table_idx, tk in enumerate(sorted(self.table_bbox.keys(),
key=lambda x: x[1], reverse=True)):
for table_idx, tk in enumerate(sorted(
self.table_bbox.keys(), key=lambda x: x[1], reverse=True)):
cols, rows = self._generate_columns_and_rows(table_idx, tk)
table = self._generate_table(table_idx, cols, rows)
_tables.append(table)
return _tables
return _tables
+154 -83
View File
@@ -1,108 +1,179 @@
import cv2
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# -*- coding: utf-8 -*-
try:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
except ImportError:
_HAS_MPL = False
else:
_HAS_MPL = True
def plot_text(text):
"""Generates a plot for all text present on the PDF page.
class PlotMethods(object):
def __call__(self, table, kind='text', filename=None):
"""Plot elements found on PDF page based on kind
specified, useful for debugging and playing with different
parameters to get the best output.
Parameters
----------
text : list
Parameters
----------
table: camelot.core.Table
A Camelot Table.
kind : str, optional (default: 'text')
{'text', 'grid', 'contour', 'joint', 'line'}
The element type for which a plot should be generated.
filepath: str, optional (default: None)
Absolute path for saving the generated plot.
"""
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
xs, ys = [], []
for t in text:
xs.extend([t[0], t[2]])
ys.extend([t[1], t[3]])
ax.add_patch(
patches.Rectangle(
(t[0], t[1]),
t[2] - t[0],
t[3] - t[1]
Returns
-------
fig : matplotlib.fig.Figure
"""
if not _HAS_MPL:
raise ImportError('matplotlib is required for plotting.')
if table.flavor == 'stream' and kind in ['contour', 'joint', 'line']:
raise NotImplementedError("Stream flavor does not support kind='{}'".format(
kind))
plot_method = getattr(self, kind)
return plot_method(table)
def text(self, table):
"""Generates a plot for all text elements present
on the PDF page.
Parameters
----------
table : camelot.core.Table
Returns
-------
fig : matplotlib.fig.Figure
"""
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
xs, ys = [], []
for t in table._text:
xs.extend([t[0], t[2]])
ys.extend([t[1], t[3]])
ax.add_patch(
patches.Rectangle(
(t[0], t[1]),
t[2] - t[0],
t[3] - t[1]
)
)
)
ax.set_xlim(min(xs) - 10, max(xs) + 10)
ax.set_ylim(min(ys) - 10, max(ys) + 10)
plt.show()
ax.set_xlim(min(xs) - 10, max(xs) + 10)
ax.set_ylim(min(ys) - 10, max(ys) + 10)
return fig
def grid(self, table):
"""Generates a plot for the detected table grids
on the PDF page.
def plot_table(table):
"""Generates a plot for the table.
Parameters
----------
table : camelot.core.Table
Parameters
----------
table : camelot.core.Table
Returns
-------
fig : matplotlib.fig.Figure
"""
for row in table.cells:
for cell in row:
if cell.left:
plt.plot([cell.lb[0], cell.lt[0]],
"""
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
for row in table.cells:
for cell in row:
if cell.left:
ax.plot([cell.lb[0], cell.lt[0]],
[cell.lb[1], cell.lt[1]])
if cell.right:
plt.plot([cell.rb[0], cell.rt[0]],
if cell.right:
ax.plot([cell.rb[0], cell.rt[0]],
[cell.rb[1], cell.rt[1]])
if cell.top:
plt.plot([cell.lt[0], cell.rt[0]],
if cell.top:
ax.plot([cell.lt[0], cell.rt[0]],
[cell.lt[1], cell.rt[1]])
if cell.bottom:
plt.plot([cell.lb[0], cell.rb[0]],
if cell.bottom:
ax.plot([cell.lb[0], cell.rb[0]],
[cell.lb[1], cell.rb[1]])
plt.show()
return fig
def contour(self, table):
"""Generates a plot for all table boundaries present
on the PDF page.
def plot_contour(image):
"""Generates a plot for all table boundaries present on the
PDF page.
Parameters
----------
table : camelot.core.Table
Parameters
----------
image : tuple
Returns
-------
fig : matplotlib.fig.Figure
"""
img, table_bbox = image
for t in table_bbox.keys():
cv2.rectangle(img, (t[0], t[1]),
(t[2], t[3]), (255, 0, 0), 20)
plt.imshow(img)
plt.show()
"""
img, table_bbox = table._image
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
for t in table_bbox.keys():
ax.add_patch(
patches.Rectangle(
(t[0], t[1]),
t[2] - t[0],
t[3] - t[1],
fill=None,
edgecolor='red'
)
)
ax.imshow(img)
return fig
def joint(self, table):
"""Generates a plot for all line intersections present
on the PDF page.
def plot_joint(image):
"""Generates a plot for all line intersections present on the
PDF page.
Parameters
----------
table : camelot.core.Table
Parameters
----------
image : tuple
Returns
-------
fig : matplotlib.fig.Figure
"""
img, table_bbox = image
x_coord = []
y_coord = []
for k in table_bbox.keys():
for coord in table_bbox[k]:
x_coord.append(coord[0])
y_coord.append(coord[1])
plt.plot(x_coord, y_coord, 'ro')
plt.imshow(img)
plt.show()
"""
img, table_bbox = table._image
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
x_coord = []
y_coord = []
for k in table_bbox.keys():
for coord in table_bbox[k]:
x_coord.append(coord[0])
y_coord.append(coord[1])
ax.plot(x_coord, y_coord, 'ro')
ax.imshow(img)
return fig
def line(self, table):
"""Generates a plot for all line segments present
on the PDF page.
def plot_line(segments):
"""Generates a plot for all line segments present on the PDF page.
Parameters
----------
table : camelot.core.Table
Parameters
----------
segments : tuple
Returns
-------
fig : matplotlib.fig.Figure
"""
vertical, horizontal = segments
for v in vertical:
plt.plot([v[0], v[2]], [v[1], v[3]])
for h in horizontal:
plt.plot([h[0], h[2]], [h[1], h[3]])
plt.show()
"""
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
vertical, horizontal = table._segments
for v in vertical:
ax.plot([v[0], v[2]], [v[1], v[3]])
for h in horizontal:
ax.plot([h[0], h[2]], [h[1], h[3]])
return fig
+7 -159
View File
@@ -1,8 +1,7 @@
from __future__ import division
import os
import shutil
import logging
import tempfile
import warnings
from itertools import groupby
from operator import itemgetter
@@ -14,7 +13,6 @@ from pdfminer.pdfpage import PDFPage
from pdfminer.pdfpage import PDFTextExtractionNotAllowed
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfdevice import PDFDevice
from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import (LAParams, LTAnno, LTChar, LTTextLineHorizontal,
LTTextLineVertical)
@@ -38,7 +36,7 @@ lattice_kwargs = [
]
def validate_input(kwargs, flavor='lattice', geometry_type=False):
def validate_input(kwargs, flavor='lattice'):
def check_intersection(parser_kwargs, input_kwargs):
isec = set(parser_kwargs).intersection(set(input_kwargs.keys()))
if isec:
@@ -49,10 +47,6 @@ def validate_input(kwargs, flavor='lattice', geometry_type=False):
check_intersection(stream_kwargs, kwargs)
else:
check_intersection(lattice_kwargs, kwargs)
if geometry_type:
if flavor != 'lattice' and geometry_type in ['contour', 'joint', 'line']:
raise ValueError("Use geometry_type='{}' with flavor='lattice'".format(
geometry_type))
def remove_extra(kwargs, flavor='lattice'):
@@ -77,35 +71,6 @@ class TemporaryDirectory(object):
shutil.rmtree(self.name)
def setup_logging(name):
"""Sets up a logger with StreamHandler.
Parameters
----------
name : str
Returns
-------
logger : logging.Logger
"""
logger = logging.getLogger(name)
format_string = '%(asctime)s - %(levelname)s - %(funcName)s - %(message)s'
formatter = logging.Formatter(format_string, datefmt='%Y-%m-%dT%H:%M:%S')
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
logger = setup_logging(__name__)
def translate(x1, x2):
"""Translates x2 by x1.
@@ -140,35 +105,6 @@ def scale(x, s):
return x
def rotate(x1, y1, x2, y2, angle):
"""Rotates point x2, y2 about point x1, y1 by angle.
Parameters
----------
x1 : float
y1 : float
x2 : float
y2 : float
angle : float
Angle in radians.
Returns
-------
xnew : float
ynew : float
"""
s = np.sin(angle)
c = np.cos(angle)
x2 = translate(-x1, x2)
y2 = translate(-y1, y2)
xnew = c * x2 - s * y2
ynew = s * x2 + c * y2
xnew = translate(x1, xnew)
ynew = translate(y1, ynew)
return xnew, ynew
def scale_pdf(k, factors):
"""Translates and scales pdf coordinate space to image
coordinate space.
@@ -340,38 +276,11 @@ def text_in_bbox(bbox, text):
lb = (bbox[0], bbox[1])
rt = (bbox[2], bbox[3])
t_bbox = [t for t in text if lb[0] - 2 <= (t.x0 + t.x1) / 2.0
<= rt[0] + 2 and lb[1] - 2 <= (t.y0 + t.y1) / 2.0
<= rt[1] + 2]
<= rt[0] + 2 and lb[1] - 2 <= (t.y0 + t.y1) / 2.0
<= rt[1] + 2]
return t_bbox
def remove_close_lines(ar, line_close_tol=2):
"""Removes lines which are within a tolerance, based on their x or
y axis projections.
Parameters
----------
ar : list
line_close_tol : int, optional (default: 2)
Returns
-------
ret : list
"""
ret = []
for a in ar:
if not ret:
ret.append(a)
else:
temp = ret[-1]
if np.isclose(temp, a, atol=line_close_tol):
pass
else:
ret.append(a)
return ret
def merge_close_lines(ar, line_close_tol=2):
"""Merges lines which are within a tolerance by calculating a
moving mean, based on their x or y axis projections.
@@ -560,11 +469,11 @@ def get_table_index(table, t, direction, split_text=False, flag_size=False):
lt_col_overlap.append(abs(left - right) / abs(c[0] - c[1]))
else:
lt_col_overlap.append(-1)
if len(filter(lambda x: x != -1, lt_col_overlap)) == 0:
if len(list(filter(lambda x: x != -1, lt_col_overlap))) == 0:
text = t.get_text().strip('\n')
text_range = (t.x0, t.x1)
col_range = (table.cols[0][0], table.cols[-1][1])
logger.info("{} {} does not lie in column range {}".format(
warnings.warn("{} {} does not lie in column range {}".format(
text, text_range, col_range))
r_idx = r
c_idx = lt_col_overlap.index(max(lt_col_overlap))
@@ -648,43 +557,6 @@ def compute_whitespace(d):
return whitespace
def remove_empty(d):
"""Removes empty rows and columns from a two-dimensional list.
Parameters
----------
d : list
Returns
-------
d : list
"""
for i, row in enumerate(d):
if row == [''] * len(row):
d.pop(i)
d = zip(*d)
d = [list(row) for row in d if any(row)]
d = zip(*d)
return d
def encode_(ar):
"""Encodes two-dimensional list into unicode.
Parameters
----------
ar : list
Returns
-------
ar : list
"""
ar = [[r.encode('utf-8') for r in row] for row in ar]
return ar
def get_page_layout(filename, char_margin=1.0, line_margin=0.5, word_margin=0.1,
detect_vertical=True, all_texts=True):
"""Returns a PDFMiner LTPage object and page dimension of a single
@@ -709,7 +581,7 @@ def get_page_layout(filename, char_margin=1.0, line_margin=0.5, word_margin=0.1,
Dimension of pdf page in the form (width, height).
"""
with open(filename, 'r') as f:
with open(filename, 'rb') as f:
parser = PDFParser(f)
document = PDFDocument(parser)
if not document.is_extractable:
@@ -767,27 +639,3 @@ def get_text_objects(layout, ltype="char", t=None):
except AttributeError:
pass
return t
def merge_tuples(tuples):
"""Merges a list of overlapping tuples.
Parameters
----------
tuples : list
List of tuples where a tuple is a single axis coordinate pair.
Yields
------
tuple
"""
merged = list(tuples[0])
for s, e in tuples:
if s <= merged[1]:
merged[1] = max(merged[1], e)
else:
yield tuple(merged)
merged[0] = s
merged[1] = e
yield tuple(merged)

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
<style type="text/css">
div.section h1 {font-size: 225%;}
div.section h1 {font-size: 210%;}
/* "Quick Search" should be capitalized. */
div#searchbox h3 {text-transform: capitalize;}
/* Make the document a little wider, less code is cut-off. */
+59 -59
View File
@@ -10,21 +10,21 @@ class FlaskyStyle(Style):
styles = {
# No corresponding class for the following:
#Text: "", # class: ''
Whitespace: "underline #f8f8f8", # class: 'w'
Error: "#a40000 border:#ef2929", # class: 'err'
Other: "#000000", # class 'x'
# Text: "", # class: ''
Whitespace: "underline #f8f8f8", # class: 'w'
Error: "#a40000 border:#ef2929", # class: 'err'
Other: "#000000", # class 'x'
Comment: "italic #8f5902", # class: 'c'
Comment.Preproc: "noitalic", # class: 'cp'
Comment: "italic #8f5902", # class: 'c'
Comment.Preproc: "noitalic", # class: 'cp'
Keyword: "bold #004461", # class: 'k'
Keyword.Constant: "bold #004461", # class: 'kc'
Keyword.Declaration: "bold #004461", # class: 'kd'
Keyword.Namespace: "bold #004461", # class: 'kn'
Keyword.Pseudo: "bold #004461", # class: 'kp'
Keyword.Reserved: "bold #004461", # class: 'kr'
Keyword.Type: "bold #004461", # class: 'kt'
Keyword: "bold #004461", # class: 'k'
Keyword.Constant: "bold #004461", # class: 'kc'
Keyword.Declaration: "bold #004461", # class: 'kd'
Keyword.Namespace: "bold #004461", # class: 'kn'
Keyword.Pseudo: "bold #004461", # class: 'kp'
Keyword.Reserved: "bold #004461", # class: 'kr'
Keyword.Type: "bold #004461", # class: 'kt'
Operator: "#582800", # class: 'o'
Operator.Word: "bold #004461", # class: 'ow' - like keywords
@@ -34,53 +34,53 @@ class FlaskyStyle(Style):
# because special names such as Name.Class, Name.Function, etc.
# are not recognized as such later in the parsing, we choose them
# to look the same as ordinary variables.
Name: "#000000", # class: 'n'
Name.Attribute: "#c4a000", # class: 'na' - to be revised
Name.Builtin: "#004461", # class: 'nb'
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
Name.Class: "#000000", # class: 'nc' - to be revised
Name.Constant: "#000000", # class: 'no' - to be revised
Name.Decorator: "#888", # class: 'nd' - to be revised
Name.Entity: "#ce5c00", # class: 'ni'
Name.Exception: "bold #cc0000", # class: 'ne'
Name.Function: "#000000", # class: 'nf'
Name.Property: "#000000", # class: 'py'
Name.Label: "#f57900", # class: 'nl'
Name.Namespace: "#000000", # class: 'nn' - to be revised
Name.Other: "#000000", # class: 'nx'
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
Name.Variable: "#000000", # class: 'nv' - to be revised
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
Name: "#000000", # class: 'n'
Name.Attribute: "#c4a000", # class: 'na' - to be revised
Name.Builtin: "#004461", # class: 'nb'
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
Name.Class: "#000000", # class: 'nc' - to be revised
Name.Constant: "#000000", # class: 'no' - to be revised
Name.Decorator: "#888", # class: 'nd' - to be revised
Name.Entity: "#ce5c00", # class: 'ni'
Name.Exception: "bold #cc0000", # class: 'ne'
Name.Function: "#000000", # class: 'nf'
Name.Property: "#000000", # class: 'py'
Name.Label: "#f57900", # class: 'nl'
Name.Namespace: "#000000", # class: 'nn' - to be revised
Name.Other: "#000000", # class: 'nx'
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
Name.Variable: "#000000", # class: 'nv' - to be revised
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
Number: "#990000", # class: 'm'
Number: "#990000", # class: 'm'
Literal: "#000000", # class: 'l'
Literal.Date: "#000000", # class: 'ld'
Literal: "#000000", # class: 'l'
Literal.Date: "#000000", # class: 'ld'
String: "#4e9a06", # class: 's'
String.Backtick: "#4e9a06", # class: 'sb'
String.Char: "#4e9a06", # class: 'sc'
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
String.Double: "#4e9a06", # class: 's2'
String.Escape: "#4e9a06", # class: 'se'
String.Heredoc: "#4e9a06", # class: 'sh'
String.Interpol: "#4e9a06", # class: 'si'
String.Other: "#4e9a06", # class: 'sx'
String.Regex: "#4e9a06", # class: 'sr'
String.Single: "#4e9a06", # class: 's1'
String.Symbol: "#4e9a06", # class: 'ss'
String: "#4e9a06", # class: 's'
String.Backtick: "#4e9a06", # class: 'sb'
String.Char: "#4e9a06", # class: 'sc'
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
String.Double: "#4e9a06", # class: 's2'
String.Escape: "#4e9a06", # class: 'se'
String.Heredoc: "#4e9a06", # class: 'sh'
String.Interpol: "#4e9a06", # class: 'si'
String.Other: "#4e9a06", # class: 'sx'
String.Regex: "#4e9a06", # class: 'sr'
String.Single: "#4e9a06", # class: 's1'
String.Symbol: "#4e9a06", # class: 'ss'
Generic: "#000000", # class: 'g'
Generic.Deleted: "#a40000", # class: 'gd'
Generic.Emph: "italic #000000", # class: 'ge'
Generic.Error: "#ef2929", # class: 'gr'
Generic.Heading: "bold #000080", # class: 'gh'
Generic.Inserted: "#00A000", # class: 'gi'
Generic.Output: "#888", # class: 'go'
Generic.Prompt: "#745334", # class: 'gp'
Generic.Strong: "bold #000000", # class: 'gs'
Generic.Subheading: "bold #800080", # class: 'gu'
Generic.Traceback: "bold #a40000", # class: 'gt'
}
Generic: "#000000", # class: 'g'
Generic.Deleted: "#a40000", # class: 'gd'
Generic.Emph: "italic #000000", # class: 'ge'
Generic.Error: "#ef2929", # class: 'gr'
Generic.Heading: "bold #000080", # class: 'gh'
Generic.Inserted: "#00A000", # class: 'gi'
Generic.Output: "#888", # class: 'go'
Generic.Prompt: "#745334", # class: 'gp'
Generic.Strong: "bold #000000", # class: 'gs'
Generic.Subheading: "bold #800080", # class: 'gu'
Generic.Traceback: "bold #a40000", # class: 'gt'
}
+2 -2
View File
@@ -63,7 +63,7 @@ master_doc = 'index'
# General information about the project.
project = u'Camelot'
copyright = u'2018, Peeply Private Ltd (Singapore)'
copyright = u'2018, <a href="https://socialcops.com" target="_blank">SocialCops</a>'
author = u'Vinayak Mehta'
# The version info for the project you're documenting, acts as replacement for
@@ -358,4 +358,4 @@ texinfo_documents = [
intersphinx_mapping = {
'https://docs.python.org/2': None,
'http://pandas.pydata.org/pandas-docs/stable': None
}
}
+17 -13
View File
@@ -7,7 +7,7 @@ If you're reading this, you're probably looking to contributing to Camelot. *Tim
This document will help you get started with contributing documentation, code, testing and filing issues. If you have any questions, feel free to reach out to `Vinayak Mehta`_, the author and maintainer.
.. _Vinayak Mehta: http://vinayak-mehta.github.io
.. _Vinayak Mehta: https://vinayak-mehta.github.io
Code Of Conduct
---------------
@@ -24,13 +24,13 @@ As the `Requests Code Of Conduct`_ states, **all contributions are welcome**, as
.. _Requests Code Of Conduct: http://docs.python-requests.org/en/master/dev/contributing/#be-cordial
Your First Contribution
Your first contribution
-----------------------
A great way to start contributing to Camelot is to pick an issue tagged with the `Contributor Friendly`_ or the `Easy`_ tags. If you're unable to find a good first issue, feel free to contact the maintainer.
A great way to start contributing to Camelot is to pick an issue tagged with the `help wanted`_ or the `good first issue`_ tags. If you're unable to find a good first issue, feel free to contact the maintainer.
.. _Contributor Friendly: https://github.com/socialcopsdev/camelot/labels/Contributor%20Friendly
.. _Easy: https://github.com/socialcopsdev/camelot/labels/Level%3A%20Easy
.. _help wanted: https://github.com/socialcopsdev/camelot/labels/help%20wanted
.. _good first issue: https://github.com/socialcopsdev/camelot/labels/good%20first%20issue
Setting up a development environment
------------------------------------
@@ -39,13 +39,17 @@ To install the dependencies needed for development, you can use pip::
$ pip install camelot-py[dev]
Alternatively, you can clone the project repository, and install using pip::
$ pip install ".[dev]"
Pull Requests
-------------
Submit a Pull Request
Submit a pull request
^^^^^^^^^^^^^^^^^^^^^
The preferred workflow for contributing to Camelot is to fork the `project repository`_ on GitHub, clone, develop on a branch and then finally submit a pull request. Steps:
The preferred workflow for contributing to Camelot is to fork the `project repository`_ on GitHub, clone, develop on a branch and then finally submit a pull request. Here are the steps:
.. _project repository: https://github.com/socialcopsdev/camelot
@@ -76,7 +80,7 @@ Now it's time to go to the your fork of Camelot and create a pull request! You c
.. _follow these instructions: https://help.github.com/articles/creating-a-pull-request-from-a-fork/
Work on your Pull Request
Work on your pull request
^^^^^^^^^^^^^^^^^^^^^^^^^
We recommend that your pull request complies with the following guidelines:
@@ -89,7 +93,7 @@ We recommend that your pull request complies with the following guidelines:
.. _numpydoc: https://numpydoc.readthedocs.io/en/latest/format.html
- Make sure your commit messages follow `the seven rules of a great git commit message`_.
- Make sure your commit messages follow `the seven rules of a great git commit message`_:
- Separate subject from body with a blank line
- Limit the subject line to 50 characters
- Capitalize the subject line
@@ -119,7 +123,7 @@ Writing documentation, function docstrings, examples and tutorials is a great wa
The documentation is written in `reStructuredText`_, with `Sphinx`_ used to generate these lovely HTML files that you're currently reading (unless you're reading this on GitHub). You can edit the documentation using any text editor and then generate the HTML output by running `make html` in the ``docs/`` directory.
The function docstrings are written using the `numpydoc`_ extension for Sphinx. Make sure you check out how its format guidelines, before you start writing one.
The function docstrings are written using the `numpydoc`_ extension for Sphinx. Make sure you check out how its format guidelines before you start writing one.
.. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText
.. _Sphinx: http://www.sphinx-doc.org/en/master/
@@ -128,14 +132,14 @@ The function docstrings are written using the `numpydoc`_ extension for Sphinx.
Filing Issues
-------------
We use `GitHub issues`_ to keep track of all issues and pull requests. Before opening an issue (which asks a question or reports a bug), it is advisable to use GitHub search to look for existing issues (both open and closed) that may be similar.
We use `GitHub issues`_ to keep track of all issues and pull requests. Before opening an issue (which asks a question or reports a bug), please use GitHub search to look for existing issues (both open and closed) that may be similar.
.. _GitHub issues: https://docs.pytest.org/en/latest/
.. _GitHub issues: https://github.com/socialcopsdev/camelot/issues
Questions
^^^^^^^^^
Please don't use GitHub issues for support questions, a better place for them would be `Stack Overflow`_. Make sure you tag them using the ``python-camelot`` tag.
Please don't use GitHub issues for support questions. A better place for them would be `Stack Overflow`_. Make sure you tag them using the ``python-camelot`` tag.
.. _Stack Overflow: http://stackoverflow.com
+34 -19
View File
@@ -8,25 +8,37 @@ Camelot: PDF Table Extraction for Humans
Release v\ |version|. (:ref:`Installation <install>`)
.. image:: https://img.shields.io/badge/license-MIT-lightgrey.svg
.. image:: https://travis-ci.org/socialcopsdev/camelot.svg?branch=master
:target: https://travis-ci.org/socialcopsdev/camelot
.. image:: https://readthedocs.org/projects/camelot-py/badge/?version=master
:target: https://camelot-py.readthedocs.io/en/master/
:alt: Documentation Status
.. image:: https://codecov.io/github/socialcopsdev/camelot/badge.svg?branch=master&service=github
:target: https://codecov.io/github/socialcopsdev/camelot?branch=master
.. image:: https://img.shields.io/pypi/v/camelot-py.svg
:target: https://pypi.org/project/camelot-py/
.. image:: https://img.shields.io/badge/python-2.7-blue.svg
.. image:: https://img.shields.io/pypi/l/camelot-py.svg
:target: https://pypi.org/project/camelot-py/
**Camelot** is a Python library which makes it easy for *anyone* to extract tables from PDF files!
.. image:: https://img.shields.io/pypi/pyversions/camelot-py.svg
:target: https://pypi.org/project/camelot-py/
.. note:: Camelot only works with:
.. image:: https://badges.gitter.im/camelot-dev/Lobby.png
:target: https://gitter.im/camelot-dev/Lobby
- Python 2, with **Python 3** support `on the way`_.
- Text-based PDFs and not scanned documents. If you can click-and-drag to select text in your table in a PDF viewer, then your PDF is text-based. Support for image-based PDFs using **OCR** is `planned`_.
**Camelot** is a Python library that makes it easy for *anyone* to extract tables from PDF files!
.. _on the way: https://github.com/socialcopsdev/camelot/issues/81
.. _planned: https://github.com/socialcopsdev/camelot/issues/101
.. note:: You can also check out `Excalibur`_, which is a web interface for Camelot!
------------------------
.. _Excalibur: https://github.com/camelot-dev/excalibur
**Here's how you can extract tables from PDF files.** Check out the PDF used in this example, `here`_.
----
**Here's how you can extract tables from PDF files.** Check out the PDF used in this example `here`_.
.. _here: _static/pdf/foo.pdf
@@ -35,7 +47,7 @@ Release v\ |version|. (:ref:`Installation <install>`)
>>> import camelot
>>> tables = camelot.read_pdf('foo.pdf')
>>> tables
<TableList tables=1>
<TableList n=1>
>>> tables.export('foo.csv', f='csv', compress=True) # json, excel, html
>>> tables[0]
<Table shape=(7, 7)>
@@ -54,14 +66,17 @@ Release v\ |version|. (:ref:`Installation <install>`)
There's a :ref:`command-line interface <cli>` too!
.. note:: Camelot only works with text-based PDFs and not scanned documents. (As Tabula `explains`_, "If you can click and drag to select text in your table in a PDF viewer, then your PDF is text-based".)
.. _explains: https://github.com/tabulapdf/tabula#why-tabula
Why Camelot?
------------
- **You are in control**: Unlike other libraries and tools which either give a nice output or fail miserably (with no in-between), Camelot gives you the power to tweak table extraction. (Since everything in the real world, including PDF table extraction, is fuzzy.)
- **Metrics**: *Bad* tables can be discarded based on metrics like accuracy and whitespace, without ever having to manually look at each table.
- Each table is a **pandas DataFrame**, which enables seamless integration into `ETL and data analysis workflows`_.
- **Export** to multiple formats, including json, excel and html.
- Simple and Elegant API, written in **Python**!
- **You are in control.** Unlike other libraries and tools which either give a nice output or fail miserably (with no in-between), Camelot gives you the power to tweak table extraction. (This is important since everything in the real world, including PDF table extraction, is fuzzy.)
- *Bad* tables can be discarded based on **metrics** like accuracy and whitespace, without ever having to manually look at each table.
- Each table is a **pandas DataFrame**, which seamlessly integrates into `ETL and data analysis workflows`_.
- **Export** to multiple formats, including JSON, Excel and HTML.
See `comparison with other PDF table extraction libraries and tools`_.
@@ -71,7 +86,7 @@ See `comparison with other PDF table extraction libraries and tools`_.
The User Guide
--------------
This part of the documentation, begins with some background information about why Camelot was created, takes a small dip into the implementation details and then focuses on step-by-step instructions for getting the most out of Camelot.
This part of the documentation begins with some background information about why Camelot was created, takes a small dip into the implementation details and then focuses on step-by-step instructions for getting the most out of Camelot.
.. toctree::
:maxdepth: 2
@@ -83,8 +98,8 @@ This part of the documentation, begins with some background information about wh
user/advanced
user/cli
The API Documentation / Guide
-----------------------------
The API Documentation/Guide
---------------------------
If you are looking for information on a specific function, class, or method,
this part of the documentation is for you.
+47 -48
View File
@@ -8,7 +8,7 @@ This page covers some of the more advanced configurations for :ref:`Lattice <lat
Process background lines
------------------------
To detect line segments, :ref:`Lattice <lattice>` needs the lines that make the table, to be in foreground. Here's an example of a table with lines in background.
To detect line segments, :ref:`Lattice <lattice>` needs the lines that make the table to be in the foreground. Here's an example of a table with lines in the background:
.. figure:: ../_static/png/background_lines.png
:scale: 50%
@@ -27,22 +27,24 @@ To process background lines, you can pass ``process_background=True``.
.. csv-table::
:file: ../_static/csv/background_lines.csv
Plot geometry
-------------
Visual debugging
----------------
You can use a :class:`table <camelot.core.Table>` object's :meth:`plot() <camelot.core.TableList.plot>` method to plot various geometries that were detected by Camelot while processing the PDF page. This can help you select table areas, column separators and debug bad table outputs, by tweaking different configuration parameters.
.. note:: Visual debugging using ``plot()`` requires `matplotlib <https://matplotlib.org/>`_ which is an optional dependency. You can install it using ``$ pip install camelot-py[plot]``.
The following geometries are available for plotting. You can pass them to the :meth:`plot() <camelot.core.TableList.plot>` method, which will then generate a `matplotlib <https://matplotlib.org/>`_ plot for the passed geometry.
You can use the :class:`plot() <camelot.plotting.PlotMethods>` method to generate a `matplotlib <https://matplotlib.org/>`_ plot of various elements that were detected on the PDF page while processing it. This can help you select table areas, column separators and debug bad table outputs, by tweaking different configuration parameters.
You can specify the type of element you want to plot using the ``kind`` keyword argument. The generated plot can be saved to a file by passing a ``filename`` keyword argument. The following plot types are supported:
- 'text'
- 'table'
- 'grid'
- 'contour'
- 'line'
- 'joint'
.. note:: The last three geometries can only be used with :ref:`Lattice <lattice>`, i.e. when ``flavor='lattice'``.
.. note:: The last three plot types can only be used with :ref:`Lattice <lattice>`, i.e. when ``flavor='lattice'``.
Let's generate a plot for each geometry using this `PDF <../_static/pdf/foo.pdf>`__ as an example. First, let's get all the tables out.
Let's generate a plot for each type using this `PDF <../_static/pdf/foo.pdf>`__ as an example. First, let's get all the tables out.
::
@@ -50,8 +52,6 @@ Let's generate a plot for each geometry using this `PDF <../_static/pdf/foo.pdf>
>>> tables
<TableList n=1>
.. _geometry_text:
text
^^^^
@@ -59,31 +59,31 @@ Let's plot all the text present on the table's PDF page.
::
>>> tables[0].plot('text')
>>> camelot.plot(tables[0], kind='text')
>>> plt.show()
.. figure:: ../_static/png/geometry_text.png
.. figure:: ../_static/png/plot_text.png
:height: 674
:width: 1366
:scale: 50%
:alt: A plot of all text on a PDF page
:align: left
This, as we shall later see, is very helpful with :ref:`Stream <stream>`, for noting table areas and column separators, in case Stream does not guess them correctly.
This, as we shall later see, is very helpful with :ref:`Stream <stream>` for noting table areas and column separators, in case Stream does not guess them correctly.
.. note:: The *x-y* coordinates shown aboe change as you move your mouse cursor on the image, which can help you note coordinates.
.. _geometry_table:
.. note:: The *x-y* coordinates shown above change as you move your mouse cursor on the image, which can help you note coordinates.
table
^^^^^
Let's plot the table (to see if it was detected correctly or not). This geometry type, along with contour, line and joint is useful for debugging and improving the extraction output, in case the table wasn't detected correctly. More on that later.
Let's plot the table (to see if it was detected correctly or not). This plot type, along with contour, line and joint is useful for debugging and improving the extraction output, in case the table wasn't detected correctly. (More on that later.)
::
>>> tables[0].plot('table')
>>> camelot.plot(tables[0], kind='table')
>>> plt.show()
.. figure:: ../_static/png/geometry_table.png
.. figure:: ../_static/png/plot_table.png
:height: 674
:width: 1366
:scale: 50%
@@ -92,8 +92,6 @@ Let's plot the table (to see if it was detected correctly or not). This geometry
The table is perfect!
.. _geometry_contour:
contour
^^^^^^^
@@ -101,17 +99,16 @@ Now, let's plot all table boundaries present on the table's PDF page.
::
>>> tables[0].plot('contour')
>>> camelot.plot(tables[0], kind='contour')
>>> plt.show()
.. figure:: ../_static/png/geometry_contour.png
.. figure:: ../_static/png/plot_contour.png
:height: 674
:width: 1366
:scale: 50%
:alt: A plot of all contours on a PDF page
:align: left
.. _geometry_line:
line
^^^^
@@ -119,17 +116,16 @@ Cool, let's plot all line segments present on the table's PDF page.
::
>>> tables[0].plot('line')
>>> camelot.plot(tables[0], kind='line')
>>> plt.show()
.. figure:: ../_static/png/geometry_line.png
.. figure:: ../_static/png/plot_line.png
:height: 674
:width: 1366
:scale: 50%
:alt: A plot of all lines on a PDF page
:align: left
.. _geometry_joint:
joint
^^^^^
@@ -137,9 +133,10 @@ Finally, let's plot all line intersections present on the table's PDF page.
::
>>> tables[0].plot('joint')
>>> camelot.plot(tables[0], kind='joint')
>>> plt.show()
.. figure:: ../_static/png/geometry_joint.png
.. figure:: ../_static/png/plot_joint.png
:height: 674
:width: 1366
:scale: 50%
@@ -149,7 +146,7 @@ Finally, let's plot all line intersections present on the table's PDF page.
Specify table areas
-------------------
Since :ref:`Stream <stream>` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can :ref:`plot the text <geometry_text>` on this page and note the left-top and right-bottom coordinates of the table.
Since :ref:`Stream <stream>` treats the whole page as a table, `for now`_, it's useful to specify table boundaries in cases such as `these <../_static/pdf/table_areas.pdf>`__. You can plot the text on this page and note the top left and bottom right coordinates of the table.
Table areas that you want Camelot to analyze can be passed as a list of comma-separated strings to :meth:`read_pdf() <camelot.read_pdf>`, using the ``table_areas`` keyword argument.
@@ -166,15 +163,15 @@ Table areas that you want Camelot to analyze can be passed as a list of comma-se
Specify column separators
-------------------------
In cases like `these <../_static/pdf/column_separators.pdf>`__, where the text is very close to each other, it is possible that Camelot may guess the column separators' coordinates incorrectly. To correct this, you can explicitly specify the *x* coordinate for each column separator by :ref:`plotting the text <geometry_text>` on the page.
In cases like `these <../_static/pdf/column_separators.pdf>`__, where the text is very close to each other, it is possible that Camelot may guess the column separators' coordinates incorrectly. To correct this, you can explicitly specify the *x* coordinate for each column separator by plotting the text on the page.
You can pass the column separators as a list of comma-separated strings to :meth:`read_pdf() <camelot.read_pdf>`, using the ``columns`` keyword argument.
In case you passed a single column separators string list, and no table area is specified, the separators will be applied to the whole page. When a list of table areas is specified and there is a need to specify column separators as well, **the length of both lists should be equal**. Each table area will be mapped to each column separators' string using their indices.
In case you passed a single column separators string list, and no table area is specified, the separators will be applied to the whole page. When a list of table areas is specified and you need to specify column separators as well, **the length of both lists should be equal**. Each table area will be mapped to each column separators' string using their indices.
For example, if you have specified two table areas, ``table_areas=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list, like this, ``columns=['10,120,200,400', '']``.
For example, if you have specified two table areas, ``table_areas=['12,23,43,54', '20,33,55,67']``, and only want to specify column separators for the first table, you can pass an empty string for the second table in the column separators' list like this, ``columns=['10,120,200,400', '']``.
Let's get back to the *x* coordinates we got from :ref:`plotting text <geometry_text>` that exists on this `PDF <../_static/pdf/column_separators.pdf>`__, and get the table out!
Let's get back to the *x* coordinates we got from plotting the text that exists on this `PDF <../_static/pdf/column_separators.pdf>`__, and get the table out!
::
@@ -188,12 +185,12 @@ Let's get back to the *x* coordinates we got from :ref:`plotting text <geometry_
"NUMBER TYPE DBA NAME","","","LICENSEE NAME","ADDRESS","CITY","ST","ZIP","PHONE NUMBER","EXPIRES"
"...","...","...","...","...","...","...","...","...","..."
Ah! Since `PDFMiner <https://euske.github.io/pdfminer/>`_ merged the strings, "NUMBER", "TYPE" and "DBA NAME"; all of them were assigned to the same cell. Let's see how we can fix this in the next section.
Ah! Since `PDFMiner <https://euske.github.io/pdfminer/>`_ merged the strings, "NUMBER", "TYPE" and "DBA NAME", all of them were assigned to the same cell. Let's see how we can fix this in the next section.
Split text along separators
---------------------------
To deal with cases like the output from the previous section, you can pass ``split_text=True`` to :meth:`read_pdf() <camelot.read_pdf>`, which will split any strings that lie in different cells but have been assigned to the a single cell (as a result of being merged together by `PDFMiner <https://euske.github.io/pdfminer/>`_).
To deal with cases like the output from the previous section, you can pass ``split_text=True`` to :meth:`read_pdf() <camelot.read_pdf>`, which will split any strings that lie in different cells but have been assigned to a single cell (as a result of being merged together by `PDFMiner <https://euske.github.io/pdfminer/>`_).
::
@@ -210,13 +207,13 @@ To deal with cases like the output from the previous section, you can pass ``spl
Flag superscripts and subscripts
--------------------------------
There might be cases where you want to differentiate between the text, and superscripts or subscripts, like this `PDF <../_static/pdf/superscript.pdf>`_.
There might be cases where you want to differentiate between the text and superscripts or subscripts, like this `PDF <../_static/pdf/superscript.pdf>`_.
.. figure:: ../_static/png/superscript.png
:alt: A PDF with superscripts
:align: left
In this case, the text that `other tools`_ return, will be ``24.912``. This is harmless as long as there is that decimal point involved. But when it isn't there, you'll be left wondering why the results of your data analysis were 10x bigger!
In this case, the text that `other tools`_ return, will be ``24.912``. This is relatively harmless when that decimal point is involved. But when it isn't there, you'll be left wondering why the results of your data analysis are 10x bigger!
You can solve this by passing ``flag_size=True``, which will enclose the superscripts and subscripts with ``<s></s>``, based on font size, as shown below.
@@ -282,23 +279,25 @@ Here's a `PDF <../_static/pdf/short_lines.pdf>`__ where small lines separating t
:alt: A PDF table with short lines
:align: left
Let's :ref:`plot the table <geometry_table>` for this PDF.
Let's plot the table for this PDF.
::
>>> tables = camelot.read_pdf('short_lines.pdf')
>>> tables[0].plot('table')
>>> camelot.plot(tables[0], kind='table')
>>> plt.show()
.. figure:: ../_static/png/short_lines_1.png
:alt: A plot of the PDF table with short lines
:align: left
Clearly, the smaller lines separating the headers, couldn't be detected. Let's try with ``line_size_scaling=40``, and `plot the table <geometry_table>`_ again.
Clearly, the smaller lines separating the headers, couldn't be detected. Let's try with ``line_size_scaling=40``, and plot the table again.
::
>>> tables = camelot.read_pdf('short_lines.pdf', line_size_scaling=40)
>>> tables[0].plot('table')
>>> camelot.plot(tables[0], kind='table')
>>> plt.show()
.. figure:: ../_static/png/short_lines_2.png
:alt: An improved plot of the PDF table with short lines
@@ -327,7 +326,7 @@ Voila! Camelot can now see those lines. Let's get our table.
Shift text in spanning cells
----------------------------
By default, the :ref:`Lattice <lattice>` method shifts text in spanning cells, first to the left and then to the top, as you can observe in the output table above. However, this behavior can be changed using the ``shift_text`` keyword argument. Think of it as setting the *gravity* for a table, it decides the direction in which the text will move and finally come to rest.
By default, the :ref:`Lattice <lattice>` method shifts text in spanning cells, first to the left and then to the top, as you can observe in the output table above. However, this behavior can be changed using the ``shift_text`` keyword argument. Think of it as setting the *gravity* for a table it decides the direction in which the text will move and finally come to rest.
``shift_text`` expects a list with one or more characters from the following set: ``('', l', 'r', 't', 'b')``, which are then applied *in order*. The default, as we discussed above, is ``['l', 't']``.
@@ -356,7 +355,7 @@ We'll use the `PDF <../_static/pdf/short_lines.pdf>`__ from the previous example
"Knowledge &Practices on HTN &","2400","Men (≥ 18 yrs)","-","-","-","1728"
"DM","2400","Women (≥ 18 yrs)","-","-","-","1728"
No surprises there, it did remain in place (observe the strings "2400" and "All the available individuals"). Let's pass ``shift_text=['r', 'b']``, to set the *gravity* to right-bottom, and move the text in that direction.
No surprises there it did remain in place (observe the strings "2400" and "All the available individuals"). Let's pass ``shift_text=['r', 'b']`` to set the *gravity* to right-bottom and move the text in that direction.
::
@@ -380,7 +379,7 @@ No surprises there, it did remain in place (observe the strings "2400" and "All
Copy text in spanning cells
---------------------------
You can copy text in spanning cells when using :ref:`Lattice <lattice>`, in either horizontal or vertical direction, or both. This behavior is disabled by default.
You can copy text in spanning cells when using :ref:`Lattice <lattice>`, in either the horizontal or vertical direction, or both. This behavior is disabled by default.
``copy_text`` expects a list with one or more characters from the following set: ``('v', 'h')``, which are then applied *in order*.
@@ -416,4 +415,4 @@ We don't need anything else. Now, let's pass ``copy_text=['v']`` to copy text in
"3","Odisha","Kalahandi","iii. Food Poisoning","42","0","02/01/14","03/01/14","Under control","..."
"4","West Bengal","West Medinipur","iv. Acute Diarrhoeal Disease","145","0","04/01/14","05/01/14","Under control","..."
"4","West Bengal","Birbhum","v. Food Poisoning","199","0","31/12/13","31/12/13","Under control","..."
"4","West Bengal","Howrah","vi. Viral Hepatitis A &E","85","0","26/12/13","27/12/13","Under surveillance","..."
"4","West Bengal","Howrah","vi. Viral Hepatitis A &E","85","0","26/12/13","27/12/13","Under surveillance","..."
+6 -4
View File
@@ -1,22 +1,23 @@
.. _cli:
Command-line interface
Command-Line Interface
======================
Camelot comes with a command-line interface.
You can print the help for the interface, by typing ``camelot --help`` in your favorite terminal program, as shown below. Furthermore, you can print the help for each command, by typing ``camelot <command> --help``, try it out!
You can print the help for the interface by typing ``camelot --help`` in your favorite terminal program, as shown below. Furthermore, you can print the help for each command by typing ``camelot <command> --help``. Try it out!
::
Usage: camelot [OPTIONS] COMMAND [ARGS]...
Camelot: PDF Table Extraction for Humans
Camelot: PDF Table Extraction for Humans
Options:
--version Show the version and exit.
-p, --pages TEXT Comma-separated page numbers. Example: 1,3,4
or 1,4-end.
-pw, --password TEXT Password for decryption.
-o, --output TEXT Output file path.
-f, --format [csv|json|excel|html]
Output file format.
@@ -27,8 +28,9 @@ You can print the help for the interface, by typing ``camelot --help`` in your f
-M, --margins <FLOAT FLOAT FLOAT>...
PDFMiner char_margin, line_margin and
word_margin.
-q, --quiet Suppress warnings.
--help Show this message and exit.
Commands:
lattice Use lines between text to parse the table.
stream Use spaces between text to parse the table.
stream Use spaces between text to parse the table.
+12 -12
View File
@@ -3,9 +3,9 @@
How It Works
============
This part of the documentation details a high-level explanation of how Camelot extracts tables from PDF files.
This part of the documentation includes a high-level explanation of how Camelot extracts tables from PDF files.
You can choose between two table parsing methods, *Stream* and *Lattice*. The naming for parsing methods inside Camelot (i.e. Stream and Lattice) was inspired from `Tabula`_.
You can choose between two table parsing methods, *Stream* and *Lattice*. These names for parsing methods inside Camelot were inspired from `Tabula`_.
.. _Tabula: https://github.com/tabulapdf/tabula
@@ -16,7 +16,7 @@ Stream
Stream can be used to parse tables that have whitespaces between cells to simulate a table structure. It looks for these spaces between text to form a table representation.
It is built on top of PDFMiner's functionality of grouping characters on a page into words and sentences, using `margins`_. After getting the words given on a page, it groups them into rows based on their *y* coordinates and tries to guess the number of columns the table might have by calculating the mode of the number of words in each row. This mode is used to calculate *x* ranges for the table's columns. It then adds columns to this column range list based on any words that may lie outside or inside the current column *x* ranges.
It is built on top of PDFMiner's functionality of grouping characters on a page into words and sentences, using `margins`_. After getting the words on a page, it groups them into rows based on their *y* coordinates. It then tries to guess the number of columns the table might have by calculating the mode of the number of words in each row. This mode is used to calculate *x* ranges for the table's columns. It then adds columns to this column range list based on any words that may lie outside or inside the current column *x* ranges.
.. _margins: https://euske.github.io/pdfminer/#tools
@@ -29,9 +29,9 @@ It is built on top of PDFMiner's functionality of grouping characters on a page
Lattice
-------
Lattice is more deterministic in nature, and does not rely on guesses. It can be used to parse tables that have demarcated lines between cells, and can automatically parse multiple tables present on a page.
Lattice is more deterministic in nature, and it does not rely on guesses. It can be used to parse tables that have demarcated lines between cells, and it can automatically parse multiple tables present on a page.
It starts by converting the PDF page to an image using ghostscript and then processing it to get horizontal and vertical line segments by applying a set of morphological transformations (erosion and dilation) using OpenCV.
It starts by converting the PDF page to an image using ghostscript, and then processes it to get horizontal and vertical line segments by applying a set of morphological transformations (erosion and dilation) using OpenCV.
Let's see how Lattice processes the second page of `this PDF`_, step-by-step.
@@ -39,7 +39,7 @@ Let's see how Lattice processes the second page of `this PDF`_, step-by-step.
1. Line segments are detected.
.. image:: ../_static/png/geometry_line.png
.. image:: ../_static/png/plot_line.png
:height: 674
:width: 1366
:scale: 50%
@@ -49,23 +49,23 @@ Let's see how Lattice processes the second page of `this PDF`_, step-by-step.
.. _and: https://en.wikipedia.org/wiki/Logical_conjunction
.. image:: ../_static/png/geometry_joint.png
.. image:: ../_static/png/plot_joint.png
:height: 674
:width: 1366
:scale: 50%
:align: left
3. Table boundaries are computed, by overlapping the detected line segments again, this time by "`or`_"ing their pixel intensities.
3. Table boundaries are computed by overlapping the detected line segments again, this time by "`or`_"ing their pixel intensities.
.. _or: https://en.wikipedia.org/wiki/Logical_disjunction
.. image:: ../_static/png/geometry_contour.png
.. image:: ../_static/png/plot_contour.png
:height: 674
:width: 1366
:scale: 50%
:align: left
4. Since dimensions of the PDF page and its image vary; the detected table boundaries, line intersections and line segments are scaled and translated to the PDF page's coordinate space, and a representation of the table is created.
4. Since dimensions of the PDF page and its image vary, the detected table boundaries, line intersections, and line segments are scaled and translated to the PDF page's coordinate space, and a representation of the table is created.
.. image:: ../_static/png/table.png
:height: 674
@@ -75,10 +75,10 @@ Let's see how Lattice processes the second page of `this PDF`_, step-by-step.
5. Spanning cells are detected using the line segments and line intersections.
.. image:: ../_static/png/geometry_table.png
.. image:: ../_static/png/plot_table.png
:height: 674
:width: 1366
:scale: 50%
:align: left
6. Finally, the words found on the page are assigned to the table's cells based on their *x* and *y* coordinates.
6. Finally, the words found on the page are assigned to the table's cells based on their *x* and *y* coordinates.
+89 -15
View File
@@ -3,40 +3,114 @@
Installation of Camelot
=======================
This part of the documentation covers the installation of Camelot. First, you'll need to install the dependencies, which include `tk`_ and `ghostscript`_.
This part of the documentation covers how to install Camelot.
.. _tk: https://packages.ubuntu.com/trusty/python-tk
.. _ghostscript: https://www.ghostscript.com/
Using conda
-----------
These can be installed using your system's package manager. You can run the following based on your OS.
The easiest way to install Camelot is to install it with `conda`_, which is a package manager and environment management system for the `Anaconda`_ distribution.
::
For Ubuntu::
$ conda install -c conda-forge camelot-py
.. note:: Camelot is available for Python 2.7, 3.5 and 3.6 on Linux, macOS and Windows. For Windows, you will need to install ghostscript which you can get from their `downloads page`_.
.. _conda: https://conda.io/docs/
.. _Anaconda: http://docs.continuum.io/anaconda/
.. _downloads page: https://www.ghostscript.com/download/gsdnld.html
.. _conda-forge: https://conda-forge.org/
Using pip
---------
First, you'll need to install the dependencies, which include `Tkinter`_ and `ghostscript`_.
.. _Tkinter: https://wiki.python.org/moin/TkInter
.. _ghostscript: https://www.ghostscript.com
These can be installed using your system's package manager. You can run one of the following, based on your OS.
For Ubuntu
^^^^^^^^^^
::
$ apt install python-tk ghostscript
For macOS::
Or for Python 3::
$ apt install python3-tk ghostscript
For macOS
^^^^^^^^^
::
$ brew install tcl-tk ghostscript
$ pip install camelot-py
------------------------
For Windows
^^^^^^^^^^^
After installing the dependencies, you can simply use pip to install Camelot::
For Tkinter, you can download the `ActiveTcl Community Edition`_ from ActiveState. For ghostscript, you can get the installer at the `ghostscript downloads page`_.
$ pip install camelot-py
After installing ghostscript, you'll need to reboot your system to make sure that the ghostscript executable's path is in the windows PATH environment variable. In case you don't want to reboot, you can manually add the ghostscript executable's path to the PATH variable, `as shown here`_.
Get the Source Code
-------------------
.. _ActiveTcl Community Edition: https://www.activestate.com/activetcl/downloads
.. _ghostscript downloads page: https://www.ghostscript.com/download/gsdnld.html
.. _as shown here: https://java.com/en/download/help/path.xml
Alternatively, you can install from source by:
----
You can do the following checks to see if the dependencies were installed correctly.
For Tkinter
^^^^^^^^^^^
Launch Python, and then at the prompt, type::
>>> import Tkinter
Or in Python 3::
>>> import tkinter
If you have Tkinter, Python will not print an error message, and if not, you will see an ``ImportError``.
For ghostscript
^^^^^^^^^^^^^^^
Run the following to check the ghostscript version.
For Ubuntu/macOS::
$ gs -version
For Windows::
C:\> gswin64c.exe -version
Or for Windows 32-bit::
C:\> gswin32c.exe -version
If you have ghostscript, you should see the ghostscript version and copyright information.
Finally, you can use pip to install Camelot::
$ pip install camelot-py[cv]
From the source code
--------------------
After `installing the dependencies`_, you can install from the source by:
1. Cloning the GitHub repository.
::
$ git clone https://www.github.com/socialcopsdev/camelot
2. And then simply using pip again.
2. Then simply using pip again.
::
$ cd camelot
$ pip install .
$ pip install ".[cv]"
.. _installing the dependencies: https://camelot-py.readthedocs.io/en/master/user/install.html#using-pip
+10 -7
View File
@@ -6,20 +6,20 @@ Introduction
The Camelot Project
-------------------
The Portable Document Format (PDF) was born out of `The Camelot Project`_ when a need was felt for "a universal to communicate documents across a wide variety of machine configurations, operating systems and communication networks". The goal was to make these documents viewable on any display and printable on any modern printers. The invention of the `PostScript`_ page description language, which enabled the creation of *fixed-layout* flat documents (with text, fonts, graphics, images encapsulated), solved the problem.
The PDF (Portable Document Format) was born out of `The Camelot Project`_ to create "a universal way to communicate documents across a wide variety of machine configurations, operating systems and communication networks". The goal was to make these documents viewable on any display and printable on any modern printers. The invention of the `PostScript`_ page description language, which enabled the creation of *fixed-layout* flat documents (with text, fonts, graphics, images encapsulated), solved this problem.
At a very high level, PostScript defines instructions, such as, "place this character at this x,y coordinate on a plane". Spaces can be *simulated* by placing characters relatively far apart. Extending from that, tables can be *simulated* by placing characters (which constitute words) in two-dimensional grids. A PDF viewer just takes these instructions and draws everything for the user to view. Since it's just characters on a plane, there is no table data structure which can be extracted and used for analysis!
At a high level, PostScript defines instructions, such as "place this character at this *x,y* coordinate on a plane". Spaces can be *simulated* by placing characters relatively far apart. Extending from that, tables can be *simulated* by placing characters (which constitute words) in two-dimensional grids. A PDF viewer just takes these instructions and draws everything for the user to view. Since a PDF is just characters on a plane, there is no table data structure that can be extracted and used for analysis!
Sadly, a lot of open data is given out as tables which are trapped inside PDF files.
Sadly, a lot of today's open data is trapped in PDF tables.
.. _PostScript: http://www.planetpdf.com/planetpdf/pdfs/warnock_camelot.pdf
Why another PDF Table Extraction library?
Why another PDF table extraction library?
-----------------------------------------
There are both open (`Tabula`_, `pdf-table-extract`_) and closed-source (`smallpdf`_, `PDFTables`_) tools that are widely used, to extract tables from PDF files. They either give a nice output, or fail miserably. There is no in-between. This is not helpful, since everything in the real world, including PDF table extraction, is fuzzy, leading to creation of adhoc table extraction scripts for each different type of PDF that the user wants to parse.
There are both open (`Tabula`_, `pdf-table-extract`_) and closed-source (`smallpdf`_, `PDFTables`_) tools that are widely used to extract tables from PDF files. They either give a nice output or fail miserably. There is no in between. This is not helpful since everything in the real world, including PDF table extraction, is fuzzy. This leads to the creation of ad-hoc table extraction scripts for each type of PDF table.
Camelot was created with the goal of offering its users complete control over table extraction. If the users are not able to get the desired output with the default configuration, they should be able to tweak it and get the job done!
Camelot was created to offer users complete control over table extraction. If you can't get your desired output with the default settings, you can tweak them and get the job done!
Here is a `comparison`_ of Camelot's output with outputs from other open-source PDF parsing libraries and tools.
@@ -32,10 +32,13 @@ Here is a `comparison`_ of Camelot's output with outputs from other open-source
What's in a name?
-----------------
As you can already guess, this library is named after `The Camelot Project`_. Fun fact, "Camelot" is the name of the castle in `Monty Python and the Holy Grail`_, where Arthur leads his men, the Knights of the Round Table, and then sets off elsewhere after deciding that it is "a silly place". Interestingly, the language in which this library is written (Python) was named after Monty Python.
As you can already guess, this library is named after `The Camelot Project`_.
Fun fact: In the British comedy film `Monty Python and the Holy Grail`_ (and in the `Arthurian legend`_ depicted in the film), "Camelot" is the name of the castle where Arthur leads his men, the Knights of the Round Table, and then sets off elsewhere after deciding that it is "a silly place". Interestingly, the language in which this library is written (Python) was named after Monty Python.
.. _The Camelot Project: http://www.planetpdf.com/planetpdf/pdfs/warnock_camelot.pdf
.. _Monty Python and the Holy Grail: https://en.wikipedia.org/wiki/Monty_Python_and_the_Holy_Grail
.. _Arthurian legend: https://en.wikipedia.org/wiki/King_Arthur
Camelot License
---------------
+29 -7
View File
@@ -3,7 +3,7 @@
Quickstart
==========
In a hurry to extract tables from PDFs? This document gives a good introduction to help you get started with using Camelot.
In a hurry to extract tables from PDFs? This document gives a good introduction to help you get started with Camelot.
Read the PDF
------------
@@ -14,7 +14,7 @@ Begin by importing the Camelot module::
>>> import camelot
Now, let's try to read a PDF. You can check out the PDF used in this example, `here`_. Since the PDF has a table with clearly demarcated lines, we will use the :ref:`Lattice <lattice>` method here. To do that we will set the ``mesh`` keyword argument to ``True``.
Now, let's try to read a PDF. (You can check out the PDF used in this example `here`_.) Since the PDF has a table with clearly demarcated lines, we will use the :ref:`Lattice <lattice>` method here. To do that, we will set the ``mesh`` keyword argument to ``True``.
.. note:: :ref:`Lattice <lattice>` is used by default. You can use :ref:`Stream <stream>` with ``flavor='stream'``.
@@ -47,7 +47,7 @@ Let's print the parsing report.
'page': 1
}
Woah! The accuracy is top-notch and whitespace is less, that means the table was extracted correctly (most probably). You can access the table as a pandas DataFrame by using the :class:`table <camelot.core.Table>` object's ``df`` property.
Woah! The accuracy is top-notch and there is less whitespace, which means the table was most likely extracted correctly. You can access the table as a pandas DataFrame by using the :class:`table <camelot.core.Table>` object's ``df`` property.
::
@@ -56,7 +56,7 @@ Woah! The accuracy is top-notch and whitespace is less, that means the table was
.. csv-table::
:file: ../_static/csv/foo.csv
Looks good! You can be export the table as a CSV file using its :meth:`to_csv() <camelot.core.Table.to_csv>` method. Alternatively you can use :meth:`to_json() <camelot.core.Table.to_json>`, :meth:`to_excel() <camelot.core.Table.to_excel>` or :meth:`to_html() <camelot.core.Table.to_html>` methods to export the table as JSON, Excel and HTML files respectively.
Looks good! You can now export the table as a CSV file using its :meth:`to_csv() <camelot.core.Table.to_csv>` method. Alternatively you can use :meth:`to_json() <camelot.core.Table.to_json>`, :meth:`to_excel() <camelot.core.Table.to_excel>` or :meth:`to_html() <camelot.core.Table.to_html>` methods to export the table as JSON, Excel and HTML files respectively.
::
@@ -85,8 +85,30 @@ By default, Camelot only uses the first page of the PDF to extract tables. To sp
>>> camelot.read_pdf('your.pdf', pages='1,2,3')
The ``pages`` keyword argument accepts pages as comma-separated string of page numbers. You can also specify page ranges, for example ``pages=1,4-10,20-30`` or ``pages=1,4-10,20-end``.
The ``pages`` keyword argument accepts pages as comma-separated string of page numbers. You can also specify page ranges for example, ``pages=1,4-10,20-30`` or ``pages=1,4-10,20-end``.
------------------------
Reading encrypted PDFs
----------------------
Ready for more? Check out the :ref:`advanced <advanced>` section.
To extract tables from encrypted PDF files you must provide a password when calling :meth:`read_pdf() <camelot.read_pdf>`.
::
>>> tables = camelot.read_pdf('foo.pdf', password='userpass')
>>> tables
<TableList n=1>
Currently Camelot only supports PDFs encrypted with ASCII passwords and algorithm `code 1 or 2`_. An exception is thrown if the PDF cannot be read. This may be due to no password being provided, an incorrect password, or an unsupported encryption algorithm.
Further encryption support may be added in future, however in the meantime if your PDF files are using unsupported encryption algorithms you are advised to remove encryption before calling :meth:`read_pdf() <camelot.read_pdf>`. This can been successfully achieved with third-party tools such as `QPDF`_.
::
$ qpdf --password=<PASSWORD> --decrypt input.pdf output.pdf
.. _code 1 or 2: https://github.com/mstamy2/PyPDF2/issues/378
.. _QPDF: https://www.github.com/qpdf/qpdf
----
Ready for more? Check out the :ref:`advanced <advanced>` section.
-3
View File
@@ -1,3 +0,0 @@
pytest==3.8.0
pytest-runner==4.2
Sphinx==1.7.9
Regular → Executable
+8 -7
View File
@@ -1,7 +1,8 @@
click==6.7
matplotlib==2.2.3
numpy==1.13.3
opencv-python==3.4.2.17
pandas==0.23.4
pdfminer==20140328
PyPDF2==1.26.0
click>=6.7
matplotlib>=2.2.3
numpy>=1.13.3
opencv-python>=3.4.2.17
openpyxl>=2.5.8
pandas>=0.23.4
pdfminer.six>=20170720
PyPDF2>=1.26.0
+1 -1
View File
@@ -2,5 +2,5 @@
test=pytest
[tool:pytest]
addopts = --verbose
addopts = --verbose --cov-config .coveragerc --cov-report term --cov-report xml --cov=camelot --mpl tests
python_files = tests/test_*.py
+42 -16
View File
@@ -2,7 +2,6 @@
import os
from setuptools import find_packages
from pkg_resources import parse_version
here = os.path.abspath(os.path.dirname(__file__))
@@ -14,29 +13,53 @@ with open('README.md', 'r') as f:
readme = f.read()
requires = [
'click>=6.7',
'numpy>=1.13.3',
'openpyxl>=2.5.8',
'pandas>=0.23.4',
'pdfminer.six>=20170720',
'PyPDF2>=1.26.0'
]
cv_requires = [
'opencv-python>=3.4.2.17'
]
plot_requires = [
'matplotlib>=2.2.3',
]
dev_requires = [
'codecov>=2.0.15',
'pytest>=3.8.0',
'pytest-cov>=2.6.0',
'pytest-mpl>=0.10',
'pytest-runner>=4.2',
'Sphinx>=1.7.9'
]
all_requires = cv_requires + plot_requires
dev_requires = dev_requires + all_requires
def setup_package():
reqs = []
with open('requirements.txt', 'r') as f:
for line in f:
reqs.append(line.strip())
dev_reqs = []
with open('requirements-dev.txt', 'r') as f:
for line in f:
dev_reqs.append(line.strip())
metadata = dict(name=about['__title__'],
version=about['__version__'],
description=about['__description__'],
long_description=readme,
long_description_content_type="text/markdown",
url=about['__url__'],
author=about['__author__'],
author_email=about['__author_email__'],
license=about['__license__'],
packages=find_packages(exclude=('tests',)),
install_requires=reqs,
install_requires=requires,
extras_require={
'dev': dev_reqs
'all': all_requires,
'cv': cv_requires,
'dev': dev_requires,
'plot': plot_requires
},
entry_points={
'console_scripts': [
@@ -47,16 +70,19 @@ def setup_package():
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2.7'
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7'
])
try:
from setuptools import setup
except:
except ImportError:
from distutils.core import setup
setup(**metadata)
if __name__ == '__main__':
setup_package()
setup_package()
+2
View File
@@ -0,0 +1,2 @@
import matplotlib
matplotlib.use('agg')
Executable
+376
View File
@@ -0,0 +1,376 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
data_stream = [
["", "Table: 5 Public Health Outlay 2012-13 (Budget Estimates) (Rs. in 000)", "", "", "", "", "", ""],
["States-A", "Revenue", "", "Capital", "", "Total", "Others(1)", "Total"],
["", "", "", "", "", "Revenue &", "", ""],
["", "Medical &", "Family", "Medical &", "Family", "", "", ""],
["", "", "", "", "", "Capital", "", ""],
["", "Public", "Welfare", "Public", "Welfare", "", "", ""],
["", "Health", "", "Health", "", "", "", ""],
["Andhra Pradesh", "47,824,589", "9,967,837", "1,275,000", "15,000", "59,082,426", "14,898,243", "73,980,669"],
["Arunachal Pradesh", "2,241,609", "107,549", "23,000", "0", "2,372,158", "86,336", "2,458,494"],
["Assam", "14,874,821", "2,554,197", "161,600", "0", "17,590,618", "4,408,505", "21,999,123"],
["Bihar", "21,016,708", "4,332,141", "5,329,000", "0", "30,677,849", "2,251,571", "32,929,420"],
["Chhattisgarh", "11,427,311", "1,415,660", "2,366,592", "0", "15,209,563", "311,163", "15,520,726"],
["Delhi", "28,084,780", "411,700", "4,550,000", "0", "33,046,480", "5,000", "33,051,480"],
["Goa", "4,055,567", "110,000", "330,053", "0", "4,495,620", "12,560", "4,508,180"],
["Gujarat", "26,328,400", "6,922,900", "12,664,000", "42,000", "45,957,300", "455,860", "46,413,160"],
["Haryana", "15,156,681", "1,333,527", "40,100", "0", "16,530,308", "1,222,698", "17,753,006"],
["Himachal Pradesh", "8,647,229", "1,331,529", "580,800", "0", "10,559,558", "725,315", "11,284,873"],
["Jammu & Kashmir", "14,411,984", "270,840", "3,188,550", "0", "17,871,374", "166,229", "18,037,603"],
["Jharkhand", "8,185,079", "3,008,077", "3,525,558", "0", "14,718,714", "745,139", "15,463,853"],
["Karnataka", "34,939,843", "4,317,801", "3,669,700", "0", "42,927,344", "631,088", "43,558,432"],
["Kerala", "27,923,965", "3,985,473", "929,503", "0", "32,838,941", "334,640", "33,173,581"],
["Madhya Pradesh", "28,459,540", "4,072,016", "3,432,711", "0", "35,964,267", "472,139", "36,436,406"],
["Maharashtra", "55,011,100", "6,680,721", "5,038,576", "0", "66,730,397", "313,762", "67,044,159"],
["Manipur", "2,494,600", "187,700", "897,400", "0", "3,579,700", "0", "3,579,700"],
["Meghalaya", "2,894,093", "342,893", "705,500", "5,000", "3,947,486", "24,128", "3,971,614"],
["Mizoram", "1,743,501", "84,185", "10,250", "0", "1,837,936", "17,060", "1,854,996"],
["Nagaland", "2,368,724", "204,329", "226,400", "0", "2,799,453", "783,054", "3,582,507"],
["Odisha", "14,317,179", "2,552,292", "1,107,250", "0", "17,976,721", "451,438", "18,428,159"],
["Puducherry", "4,191,757", "52,249", "192,400", "0", "4,436,406", "2,173", "4,438,579"],
["Punjab", "19,775,485", "2,208,343", "2,470,882", "0", "24,454,710", "1,436,522", "25,891,232"],
["", "Health Sector Financing by Centre and States/UTs in India [2009-10 to 2012-13](Revised) P a g e |23", "", "", "", "", "", ""]
]
data_stream_table_rotated = [
["", "", "Table 21 Current use of contraception by background characteristics\u2014Continued", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "", "", "", "", "", "Modern method", "", "", "", "", "", "", "Traditional method", "", "", "", ""],
["", "", "", "Any", "", "", "", "", "", "", "Other", "Any", "", "", "", "Not", "", "Number"],
["", "", "Any", "modern", "Female", "Male", "", "", "", "Condom/", "modern", "traditional", "", "With-", "Folk", "currently", "", "of"],
["", "Background characteristic", "method", "method", "sterilization", "sterilization", "Pill", "IUD", "Injectables", "Nirodh", "method", "method", "Rhythm", "drawal", "method", "using", "Total", "women"],
["", "Caste/tribe", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "Scheduled caste", "74.8", "55.8", "42.9", "0.9", "9.7", "0.0", "0.2", "2.2", "0.0", "19.0", "11.2", "7.4", "0.4", "25.2", "100.0", "1,363"],
["", "Scheduled tribe", "59.3", "39.0", "26.8", "0.6", "6.4", "0.6", "1.2", "3.5", "0.0", "20.3", "10.4", "5.8", "4.1", "40.7", "100.0", "256"],
["", "Other backward class", "71.4", "51.1", "34.9", "0.0", "8.6", "1.4", "0.0", "6.2", "0.0", "20.4", "12.6", "7.8", "0.0", "28.6", "100.0", "211"],
["", "Other", "71.1", "48.8", "28.2", "0.8", "13.3", "0.9", "0.3", "5.2", "0.1", "22.3", "12.9", "9.1", "0.3", "28.9", "100.0", "3,319"],
["", "Wealth index", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "Lowest", "64.5", "48.6", "34.3", "0.5", "10.5", "0.6", "0.7", "2.0", "0.0", "15.9", "9.9", "4.6", "1.4", "35.5", "100.0", "1,258"],
["", "Second", "68.5", "50.4", "36.2", "1.1", "11.4", "0.5", "0.1", "1.1", "0.0", "18.1", "11.2", "6.7", "0.2", "31.5", "100.0", "1,317"],
["", "Middle", "75.5", "52.8", "33.6", "0.6", "14.2", "0.4", "0.5", "3.4", "0.1", "22.7", "13.4", "8.9", "0.4", "24.5", "100.0", "1,018"],
["", "Fourth", "73.9", "52.3", "32.0", "0.5", "12.5", "0.6", "0.2", "6.3", "0.2", "21.6", "11.5", "9.9", "0.2", "26.1", "100.0", "908"],
["", "Highest", "78.3", "44.4", "19.5", "1.0", "9.7", "1.4", "0.0", "12.7", "0.0", "33.8", "18.2", "15.6", "0.0", "21.7", "100.0", "733"],
["", "Number of living children", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "No children", "25.1", "7.6", "0.3", "0.5", "2.0", "0.0",
"0.0", "4.8", "0.0", "17.5", "9.0", "8.5", "0.0", "74.9", "100.0", "563"],
["", "1 child", "66.5", "32.1", "3.7", "0.7", "20.1", "0.7", "0.1", "6.9", "0.0", "34.3", "18.9", "15.2", "0.3", "33.5", "100.0", "1,190"],
["\x18\x18", "1 son", "66.8", "33.2", "4.1", "0.7", "21.1", "0.5", "0.3", "6.6", "0.0", "33.5", "21.2", "12.3", "0.0", "33.2", "100.0", "672"],
["", "No sons", "66.1", "30.7", "3.1", "0.6", "18.8", "0.8", "0.0", "7.3", "0.0", "35.4", "15.8", "19.0", "0.6", "33.9", "100.0", "517"],
["", "2 children", "81.6", "60.5", "41.8", "0.9", "11.6", "0.8", "0.3", "4.8", "0.2", "21.1", "12.2", "8.3", "0.6", "18.4", "100.0", "1,576"],
["", "1 or more sons", "83.7", "64.2", "46.4", "0.9", "10.8", "0.8", "0.4", "4.8", "0.1", "19.5", "11.1", "7.6", "0.7", "16.3", "100.0", "1,268"],
["", "No sons", "73.2", "45.5", "23.2", "1.0", "15.1", "0.9", "0.0", "4.8", "0.5", "27.7", "16.8", "11.0", "0.0", "26.8", "100.0", "308"],
["", "3 children", "83.9", "71.2", "57.7", "0.8", "9.8", "0.6", "0.5", "1.8", "0.0", "12.7", "8.7", "3.3", "0.8", "16.1", "100.0", "961"],
["", "1 or more sons", "85.0", "73.2", "60.3", "0.9", "9.4", "0.5", "0.5", "1.6", "0.0", "11.8", "8.1", "3.0", "0.7", "15.0", "100.0", "860"],
["", "No sons", "74.7", "53.8", "35.3", "0.0", "13.7", "1.6", "0.0", "3.2", "0.0", "20.9", "13.4", "6.1", "1.5", "25.3", "100.0", "101"],
["", "4+ children", "74.3", "58.1", "45.1", "0.6", "8.7", "0.6", "0.7", "2.4", "0.0", "16.1", "9.9", "5.4", "0.8", "25.7", "100.0", "944"],
["", "1 or more sons", "73.9", "58.2", "46.0", "0.7", "8.3", "0.7", "0.7", "1.9", "0.0", "15.7", "9.4", "5.5", "0.8", "26.1", "100.0", "901"],
["", "No sons", "(82.1)", "(57.3)", "(25.6)", "(0.0)", "(17.8)", "(0.0)", "(0.0)", "(13.9)", "(0.0)", "(24.8)", "(21.3)", "(3.5)", "(0.0)", "(17.9)", "100.0", "43"],
["", "Total", "71.2", "49.9", "32.2",
"0.7", "11.7", "0.6", "0.3", "4.3", "0.1", "21.3", "12.3", "8.4", "0.5", "28.8", "100.0", "5,234"],
["", "NFHS-2 (1998-99)", "66.6", "47.3", "32.0", "1.8", "9.2", "1.4", "na", "2.9", "na", "na", "8.7", "9.8", "na", "33.4", "100.0", "4,116"],
["", "NFHS-1 (1992-93)", "57.7", "37.6", "26.5", "4.3", "3.6", "1.3", "0.1", "1.9", "na", "na", "11.3", "8.3", "na", "42.3", "100.0", "3,970"],
["", "", "Note: If more than one method is used, only the most effective method is considered in this tabulation. Total includes women for whom caste/tribe was not known or is missing, who are", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "not shown separately.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "na = Not available", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "", "ns = Not shown; see table 2b, footnote 1", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "( ) Based on 25-49 unweighted cases.", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
["", "", "", "", "", "", "", "", "54", "", "", "", "", "", "", "", "", ""]
]
data_stream_table_areas = [
["", "One Withholding"],
["Payroll Period", "Allowance"],
["Weekly", "$71.15"],
["Biweekly", "142.31"],
["Semimonthly", "154.17"],
["Monthly", "308.33"],
["Quarterly", "925.00"],
["Semiannually", "1,850.00"],
["Annually", "3,700.00"],
["Daily or Miscellaneous", "14.23"],
["(each day of the payroll period)", ""]
]
data_stream_columns = [
["Clave", "Nombre Entidad", "Clave", "Nombre Municipio", "Clave", "Nombre Localidad"],
["Entidad", "", "Municipio", "", "Localidad", ""],
["01", "Aguascalientes", "001", "Aguascalientes", "0094", "Granja Adelita"],
["01", "Aguascalientes", "001", "Aguascalientes", "0096", "Agua Azul"],
["01", "Aguascalientes", "001", "Aguascalientes", "0100", "Rancho Alegre"],
["01", "Aguascalientes", "001", "Aguascalientes", "0102", "Los Arbolitos [Rancho]"],
["01", "Aguascalientes", "001", "Aguascalientes", "0104", "Ardillas de Abajo (Las Ardillas)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0106", "Arellano"],
["01", "Aguascalientes", "001", "Aguascalientes", "0112", "Baj\xedo los V\xe1zquez"],
["01", "Aguascalientes", "001", "Aguascalientes", "0113", "Baj\xedo de Montoro"],
["01", "Aguascalientes", "001", "Aguascalientes", "0114", "Residencial San Nicol\xe1s [Ba\xf1os la Cantera]"],
["01", "Aguascalientes", "001", "Aguascalientes", "0120", "Buenavista de Pe\xf1uelas"],
["01", "Aguascalientes", "001", "Aguascalientes", "0121", "Cabecita 3 Mar\xedas (Rancho Nuevo)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0125", "Ca\xf1ada Grande de Cotorina"],
["01", "Aguascalientes", "001", "Aguascalientes", "0126", "Ca\xf1ada Honda [Estaci\xf3n]"],
["01", "Aguascalientes", "001", "Aguascalientes", "0127", "Los Ca\xf1os"],
["01", "Aguascalientes", "001", "Aguascalientes", "0128", "El Cari\xf1\xe1n"],
["01", "Aguascalientes", "001", "Aguascalientes", "0129", "El Carmen [Granja]"],
["01", "Aguascalientes", "001", "Aguascalientes", "0135", "El Cedazo (Cedazo de San Antonio)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0138", "Centro de Arriba (El Taray)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0139", "Cieneguilla (La Lumbrera)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0141", "Cobos"],
["01", "Aguascalientes", "001", "Aguascalientes", "0144", "El Colorado (El Soyatal)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0146", "El Conejal"],
["01", "Aguascalientes", "001", "Aguascalientes", "0157", "Cotorina de Abajo"],
["01", "Aguascalientes", "001", "Aguascalientes", "0162", "Coyotes"],
["01", "Aguascalientes", "001", "Aguascalientes", "0166", "La Huerta (La Cruz)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0170", "Cuauht\xe9moc (Las Palomas)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0171", "Los Cuervos (Los Ojos de Agua)"],
["01", "Aguascalientes", "001", "Aguascalientes", "0172", "San Jos\xe9 [Granja]"],
["01", "Aguascalientes", "001", "Aguascalientes", "0176", "La Chiripa"],
["01", "Aguascalientes", "001", "Aguascalientes", "0182", "Dolores"],
["01", "Aguascalientes", "001", "Aguascalientes", "0183", "Los Dolores"],
["01", "Aguascalientes", "001", "Aguascalientes", "0190", "El Duraznillo"],
["01", "Aguascalientes", "001", "Aguascalientes", "0191", "Los Dur\xf3n"],
["01", "Aguascalientes", "001", "Aguascalientes", "0197", "La Escondida"],
["01", "Aguascalientes", "001", "Aguascalientes", "0201", "Brande Vin [Bodegas]"],
["01", "Aguascalientes", "001", "Aguascalientes", "0207", "Valle Redondo"],
["01", "Aguascalientes", "001", "Aguascalientes", "0209", "La Fortuna"],
["01", "Aguascalientes", "001", "Aguascalientes", "0212", "Lomas del Gachup\xedn"],
["01", "Aguascalientes", "001", "Aguascalientes", "0213", "El Carmen (Gallinas G\xfceras) [Rancho]"],
["01", "Aguascalientes", "001", "Aguascalientes", "0216", "La Gloria"],
["01", "Aguascalientes", "001", "Aguascalientes", "0226", "Hacienda Nueva"]
]
data_stream_split_text = [
["FEB", "RUAR", "Y 2014 M27 (BUS)", "", "ALPHABETIC LISTING BY T", "YPE", "", "", "", "ABLPDM27"],
["", "", "", "", "OF ACTIVE LICENSES", "", "", "", "", "3/19/2014"],
["", "", "", "", "OKLAHOMA ABLE COMMIS", "SION", "", "", "", ""],
["LICENSE", "", "", "", "PREMISE", "", "", "", "", ""],
["NUMBER", "TYPE", "DBA NAME", "LICENSEE NAME", "ADDRESS", "CITY", "ST", "ZIP", "PHONE NUMBER", "EXPIRES"],
["648765", "AAA", "ALLEGIANT AIR", "ALLEGIANT AIR LLC", "7100 TERMINAL DRIVE", "OKLAHOMA CITY", "OK", "73159", "-", "2014/12/03"],
["", "", "", "", "7777 EAST APACHE", "", "", "", "", ""],
["648766", "AAA", "ALLEGIANT AIR", "ALLEGIANT AIR LLC", "STREET", "TULSA", "OK", "74115", "-", "2014/12/16"],
["82030", "AAA", "AMERICAN AIRLINES", "AMERICAN AIRLINES INC", "7100 TERMINAL DRIVE", "OKLAHOMA CITY", "OK", "73159", "(405) 680-3701", "2014/09/14"],
["509462", "AAA", "AMERICAN AIRLINES", "AMERICAN AIRLINES INC", "7777 EAST APACHE DRIVE", "TULSA", "OK", "74115", "(918) 831-6302", "2014/08/19"],
["", "", "", "AMERICAN EAGLE", "", "", "", "", "", ""],
["509609", "AAA", "AMERICAN EAGLE", "AIRLINES INC", "7100 TERMINAL DRIVE", "OKLAHOMA CITY", "OK", "73159", "(405) 680-3701", "2014/08/19"],
["", "", "", "AMERICAN EAGLE", "", "", "", "", "", ""],
["402986", "AAA", "AMERICAN EAGLE", "AIRLINES INC", "7777 EAST APACHE DRIVE", "TULSA", "OK", "74115", "(859) 767-3747", "2014/10/22"],
["", "", "", "", "WILL ROGERS AIRPORT", "", "", "", "", ""],
["79145", "AAA", "DELTA AIR LINES", "DELTA AIR LINES INC", "BOX 59975", "OKLAHOMA CITY", "OK", "73159", "(404) 773-9745", "2014/05/11"],
["600941", "AAA", "ENDEAVOR AIR", "ENDEAVOR AIR INC", "7100 TERMINAL DRIVE", "OKLAHOMA CITY", "OK", "73159", "(901) 348-4100", "2015/03/26"],
["", "", "", "", "7100 TERMINAL DRIVE", "", "", "", "", ""],
["478482", "AAA", "EXPRESSJET AIRLINES", "EXPRESSJET AIRLINES INC", "WILL ROGERS AIRPORT", "OKLAHOMA CITY", "OK", "73159", "(832) 353-1201", "2014/05/08"],
["505981", "AAA", "SKYWEST AIRLINES", "SKYWEST INC", "7100 TERMINAL DRIVE", "OKLAHOMA CITY", "OK", "73159", "(405) 634-3000", "2014/05/28"],
["429754", "AAA", "SOUTHWEST AIRLINES", "SOUTHWEST AIRLINES CO", "7100 TERMINAL DRIVE", "OKLAHOMA CITY", "OK", "73159", "(405) 682-4183", "2015/02/15"],
["", "", "TULSA INTERNATIONAL", "", "", "", "", "", "", ""],
["429755", "AAA", "AIRPORT", "SOUTHWEST AIRLINES CO", "7777 EAST APACHE DRIVE", "TULSA", "OK", "74115", "(918) 834-4495", "2015/02/16"],
["415051", "AAA", "UNITED AIRLINES", "UNITED AIRLINES INC", "7777 EAST APACHE DRIVE", "TULSA", "OK", "74115", "(872) 825-8309", "2014/05/12"],
["106719", "AAA", "UNITED AIRLINES", "UNITED AIRLINES INC", "WILL ROGERS AIRPORT", "OKLAHOMA CITY", "OK", "73159", "(872) 825-8309", "2014/04/11"],
["", "", "A SENSU JAPANESE", "", "7123 SOUTH 92ND EAST", "", "", "", "", ""],
["625422", "BAW", "RESTAURANT", "INFORMAL PARTNERSHIP", "AVENUE SUITE J", "TULSA", "OK", "74133", "(918) 252-0333", "2015/02/14"],
["", "", "ADAMO'S ROUTE 66", "", "2132 WEST GARY", "", "", "", "", ""],
["464828", "BAW", "ITALIAN VILLA", "TADJ INC", "BOULEVARD", "CLINTON", "OK", "73601", "(580) 323-5900", "2015/02/11"],
["", "", "", "", "12215 NORTH", "", "", "", "", ""],
["184066", "BAW", "AJANTA", "CABAB N' CURRY INC", "PENNSYLVANIA", "OKLAHOMA CITY", "OK", "73120", "(405) 752-5283", "2014/07/27"],
["", "", "", "SAYRE LODGING", "", "", "", "", "", ""],
["547693", "BAW", "AMERICINN OF SAYRE", "ENTERPRISES LLC", "2405 SOUTH EL CAMINO", "SAYRE", "OK", "73662", "(580) 928-2700", "2014/09/08"],
["", "", "ANDOLINI'S PIZZERIA &", "", "12140 EAST 96TH STREET", "", "", "", "", ""],
["428377", "BAW", "ITALIAN RESTAURANT", "ANDOLINI'S LLC", "NORTH #106", "OWASSO", "OK", "74055", "(918) 272-9325", "2015/02/10"],
["", "", "ASAHI JAPANESE", "", "", "", "", "", "", ""],
["446957", "BAW", "RESTAURANT", "JIN CORPORATION", "7831 EAST 71ST STREET", "TULSA", "OK", "74133", "(918) 307-9151", "2014/12/22"],
["", "", "", "SMOKEHOUSE", "", "", "", "", "", ""],
["632501", "BAW", "BACK DOOR BARBECUE", "ASSOCIATES INC", "315 NORTHWEST 23RD", "OKLAHOMA CITY", "OK", "73103", "-", "2014/08/01"],
["598515", "BAW", "BAMBOO THAI BISTRO", "BAMBOO THAI BISTRO INC", "5079 SOUTH YALE AVENUE", "TULSA", "OK", "74135", "(918) 828-0740", "2015/03/11"],
["", "", "BANDANA RED'S", "", "", "", "", "", "", ""],
["618693", "BAW", "STEAKHOUSE", "BRADSHAW, STEVE_LEN", "37808 OLD HIGHWAY 270", "SHAWNEE", "OK", "74804", "-", "2014/08/20"],
["", "", "", "", "1522 WEST LINDSEY", "", "", "", "", ""],
["632575", "BAW", "BASHU LEGENDS", "HYH HE CHUANG LLC", "STREET", "NORMAN", "OK", "73069", "-", "2014/07/21"],
["", "", "", "DEEP FORK HOLDINGS", "", "", "", "", "", ""],
["543149", "BAW", "BEDLAM BAR-B-Q", "LLC", "610 NORTHEAST 50TH", "OKLAHOMA CITY", "OK", "73105", "(405) 528-7427", "2015/02/23"],
["", "", "", "", "Page 1 of 151", "", "", "", "", ""]
]
data_stream_flag_size = [
["", "TABLE 125: STATE-WISE COMPOSITION OF OUTSTANDING LIABILITIES - 1997 <s>(Contd.)</s>", "", "", "", "", "", "", "", "", ""],
["", "", "", "", "(As at end-March)", "", "", "", "", "", ""],
["", "", "", "", "", "", "", "", "", "", "(<s>`</s> Billion)"],
["States", "Total", "Market", "NSSF", "WMA", "Loans", "Loans", "Loans", "Loans", "Loans", "Loans"],
["", "Internal", "Loans", "", "from", "from", "from", "from", "from", "from SBI", "from"],
["", "Debt", "", "", "RBI", "Banks", "LIC", "GIC", "NABARD", "& Other", "NCDC"],
["", "", "", "", "", "& FIs", "", "", "", "Banks", ""],
["1", "2=", "3", "4", "5", "6=", "7", "8", "9", "10", "11"],
["", "(3 to 6)+14", "", "", "", "(7 to13)", "", "", "", "", ""],
["Andhra Pradesh", "48.11", "40.45", "-", "3.26", "4.4", "2.62", "-", "0.91", "-", "0.25"],
["Arunachal Pradesh", "1.23", "1.1", "-", "-", "0.13", "-", "-", "-", "-", "-"],
["Assam", "12.69", "10.02", "-", "2.41", "0.26", "0.08", "-", "-0.06", "0.01", "0.24"],
["Bihar", "40.75", "41.54", "-", "-", "-1.42", "0.19", "-", "-1.01", "-0.36", "0.2"],
["Chhattisgarh", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"],
["Goa", "1.4", "1.02", "-", "-", "0.38", "0.31", "-", "0.07", "-", "-"],
["Gujarat", "19.75", "17.1", "-", "-", "2.64", "1.17", "-", "1.11", "-", "0.44"],
["Haryana", "11.53", "9.67", "-", "0.06", "1.8", "0.55", "-", "0.64", "-", "0.49"],
["Himachal Pradesh", "8.02", "2.94", "-", "4.55", "0.53", "0.13", "-", "0.05", "-", "0.25"],
["Jammu and Kashmir", "11.72", "4.49", "-", "-", "7.23", "0.66", "-", "0.02", "6.08", "-"],
["Jharkhand", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"],
["Karnataka", "22.44", "19.59", "-", "-", "2.86", "1.22", "-", "0.89", "-", "0.69"],
["Kerala", "29.03", "24.91<s>2</s>", "-", "-", "4.11", "1.77", "-", "0.48", "-", "1.45"],
["Madhya Pradesh", "27.13", "23.57", "-", "-", "3.56", "0.38", "-", "1.86", "-", "1.28"],
["Maharashtra", "30.47", "26.07", "-", "-", "4.39", "0.21", "-", "-0.12", "0.02", "2.89"],
["Manipur", "2.17", "1.61", "-", "0.26", "0.29", "0.08", "-", "-", "-", "0.09"],
["Meghalaya", "1.36", "1.38", "-", "-", "-0.02", "0.04", "-", "-0.05", "-", "0.03"],
["Mizoram", "1.17", "0.46", "-", "0.27", "0.43", "0.11", "-", "-", "-", "0.03"],
["Nagaland", "2.99", "2.6", "-", "-", "0.39", "0.24", "-", "-", "-", "0.04"],
["Odisha", "34.04", "27.58", "-", "4.4", "2.06", "0.56", "-", "0.66", "-", "0.2"],
["Punjab", "19.18", "10.93", "-", "1.03", "7.23", "0.17", "-", "0.71", "5.9", "0.46"],
["Rajasthan", "36.77", "28.63", "-", "4.99", "3.16", "0.57", "-", "1.64", "-", "0.81"],
["Sikkim", "0.16", "-", "-", "-", "0.16", "0.03", "-", "-", "-", "0.01"],
["Tamil Nadu", "34.11", "31.41", "-", "-", "2.7", "1.3", "-", "0.6", "-", "0.68"],
["Tripura", "2.3", "1.89", "-", "-", "0.41", "0.41", "-", "-0.05", "-", "0.02"],
["Uttaranchal", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"],
["Uttar Pradesh", "80.62", "74.89", "-", "4.34", "1.34", "0.6", "-", "-0.21", "0.18", "0.03"],
["West Bengal", "34.23", "32.19", "-", "-", "2.04", "0.77", "-", "0.06", "-", "0.51"],
["NCT Delhi", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"],
["ALL STATES", "513.38", "436.02", "-", "25.57", "51.06", "14.18", "-", "8.21", "11.83", "11.08"],
["<s>2</s> Includes `2.45 crore outstanding under “Market Loan Suspense”.", "", "", "", "", "", "", "", "", "", ""],
["", "", "", "", "445", "", "", "", "", "", ""]
]
data_lattice = [
["Cycle Name", "KI (1/km)", "Distance (mi)", "Percent Fuel Savings", "", "", ""],
["", "", "", "Improved Speed", "Decreased Accel", "Eliminate Stops", "Decreased Idle"],
["2012_2", "3.30", "1.3", "5.9%", "9.5%", "29.2%", "17.4%"],
["2145_1", "0.68", "11.2", "2.4%", "0.1%", "9.5%", "2.7%"],
["4234_1", "0.59", "58.7", "8.5%", "1.3%", "8.5%", "3.3%"],
["2032_2", "0.17", "57.8", "21.7%", "0.3%", "2.7%", "1.2%"],
["4171_1", "0.07", "173.9", "58.1%", "1.6%", "2.1%", "0.5%"]
]
data_lattice_table_rotated = [
["State", "Nutritional Assessment (No. of individuals)", "", "", "", "IYCF Practices (No. of mothers: 2011-12)", "Blood Pressure (No. of adults: 2011-12)", "", "Fasting Blood Sugar (No. of adults:2011-12)", ""],
["", "1975-79", "1988-90", "1996-97", "2011-12", "", "Men", "Women", "Men", "Women"],
["Kerala", "5738", "6633", "8864", "8297", "245", "2161", "3195", "1645", "2391"],
["Tamil Nadu", "7387", "10217", "5813", "7851", "413", "2134", "2858", "1119", "1739"],
["Karnataka", "6453", "8138", "12606", "8958", "428", "2467", "2894", "1628", "2028"],
["Andhra Pradesh", "5844", "9920", "9545", "8300", "557", "1899", "2493", "1111", "1529"],
["Maharashtra", "5161", "7796", "6883", "9525", "467", "2368", "2648", "1417", "1599"],
["Gujarat", "4403", "5374", "4866", "9645", "477", "2687", "3021", "2122", "2503"],
["Madhya Pradesh", "*", "*", "*", "7942", "470", "1965", "2150", "1579", "1709"],
["Orissa", "3756", "5540", "12024", "8473", "398", "2040", "2624", "1093", "1628"],
["West Bengal", "*", "*", "*", "8047", "423", "2058", "2743", "1413", "2027"],
["Uttar Pradesh", "*", "*", "*", "9860", "581", "2139", "2415", "1185", "1366"],
["Pooled", "38742", "53618", "60601", "86898", "4459", "21918", "27041", "14312", "18519"]
]
data_lattice_table_areas = [
["", "", "", "", "", "", "", "", ""],
["State", "n", "Literacy Status", "", "", "", "", "", ""],
["", "", "Illiterate", "Read & Write", "1-4 std.", "5-8 std.", "9-12 std.", "College", ""],
["Kerala", "2400", "7.2", "0.5", "25.3", "20.1", "41.5", "5.5", ""],
["Tamil Nadu", "2400", "21.4", "2.3", "8.8", "35.5", "25.8", "6.2", ""],
["Karnataka", "2399", "37.4", "2.8", "12.5", "18.3", "23.1", "5.8", ""],
["Andhra Pradesh", "2400", "54.0", "1.7", "8.4", "13.2", "18.8", "3.9", ""],
["Maharashtra", "2400", "22.0", "0.9", "17.3", "20.3", "32.6", "7.0", ""],
["Gujarat", "2390", "28.6", "0.1", "14.4", "23.1", "26.9", "6.8", ""],
["Madhya Pradesh", "2402", "29.1", "3.4", "8.5", "35.1", "13.3", "10.6", ""],
["Orissa", "2405", "33.2", "1.0", "10.4", "25.7", "21.2", "8.5", ""],
["West Bengal", "2293", "41.7", "4.4", "13.2", "17.1", "21.2", "2.4", ""],
["Uttar Pradesh", "2400", "35.3", "2.1", "4.5", "23.3", "27.1", "7.6", ""],
["Pooled", "23889", "30.9", "1.9", "12.3", "23.2", "25.2", "6.4", ""],
["", "", "", "", "", "", "", "", ""]
]
data_lattice_process_background = [
["State", "Date", "Halt stations", "Halt days", "Persons directly reached(in lakh)", "Persons trained", "Persons counseled" ,"Persons testedfor HIV"],
["Delhi", "1.12.2009", "8", "17", "1.29", "3,665", "2,409", "1,000"],
["Rajasthan", "2.12.2009 to 19.12.2009", "", "", "", "", "", ""],
["Gujarat", "20.12.2009 to 3.1.2010", "6", "13", "6.03", "3,810", "2,317", "1,453"],
["Maharashtra", "4.01.2010 to 1.2.2010", "13", "26", "1.27", "5,680", "9,027", "4,153"],
["Karnataka", "2.2.2010 to 22.2.2010", "11", "19", "1.80", "5,741", "3,658", "3,183"],
["Kerala", "23.2.2010 to 11.3.2010", "9", "17", "1.42", "3,559", "2,173", "855"],
["Total", "", "47", "92", "11.81", "22,455", "19,584", "10,644"]
]
data_lattice_copy_text = [
["Plan Type", "County", "Plan Name", "Totals"],
["GMC", "Sacramento", "Anthem Blue Cross", "164,380"],
["GMC", "Sacramento", "Health Net", "126,547"],
["GMC", "Sacramento", "Kaiser Foundation", "74,620"],
["GMC", "Sacramento", "Molina Healthcare", "59,989"],
["GMC", "San Diego", "Care 1st Health Plan", "71,831"],
["GMC", "San Diego", "Community Health Group", "264,639"],
["GMC", "San Diego", "Health Net", "72,404"],
["GMC", "San Diego", "Kaiser", "50,415"],
["GMC", "San Diego", "Molina Healthcare", "206,430"],
["GMC", "Total GMC Enrollment", "", "1,091,255"],
["COHS", "Marin", "Partnership Health Plan of CA", "36,006"],
["COHS", "Mendocino", "Partnership Health Plan of CA", "37,243"],
["COHS", "Napa", "Partnership Health Plan of CA", "28,398"],
["COHS", "Solano", "Partnership Health Plan of CA", "113,220"],
["COHS", "Sonoma", "Partnership Health Plan of CA", "112,271"],
["COHS", "Yolo", "Partnership Health Plan of CA", "52,674"],
["COHS", "Del Norte", "Partnership Health Plan of CA", "11,242"],
["COHS", "Humboldt", "Partnership Health Plan of CA", "49,911"],
["COHS", "Lake", "Partnership Health Plan of CA", "29,149"],
["COHS", "Lassen", "Partnership Health Plan of CA", "7,360"],
["COHS", "Modoc", "Partnership Health Plan of CA", "2,940"],
["COHS", "Shasta", "Partnership Health Plan of CA", "61,763"],
["COHS", "Siskiyou", "Partnership Health Plan of CA", "16,715"],
["COHS", "Trinity", "Partnership Health Plan of CA", "4,542"],
["COHS", "Merced", "Central California Alliance for Health", "123,907"],
["COHS", "Monterey", "Central California Alliance for Health", "147,397"],
["COHS", "Santa Cruz", "Central California Alliance for Health", "69,458"],
["COHS", "Santa Barbara", "CenCal", "117,609"],
["COHS", "San Luis Obispo", "CenCal", "55,761"],
["COHS", "Orange", "CalOptima", "783,079"],
["COHS", "San Mateo", "Health Plan of San Mateo", "113,202"],
["COHS", "Ventura", "Gold Coast Health Plan", "202,217"],
["COHS", "Total COHS Enrollment", "", "2,176,064"],
["Subtotal for Two-Plan, Regional Model, GMC and COHS", "", "", "10,132,022"],
["PCCM", "Los Angeles", "AIDS Healthcare Foundation", "828"],
["PCCM", "San Francisco", "Family Mosaic", "25"],
["PCCM", "Total PHP Enrollment", "", "853"],
["All Models Total Enrollments", "", "", "10,132,875"],
["Source: Data Warehouse 12/14/15", "", "", ""]
]
data_lattice_shift_text_left_top = [
["Investigations", "No. ofHHs", "Age/Sex/Physiological Group", "Preva-lence", "C.I*", "RelativePrecision", "Sample sizeper State"],
["Anthropometry", "2400", "All the available individuals", "", "", "", ""],
["Clinical Examination", "", "", "", "", "", ""],
["History of morbidity", "", "", "", "", "", ""],
["Diet survey", "1200", "All the individuals partaking meals in the HH", "", "", "", ""],
["Blood Pressure #", "2400", "Men (≥ 18yrs)", "10%", "95%", "20%", "1728"],
["", "", "Women (≥ 18 yrs)", "", "", "", "1728"],
["Fasting blood glucose", "2400", "Men (≥ 18 yrs)", "5%", "95%", "20%", "1825"],
["", "", "Women (≥ 18 yrs)", "", "", "", "1825"],
["Knowledge &Practices on HTN &DM", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"],
["", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"]
]
data_lattice_shift_text_disable = [
["Investigations", "No. ofHHs", "Age/Sex/Physiological Group", "Preva-lence", "C.I*", "RelativePrecision", "Sample sizeper State"],
["Anthropometry", "", "", "", "", "", ""],
["Clinical Examination", "2400", "", "All the available individuals", "", "", ""],
["History of morbidity", "", "", "", "", "", ""],
["Diet survey", "1200", "", "All the individuals partaking meals in the HH", "", "", ""],
["", "", "Men (≥ 18yrs)", "", "", "", "1728"],
["Blood Pressure #", "2400", "Women (≥ 18 yrs)", "10%", "95%", "20%", "1728"],
["", "", "Men (≥ 18 yrs)", "", "", "", "1825"],
["Fasting blood glucose", "2400", "Women (≥ 18 yrs)", "5%", "95%", "20%", "1825"],
["Knowledge &Practices on HTN &", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"],
["DM", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"]
]
data_lattice_shift_text_right_bottom = [
["Investigations", "No. ofHHs", "Age/Sex/Physiological Group", "Preva-lence", "C.I*", "RelativePrecision", "Sample sizeper State"],
["Anthropometry", "", "", "", "", "", ""],
["Clinical Examination", "", "", "", "", "", ""],
["History of morbidity", "2400", "", "", "", "", "All the available individuals"],
["Diet survey", "1200", "", "", "", "", "All the individuals partaking meals in the HH"],
["", "", "Men (≥ 18yrs)", "", "", "", "1728"],
["Blood Pressure #", "2400", "Women (≥ 18 yrs)", "10%", "95%", "20%", "1728"],
["", "", "Men (≥ 18 yrs)", "", "", "", "1825"],
["Fasting blood glucose", "2400", "Women (≥ 18 yrs)", "5%", "95%", "20%", "1825"],
["", "2400", "Men (≥ 18 yrs)", "-", "-", "-", "1728"],
["Knowledge &Practices on HTN &DM", "2400", "Women (≥ 18 yrs)", "-", "-", "-", "1728"]
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
"a","b"
"1","2"
1 a b
2 1 2
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+117
View File
@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
import os
from click.testing import CliRunner
from camelot.cli import cli
from camelot.utils import TemporaryDirectory
testdir = os.path.dirname(os.path.abspath(__file__))
testdir = os.path.join(testdir, 'files')
def test_cli_lattice():
with TemporaryDirectory() as tempdir:
infile = os.path.join(testdir, 'foo.pdf')
outfile = os.path.join(tempdir, 'foo.csv')
runner = CliRunner()
result = runner.invoke(cli, ['--format', 'csv', '--output', outfile,
'lattice', infile])
assert result.exit_code == 0
assert result.output == 'Found 1 tables\n'
result = runner.invoke(cli, ['--format', 'csv',
'lattice', infile])
output_error = 'Error: Please specify output file path using --output'
assert output_error in result.output
result = runner.invoke(cli, ['--output', outfile,
'lattice', infile])
format_error = 'Please specify output file format using --format'
assert format_error in result.output
def test_cli_stream():
with TemporaryDirectory() as tempdir:
infile = os.path.join(testdir, 'budget.pdf')
outfile = os.path.join(tempdir, 'budget.csv')
runner = CliRunner()
result = runner.invoke(cli, ['--format', 'csv', '--output', outfile,
'stream', infile])
assert result.exit_code == 0
assert result.output == 'Found 1 tables\n'
result = runner.invoke(cli, ['--format', 'csv', 'stream', infile])
output_error = 'Error: Please specify output file path using --output'
assert output_error in result.output
result = runner.invoke(cli, ['--output', outfile, 'stream', infile])
format_error = 'Please specify output file format using --format'
assert format_error in result.output
def test_cli_password():
with TemporaryDirectory() as tempdir:
infile = os.path.join(testdir, 'health_protected.pdf')
outfile = os.path.join(tempdir, 'health_protected.csv')
runner = CliRunner()
result = runner.invoke(cli, ['--password', 'userpass',
'--format', 'csv', '--output', outfile,
'stream', infile])
assert result.exit_code == 0
assert result.output == 'Found 1 tables\n'
output_error = 'file has not been decrypted'
# no password
result = runner.invoke(cli, ['--format', 'csv', '--output', outfile,
'stream', infile])
assert output_error in str(result.exception)
# bad password
result = runner.invoke(cli, ['--password', 'wrongpass',
'--format', 'csv', '--output', outfile,
'stream', infile])
assert output_error in str(result.exception)
def test_cli_output_format():
with TemporaryDirectory() as tempdir:
infile = os.path.join(testdir, 'health.pdf')
outfile = os.path.join(tempdir, 'health.{}')
runner = CliRunner()
# json
result = runner.invoke(cli, ['--format', 'json', '--output', outfile.format('json'),
'stream', infile])
assert result.exit_code == 0
# excel
result = runner.invoke(cli, ['--format', 'excel', '--output', outfile.format('xlsx'),
'stream', infile])
assert result.exit_code == 0
# html
result = runner.invoke(cli, ['--format', 'html', '--output', outfile.format('html'),
'stream', infile])
assert result.exit_code == 0
# zip
result = runner.invoke(cli, ['--zip', '--format', 'csv', '--output', outfile.format('csv'),
'stream', infile])
assert result.exit_code == 0
def test_cli_quiet():
with TemporaryDirectory() as tempdir:
infile = os.path.join(testdir, 'blank.pdf')
outfile = os.path.join(tempdir, 'blank.csv')
runner = CliRunner()
result = runner.invoke(cli, ['--format', 'csv', '--output', outfile,
'stream', infile])
assert 'No tables found on page-1' in result.output
result = runner.invoke(cli, ['--quiet', '--format', 'csv',
'--output', outfile, 'stream', infile])
assert 'No tables found on page-1' not in result.output
+85 -8
View File
@@ -6,14 +6,42 @@ import pandas as pd
import camelot
from test_data import *
from .data import *
testdir = os.path.dirname(os.path.abspath(__file__))
testdir = os.path.join(testdir, "files")
def test_parsing_report():
parsing_report = {
'accuracy': 99.02,
'whitespace': 12.24,
'order': 1,
'page': 1
}
filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename)
assert tables[0].parsing_report == parsing_report
def test_password():
df = pd.DataFrame(data_stream)
filename = os.path.join(testdir, "health_protected.pdf")
tables = camelot.read_pdf(filename, password="ownerpass", flavor="stream")
assert df.equals(tables[0].df)
tables = camelot.read_pdf(filename, password="userpass", flavor="stream")
assert df.equals(tables[0].df)
def test_stream():
pass
df = pd.DataFrame(data_stream)
filename = os.path.join(testdir, "health.pdf")
tables = camelot.read_pdf(filename, flavor="stream")
assert df.equals(tables[0].df)
def test_stream_table_rotated():
@@ -28,11 +56,11 @@ def test_stream_table_rotated():
assert df.equals(tables[0].df)
def test_stream_table_area():
df = pd.DataFrame(data_stream_table_area_single)
def test_stream_table_areas():
df = pd.DataFrame(data_stream_table_areas)
filename = os.path.join(testdir, "tabula/us-007.pdf")
tables = camelot.read_pdf(filename, flavor="stream", table_area=["320,500,573,335"])
tables = camelot.read_pdf(filename, flavor="stream", table_areas=["320,500,573,335"])
assert df.equals(tables[0].df)
@@ -45,11 +73,28 @@ def test_stream_columns():
assert df.equals(tables[0].df)
def test_stream_split_text():
df = pd.DataFrame(data_stream_split_text)
filename = os.path.join(testdir, "tabula/m27.pdf")
tables = camelot.read_pdf(
filename, flavor="stream", columns=["72,95,209,327,442,529,566,606,683"], split_text=True)
assert df.equals(tables[0].df)
def test_stream_flag_size():
df = pd.DataFrame(data_stream_flag_size)
filename = os.path.join(testdir, "superscript.pdf")
tables = camelot.read_pdf(filename, flavor="stream", flag_size=True)
assert df.equals(tables[0].df)
def test_lattice():
df = pd.DataFrame(data_lattice)
filename = os.path.join(testdir,
"tabula/icdar2013-dataset/competition-dataset-us/us-030.pdf")
filename = os.path.join(
testdir, "tabula/icdar2013-dataset/competition-dataset-us/us-030.pdf")
tables = camelot.read_pdf(filename, pages="2")
assert df.equals(tables[0].df)
@@ -66,6 +111,14 @@ def test_lattice_table_rotated():
assert df.equals(tables[0].df)
def test_lattice_table_areas():
df = pd.DataFrame(data_lattice_table_areas)
filename = os.path.join(testdir, "twotables_2.pdf")
tables = camelot.read_pdf(filename, table_areas=["80,693,535,448"])
assert df.equals(tables[0].df)
def test_lattice_process_background():
df = pd.DataFrame(data_lattice_process_background)
@@ -79,4 +132,28 @@ def test_lattice_copy_text():
filename = os.path.join(testdir, "row_span_1.pdf")
tables = camelot.read_pdf(filename, line_size_scaling=60, copy_text="v")
assert df.equals(tables[0].df)
assert df.equals(tables[0].df)
def test_lattice_shift_text():
df_lt = pd.DataFrame(data_lattice_shift_text_left_top)
df_disable = pd.DataFrame(data_lattice_shift_text_disable)
df_rb = pd.DataFrame(data_lattice_shift_text_right_bottom)
filename = os.path.join(testdir, "column_span_2.pdf")
tables = camelot.read_pdf(filename, line_size_scaling=40)
assert df_lt.equals(tables[0].df)
tables = camelot.read_pdf(filename, line_size_scaling=40, shift_text=[''])
assert df_disable.equals(tables[0].df)
tables = camelot.read_pdf(filename, line_size_scaling=40, shift_text=['r', 'b'])
assert df_rb.equals(tables[0].df)
def test_repr():
filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename)
assert repr(tables) == "<TableList n=1>"
assert repr(tables[0]) == "<Table shape=(7, 7)>"
assert repr(tables[0].cells[0][0]) == "<Cell x1=120.48 y1=218.42 x2=164.64 y2=233.89>"
-189
View File
@@ -1,189 +0,0 @@
# -*- coding: utf-8 -*-
data_stream_table_rotated = [
["","","Table 21 Current use of contraception by background characteristics—Continued","","","","","","","","","","","","","","",""],
["","","","","","","Modern method","","","","","","","Traditional method","","","",""],
["","","","Any","","","","","","","Other","Any","","","","Not","","Number"],
["","","Any","modern","Female","Male","","","","Condom/","modern","traditional","","With-","Folk","currently","","of"],
["","Background characteristic","method","method","sterilization","sterilization","Pill","IUD","Injectables","Nirodh","method","method","Rhythm","drawal","method","using","Total","women"],
["","Caste/tribe","","","","","","","","","","","","","","","",""],
["","Scheduled caste","74.8","55.8","42.9","0.9","9.7","0.0","0.2","2.2","0.0","19.0","11.2","7.4","0.4","25.2","100.0","1,363"],
["","Scheduled tribe","59.3","39.0","26.8","0.6","6.4","0.6","1.2","3.5","0.0","20.3","10.4","5.8","4.1","40.7","100.0","256"],
["","Other backward class","71.4","51.1","34.9","0.0","8.6","1.4","0.0","6.2","0.0","20.4","12.6","7.8","0.0","28.6","100.0","211"],
["","Other","71.1","48.8","28.2","0.8","13.3","0.9","0.3","5.2","0.1","22.3","12.9","9.1","0.3","28.9","100.0","3,319"],
["","Wealth index","","","","","","","","","","","","","","","",""],
["","Lowest","64.5","48.6","34.3","0.5","10.5","0.6","0.7","2.0","0.0","15.9","9.9","4.6","1.4","35.5","100.0","1,258"],
["","Second","68.5","50.4","36.2","1.1","11.4","0.5","0.1","1.1","0.0","18.1","11.2","6.7","0.2","31.5","100.0","1,317"],
["","Middle","75.5","52.8","33.6","0.6","14.2","0.4","0.5","3.4","0.1","22.7","13.4","8.9","0.4","24.5","100.0","1,018"],
["","Fourth","73.9","52.3","32.0","0.5","12.5","0.6","0.2","6.3","0.2","21.6","11.5","9.9","0.2","26.1","100.0","908"],
["","Highest","78.3","44.4","19.5","1.0","9.7","1.4","0.0","12.7","0.0","33.8","18.2","15.6","0.0","21.7","100.0","733"],
["","Number of living children","","","","","","","","","","","","","","","",""],
["","No children","25.1","7.6","0.3","0.5","2.0","0.0","0.0","4.8","0.0","17.5","9.0","8.5","0.0","74.9","100.0","563"],
["","1 child","66.5","32.1","3.7","0.7","20.1","0.7","0.1","6.9","0.0","34.3","18.9","15.2","0.3","33.5","100.0","1,190"],
["","1 son","66.8","33.2","4.1","0.7","21.1","0.5","0.3","6.6","0.0","33.5","21.2","12.3","0.0","33.2","100.0","672"],
["","No sons","66.1","30.7","3.1","0.6","18.8","0.8","0.0","7.3","0.0","35.4","15.8","19.0","0.6","33.9","100.0","517"],
["","2 children","81.6","60.5","41.8","0.9","11.6","0.8","0.3","4.8","0.2","21.1","12.2","8.3","0.6","18.4","100.0","1,576"],
["","1 or more sons","83.7","64.2","46.4","0.9","10.8","0.8","0.4","4.8","0.1","19.5","11.1","7.6","0.7","16.3","100.0","1,268"],
["","No sons","73.2","45.5","23.2","1.0","15.1","0.9","0.0","4.8","0.5","27.7","16.8","11.0","0.0","26.8","100.0","308"],
["","3 children","83.9","71.2","57.7","0.8","9.8","0.6","0.5","1.8","0.0","12.7","8.7","3.3","0.8","16.1","100.0","961"],
["","1 or more sons","85.0","73.2","60.3","0.9","9.4","0.5","0.5","1.6","0.0","11.8","8.1","3.0","0.7","15.0","100.0","860"],
["","No sons","74.7","53.8","35.3","0.0","13.7","1.6","0.0","3.2","0.0","20.9","13.4","6.1","1.5","25.3","100.0","101"],
["","4+ children","74.3","58.1","45.1","0.6","8.7","0.6","0.7","2.4","0.0","16.1","9.9","5.4","0.8","25.7","100.0","944"],
["","1 or more sons","73.9","58.2","46.0","0.7","8.3","0.7","0.7","1.9","0.0","15.7","9.4","5.5","0.8","26.1","100.0","901"],
["","No sons","(82.1)","(57.3)","(25.6)","(0.0)","(17.8)","(0.0)","(0.0)","(13.9)","(0.0)","(24.8)","(21.3)","(3.5)","(0.0)","(17.9)","100.0","43"],
["","Total","71.2","49.9","32.2","0.7","11.7","0.6","0.3","4.3","0.1","21.3","12.3","8.4","0.5","28.8","100.0","5,234"],
["","NFHS-2 (1998-99)","66.6","47.3","32.0","1.8","9.2","1.4","na","2.9","na","na","8.7","9.8","na","33.4","100.0","4,116"],
["","NFHS-1 (1992-93)","57.7","37.6","26.5","4.3","3.6","1.3","0.1","1.9","na","na","11.3","8.3","na","42.3","100.0","3,970"],
["","","Note: If more than one method is used, only the most effective method is considered in this tabulation. Total includes women for whom caste/tribe was not known or is missing, who are","","","","","","","","","","","","","","",""],
["","not shown separately.","","","","","","","","","","","","","","","",""],
["","na = Not available","","","","","","","","","","","","","","","",""],
["","","ns = Not shown; see table 2b, footnote 1","","","","","","","","","","","","","","",""],
["","( ) Based on 25-49 unweighted cases.","","","","","","","","","","","","","","","",""],
["","","","","","","","","54","","","","","","","","",""]
]
data_stream_table_area_single = [
["","One Withholding"],
["Payroll Period","Allowance"],
["Weekly","$71.15"],
["Biweekly","142.31"],
["Semimonthly","154.17"],
["Monthly","308.33"],
["Quarterly","925.00"],
["Semiannually","1,850.00"],
["Annually","3,700.00"],
["Daily or Miscellaneous","14.23"],
["(each day of the payroll period)",""]
]
data_stream_columns = [
["Clave","Nombre Entidad","Clave","Nombre Municipio","Clave","Nombre Localidad"],
["Entidad","","Municipio","","Localidad",""],
["01","Aguascalientes","001","Aguascalientes","0094","Granja Adelita"],
["01","Aguascalientes","001","Aguascalientes","0096","Agua Azul"],
["01","Aguascalientes","001","Aguascalientes","0100","Rancho Alegre"],
["01","Aguascalientes","001","Aguascalientes","0102","Los Arbolitos [Rancho]"],
["01","Aguascalientes","001","Aguascalientes","0104","Ardillas de Abajo (Las Ardillas)"],
["01","Aguascalientes","001","Aguascalientes","0106","Arellano"],
["01","Aguascalientes","001","Aguascalientes","0112","Bajío los Vázquez"],
["01","Aguascalientes","001","Aguascalientes","0113","Bajío de Montoro"],
["01","Aguascalientes","001","Aguascalientes","0114","Residencial San Nicolás [Baños la Cantera]"],
["01","Aguascalientes","001","Aguascalientes","0120","Buenavista de Peñuelas"],
["01","Aguascalientes","001","Aguascalientes","0121","Cabecita 3 Marías (Rancho Nuevo)"],
["01","Aguascalientes","001","Aguascalientes","0125","Cañada Grande de Cotorina"],
["01","Aguascalientes","001","Aguascalientes","0126","Cañada Honda [Estación]"],
["01","Aguascalientes","001","Aguascalientes","0127","Los Caños"],
["01","Aguascalientes","001","Aguascalientes","0128","El Cariñán"],
["01","Aguascalientes","001","Aguascalientes","0129","El Carmen [Granja]"],
["01","Aguascalientes","001","Aguascalientes","0135","El Cedazo (Cedazo de San Antonio)"],
["01","Aguascalientes","001","Aguascalientes","0138","Centro de Arriba (El Taray)"],
["01","Aguascalientes","001","Aguascalientes","0139","Cieneguilla (La Lumbrera)"],
["01","Aguascalientes","001","Aguascalientes","0141","Cobos"],
["01","Aguascalientes","001","Aguascalientes","0144","El Colorado (El Soyatal)"],
["01","Aguascalientes","001","Aguascalientes","0146","El Conejal"],
["01","Aguascalientes","001","Aguascalientes","0157","Cotorina de Abajo"],
["01","Aguascalientes","001","Aguascalientes","0162","Coyotes"],
["01","Aguascalientes","001","Aguascalientes","0166","La Huerta (La Cruz)"],
["01","Aguascalientes","001","Aguascalientes","0170","Cuauhtémoc (Las Palomas)"],
["01","Aguascalientes","001","Aguascalientes","0171","Los Cuervos (Los Ojos de Agua)"],
["01","Aguascalientes","001","Aguascalientes","0172","San José [Granja]"],
["01","Aguascalientes","001","Aguascalientes","0176","La Chiripa"],
["01","Aguascalientes","001","Aguascalientes","0182","Dolores"],
["01","Aguascalientes","001","Aguascalientes","0183","Los Dolores"],
["01","Aguascalientes","001","Aguascalientes","0190","El Duraznillo"],
["01","Aguascalientes","001","Aguascalientes","0191","Los Durón"],
["01","Aguascalientes","001","Aguascalientes","0197","La Escondida"],
["01","Aguascalientes","001","Aguascalientes","0201","Brande Vin [Bodegas]"],
["01","Aguascalientes","001","Aguascalientes","0207","Valle Redondo"],
["01","Aguascalientes","001","Aguascalientes","0209","La Fortuna"],
["01","Aguascalientes","001","Aguascalientes","0212","Lomas del Gachupín"],
["01","Aguascalientes","001","Aguascalientes","0213","El Carmen (Gallinas Güeras) [Rancho]"],
["01","Aguascalientes","001","Aguascalientes","0216","La Gloria"],
["01","Aguascalientes","001","Aguascalientes","0226","Hacienda Nueva"],
]
data_lattice = [
["Cycle Name","KI (1/km)","Distance (mi)","Percent Fuel Savings","","",""],
["","","","Improved Speed","Decreased Accel","Eliminate Stops","Decreased Idle"],
["2012_2","3.30","1.3","5.9%","9.5%","29.2%","17.4%"],
["2145_1","0.68","11.2","2.4%","0.1%","9.5%","2.7%"],
["4234_1","0.59","58.7","8.5%","1.3%","8.5%","3.3%"],
["2032_2","0.17","57.8","21.7%","0.3%","2.7%","1.2%"],
["4171_1","0.07","173.9","58.1%","1.6%","2.1%","0.5%"]
]
data_lattice_table_rotated = [
["State","Nutritional Assessment (No. of individuals)","","","","IYCF Practices (No. of mothers: 2011-12)","Blood Pressure (No. of adults: 2011-12)","","Fasting Blood Sugar (No. of adults:2011-12)",""],
["","1975-79","1988-90","1996-97","2011-12","","Men","Women","Men","Women"],
["Kerala","5738","6633","8864","8297","245","2161","3195","1645","2391"],
["Tamil Nadu","7387","10217","5813","7851","413","2134","2858","1119","1739"],
["Karnataka","6453","8138","12606","8958","428","2467","2894","1628","2028"],
["Andhra Pradesh","5844","9920","9545","8300","557","1899","2493","1111","1529"],
["Maharashtra","5161","7796","6883","9525","467","2368","2648","1417","1599"],
["Gujarat","4403","5374","4866","9645","477","2687","3021","2122","2503"],
["Madhya Pradesh","*","*","*","7942","470","1965","2150","1579","1709"],
["Orissa","3756","5540","12024","8473","398","2040","2624","1093","1628"],
["West Bengal","*","*","*","8047","423","2058","2743","1413","2027"],
["Uttar Pradesh","*","*","*","9860","581","2139","2415","1185","1366"],
["Pooled","38742","53618","60601","86898","4459","21918","27041","14312","18519"]
]
data_lattice_process_background = [
["State","Date","Halt stations","Halt days","Persons directly reached(in lakh)","Persons trained","Persons counseled","Persons testedfor HIV"],
["Delhi","1.12.2009","8","17","1.29","3,665","2,409","1,000"],
["Rajasthan","2.12.2009 to 19.12.2009","","","","","",""],
["Gujarat","20.12.2009 to 3.1.2010","6","13","6.03","3,810","2,317","1,453"],
["Maharashtra","4.01.2010 to 1.2.2010","13","26","1.27","5,680","9,027","4,153"],
["Karnataka","2.2.2010 to 22.2.2010","11","19","1.80","5,741","3,658","3,183"],
["Kerala","23.2.2010 to 11.3.2010","9","17","1.42","3,559","2,173","855"],
["Total","","47","92","11.81","22,455","19,584","10,644"]
]
data_lattice_copy_text = [
["Plan Type","County","Plan Name","Totals"],
["GMC","Sacramento","Anthem Blue Cross","164,380"],
["GMC","Sacramento","Health Net","126,547"],
["GMC","Sacramento","Kaiser Foundation","74,620"],
["GMC","Sacramento","Molina Healthcare","59,989"],
["GMC","San Diego","Care 1st Health Plan","71,831"],
["GMC","San Diego","Community Health Group","264,639"],
["GMC","San Diego","Health Net","72,404"],
["GMC","San Diego","Kaiser","50,415"],
["GMC","San Diego","Molina Healthcare","206,430"],
["GMC","Total GMC Enrollment","","1,091,255"],
["COHS","Marin","Partnership Health Plan of CA","36,006"],
["COHS","Mendocino","Partnership Health Plan of CA","37,243"],
["COHS","Napa","Partnership Health Plan of CA","28,398"],
["COHS","Solano","Partnership Health Plan of CA","113,220"],
["COHS","Sonoma","Partnership Health Plan of CA","112,271"],
["COHS","Yolo","Partnership Health Plan of CA","52,674"],
["COHS","Del Norte","Partnership Health Plan of CA","11,242"],
["COHS","Humboldt","Partnership Health Plan of CA","49,911"],
["COHS","Lake","Partnership Health Plan of CA","29,149"],
["COHS","Lassen","Partnership Health Plan of CA","7,360"],
["COHS","Modoc","Partnership Health Plan of CA","2,940"],
["COHS","Shasta","Partnership Health Plan of CA","61,763"],
["COHS","Siskiyou","Partnership Health Plan of CA","16,715"],
["COHS","Trinity","Partnership Health Plan of CA","4,542"],
["COHS","Merced","Central California Alliance for Health","123,907"],
["COHS","Monterey","Central California Alliance for Health","147,397"],
["COHS","Santa Cruz","Central California Alliance for Health","69,458"],
["COHS","Santa Barbara","CenCal","117,609"],
["COHS","San Luis Obispo","CenCal","55,761"],
["COHS","Orange","CalOptima","783,079"],
["COHS","San Mateo","Health Plan of San Mateo","113,202"],
["COHS","Ventura","Gold Coast Health Plan","202,217"],
["COHS","Total COHS Enrollment","","2,176,064"],
["Subtotal for Two-Plan, Regional Model, GMC and COHS","","","10,132,022"],
["PCCM","Los Angeles","AIDS Healthcare Foundation","828"],
["PCCM","San Francisco","Family Mosaic","25"],
["PCCM","Total PHP Enrollment","","853"],
["All Models Total Enrollments","","","10,132,875"],
["Source: Data Warehouse 12/14/15","","",""]
]
+91
View File
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
import os
import warnings
import pytest
import camelot
testdir = os.path.dirname(os.path.abspath(__file__))
testdir = os.path.join(testdir, "files")
filename = os.path.join(testdir, 'foo.pdf')
def test_unknown_flavor():
message = ("Unknown flavor specified."
" Use either 'lattice' or 'stream'")
with pytest.raises(NotImplementedError, message=message):
tables = camelot.read_pdf(filename, flavor='chocolate')
def test_input_kwargs():
message = "columns cannot be used with flavor='lattice'"
with pytest.raises(ValueError, message=message):
tables = camelot.read_pdf(filename, columns=['10,20,30,40'])
def test_unsupported_format():
message = 'File format not supported'
filename = os.path.join(testdir, 'foo.csv')
with pytest.raises(NotImplementedError, message=message):
tables = camelot.read_pdf(filename)
def test_stream_equal_length():
message = ("Length of table_areas and columns"
" should be equal")
with pytest.raises(ValueError, message=message):
tables = camelot.read_pdf(filename, flavor='stream',
table_areas=['10,20,30,40'], columns=['10,20,30,40', '10,20,30,40'])
def test_no_tables_found():
filename = os.path.join(testdir, 'blank.pdf')
with warnings.catch_warnings():
warnings.simplefilter('error')
with pytest.raises(UserWarning) as e:
tables = camelot.read_pdf(filename)
assert str(e.value) == 'No tables found on page-1'
def test_no_tables_found_warnings_suppressed():
filename = os.path.join(testdir, 'blank.pdf')
with warnings.catch_warnings():
# the test should fail if any warning is thrown
warnings.simplefilter('error')
try:
tables = camelot.read_pdf(filename, suppress_warnings=True)
except Warning as e:
warning_text = str(e)
pytest.fail('Unexpected warning: {}'.format(warning_text))
def test_ghostscript_not_found(monkeypatch):
import distutils
def _find_executable_patch(arg):
return ''
monkeypatch.setattr(distutils.spawn, 'find_executable', _find_executable_patch)
message = ('Please make sure that Ghostscript is installed and available'
' on the PATH environment variable')
filename = os.path.join(testdir, 'foo.pdf')
with pytest.raises(Exception, message=message):
tables = camelot.read_pdf(filename)
def test_no_password():
filename = os.path.join(testdir, 'health_protected.pdf')
message = 'file has not been decrypted'
with pytest.raises(Exception, message=message):
tables = camelot.read_pdf(filename)
def test_bad_password():
filename = os.path.join(testdir, 'health_protected.pdf')
message = 'file has not been decrypted'
with pytest.raises(Exception, message=message):
tables = camelot.read_pdf(filename, password='wrongpass')
+51
View File
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import os
import pytest
import camelot
testdir = os.path.dirname(os.path.abspath(__file__))
testdir = os.path.join(testdir, "files")
@pytest.mark.mpl_image_compare(
baseline_dir="files/baseline_plots", remove_text=True)
def test_text_plot():
filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='text')
@pytest.mark.mpl_image_compare(
baseline_dir="files/baseline_plots", remove_text=True)
def test_grid_plot():
filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='grid')
@pytest.mark.mpl_image_compare(
baseline_dir="files/baseline_plots", remove_text=True)
def test_contour_plot():
filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='contour')
@pytest.mark.mpl_image_compare(
baseline_dir="files/baseline_plots", remove_text=True)
def test_line_plot():
filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='line')
@pytest.mark.mpl_image_compare(
baseline_dir="files/baseline_plots", remove_text=True)
def test_joint_plot():
filename = os.path.join(testdir, "foo.pdf")
tables = camelot.read_pdf(filename)
return camelot.plot(tables[0], kind='joint')