Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b310f16dba | |||
| defaead679 | |||
| a60ce38d4d | |||
| 36006cadc5 | |||
| db3f8c6897 | |||
| c0e9235164 | |||
| 79db6e3d1b | |||
| 29f22ad1a6 | |||
| e8af4c2c1c | |||
| 220d6ad29c | |||
| f73062c1c4 | |||
| 9cffe0adbe | |||
| 429640feea | |||
| 4366313484 | |||
| 2830ed9418 | |||
| 39cf65ffef | |||
| 32df09ad1c | |||
| 8205e0e9ab | |||
| a78ef7f841 | |||
| f734af3a0b | |||
| 61963aabb6 | |||
| 60c1270745 | |||
| 72481bc1b5 | |||
| c5c85a2dc8 | |||
| 9c6ec49652 | |||
| 2a60d1fd54 | |||
| 2022a8abc9 | |||
| 3def4a5aea | |||
| 1d064adc3e | |||
| 7baea06bca | |||
| 5645ef5b62 | |||
| 7a3b76cb76 | |||
| 970f906435 | |||
| 297888b18c | |||
| 9362175a82 | |||
| 9e6474e5a6 | |||
| 1ba0cfc7bc | |||
| 8d38907832 | |||
| ac2d40aa44 | |||
| c33bf9c168 | |||
| d628e9b5df | |||
| 750f955f9c | |||
| 898646b73b | |||
| 45e7f7570e | |||
| 296be21d9d | |||
| 1a358f603a | |||
| fe68328ef2 | |||
| 9b2fc53e58 | |||
| 80f6870117 | |||
| c53ea795fd | |||
| f13337d50a | |||
| 192f12a710 | |||
| 90aaba6eec | |||
| 6e8079df84 | |||
| cf7823f33c | |||
| f7e69bbbfe | |||
| 58eddd0804 | |||
| 9d00937ec7 | |||
| 9ff61c70d3 | |||
| 2a1d21af32 | |||
| 2b8ae5fa4d | |||
| ea79cf2fb7 | |||
| 9537143fe0 | |||
| c5bde5e2ad | |||
| f1bf4309ec | |||
| 09ed772b6a | |||
| fc0542bd3c | |||
| 82463e10b4 | |||
| b9648f3b39 | |||
| dfb0d4fb4c | |||
| 00b7b2aeba | |||
| 759e635a3c | |||
| 6298f3a0f6 | |||
| 08f638243e | |||
| 6703f2f254 | |||
| 13f2b199c9 | |||
| 7731497a5b | |||
| 9c4e7bf3f6 |
@@ -1,6 +1,2 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = camelot
|
||||
include = */camelot/*
|
||||
omit =
|
||||
*/setup.py
|
||||
branch = True
|
||||
@@ -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,7 +5,12 @@ __pycache__/
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
.coverage
|
||||
coverage.xml
|
||||
|
||||
.pytest_cache/
|
||||
_build/
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
Release History
|
||||
===============
|
||||
|
||||
master
|
||||
------
|
||||
|
||||
0.3.2 (2018-11-04)
|
||||
------------------
|
||||
|
||||
**Improvements**
|
||||
|
||||
* [#186](https://github.com/socialcopsdev/camelot/issues/186) Add `_bbox` attribute to table. [#193](https://github.com/socialcopsdev/camelot/pull/193) by Vinayak Mehta.
|
||||
* You can use `table._bbox` to get coordinates of the detected table.
|
||||
|
||||
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!
|
||||
@@ -0,0 +1 @@
|
||||
include MANIFEST.in README.md HISTORY.md LICENSE setup.py setup.cfg
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
 
|
||||
[](https://travis-ci.org/socialcopsdev/camelot) [](https://camelot-py.readthedocs.io/en/master/)
|
||||
[](https://codecov.io/github/socialcopsdev/camelot?branch=master)
|
||||
[](https://pypi.org/project/camelot-py/) [](https://pypi.org/project/camelot-py/) [](https://pypi.org/project/camelot-py/) [](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!
|
||||
|
||||

|
||||
**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
|
||||
<TableList tables=1>
|
||||
<TableList n=1>
|
||||
>>> tables.export('foo.csv', f='csv', compress=True) # json, excel, html
|
||||
>>> tables[0]
|
||||
<Table shape=(7, 7)>
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
VERSION = (0, 1, 0)
|
||||
VERSION = (0, 3, 2)
|
||||
|
||||
__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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .stream import Stream
|
||||
from .lattice import Lattice
|
||||
from .lattice import Lattice
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,11 @@ 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)
|
||||
table._bbox = tk
|
||||
_tables.append(table)
|
||||
|
||||
return _tables
|
||||
return _tables
|
||||
|
||||
@@ -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,11 @@ 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)
|
||||
table._bbox = tk
|
||||
_tables.append(table)
|
||||
|
||||
return _tables
|
||||
return _tables
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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. */
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,54,43,23', '20,67,55,33']``, 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","..."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
---------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
pytest==3.8.0
|
||||
pytest-runner==4.2
|
||||
Sphinx==1.7.9
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import matplotlib
|
||||
matplotlib.use('agg')
|
||||
@@ -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"]
|
||||
]
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,2 @@
|
||||
"a","b"
|
||||
"1","2"
|
||||
|
@@ -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
|
||||
@@ -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>"
|
||||
|
||||
@@ -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","","",""]
|
||||
]
|
||||
@@ -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')
|
||||
@@ -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')
|
||||