Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea5747c5c4 | |||
| 0b85c77425 | |||
| 62ed4753cd | |||
| c78957ae5a | |||
| 2b3461deab | |||
| 0198f5527c | |||
| 175ba32d38 | |||
| be1f0a2884 | |||
| 50b4468aff | |||
| f6aa21c31f | |||
| a38d52c7b2 | |||
| 3f5af18738 | |||
| e0090fbb0a | |||
| e89e147b5c | |||
| e0cb935130 | |||
| 17d48be46e | |||
| 48b2dce633 | |||
| 6301fee523 | |||
| 01dab12fbc | |||
| ca6cefa362 | |||
| d918293fea | |||
| eb7be9c8e6 | |||
| 3ef50f6f8d | |||
| 2dc48f43d6 | |||
| d662819755 |
@@ -4,6 +4,21 @@ Release History
|
|||||||
master
|
master
|
||||||
------
|
------
|
||||||
|
|
||||||
|
0.6.0 (2018-12-24)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
**Improvements**
|
||||||
|
|
||||||
|
* [#91](https://github.com/socialcopsdev/camelot/issues/91) Add support to read from url. [#236](https://github.com/socialcopsdev/camelot/pull/236) by Vinayak Mehta.
|
||||||
|
* [#229](https://github.com/socialcopsdev/camelot/issues/229), [#230](https://github.com/socialcopsdev/camelot/issues/230) and [#233](https://github.com/socialcopsdev/camelot/issues/233) New configuration parameters. [#234](https://github.com/socialcopsdev/camelot/pull/234) by Vinayak Mehta.
|
||||||
|
* `strip_text`: To define characters that should be stripped from each string.
|
||||||
|
* `edge_tol`: Tolerance parameter for extending textedges vertically.
|
||||||
|
* `resolution`: Resolution used for PDF to PNG conversion.
|
||||||
|
* Check out the [advanced docs](https://camelot-py.readthedocs.io/en/master/user/advanced.html#strip-characters-from-text) for usage details.
|
||||||
|
* [#170](https://github.com/socialcopsdev/camelot/issues/170) Add option to pass pdfminer layout kwargs. [#232](https://github.com/socialcopsdev/camelot/pull/232) by Vinayak Mehta.
|
||||||
|
* Keyword arguments for [pdfminer.layout.LAParams](https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py#L33) can now be passed using `layout_kwargs` in `read_pdf()`.
|
||||||
|
* The `margins` keyword argument in `read_pdf()` is now deprecated.
|
||||||
|
|
||||||
0.5.0 (2018-12-13)
|
0.5.0 (2018-12-13)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
VERSION = (0, 5, 0)
|
VERSION = (0, 6, 0)
|
||||||
PRERELEASE = None # alpha, beta or rc
|
PRERELEASE = None # alpha, beta or rc
|
||||||
REVISION = None
|
REVISION = None
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ pass_config = click.make_pass_decorator(Config)
|
|||||||
help='Split text that spans across multiple cells.')
|
help='Split text that spans across multiple cells.')
|
||||||
@click.option('-flag', '--flag_size', is_flag=True, help='Flag text based on'
|
@click.option('-flag', '--flag_size', is_flag=True, help='Flag text based on'
|
||||||
' font size. Useful to detect super/subscripts.')
|
' font size. Useful to detect super/subscripts.')
|
||||||
|
@click.option('-strip', '--strip_text', help='Characters that should be stripped from a string before'
|
||||||
|
' assigning it to a cell.')
|
||||||
@click.option('-M', '--margins', nargs=3, default=(1.0, 0.5, 0.1),
|
@click.option('-M', '--margins', nargs=3, default=(1.0, 0.5, 0.1),
|
||||||
help='PDFMiner char_margin, line_margin and word_margin.')
|
help='PDFMiner char_margin, line_margin and word_margin.')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@@ -68,10 +70,10 @@ def cli(ctx, *args, **kwargs):
|
|||||||
@click.option('-shift', '--shift_text', default=['l', 't'],
|
@click.option('-shift', '--shift_text', default=['l', 't'],
|
||||||
type=click.Choice(['', 'l', 'r', 't', 'b']), multiple=True,
|
type=click.Choice(['', 'l', 'r', 't', 'b']), multiple=True,
|
||||||
help='Direction in which text in a spanning cell will flow.')
|
help='Direction in which text in a spanning cell will flow.')
|
||||||
@click.option('-l', '--line_close_tol', default=2,
|
@click.option('-l', '--line_tol', default=2,
|
||||||
help='Tolerance parameter used to merge close vertical'
|
help='Tolerance parameter used to merge close vertical'
|
||||||
' and horizontal lines.')
|
' and horizontal lines.')
|
||||||
@click.option('-j', '--joint_close_tol', default=2,
|
@click.option('-j', '--joint_tol', default=2,
|
||||||
help='Tolerance parameter used to decide whether'
|
help='Tolerance parameter used to decide whether'
|
||||||
' the detected lines and points lie close to each other.')
|
' the detected lines and points lie close to each other.')
|
||||||
@click.option('-block', '--threshold_blocksize', default=15,
|
@click.option('-block', '--threshold_blocksize', default=15,
|
||||||
@@ -84,6 +86,8 @@ def cli(ctx, *args, **kwargs):
|
|||||||
' may be zero or negative as well.')
|
' may be zero or negative as well.')
|
||||||
@click.option('-I', '--iterations', default=0,
|
@click.option('-I', '--iterations', default=0,
|
||||||
help='Number of times for erosion/dilation will be applied.')
|
help='Number of times for erosion/dilation will be applied.')
|
||||||
|
@click.option('-res', '--resolution', default=300,
|
||||||
|
help='Resolution used for PDF to PNG conversion.')
|
||||||
@click.option('-plot', '--plot_type',
|
@click.option('-plot', '--plot_type',
|
||||||
type=click.Choice(['text', 'grid', 'contour', 'joint', 'line']),
|
type=click.Choice(['text', 'grid', 'contour', 'joint', 'line']),
|
||||||
help='Plot elements found on PDF page for visual debugging.')
|
help='Plot elements found on PDF page for visual debugging.')
|
||||||
@@ -133,9 +137,11 @@ def lattice(c, *args, **kwargs):
|
|||||||
' where x1, y1 -> left-top and x2, y2 -> right-bottom.')
|
' where x1, y1 -> left-top and x2, y2 -> right-bottom.')
|
||||||
@click.option('-C', '--columns', default=[], multiple=True,
|
@click.option('-C', '--columns', default=[], multiple=True,
|
||||||
help='X coordinates of column separators.')
|
help='X coordinates of column separators.')
|
||||||
@click.option('-r', '--row_close_tol', default=2, help='Tolerance parameter'
|
@click.option('-e', '--edge_tol', default=50, help='Tolerance parameter'
|
||||||
|
' for extending textedges vertically.')
|
||||||
|
@click.option('-r', '--row_tol', default=2, help='Tolerance parameter'
|
||||||
' used to combine text vertically, to generate rows.')
|
' used to combine text vertically, to generate rows.')
|
||||||
@click.option('-c', '--col_close_tol', default=0, help='Tolerance parameter'
|
@click.option('-c', '--column_tol', default=0, help='Tolerance parameter'
|
||||||
' used to combine text horizontally, to generate columns.')
|
' used to combine text horizontally, to generate columns.')
|
||||||
@click.option('-plot', '--plot_type',
|
@click.option('-plot', '--plot_type',
|
||||||
type=click.Choice(['text', 'grid', 'contour', 'textedge']),
|
type=click.Choice(['text', 'grid', 'contour', 'textedge']),
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import pandas as pd
|
|||||||
# minimum number of vertical textline intersections for a textedge
|
# minimum number of vertical textline intersections for a textedge
|
||||||
# to be considered valid
|
# to be considered valid
|
||||||
TEXTEDGE_REQUIRED_ELEMENTS = 4
|
TEXTEDGE_REQUIRED_ELEMENTS = 4
|
||||||
# y coordinate tolerance for extending textedge
|
|
||||||
TEXTEDGE_EXTEND_TOLERANCE = 50
|
|
||||||
# padding added to table area on the left, right and bottom
|
# padding added to table area on the left, right and bottom
|
||||||
TABLE_AREA_PADDING = 10
|
TABLE_AREA_PADDING = 10
|
||||||
|
|
||||||
@@ -55,11 +53,11 @@ class TextEdge(object):
|
|||||||
return '<TextEdge x={} y0={} y1={} align={} valid={}>'.format(
|
return '<TextEdge x={} y0={} y1={} align={} valid={}>'.format(
|
||||||
round(self.x, 2), round(self.y0, 2), round(self.y1, 2), self.align, self.is_valid)
|
round(self.x, 2), round(self.y0, 2), round(self.y1, 2), self.align, self.is_valid)
|
||||||
|
|
||||||
def update_coords(self, x, y0):
|
def update_coords(self, x, y0, edge_tol=50):
|
||||||
"""Updates the text edge's x and bottom y coordinates and sets
|
"""Updates the text edge's x and bottom y coordinates and sets
|
||||||
the is_valid attribute.
|
the is_valid attribute.
|
||||||
"""
|
"""
|
||||||
if np.isclose(self.y0, y0, atol=TEXTEDGE_EXTEND_TOLERANCE):
|
if np.isclose(self.y0, y0, atol=edge_tol):
|
||||||
self.x = (self.intersections * self.x + x) / float(self.intersections + 1)
|
self.x = (self.intersections * self.x + x) / float(self.intersections + 1)
|
||||||
self.y0 = y0
|
self.y0 = y0
|
||||||
self.intersections += 1
|
self.intersections += 1
|
||||||
@@ -74,7 +72,8 @@ class TextEdges(object):
|
|||||||
the PDF page. The dict has three keys based on the alignments,
|
the PDF page. The dict has three keys based on the alignments,
|
||||||
and each key's value is a list of camelot.core.TextEdge objects.
|
and each key's value is a list of camelot.core.TextEdge objects.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self, edge_tol=50):
|
||||||
|
self.edge_tol = edge_tol
|
||||||
self._textedges = {'left': [], 'right': [], 'middle': []}
|
self._textedges = {'left': [], 'right': [], 'middle': []}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -115,7 +114,8 @@ class TextEdges(object):
|
|||||||
if idx is None:
|
if idx is None:
|
||||||
self.add(textline, align)
|
self.add(textline, align)
|
||||||
else:
|
else:
|
||||||
self._textedges[align][idx].update_coords(x_coord, textline.y0)
|
self._textedges[align][idx].update_coords(
|
||||||
|
x_coord, textline.y0, edge_tol=self.edge_tol)
|
||||||
|
|
||||||
def generate(self, textlines):
|
def generate(self, textlines):
|
||||||
"""Generates the text edges dict based on horizontal text
|
"""Generates the text edges dict based on horizontal text
|
||||||
@@ -359,7 +359,7 @@ class Table(object):
|
|||||||
cell.left = cell.right = cell.top = cell.bottom = True
|
cell.left = cell.right = cell.top = cell.bottom = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_edges(self, vertical, horizontal, joint_close_tol=2):
|
def set_edges(self, vertical, horizontal, joint_tol=2):
|
||||||
"""Sets a cell's edges to True depending on whether the cell's
|
"""Sets a cell's edges to True depending on whether the cell's
|
||||||
coordinates overlap with the line's coordinates within a
|
coordinates overlap with the line's coordinates within a
|
||||||
tolerance.
|
tolerance.
|
||||||
@@ -376,11 +376,11 @@ class Table(object):
|
|||||||
# find closest x coord
|
# find closest x coord
|
||||||
# iterate over y coords and find closest start and end points
|
# iterate over y coords and find closest start and end points
|
||||||
i = [i for i, t in enumerate(self.cols)
|
i = [i for i, t in enumerate(self.cols)
|
||||||
if np.isclose(v[0], t[0], atol=joint_close_tol)]
|
if np.isclose(v[0], t[0], atol=joint_tol)]
|
||||||
j = [j for j, t in enumerate(self.rows)
|
j = [j for j, t in enumerate(self.rows)
|
||||||
if np.isclose(v[3], t[0], atol=joint_close_tol)]
|
if np.isclose(v[3], t[0], atol=joint_tol)]
|
||||||
k = [k for k, t in enumerate(self.rows)
|
k = [k for k, t in enumerate(self.rows)
|
||||||
if np.isclose(v[1], t[0], atol=joint_close_tol)]
|
if np.isclose(v[1], t[0], atol=joint_tol)]
|
||||||
if not j:
|
if not j:
|
||||||
continue
|
continue
|
||||||
J = j[0]
|
J = j[0]
|
||||||
@@ -427,11 +427,11 @@ class Table(object):
|
|||||||
# find closest y coord
|
# find closest y coord
|
||||||
# iterate over x coords and find closest start and end points
|
# iterate over x coords and find closest start and end points
|
||||||
i = [i for i, t in enumerate(self.rows)
|
i = [i for i, t in enumerate(self.rows)
|
||||||
if np.isclose(h[1], t[0], atol=joint_close_tol)]
|
if np.isclose(h[1], t[0], atol=joint_tol)]
|
||||||
j = [j for j, t in enumerate(self.cols)
|
j = [j for j, t in enumerate(self.cols)
|
||||||
if np.isclose(h[0], t[0], atol=joint_close_tol)]
|
if np.isclose(h[0], t[0], atol=joint_tol)]
|
||||||
k = [k for k, t in enumerate(self.cols)
|
k = [k for k, t in enumerate(self.cols)
|
||||||
if np.isclose(h[2], t[0], atol=joint_close_tol)]
|
if np.isclose(h[2], t[0], atol=joint_tol)]
|
||||||
if not j:
|
if not j:
|
||||||
continue
|
continue
|
||||||
J = j[0]
|
J = j[0]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from PyPDF2 import PdfFileReader, PdfFileWriter
|
|||||||
from .core import TableList
|
from .core import TableList
|
||||||
from .parsers import Stream, Lattice
|
from .parsers import Stream, Lattice
|
||||||
from .utils import (TemporaryDirectory, get_page_layout, get_text_objects,
|
from .utils import (TemporaryDirectory, get_page_layout, get_text_objects,
|
||||||
get_rotation)
|
get_rotation, is_url, download_url)
|
||||||
|
|
||||||
|
|
||||||
class PDFHandler(object):
|
class PDFHandler(object):
|
||||||
@@ -18,8 +18,8 @@ class PDFHandler(object):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
filename : str
|
filepath : str
|
||||||
Path to PDF file.
|
Filepath or URL of the PDF file.
|
||||||
pages : str, optional (default: '1')
|
pages : str, optional (default: '1')
|
||||||
Comma-separated page numbers.
|
Comma-separated page numbers.
|
||||||
Example: '1,3,4' or '1,4-end'.
|
Example: '1,3,4' or '1,4-end'.
|
||||||
@@ -27,11 +27,13 @@ class PDFHandler(object):
|
|||||||
Password for decryption.
|
Password for decryption.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, filename, pages='1', password=None):
|
def __init__(self, filepath, pages='1', password=None):
|
||||||
self.filename = filename
|
if is_url(filepath):
|
||||||
if not filename.lower().endswith('.pdf'):
|
filepath = download_url(filepath)
|
||||||
|
self.filepath = filepath
|
||||||
|
if not filepath.lower().endswith('.pdf'):
|
||||||
raise NotImplementedError("File format not supported")
|
raise NotImplementedError("File format not supported")
|
||||||
self.pages = self._get_pages(self.filename, pages)
|
self.pages = self._get_pages(self.filepath, pages)
|
||||||
if password is None:
|
if password is None:
|
||||||
self.password = ''
|
self.password = ''
|
||||||
else:
|
else:
|
||||||
@@ -39,13 +41,13 @@ class PDFHandler(object):
|
|||||||
if sys.version_info[0] < 3:
|
if sys.version_info[0] < 3:
|
||||||
self.password = self.password.encode('ascii')
|
self.password = self.password.encode('ascii')
|
||||||
|
|
||||||
def _get_pages(self, filename, pages):
|
def _get_pages(self, filepath, pages):
|
||||||
"""Converts pages string to list of ints.
|
"""Converts pages string to list of ints.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
filename : str
|
filepath : str
|
||||||
Path to PDF file.
|
Filepath or URL of the PDF file.
|
||||||
pages : str, optional (default: '1')
|
pages : str, optional (default: '1')
|
||||||
Comma-separated page numbers.
|
Comma-separated page numbers.
|
||||||
Example: 1,3,4 or 1,4-end.
|
Example: 1,3,4 or 1,4-end.
|
||||||
@@ -60,7 +62,7 @@ class PDFHandler(object):
|
|||||||
if pages == '1':
|
if pages == '1':
|
||||||
page_numbers.append({'start': 1, 'end': 1})
|
page_numbers.append({'start': 1, 'end': 1})
|
||||||
else:
|
else:
|
||||||
infile = PdfFileReader(open(filename, 'rb'), strict=False)
|
infile = PdfFileReader(open(filepath, 'rb'), strict=False)
|
||||||
if infile.isEncrypted:
|
if infile.isEncrypted:
|
||||||
infile.decrypt(self.password)
|
infile.decrypt(self.password)
|
||||||
if pages == 'all':
|
if pages == 'all':
|
||||||
@@ -79,20 +81,20 @@ class PDFHandler(object):
|
|||||||
P.extend(range(p['start'], p['end'] + 1))
|
P.extend(range(p['start'], p['end'] + 1))
|
||||||
return sorted(set(P))
|
return sorted(set(P))
|
||||||
|
|
||||||
def _save_page(self, filename, page, temp):
|
def _save_page(self, filepath, page, temp):
|
||||||
"""Saves specified page from PDF into a temporary directory.
|
"""Saves specified page from PDF into a temporary directory.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
filename : str
|
filepath : str
|
||||||
Path to PDF file.
|
Filepath or URL of the PDF file.
|
||||||
page : int
|
page : int
|
||||||
Page number.
|
Page number.
|
||||||
temp : str
|
temp : str
|
||||||
Tmp directory.
|
Tmp directory.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
with open(filename, 'rb') as fileobj:
|
with open(filepath, 'rb') as fileobj:
|
||||||
infile = PdfFileReader(fileobj, strict=False)
|
infile = PdfFileReader(fileobj, strict=False)
|
||||||
if infile.isEncrypted:
|
if infile.isEncrypted:
|
||||||
infile.decrypt(self.password)
|
infile.decrypt(self.password)
|
||||||
@@ -125,7 +127,7 @@ class PDFHandler(object):
|
|||||||
with open(fpath, 'wb') as f:
|
with open(fpath, 'wb') as f:
|
||||||
outfile.write(f)
|
outfile.write(f)
|
||||||
|
|
||||||
def parse(self, flavor='lattice', suppress_stdout=False, **kwargs):
|
def parse(self, flavor='lattice', suppress_stdout=False, layout_kwargs={}, **kwargs):
|
||||||
"""Extracts tables by calling parser.get_tables on all single
|
"""Extracts tables by calling parser.get_tables on all single
|
||||||
page PDFs.
|
page PDFs.
|
||||||
|
|
||||||
@@ -136,6 +138,8 @@ class PDFHandler(object):
|
|||||||
Lattice is used by default.
|
Lattice is used by default.
|
||||||
suppress_stdout : str (default: False)
|
suppress_stdout : str (default: False)
|
||||||
Suppress logs and warnings.
|
Suppress logs and warnings.
|
||||||
|
layout_kwargs : dict, optional (default: {})
|
||||||
|
A dict of `pdfminer.layout.LAParams <https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py#L33>`_ kwargs.
|
||||||
kwargs : dict
|
kwargs : dict
|
||||||
See camelot.read_pdf kwargs.
|
See camelot.read_pdf kwargs.
|
||||||
|
|
||||||
@@ -148,11 +152,12 @@ class PDFHandler(object):
|
|||||||
tables = []
|
tables = []
|
||||||
with TemporaryDirectory() as tempdir:
|
with TemporaryDirectory() as tempdir:
|
||||||
for p in self.pages:
|
for p in self.pages:
|
||||||
self._save_page(self.filename, p, tempdir)
|
self._save_page(self.filepath, p, tempdir)
|
||||||
pages = [os.path.join(tempdir, 'page-{0}.pdf'.format(p))
|
pages = [os.path.join(tempdir, 'page-{0}.pdf'.format(p))
|
||||||
for p in self.pages]
|
for p in self.pages]
|
||||||
parser = Lattice(**kwargs) if flavor == 'lattice' else Stream(**kwargs)
|
parser = Lattice(**kwargs) if flavor == 'lattice' else Stream(**kwargs)
|
||||||
for p in pages:
|
for p in pages:
|
||||||
t = parser.extract_tables(p, suppress_stdout=suppress_stdout)
|
t = parser.extract_tables(p, suppress_stdout=suppress_stdout,
|
||||||
|
layout_kwargs=layout_kwargs)
|
||||||
tables.extend(t)
|
tables.extend(t)
|
||||||
return TableList(tables)
|
return TableList(tables)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from .utils import validate_input, remove_extra
|
|||||||
|
|
||||||
|
|
||||||
def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
||||||
suppress_stdout=False, **kwargs):
|
suppress_stdout=False, layout_kwargs={}, **kwargs):
|
||||||
"""Read PDF and return extracted tables.
|
"""Read PDF and return extracted tables.
|
||||||
|
|
||||||
Note: kwargs annotated with ^ can only be used with flavor='stream'
|
Note: kwargs annotated with ^ can only be used with flavor='stream'
|
||||||
@@ -15,7 +15,7 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
filepath : str
|
filepath : str
|
||||||
Path to PDF file.
|
Filepath or URL of the PDF file.
|
||||||
pages : str, optional (default: '1')
|
pages : str, optional (default: '1')
|
||||||
Comma-separated page numbers.
|
Comma-separated page numbers.
|
||||||
Example: '1,3,4' or '1,4-end'.
|
Example: '1,3,4' or '1,4-end'.
|
||||||
@@ -26,6 +26,8 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
|||||||
Lattice is used by default.
|
Lattice is used by default.
|
||||||
suppress_stdout : bool, optional (default: True)
|
suppress_stdout : bool, optional (default: True)
|
||||||
Print all logs and warnings.
|
Print all logs and warnings.
|
||||||
|
layout_kwargs : dict, optional (default: {})
|
||||||
|
A dict of `pdfminer.layout.LAParams <https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py#L33>`_ kwargs.
|
||||||
table_areas : list, optional (default: None)
|
table_areas : list, optional (default: None)
|
||||||
List of table area strings of the form x1,y1,x2,y2
|
List of table area strings of the form x1,y1,x2,y2
|
||||||
where (x1, y1) -> left-top and (x2, y2) -> right-bottom
|
where (x1, y1) -> left-top and (x2, y2) -> right-bottom
|
||||||
@@ -38,10 +40,13 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
|||||||
flag_size : bool, optional (default: False)
|
flag_size : bool, optional (default: False)
|
||||||
Flag text based on font size. Useful to detect
|
Flag text based on font size. Useful to detect
|
||||||
super/subscripts. Adds <s></s> around flagged text.
|
super/subscripts. Adds <s></s> around flagged text.
|
||||||
row_close_tol^ : int, optional (default: 2)
|
strip_text : str, optional (default: '')
|
||||||
|
Characters that should be stripped from a string before
|
||||||
|
assigning it to a cell.
|
||||||
|
row_tol^ : int, optional (default: 2)
|
||||||
Tolerance parameter used to combine text vertically,
|
Tolerance parameter used to combine text vertically,
|
||||||
to generate rows.
|
to generate rows.
|
||||||
col_close_tol^ : int, optional (default: 0)
|
column_tol^ : int, optional (default: 0)
|
||||||
Tolerance parameter used to combine text horizontally,
|
Tolerance parameter used to combine text horizontally,
|
||||||
to generate columns.
|
to generate columns.
|
||||||
process_background* : bool, optional (default: False)
|
process_background* : bool, optional (default: False)
|
||||||
@@ -57,10 +62,10 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
|||||||
shift_text* : list, optional (default: ['l', 't'])
|
shift_text* : list, optional (default: ['l', 't'])
|
||||||
{'l', 'r', 't', 'b'}
|
{'l', 'r', 't', 'b'}
|
||||||
Direction in which text in a spanning cell will flow.
|
Direction in which text in a spanning cell will flow.
|
||||||
line_close_tol* : int, optional (default: 2)
|
line_tol* : int, optional (default: 2)
|
||||||
Tolerance parameter used to merge close vertical and horizontal
|
Tolerance parameter used to merge close vertical and horizontal
|
||||||
lines.
|
lines.
|
||||||
joint_close_tol* : int, optional (default: 2)
|
joint_tol* : int, optional (default: 2)
|
||||||
Tolerance parameter used to decide whether the detected lines
|
Tolerance parameter used to decide whether the detected lines
|
||||||
and points lie close to each other.
|
and points lie close to each other.
|
||||||
threshold_blocksize* : int, optional (default: 15)
|
threshold_blocksize* : int, optional (default: 15)
|
||||||
@@ -77,10 +82,8 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
|||||||
Number of times for erosion/dilation is applied.
|
Number of times for erosion/dilation is applied.
|
||||||
|
|
||||||
For more information, refer `OpenCV's dilate <https://docs.opencv.org/2.4/modules/imgproc/doc/filtering.html#dilate>`_.
|
For more information, refer `OpenCV's dilate <https://docs.opencv.org/2.4/modules/imgproc/doc/filtering.html#dilate>`_.
|
||||||
margins : tuple
|
resolution* : int, optional (default: 300)
|
||||||
PDFMiner char_margin, line_margin and word_margin.
|
Resolution used for PDF to PNG conversion.
|
||||||
|
|
||||||
For more information, refer `PDFMiner docs <https://euske.github.io/pdfminer/>`_.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -98,5 +101,6 @@ def read_pdf(filepath, pages='1', password=None, flavor='lattice',
|
|||||||
validate_input(kwargs, flavor=flavor)
|
validate_input(kwargs, flavor=flavor)
|
||||||
p = PDFHandler(filepath, pages=pages, password=password)
|
p = PDFHandler(filepath, pages=pages, password=password)
|
||||||
kwargs = remove_extra(kwargs, flavor=flavor)
|
kwargs = remove_extra(kwargs, flavor=flavor)
|
||||||
tables = p.parse(flavor=flavor, suppress_stdout=suppress_stdout, **kwargs)
|
tables = p.parse(flavor=flavor, suppress_stdout=suppress_stdout,
|
||||||
|
layout_kwargs=layout_kwargs, **kwargs)
|
||||||
return tables
|
return tables
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ from ..utils import get_page_layout, get_text_objects
|
|||||||
class BaseParser(object):
|
class BaseParser(object):
|
||||||
"""Defines a base parser.
|
"""Defines a base parser.
|
||||||
"""
|
"""
|
||||||
def _generate_layout(self, filename):
|
def _generate_layout(self, filename, layout_kwargs):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
self.layout_kwargs = layout_kwargs
|
||||||
self.layout, self.dimensions = get_page_layout(
|
self.layout, self.dimensions = get_page_layout(
|
||||||
self.filename,
|
filename, **layout_kwargs)
|
||||||
char_margin=self.char_margin,
|
|
||||||
line_margin=self.line_margin,
|
|
||||||
word_margin=self.word_margin)
|
|
||||||
self.horizontal_text = get_text_objects(self.layout, ltype="lh")
|
self.horizontal_text = get_text_objects(self.layout, ltype="lh")
|
||||||
self.vertical_text = get_text_objects(self.layout, ltype="lv")
|
self.vertical_text = get_text_objects(self.layout, ltype="lv")
|
||||||
self.pdf_width, self.pdf_height = self.dimensions
|
self.pdf_width, self.pdf_height = self.dimensions
|
||||||
|
|||||||
@@ -50,10 +50,13 @@ class Lattice(BaseParser):
|
|||||||
flag_size : bool, optional (default: False)
|
flag_size : bool, optional (default: False)
|
||||||
Flag text based on font size. Useful to detect
|
Flag text based on font size. Useful to detect
|
||||||
super/subscripts. Adds <s></s> around flagged text.
|
super/subscripts. Adds <s></s> around flagged text.
|
||||||
line_close_tol : int, optional (default: 2)
|
strip_text : str, optional (default: '')
|
||||||
|
Characters that should be stripped from a string before
|
||||||
|
assigning it to a cell.
|
||||||
|
line_tol : int, optional (default: 2)
|
||||||
Tolerance parameter used to merge close vertical and horizontal
|
Tolerance parameter used to merge close vertical and horizontal
|
||||||
lines.
|
lines.
|
||||||
joint_close_tol : int, optional (default: 2)
|
joint_tol : int, optional (default: 2)
|
||||||
Tolerance parameter used to decide whether the detected lines
|
Tolerance parameter used to decide whether the detected lines
|
||||||
and points lie close to each other.
|
and points lie close to each other.
|
||||||
threshold_blocksize : int, optional (default: 15)
|
threshold_blocksize : int, optional (default: 15)
|
||||||
@@ -70,17 +73,15 @@ class Lattice(BaseParser):
|
|||||||
Number of times for erosion/dilation is applied.
|
Number of times for erosion/dilation is applied.
|
||||||
|
|
||||||
For more information, refer `OpenCV's dilate <https://docs.opencv.org/2.4/modules/imgproc/doc/filtering.html#dilate>`_.
|
For more information, refer `OpenCV's dilate <https://docs.opencv.org/2.4/modules/imgproc/doc/filtering.html#dilate>`_.
|
||||||
margins : tuple
|
resolution : int, optional (default: 300)
|
||||||
PDFMiner char_margin, line_margin and word_margin.
|
Resolution used for PDF to PNG conversion.
|
||||||
|
|
||||||
For more information, refer `PDFMiner docs <https://euske.github.io/pdfminer/>`_.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, table_areas=None, process_background=False,
|
def __init__(self, table_areas=None, process_background=False,
|
||||||
line_size_scaling=15, copy_text=None, shift_text=['l', 't'],
|
line_size_scaling=15, copy_text=None, shift_text=['l', 't'],
|
||||||
split_text=False, flag_size=False, line_close_tol=2,
|
split_text=False, flag_size=False, strip_text='', line_tol=2,
|
||||||
joint_close_tol=2, threshold_blocksize=15, threshold_constant=-2,
|
joint_tol=2, threshold_blocksize=15, threshold_constant=-2,
|
||||||
iterations=0, margins=(1.0, 0.5, 0.1), **kwargs):
|
iterations=0, resolution=300, **kwargs):
|
||||||
self.table_areas = table_areas
|
self.table_areas = table_areas
|
||||||
self.process_background = process_background
|
self.process_background = process_background
|
||||||
self.line_size_scaling = line_size_scaling
|
self.line_size_scaling = line_size_scaling
|
||||||
@@ -88,12 +89,13 @@ class Lattice(BaseParser):
|
|||||||
self.shift_text = shift_text
|
self.shift_text = shift_text
|
||||||
self.split_text = split_text
|
self.split_text = split_text
|
||||||
self.flag_size = flag_size
|
self.flag_size = flag_size
|
||||||
self.line_close_tol = line_close_tol
|
self.strip_text = strip_text
|
||||||
self.joint_close_tol = joint_close_tol
|
self.line_tol = line_tol
|
||||||
|
self.joint_tol = joint_tol
|
||||||
self.threshold_blocksize = threshold_blocksize
|
self.threshold_blocksize = threshold_blocksize
|
||||||
self.threshold_constant = threshold_constant
|
self.threshold_constant = threshold_constant
|
||||||
self.iterations = iterations
|
self.iterations = iterations
|
||||||
self.char_margin, self.line_margin, self.word_margin = margins
|
self.resolution = resolution
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _reduce_index(t, idx, shift_text):
|
def _reduce_index(t, idx, shift_text):
|
||||||
@@ -214,7 +216,7 @@ class Lattice(BaseParser):
|
|||||||
'-sDEVICE=png16m',
|
'-sDEVICE=png16m',
|
||||||
'-o',
|
'-o',
|
||||||
self.imagename,
|
self.imagename,
|
||||||
'-r600',
|
'-r{}'.format(self.resolution),
|
||||||
self.filename
|
self.filename
|
||||||
]
|
]
|
||||||
gs = get_executable()
|
gs = get_executable()
|
||||||
@@ -283,9 +285,9 @@ class Lattice(BaseParser):
|
|||||||
rows.extend([tk[1], tk[3]])
|
rows.extend([tk[1], tk[3]])
|
||||||
# sort horizontal and vertical segments
|
# sort horizontal and vertical segments
|
||||||
cols = merge_close_lines(
|
cols = merge_close_lines(
|
||||||
sorted(cols), line_close_tol=self.line_close_tol)
|
sorted(cols), line_tol=self.line_tol)
|
||||||
rows = merge_close_lines(
|
rows = merge_close_lines(
|
||||||
sorted(rows, reverse=True), line_close_tol=self.line_close_tol)
|
sorted(rows, reverse=True), line_tol=self.line_tol)
|
||||||
# make grid using x and y coord of shortlisted rows and cols
|
# make grid using x and y coord of shortlisted rows and cols
|
||||||
cols = [(cols[i], cols[i + 1])
|
cols = [(cols[i], cols[i + 1])
|
||||||
for i in range(0, len(cols) - 1)]
|
for i in range(0, len(cols) - 1)]
|
||||||
@@ -302,7 +304,7 @@ class Lattice(BaseParser):
|
|||||||
|
|
||||||
table = Table(cols, rows)
|
table = Table(cols, rows)
|
||||||
# set table edges to True using ver+hor lines
|
# set table edges to True using ver+hor lines
|
||||||
table = table.set_edges(v_s, h_s, joint_close_tol=self.joint_close_tol)
|
table = table.set_edges(v_s, h_s, joint_tol=self.joint_tol)
|
||||||
# set table border edges to True
|
# set table border edges to True
|
||||||
table = table.set_border()
|
table = table.set_border()
|
||||||
# set spanning cells to True
|
# set spanning cells to True
|
||||||
@@ -315,7 +317,7 @@ class Lattice(BaseParser):
|
|||||||
for t in self.t_bbox[direction]:
|
for t in self.t_bbox[direction]:
|
||||||
indices, error = get_table_index(
|
indices, error = get_table_index(
|
||||||
table, t, direction, split_text=self.split_text,
|
table, t, direction, split_text=self.split_text,
|
||||||
flag_size=self.flag_size)
|
flag_size=self.flag_size, strip_text=self.strip_text)
|
||||||
if indices[:2] != (-1, -1):
|
if indices[:2] != (-1, -1):
|
||||||
pos_errors.append(error)
|
pos_errors.append(error)
|
||||||
indices = Lattice._reduce_index(table, indices, shift_text=self.shift_text)
|
indices = Lattice._reduce_index(table, indices, shift_text=self.shift_text)
|
||||||
@@ -348,8 +350,8 @@ class Lattice(BaseParser):
|
|||||||
|
|
||||||
return table
|
return table
|
||||||
|
|
||||||
def extract_tables(self, filename, suppress_stdout=False):
|
def extract_tables(self, filename, suppress_stdout=False, layout_kwargs={}):
|
||||||
self._generate_layout(filename)
|
self._generate_layout(filename, layout_kwargs)
|
||||||
if not suppress_stdout:
|
if not suppress_stdout:
|
||||||
logger.info('Processing {}'.format(os.path.basename(self.rootname)))
|
logger.info('Processing {}'.format(os.path.basename(self.rootname)))
|
||||||
|
|
||||||
|
|||||||
@@ -38,29 +38,31 @@ class Stream(BaseParser):
|
|||||||
flag_size : bool, optional (default: False)
|
flag_size : bool, optional (default: False)
|
||||||
Flag text based on font size. Useful to detect
|
Flag text based on font size. Useful to detect
|
||||||
super/subscripts. Adds <s></s> around flagged text.
|
super/subscripts. Adds <s></s> around flagged text.
|
||||||
row_close_tol : int, optional (default: 2)
|
strip_text : str, optional (default: '')
|
||||||
|
Characters that should be stripped from a string before
|
||||||
|
assigning it to a cell.
|
||||||
|
edge_tol : int, optional (default: 50)
|
||||||
|
Tolerance parameter for extending textedges vertically.
|
||||||
|
row_tol : int, optional (default: 2)
|
||||||
Tolerance parameter used to combine text vertically,
|
Tolerance parameter used to combine text vertically,
|
||||||
to generate rows.
|
to generate rows.
|
||||||
col_close_tol : int, optional (default: 0)
|
column_tol : int, optional (default: 0)
|
||||||
Tolerance parameter used to combine text horizontally,
|
Tolerance parameter used to combine text horizontally,
|
||||||
to generate columns.
|
to generate columns.
|
||||||
margins : tuple, optional (default: (1.0, 0.5, 0.1))
|
|
||||||
PDFMiner char_margin, line_margin and word_margin.
|
|
||||||
|
|
||||||
For more information, refer `PDFMiner docs <https://euske.github.io/pdfminer/>`_.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, table_areas=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,
|
flag_size=False, strip_text='', edge_tol=50, row_tol=2,
|
||||||
margins=(1.0, 0.5, 0.1), **kwargs):
|
column_tol=0, **kwargs):
|
||||||
self.table_areas = table_areas
|
self.table_areas = table_areas
|
||||||
self.columns = columns
|
self.columns = columns
|
||||||
self._validate_columns()
|
self._validate_columns()
|
||||||
self.split_text = split_text
|
self.split_text = split_text
|
||||||
self.flag_size = flag_size
|
self.flag_size = flag_size
|
||||||
self.row_close_tol = row_close_tol
|
self.strip_text = strip_text
|
||||||
self.col_close_tol = col_close_tol
|
self.edge_tol = edge_tol
|
||||||
self.char_margin, self.line_margin, self.word_margin = margins
|
self.row_tol = row_tol
|
||||||
|
self.column_tol = column_tol
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _text_bbox(t_bbox):
|
def _text_bbox(t_bbox):
|
||||||
@@ -86,7 +88,7 @@ class Stream(BaseParser):
|
|||||||
return text_bbox
|
return text_bbox
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _group_rows(text, row_close_tol=2):
|
def _group_rows(text, row_tol=2):
|
||||||
"""Groups PDFMiner text objects into rows vertically
|
"""Groups PDFMiner text objects into rows vertically
|
||||||
within a tolerance.
|
within a tolerance.
|
||||||
|
|
||||||
@@ -94,7 +96,7 @@ class Stream(BaseParser):
|
|||||||
----------
|
----------
|
||||||
text : list
|
text : list
|
||||||
List of PDFMiner text objects.
|
List of PDFMiner text objects.
|
||||||
row_close_tol : int, optional (default: 2)
|
row_tol : int, optional (default: 2)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -110,7 +112,7 @@ class Stream(BaseParser):
|
|||||||
# if t.get_text().strip() and all([obj.upright for obj in t._objs if
|
# if t.get_text().strip() and all([obj.upright for obj in t._objs if
|
||||||
# type(obj) is LTChar]):
|
# type(obj) is LTChar]):
|
||||||
if t.get_text().strip():
|
if t.get_text().strip():
|
||||||
if not np.isclose(row_y, t.y0, atol=row_close_tol):
|
if not np.isclose(row_y, t.y0, atol=row_tol):
|
||||||
rows.append(sorted(temp, key=lambda t: t.x0))
|
rows.append(sorted(temp, key=lambda t: t.x0))
|
||||||
temp = []
|
temp = []
|
||||||
row_y = t.y0
|
row_y = t.y0
|
||||||
@@ -120,7 +122,7 @@ class Stream(BaseParser):
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _merge_columns(l, col_close_tol=0):
|
def _merge_columns(l, column_tol=0):
|
||||||
"""Merges column boundaries horizontally if they overlap
|
"""Merges column boundaries horizontally if they overlap
|
||||||
or lie within a tolerance.
|
or lie within a tolerance.
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ class Stream(BaseParser):
|
|||||||
----------
|
----------
|
||||||
l : list
|
l : list
|
||||||
List of column x-coordinate tuples.
|
List of column x-coordinate tuples.
|
||||||
col_close_tol : int, optional (default: 0)
|
column_tol : int, optional (default: 0)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -142,17 +144,17 @@ class Stream(BaseParser):
|
|||||||
merged.append(higher)
|
merged.append(higher)
|
||||||
else:
|
else:
|
||||||
lower = merged[-1]
|
lower = merged[-1]
|
||||||
if col_close_tol >= 0:
|
if column_tol >= 0:
|
||||||
if (higher[0] <= lower[1] or
|
if (higher[0] <= lower[1] or
|
||||||
np.isclose(higher[0], lower[1], atol=col_close_tol)):
|
np.isclose(higher[0], lower[1], atol=column_tol)):
|
||||||
upper_bound = max(lower[1], higher[1])
|
upper_bound = max(lower[1], higher[1])
|
||||||
lower_bound = min(lower[0], higher[0])
|
lower_bound = min(lower[0], higher[0])
|
||||||
merged[-1] = (lower_bound, upper_bound)
|
merged[-1] = (lower_bound, upper_bound)
|
||||||
else:
|
else:
|
||||||
merged.append(higher)
|
merged.append(higher)
|
||||||
elif col_close_tol < 0:
|
elif column_tol < 0:
|
||||||
if higher[0] <= lower[1]:
|
if higher[0] <= lower[1]:
|
||||||
if np.isclose(higher[0], lower[1], atol=abs(col_close_tol)):
|
if np.isclose(higher[0], lower[1], atol=abs(column_tol)):
|
||||||
merged.append(higher)
|
merged.append(higher)
|
||||||
else:
|
else:
|
||||||
upper_bound = max(lower[1], higher[1])
|
upper_bound = max(lower[1], higher[1])
|
||||||
@@ -189,7 +191,7 @@ class Stream(BaseParser):
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_columns(cols, text, row_close_tol):
|
def _add_columns(cols, text, row_tol):
|
||||||
"""Adds columns to existing list by taking into account
|
"""Adds columns to existing list by taking into account
|
||||||
the text that lies outside the current column x-coordinates.
|
the text that lies outside the current column x-coordinates.
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ class Stream(BaseParser):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if text:
|
if text:
|
||||||
text = Stream._group_rows(text, row_close_tol=row_close_tol)
|
text = Stream._group_rows(text, row_tol=row_tol)
|
||||||
elements = [len(r) for r in text]
|
elements = [len(r) for r in text]
|
||||||
new_cols = [(t.x0, t.x1)
|
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]
|
||||||
@@ -254,11 +256,10 @@ class Stream(BaseParser):
|
|||||||
Assumes that tables are situated relatively far apart
|
Assumes that tables are situated relatively far apart
|
||||||
vertically.
|
vertically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: add support for arabic text #141
|
# TODO: add support for arabic text #141
|
||||||
# sort textlines in reading order
|
# sort textlines in reading order
|
||||||
textlines.sort(key=lambda x: (-x.y0, x.x0))
|
textlines.sort(key=lambda x: (-x.y0, x.x0))
|
||||||
textedges = TextEdges()
|
textedges = TextEdges(edge_tol=self.edge_tol)
|
||||||
# generate left, middle and right textedges
|
# generate left, middle and right textedges
|
||||||
textedges.generate(textlines)
|
textedges.generate(textlines)
|
||||||
# select relevant edges
|
# select relevant edges
|
||||||
@@ -300,7 +301,7 @@ class Stream(BaseParser):
|
|||||||
self.t_bbox = t_bbox
|
self.t_bbox = t_bbox
|
||||||
|
|
||||||
text_x_min, text_y_min, text_x_max, text_y_max = self._text_bbox(self.t_bbox)
|
text_x_min, text_y_min, text_x_max, text_y_max = self._text_bbox(self.t_bbox)
|
||||||
rows_grouped = self._group_rows(self.t_bbox['horizontal'], row_close_tol=self.row_close_tol)
|
rows_grouped = self._group_rows(self.t_bbox['horizontal'], row_tol=self.row_tol)
|
||||||
rows = self._join_rows(rows_grouped, text_y_max, text_y_min)
|
rows = self._join_rows(rows_grouped, text_y_max, text_y_min)
|
||||||
elements = [len(r) for r in rows_grouped]
|
elements = [len(r) for r in rows_grouped]
|
||||||
|
|
||||||
@@ -331,7 +332,7 @@ class Stream(BaseParser):
|
|||||||
warnings.warn("No tables found in table area {}".format(
|
warnings.warn("No tables found in table area {}".format(
|
||||||
table_idx + 1))
|
table_idx + 1))
|
||||||
cols = [(t.x0, t.x1) for r in rows_grouped if len(r) == ncols for t in r]
|
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)
|
cols = self._merge_columns(sorted(cols), column_tol=self.column_tol)
|
||||||
inner_text = []
|
inner_text = []
|
||||||
for i in range(1, len(cols)):
|
for i in range(1, len(cols)):
|
||||||
left = cols[i - 1][1]
|
left = cols[i - 1][1]
|
||||||
@@ -343,7 +344,7 @@ class Stream(BaseParser):
|
|||||||
for t in self.t_bbox[direction]
|
for t in self.t_bbox[direction]
|
||||||
if t.x0 > cols[-1][1] or t.x1 < cols[0][0]]
|
if t.x0 > cols[-1][1] or t.x1 < cols[0][0]]
|
||||||
inner_text.extend(outer_text)
|
inner_text.extend(outer_text)
|
||||||
cols = self._add_columns(cols, inner_text, self.row_close_tol)
|
cols = self._add_columns(cols, inner_text, self.row_tol)
|
||||||
cols = self._join_columns(cols, text_x_min, text_x_max)
|
cols = self._join_columns(cols, text_x_min, text_x_max)
|
||||||
|
|
||||||
return cols, rows
|
return cols, rows
|
||||||
@@ -359,7 +360,7 @@ class Stream(BaseParser):
|
|||||||
for t in self.t_bbox[direction]:
|
for t in self.t_bbox[direction]:
|
||||||
indices, error = get_table_index(
|
indices, error = get_table_index(
|
||||||
table, t, direction, split_text=self.split_text,
|
table, t, direction, split_text=self.split_text,
|
||||||
flag_size=self.flag_size)
|
flag_size=self.flag_size, strip_text=self.strip_text)
|
||||||
if indices[:2] != (-1, -1):
|
if indices[:2] != (-1, -1):
|
||||||
pos_errors.append(error)
|
pos_errors.append(error)
|
||||||
for r_idx, c_idx, text in indices:
|
for r_idx, c_idx, text in indices:
|
||||||
@@ -388,8 +389,8 @@ class Stream(BaseParser):
|
|||||||
|
|
||||||
return table
|
return table
|
||||||
|
|
||||||
def extract_tables(self, filename, suppress_stdout=False):
|
def extract_tables(self, filename, suppress_stdout=False, layout_kwargs={}):
|
||||||
self._generate_layout(filename)
|
self._generate_layout(filename, layout_kwargs)
|
||||||
if not suppress_stdout:
|
if not suppress_stdout:
|
||||||
logger.info('Processing {}'.format(os.path.basename(self.rootname)))
|
logger.info('Processing {}'.format(os.path.basename(self.rootname)))
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
|
import string
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from pdfminer.pdfparser import PDFParser
|
from pdfminer.pdfparser import PDFParser
|
||||||
from pdfminer.pdfdocument import PDFDocument
|
from pdfminer.pdfdocument import PDFDocument
|
||||||
from pdfminer.pdfpage import PDFPage
|
from pdfminer.pdfpage import PDFPage
|
||||||
@@ -18,18 +23,89 @@ from pdfminer.layout import (LAParams, LTAnno, LTChar, LTTextLineHorizontal,
|
|||||||
LTTextLineVertical)
|
LTTextLineVertical)
|
||||||
|
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] >= 3
|
||||||
|
if PY3:
|
||||||
|
from urllib.request import urlopen
|
||||||
|
from urllib.parse import urlparse as parse_url
|
||||||
|
from urllib.parse import uses_relative, uses_netloc, uses_params
|
||||||
|
else:
|
||||||
|
from urllib2 import urlopen
|
||||||
|
from urlparse import urlparse as parse_url
|
||||||
|
from urlparse import uses_relative, uses_netloc, uses_params
|
||||||
|
|
||||||
|
|
||||||
|
_VALID_URLS = set(uses_relative + uses_netloc + uses_params)
|
||||||
|
_VALID_URLS.discard('')
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/pandas-dev/pandas/blob/master/pandas/io/common.py
|
||||||
|
def is_url(url):
|
||||||
|
"""Check to see if a URL has a valid protocol.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url : str or unicode
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
isurl : bool
|
||||||
|
If url has a valid protocol return True otherwise False.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return parse_url(url).scheme in _VALID_URLS
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def random_string(length):
|
||||||
|
ret = ''
|
||||||
|
while length:
|
||||||
|
ret += random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase)
|
||||||
|
length -= 1
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def download_url(url):
|
||||||
|
"""Download file from specified URL.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url : str or unicode
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
filepath : str or unicode
|
||||||
|
Temporary filepath.
|
||||||
|
|
||||||
|
"""
|
||||||
|
filename = '{}.pdf'.format(random_string(6))
|
||||||
|
with tempfile.NamedTemporaryFile('wb', delete=False) as f:
|
||||||
|
obj = urlopen(url)
|
||||||
|
if PY3:
|
||||||
|
content_type = obj.info().get_content_type()
|
||||||
|
else:
|
||||||
|
content_type = obj.info().getheader('Content-Type')
|
||||||
|
if content_type != 'application/pdf':
|
||||||
|
raise NotImplementedError("File format not supported")
|
||||||
|
f.write(obj.read())
|
||||||
|
filepath = os.path.join(os.path.dirname(f.name), filename)
|
||||||
|
shutil.move(f.name, filepath)
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
stream_kwargs = [
|
stream_kwargs = [
|
||||||
'columns',
|
'columns',
|
||||||
'row_close_tol',
|
'row_tol',
|
||||||
'col_close_tol'
|
'column_tol'
|
||||||
]
|
]
|
||||||
lattice_kwargs = [
|
lattice_kwargs = [
|
||||||
'process_background',
|
'process_background',
|
||||||
'line_size_scaling',
|
'line_size_scaling',
|
||||||
'copy_text',
|
'copy_text',
|
||||||
'shift_text',
|
'shift_text',
|
||||||
'line_close_tol',
|
'line_tol',
|
||||||
'joint_close_tol',
|
'joint_tol',
|
||||||
'threshold_blocksize',
|
'threshold_blocksize',
|
||||||
'threshold_constant',
|
'threshold_constant',
|
||||||
'iterations'
|
'iterations'
|
||||||
@@ -281,14 +357,14 @@ def text_in_bbox(bbox, text):
|
|||||||
return t_bbox
|
return t_bbox
|
||||||
|
|
||||||
|
|
||||||
def merge_close_lines(ar, line_close_tol=2):
|
def merge_close_lines(ar, line_tol=2):
|
||||||
"""Merges lines which are within a tolerance by calculating a
|
"""Merges lines which are within a tolerance by calculating a
|
||||||
moving mean, based on their x or y axis projections.
|
moving mean, based on their x or y axis projections.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
ar : list
|
ar : list
|
||||||
line_close_tol : int, optional (default: 2)
|
line_tol : int, optional (default: 2)
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -301,7 +377,7 @@ def merge_close_lines(ar, line_close_tol=2):
|
|||||||
ret.append(a)
|
ret.append(a)
|
||||||
else:
|
else:
|
||||||
temp = ret[-1]
|
temp = ret[-1]
|
||||||
if np.isclose(temp, a, atol=line_close_tol):
|
if np.isclose(temp, a, atol=line_tol):
|
||||||
temp = (temp + a) / 2.0
|
temp = (temp + a) / 2.0
|
||||||
ret[-1] = temp
|
ret[-1] = temp
|
||||||
else:
|
else:
|
||||||
@@ -309,7 +385,12 @@ def merge_close_lines(ar, line_close_tol=2):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def flag_font_size(textline, direction):
|
# TODO: combine the following functions into a TextProcessor class which
|
||||||
|
# applies corresponding transformations sequentially
|
||||||
|
# (inspired from sklearn.pipeline.Pipeline)
|
||||||
|
|
||||||
|
|
||||||
|
def flag_font_size(textline, direction, strip_text=''):
|
||||||
"""Flags super/subscripts in text by enclosing them with <s></s>.
|
"""Flags super/subscripts in text by enclosing them with <s></s>.
|
||||||
May give false positives.
|
May give false positives.
|
||||||
|
|
||||||
@@ -319,6 +400,9 @@ def flag_font_size(textline, direction):
|
|||||||
List of PDFMiner LTChar objects.
|
List of PDFMiner LTChar objects.
|
||||||
direction : string
|
direction : string
|
||||||
Direction of the PDFMiner LTTextLine object.
|
Direction of the PDFMiner LTTextLine object.
|
||||||
|
strip_text : str, optional (default: '')
|
||||||
|
Characters that should be stripped from a string before
|
||||||
|
assigning it to a cell.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -344,13 +428,13 @@ def flag_font_size(textline, direction):
|
|||||||
fchars = [t[0] for t in chars]
|
fchars = [t[0] for t in chars]
|
||||||
if ''.join(fchars).strip():
|
if ''.join(fchars).strip():
|
||||||
flist.append(''.join(fchars))
|
flist.append(''.join(fchars))
|
||||||
fstring = ''.join(flist)
|
fstring = ''.join(flist).strip(strip_text)
|
||||||
else:
|
else:
|
||||||
fstring = ''.join([t.get_text() for t in textline])
|
fstring = ''.join([t.get_text() for t in textline]).strip(strip_text)
|
||||||
return fstring
|
return fstring
|
||||||
|
|
||||||
|
|
||||||
def split_textline(table, textline, direction, flag_size=False):
|
def split_textline(table, textline, direction, flag_size=False, strip_text=''):
|
||||||
"""Splits PDFMiner LTTextLine into substrings if it spans across
|
"""Splits PDFMiner LTTextLine into substrings if it spans across
|
||||||
multiple rows/columns.
|
multiple rows/columns.
|
||||||
|
|
||||||
@@ -365,6 +449,9 @@ def split_textline(table, textline, direction, flag_size=False):
|
|||||||
Whether or not to highlight a substring using <s></s>
|
Whether or not to highlight a substring using <s></s>
|
||||||
if its size is different from rest of the string. (Useful for
|
if its size is different from rest of the string. (Useful for
|
||||||
super and subscripts.)
|
super and subscripts.)
|
||||||
|
strip_text : str, optional (default: '')
|
||||||
|
Characters that should be stripped from a string before
|
||||||
|
assigning it to a cell.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -416,14 +503,15 @@ def split_textline(table, textline, direction, flag_size=False):
|
|||||||
grouped_chars = []
|
grouped_chars = []
|
||||||
for key, chars in groupby(cut_text, itemgetter(0, 1)):
|
for key, chars in groupby(cut_text, itemgetter(0, 1)):
|
||||||
if flag_size:
|
if flag_size:
|
||||||
grouped_chars.append((key[0], key[1], flag_font_size([t[2] for t in chars], direction)))
|
grouped_chars.append((key[0], key[1],
|
||||||
|
flag_font_size([t[2] for t in chars], direction, strip_text=strip_text)))
|
||||||
else:
|
else:
|
||||||
gchars = [t[2].get_text() for t in chars]
|
gchars = [t[2].get_text() for t in chars]
|
||||||
grouped_chars.append((key[0], key[1], ''.join(gchars)))
|
grouped_chars.append((key[0], key[1], ''.join(gchars).strip(strip_text)))
|
||||||
return grouped_chars
|
return grouped_chars
|
||||||
|
|
||||||
|
|
||||||
def get_table_index(table, t, direction, split_text=False, flag_size=False):
|
def get_table_index(table, t, direction, split_text=False, flag_size=False, strip_text='',):
|
||||||
"""Gets indices of the table cell where given text object lies by
|
"""Gets indices of the table cell where given text object lies by
|
||||||
comparing their y and x-coordinates.
|
comparing their y and x-coordinates.
|
||||||
|
|
||||||
@@ -441,6 +529,9 @@ def get_table_index(table, t, direction, split_text=False, flag_size=False):
|
|||||||
Whether or not to highlight a substring using <s></s>
|
Whether or not to highlight a substring using <s></s>
|
||||||
if its size is different from rest of the string. (Useful for
|
if its size is different from rest of the string. (Useful for
|
||||||
super and subscripts)
|
super and subscripts)
|
||||||
|
strip_text : str, optional (default: '')
|
||||||
|
Characters that should be stripped from a string before
|
||||||
|
assigning it to a cell.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -495,12 +586,12 @@ def get_table_index(table, t, direction, split_text=False, flag_size=False):
|
|||||||
error = ((X * (y0_offset + y1_offset)) + (Y * (x0_offset + x1_offset))) / charea
|
error = ((X * (y0_offset + y1_offset)) + (Y * (x0_offset + x1_offset))) / charea
|
||||||
|
|
||||||
if split_text:
|
if split_text:
|
||||||
return split_textline(table, t, direction, flag_size=flag_size), error
|
return split_textline(table, t, direction, flag_size=flag_size, strip_text=strip_text), error
|
||||||
else:
|
else:
|
||||||
if flag_size:
|
if flag_size:
|
||||||
return [(r_idx, c_idx, flag_font_size(t._objs, direction))], error
|
return [(r_idx, c_idx, flag_font_size(t._objs, direction, strip_text=strip_text))], error
|
||||||
else:
|
else:
|
||||||
return [(r_idx, c_idx, t.get_text())], error
|
return [(r_idx, c_idx, t.get_text().strip(strip_text))], error
|
||||||
|
|
||||||
|
|
||||||
def compute_accuracy(error_weights):
|
def compute_accuracy(error_weights):
|
||||||
@@ -558,7 +649,7 @@ def compute_whitespace(d):
|
|||||||
|
|
||||||
|
|
||||||
def get_page_layout(filename, char_margin=1.0, line_margin=0.5, word_margin=0.1,
|
def get_page_layout(filename, char_margin=1.0, line_margin=0.5, word_margin=0.1,
|
||||||
detect_vertical=True, all_texts=True):
|
detect_vertical=True, all_texts=True):
|
||||||
"""Returns a PDFMiner LTPage object and page dimension of a single
|
"""Returns a PDFMiner LTPage object and page dimension of a single
|
||||||
page pdf. See https://euske.github.io/pdfminer/ to get definitions
|
page pdf. See https://euske.github.io/pdfminer/ to get definitions
|
||||||
of kwargs.
|
of kwargs.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -24,6 +24,12 @@ To process background lines, you can pass ``process_background=True``.
|
|||||||
>>> tables = camelot.read_pdf('background_lines.pdf', process_background=True)
|
>>> tables = camelot.read_pdf('background_lines.pdf', process_background=True)
|
||||||
>>> tables[1].df
|
>>> tables[1].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -back background_lines.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
:file: ../_static/csv/background_lines.csv
|
:file: ../_static/csv/background_lines.csv
|
||||||
|
|
||||||
@@ -63,6 +69,12 @@ Let's plot all the text present on the table's PDF page.
|
|||||||
>>> camelot.plot(tables[0], kind='text')
|
>>> camelot.plot(tables[0], kind='text')
|
||||||
>>> plt.show()
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -plot text foo.pdf
|
||||||
|
|
||||||
.. figure:: ../_static/png/plot_text.png
|
.. figure:: ../_static/png/plot_text.png
|
||||||
:height: 674
|
:height: 674
|
||||||
:width: 1366
|
:width: 1366
|
||||||
@@ -84,6 +96,12 @@ Let's plot the table (to see if it was detected correctly or not). This plot typ
|
|||||||
>>> camelot.plot(tables[0], kind='grid')
|
>>> camelot.plot(tables[0], kind='grid')
|
||||||
>>> plt.show()
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -plot grid foo.pdf
|
||||||
|
|
||||||
.. figure:: ../_static/png/plot_table.png
|
.. figure:: ../_static/png/plot_table.png
|
||||||
:height: 674
|
:height: 674
|
||||||
:width: 1366
|
:width: 1366
|
||||||
@@ -103,6 +121,12 @@ Now, let's plot all table boundaries present on the table's PDF page.
|
|||||||
>>> camelot.plot(tables[0], kind='contour')
|
>>> camelot.plot(tables[0], kind='contour')
|
||||||
>>> plt.show()
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -plot contour foo.pdf
|
||||||
|
|
||||||
.. figure:: ../_static/png/plot_contour.png
|
.. figure:: ../_static/png/plot_contour.png
|
||||||
:height: 674
|
:height: 674
|
||||||
:width: 1366
|
:width: 1366
|
||||||
@@ -120,6 +144,12 @@ Cool, let's plot all line segments present on the table's PDF page.
|
|||||||
>>> camelot.plot(tables[0], kind='line')
|
>>> camelot.plot(tables[0], kind='line')
|
||||||
>>> plt.show()
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -plot line foo.pdf
|
||||||
|
|
||||||
.. figure:: ../_static/png/plot_line.png
|
.. figure:: ../_static/png/plot_line.png
|
||||||
:height: 674
|
:height: 674
|
||||||
:width: 1366
|
:width: 1366
|
||||||
@@ -137,6 +167,12 @@ Finally, let's plot all line intersections present on the table's PDF page.
|
|||||||
>>> camelot.plot(tables[0], kind='joint')
|
>>> camelot.plot(tables[0], kind='joint')
|
||||||
>>> plt.show()
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -plot joint foo.pdf
|
||||||
|
|
||||||
.. figure:: ../_static/png/plot_joint.png
|
.. figure:: ../_static/png/plot_joint.png
|
||||||
:height: 674
|
:height: 674
|
||||||
:width: 1366
|
:width: 1366
|
||||||
@@ -154,6 +190,12 @@ You can also visualize the textedges found on a page by specifying ``kind='texte
|
|||||||
>>> camelot.plot(tables[0], kind='textedge')
|
>>> camelot.plot(tables[0], kind='textedge')
|
||||||
>>> plt.show()
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot stream -plot textedge foo.pdf
|
||||||
|
|
||||||
.. figure:: ../_static/png/plot_textedge.png
|
.. figure:: ../_static/png/plot_textedge.png
|
||||||
:height: 674
|
:height: 674
|
||||||
:width: 1366
|
:width: 1366
|
||||||
@@ -175,6 +217,12 @@ Table areas that you want Camelot to analyze can be passed as a list of comma-se
|
|||||||
>>> tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_areas=['316,499,566,337'])
|
>>> tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_areas=['316,499,566,337'])
|
||||||
>>> tables[0].df
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot stream -T 316,499,566,337 table_areas.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
:file: ../_static/csv/table_areas.csv
|
:file: ../_static/csv/table_areas.csv
|
||||||
|
|
||||||
@@ -196,6 +244,12 @@ Let's get back to the *x* coordinates we got from plotting the text that exists
|
|||||||
>>> tables = camelot.read_pdf('column_separators.pdf', flavor='stream', columns=['72,95,209,327,442,529,566,606,683'])
|
>>> tables = camelot.read_pdf('column_separators.pdf', flavor='stream', columns=['72,95,209,327,442,529,566,606,683'])
|
||||||
>>> tables[0].df
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot stream -C 72,95,209,327,442,529,566,606,683 column_separators.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
|
|
||||||
"...","...","...","...","...","...","...","...","...","..."
|
"...","...","...","...","...","...","...","...","...","..."
|
||||||
@@ -215,6 +269,12 @@ To deal with cases like the output from the previous section, you can pass ``spl
|
|||||||
>>> tables = camelot.read_pdf('column_separators.pdf', flavor='stream', columns=['72,95,209,327,442,529,566,606,683'], split_text=True)
|
>>> tables = camelot.read_pdf('column_separators.pdf', flavor='stream', columns=['72,95,209,327,442,529,566,606,683'], split_text=True)
|
||||||
>>> tables[0].df
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot -split stream -C 72,95,209,327,442,529,566,606,683 column_separators.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
|
|
||||||
"...","...","...","...","...","...","...","...","...","..."
|
"...","...","...","...","...","...","...","...","...","..."
|
||||||
@@ -242,6 +302,12 @@ You can solve this by passing ``flag_size=True``, which will enclose the supersc
|
|||||||
>>> tables = camelot.read_pdf('superscript.pdf', flavor='stream', flag_size=True)
|
>>> tables = camelot.read_pdf('superscript.pdf', flavor='stream', flag_size=True)
|
||||||
>>> tables[0].df
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot -flag stream superscript.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
|
|
||||||
"...","...","...","...","...","...","...","...","...","...","..."
|
"...","...","...","...","...","...","...","...","...","...","..."
|
||||||
@@ -250,10 +316,87 @@ You can solve this by passing ``flag_size=True``, which will enclose the supersc
|
|||||||
"Madhya Pradesh","27.13","23.57","-","-","3.56","0.38","-","1.86","-","1.28"
|
"Madhya Pradesh","27.13","23.57","-","-","3.56","0.38","-","1.86","-","1.28"
|
||||||
"...","...","...","...","...","...","...","...","...","...","..."
|
"...","...","...","...","...","...","...","...","...","...","..."
|
||||||
|
|
||||||
Control how text is grouped into rows
|
Strip characters from text
|
||||||
-------------------------------------
|
--------------------------
|
||||||
|
|
||||||
You can pass ``row_close_tol=<+int>`` to group the rows closer together, as shown below.
|
You can strip unwanted characters like spaces, dots and newlines from a string using the ``strip_text`` keyword argument. Take a look at `this PDF <https://github.com/socialcopsdev/camelot/blob/master/tests/files/tabula/12s0324.pdf>`_ as an example, the text at the start of each row contains a lot of unwanted spaces, dots and newlines.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>>> tables = camelot.read_pdf('12s0324.pdf', flavor='stream', strip_text=' .\n')
|
||||||
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot -strip ' .\n' stream 12s0324.pdf
|
||||||
|
|
||||||
|
.. csv-table::
|
||||||
|
|
||||||
|
"...","...","...","...","...","...","...","...","...","..."
|
||||||
|
"Forcible rape","17.5","2.6","14.9","17.2","2.5","14.7","–","–","–"
|
||||||
|
"Robbery","102.1","25.5","76.6","90.0","22.9","67.1","12.1","2.5","9.5"
|
||||||
|
"Aggravated assault","338.4","40.1","298.3","264.0","30.2","233.8","74.4","9.9","64.5"
|
||||||
|
"Property crime","1,396 .4","338 .7","1,057 .7","875 .9","210 .8","665 .1","608 .2","127 .9","392 .6"
|
||||||
|
"Burglary","240.9","60.3","180.6","205.0","53.4","151.7","35.9","6.9","29.0"
|
||||||
|
"...","...","...","...","...","...","...","...","...","..."
|
||||||
|
|
||||||
|
Improve guessed table areas
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
While using :ref:`Stream <stream>`, automatic table detection can fail for PDFs like `this one <https://github.com/socialcopsdev/camelot/blob/master/tests/files/edge_tol.pdf>`_. That's because the text is relatively far apart vertically, which can lead to shorter textedges being calculated.
|
||||||
|
|
||||||
|
.. note:: To know more about how textedges are calculated to guess table areas, you can see pages 20, 35 and 40 of `Anssi Nurminen's master's thesis <http://dspace.cc.tut.fi/dpub/bitstream/handle/123456789/21520/Nurminen.pdf?sequence=3>`_.
|
||||||
|
|
||||||
|
Let's see the table area that is detected by default.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>>> tables = camelot.read_pdf('edge_tol.pdf', flavor='stream')
|
||||||
|
>>> camelot.plot(tables[0], kind='contour')
|
||||||
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot stream -plot contour edge.pdf
|
||||||
|
|
||||||
|
.. figure:: ../_static/png/edge_tol_1.png
|
||||||
|
:height: 674
|
||||||
|
:width: 1366
|
||||||
|
:scale: 50%
|
||||||
|
:alt: Table area with default edge_tol
|
||||||
|
:align: left
|
||||||
|
|
||||||
|
To improve the detected area, you can increase the ``edge_tol`` (default: 50) value to counter the effect of text being placed relatively far apart vertically. Larger ``edge_tol`` will lead to longer textedges being detected, leading to an improved guess of the table area. Let's use a value of 500.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>>> tables = camelot.read_pdf('edge_tol.pdf', flavor='stream', edge_tol=500)
|
||||||
|
>>> camelot.plot(tables[0], kind='contour')
|
||||||
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot stream -e 500 -plot contour edge.pdf
|
||||||
|
|
||||||
|
.. figure:: ../_static/png/edge_tol_2.png
|
||||||
|
:height: 674
|
||||||
|
:width: 1366
|
||||||
|
:scale: 50%
|
||||||
|
:alt: Table area with default edge_tol
|
||||||
|
:align: left
|
||||||
|
|
||||||
|
As you can see, the guessed table area has improved!
|
||||||
|
|
||||||
|
Improve guessed table rows
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
You can pass ``row_tol=<+int>`` to group the rows closer together, as shown below.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -271,9 +414,15 @@ You can pass ``row_close_tol=<+int>`` to group the rows closer together, as show
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
>>> tables = camelot.read_pdf('group_rows.pdf', flavor='stream', row_close_tol=10)
|
>>> tables = camelot.read_pdf('group_rows.pdf', flavor='stream', row_tol=10)
|
||||||
>>> tables[0].df
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot stream -r 10 group_rows.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
|
|
||||||
"Clave","Nombre Entidad","Clave","","Nombre Municipio","Clave","Nombre Localidad"
|
"Clave","Nombre Entidad","Clave","","Nombre Municipio","Clave","Nombre Localidad"
|
||||||
@@ -317,6 +466,12 @@ Clearly, the smaller lines separating the headers, couldn't be detected. Let's t
|
|||||||
>>> camelot.plot(tables[0], kind='grid')
|
>>> camelot.plot(tables[0], kind='grid')
|
||||||
>>> plt.show()
|
>>> plt.show()
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -scale 40 -plot grid short_lines.pdf
|
||||||
|
|
||||||
.. figure:: ../_static/png/short_lines_2.png
|
.. figure:: ../_static/png/short_lines_2.png
|
||||||
:alt: An improved plot of the PDF table with short lines
|
:alt: An improved plot of the PDF table with short lines
|
||||||
:align: left
|
:align: left
|
||||||
@@ -380,6 +535,12 @@ No surprises there — it did remain in place (observe the strings "2400" and "A
|
|||||||
>>> tables = camelot.read_pdf('short_lines.pdf', line_size_scaling=40, shift_text=['r', 'b'])
|
>>> tables = camelot.read_pdf('short_lines.pdf', line_size_scaling=40, shift_text=['r', 'b'])
|
||||||
>>> tables[0].df
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -scale 40 -shift r -shift b short_lines.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
|
|
||||||
"Investigations","No. ofHHs","Age/Sex/Physiological Group","Preva-lence","C.I*","RelativePrecision","Sample sizeper State"
|
"Investigations","No. ofHHs","Age/Sex/Physiological Group","Preva-lence","C.I*","RelativePrecision","Sample sizeper State"
|
||||||
@@ -425,6 +586,12 @@ We don't need anything else. Now, let's pass ``copy_text=['v']`` to copy text in
|
|||||||
>>> tables = camelot.read_pdf('copy_text.pdf', copy_text=['v'])
|
>>> tables = camelot.read_pdf('copy_text.pdf', copy_text=['v'])
|
||||||
>>> tables[0].df
|
>>> tables[0].df
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot lattice -copy v copy_text.pdf
|
||||||
|
|
||||||
.. csv-table::
|
.. csv-table::
|
||||||
|
|
||||||
"Sl. No.","Name of State/UT","Name of District","Disease/ Illness","No. of Cases","No. of Deaths","Date of start of outbreak","Date of reporting","Current Status","..."
|
"Sl. No.","Name of State/UT","Name of District","Disease/ Illness","No. of Cases","No. of Deaths","Date of start of outbreak","Date of reporting","Current Status","..."
|
||||||
@@ -434,3 +601,14 @@ We don't need anything else. Now, let's pass ``copy_text=['v']`` to copy text in
|
|||||||
"4","West Bengal","West Medinipur","iv. Acute Diarrhoeal Disease","145","0","04/01/14","05/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","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","..."
|
||||||
|
|
||||||
|
Tweak layout generation
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Camelot is built on top of PDFMiner's functionality of grouping characters on a page into words and sentences. In some cases (such as `#170 <https://github.com/socialcopsdev/camelot/issues/170>`_ and `#215 <https://github.com/socialcopsdev/camelot/issues/215>`_), PDFMiner can group characters that should belong to the same sentence into separate sentences.
|
||||||
|
|
||||||
|
To deal with such cases, you can tweak PDFMiner's `LAParams kwargs <https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py#L33>`_ to improve layout generation, by passing the keyword arguments as a dict using ``layout_kwargs`` in :meth:`read_pdf() <camelot.read_pdf>`. To know more about the parameters you can tweak, you can check out `PDFMiner docs <https://euske.github.io/pdfminer/>`_.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>>> tables = camelot.read_pdf('foo.pdf', layout_kwargs={'detect_vertical': False})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ You can print the help for the interface by typing ``camelot --help`` in your fa
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--version Show the version and exit.
|
--version Show the version and exit.
|
||||||
-v, --verbose Verbose.
|
-q, --quiet TEXT Suppress logs and warnings.
|
||||||
-p, --pages TEXT Comma-separated page numbers. Example: 1,3,4
|
-p, --pages TEXT Comma-separated page numbers. Example: 1,3,4
|
||||||
or 1,4-end.
|
or 1,4-end.
|
||||||
-pw, --password TEXT Password for decryption.
|
-pw, --password TEXT Password for decryption.
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ You can also export all tables at once, using the :class:`tables <camelot.core.T
|
|||||||
|
|
||||||
>>> tables.export('foo.csv', f='csv')
|
>>> tables.export('foo.csv', f='csv')
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot --format csv --output foo.csv lattice foo.pdf
|
||||||
|
|
||||||
This will export all tables as CSV files at the path specified. Alternatively, you can use ``f='json'``, ``f='excel'`` or ``f='html'``.
|
This will export all tables as CSV files at the path specified. Alternatively, you can use ``f='json'``, ``f='excel'`` or ``f='html'``.
|
||||||
|
|
||||||
.. note:: The :meth:`export() <camelot.core.TableList.export>` method exports files with a ``page-*-table-*`` suffix. In the example above, the single table in the list will be exported to ``foo-page-1-table-1.csv``. If the list contains multiple tables, multiple CSV files will be created. To avoid filling up your path with multiple files, you can use ``compress=True``, which will create a single ZIP file at your path with all the CSV files.
|
.. note:: The :meth:`export() <camelot.core.TableList.export>` method exports files with a ``page-*-table-*`` suffix. In the example above, the single table in the list will be exported to ``foo-page-1-table-1.csv``. If the list contains multiple tables, multiple CSV files will be created. To avoid filling up your path with multiple files, you can use ``compress=True``, which will create a single ZIP file at your path with all the CSV files.
|
||||||
@@ -85,6 +91,12 @@ 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')
|
>>> camelot.read_pdf('your.pdf', pages='1,2,3')
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot --pages 1,2,3 lattice your.pdf
|
||||||
|
|
||||||
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
|
Reading encrypted PDFs
|
||||||
@@ -98,6 +110,12 @@ To extract tables from encrypted PDF files you must provide a password when call
|
|||||||
>>> tables
|
>>> tables
|
||||||
<TableList n=1>
|
<TableList n=1>
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
Here's how you can do the same with the :ref:`command-line interface <cli>`.
|
||||||
|
::
|
||||||
|
|
||||||
|
$ camelot --password userpass lattice foo.pdf
|
||||||
|
|
||||||
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.
|
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`_.
|
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`_.
|
||||||
|
|||||||
@@ -312,6 +312,63 @@ data_stream_flag_size = [
|
|||||||
["ALL STATES", "513.38", "436.02", "-", "25.57", "51.06", "14.18", "-", "8.21", "11.83", "11.08"]
|
["ALL STATES", "513.38", "436.02", "-", "25.57", "51.06", "14.18", "-", "8.21", "11.83", "11.08"]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
data_stream_strip_text = [
|
||||||
|
["V i n s a u Ve r r e", ""],
|
||||||
|
["Les Blancs", "12.5CL"],
|
||||||
|
["A.O.P Côtes du Rhône", ""],
|
||||||
|
["Domaine de la Guicharde « Autour de la chapelle » 2016", "8 €"],
|
||||||
|
["A.O.P Vacqueyras", ""],
|
||||||
|
["Domaine de Montvac « Melodine » 2016", "10 €"],
|
||||||
|
["A.O.P Châteauneuf du Pape", ""],
|
||||||
|
["Domaine de Beaurenard 2017", "13 €"],
|
||||||
|
["A.O.P Côteaux du Languedoc", ""],
|
||||||
|
["Villa Tempora « Un temps pour elle » 2014", "9 €"],
|
||||||
|
["A.O.P Côtes de Provence", ""],
|
||||||
|
["Château Grand Boise 2017", "9 €"],
|
||||||
|
["Les Rosés", "12,5 CL"],
|
||||||
|
["A.O.P Côtes du Rhône", ""],
|
||||||
|
["Domaine de la Florane « A fleur de Pampre » 2016", "8 €"],
|
||||||
|
["Famille Coulon (Domaine Beaurenard) Biotifulfox 2017", "8 €"],
|
||||||
|
["A.O.P Vacqueyras", ""],
|
||||||
|
["Domaine de Montvac 2017", "9 €"],
|
||||||
|
["A.O.P Languedoc", ""],
|
||||||
|
["Domaine de Joncas « Nébla » 2015", "8 €"],
|
||||||
|
["Villa Tempora « L’arroseur arrosé » 2015", "9 €"],
|
||||||
|
["A.O.P Côtes de Provence", ""],
|
||||||
|
["Château Grand Boise « Sainte Victoire » 2017", "9 €"],
|
||||||
|
["Château Léoube 2016", "10 €"]
|
||||||
|
]
|
||||||
|
|
||||||
|
data_stream_edge_tol = [
|
||||||
|
["Key figures", ""],
|
||||||
|
["", "2016"],
|
||||||
|
["(all amounts in EUR)", ""],
|
||||||
|
["C\nlass A", ""],
|
||||||
|
["N\net Asset Value at 31 December", "5,111,372"],
|
||||||
|
["N\number of outstanding units at 31 December", "49,136"],
|
||||||
|
["N\net Asset Value per unit at 31 December", "104.03"],
|
||||||
|
["C\nlass B", ""],
|
||||||
|
["N\net Asset Value at 31 December", "49,144,825"],
|
||||||
|
["N\number of outstanding units at 31 December", "471,555"],
|
||||||
|
["N\net Asset Value per unit at 31 December", "104.22"],
|
||||||
|
["T\notal for the Fund", ""],
|
||||||
|
["N\net Asset Value at 31 December", "54,256,197"],
|
||||||
|
["N\number of outstanding units at 31 December", "520,691"],
|
||||||
|
["I\nnvestment result", ""],
|
||||||
|
["Direct result", "-"],
|
||||||
|
["Revaluation", "2,076,667"],
|
||||||
|
["Costs", "(106,870)"],
|
||||||
|
["T\notal investment result for the period1", "1,969,797"],
|
||||||
|
["I\nnvestment result per unit2", ""],
|
||||||
|
["Direct result", "-"],
|
||||||
|
["Revaluation", "3.99"],
|
||||||
|
["Costs", "(0.21)"],
|
||||||
|
["T\notal investment result per unit", "3.78"],
|
||||||
|
["1 The results cover the period from inception of the Fund at 8 April 2016 through 31 December 2016.", ""],
|
||||||
|
["2 The result per unit is calculated using the total number of outstanding unit as per the end of the", ""],
|
||||||
|
["period.", ""]
|
||||||
|
]
|
||||||
|
|
||||||
data_lattice = [
|
data_lattice = [
|
||||||
["Cycle \nName", "KI \n(1/km)", "Distance \n(mi)", "Percent Fuel Savings", "", "", ""],
|
["Cycle \nName", "KI \n(1/km)", "Distance \n(mi)", "Percent Fuel Savings", "", "", ""],
|
||||||
["", "", "", "Improved \nSpeed", "Decreased \nAccel", "Eliminate \nStops", "Decreased \nIdle"],
|
["", "", "", "Improved \nSpeed", "Decreased \nAccel", "Eliminate \nStops", "Decreased \nIdle"],
|
||||||
@@ -485,9 +542,49 @@ data_lattice_shift_text_right_bottom = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
data_arabic = [
|
data_arabic = [
|
||||||
['ً\n\xa0\nﺎﺒﺣﺮﻣ', 'ﻥﺎﻄﻠﺳ\xa0ﻲﻤﺳﺍ'],
|
["ً\n\xa0\nﺎﺒﺣﺮﻣ", "ﻥﺎﻄﻠﺳ\xa0ﻲﻤﺳﺍ"],
|
||||||
['ﻝﺎﻤﺸﻟﺍ\xa0ﺎﻨﻴﻟﻭﺭﺎﻛ\xa0ﺔﻳﻻﻭ\xa0ﻦﻣ\xa0ﺎﻧﺍ', '؟ﺖﻧﺍ\xa0ﻦﻳﺍ\xa0ﻦﻣ'],
|
["ﻝﺎﻤﺸﻟﺍ\xa0ﺎﻨﻴﻟﻭﺭﺎﻛ\xa0ﺔﻳﻻﻭ\xa0ﻦﻣ\xa0ﺎﻧﺍ", "؟ﺖﻧﺍ\xa0ﻦﻳﺍ\xa0ﻦﻣ"],
|
||||||
['1234', 'ﻂﻄﻗ\xa047\xa0ﻱﺪﻨﻋ'],
|
["1234", "ﻂﻄﻗ\xa047\xa0ﻱﺪﻨﻋ"],
|
||||||
['؟ﻙﺎﺒﺷ\xa0ﺖﻧﺍ\xa0ﻞﻫ', 'ﺔﻳﺰﻴﻠﺠﻧﻻﺍ\xa0ﻲﻓ\xa0Jeremy\xa0ﻲﻤﺳﺍ'],
|
["؟ﻙﺎﺒﺷ\xa0ﺖﻧﺍ\xa0ﻞﻫ", "ﺔﻳﺰﻴﻠﺠﻧﻻﺍ\xa0ﻲﻓ\xa0Jeremy\xa0ﻲﻤﺳﺍ"],
|
||||||
['Jeremy\xa0is\xa0ﻲﻣﺮﺟ\xa0in\xa0Arabic', '']
|
["Jeremy\xa0is\xa0ﻲﻣﺮﺟ\xa0in\xa0Arabic", ""]
|
||||||
|
]
|
||||||
|
|
||||||
|
data_stream_layout_kwargs = [
|
||||||
|
["V i n s a u Ve r r e", ""],
|
||||||
|
["Les Blancs", "12.5CL"],
|
||||||
|
["A.O.P Côtes du Rhône", ""],
|
||||||
|
["Domaine de la Guicharde « Autour de la chapelle » 2016", "8 €"],
|
||||||
|
["A.O.P Vacqueyras", ""],
|
||||||
|
["Domaine de Montvac « Melodine » 2016", "10 €"],
|
||||||
|
["A.O.P Châteauneuf du Pape", ""],
|
||||||
|
["Domaine de Beaurenard 2017", "13 €"],
|
||||||
|
["A.O.P Côteaux du Languedoc", ""],
|
||||||
|
["Villa Tempora « Un temps pour elle » 2014", "9 €"],
|
||||||
|
["A.O.P Côtes de Provence", ""],
|
||||||
|
["Château Grand Boise 2017", "9 €"],
|
||||||
|
["Les Rosés", "12,5 CL"],
|
||||||
|
["A.O.P Côtes du Rhône", ""],
|
||||||
|
["Domaine de la Florane « A fleur de Pampre » 2016", "8 €"],
|
||||||
|
["Famille Coulon (Domaine Beaurenard) Biotifulfox 2017", "8 €"],
|
||||||
|
["A.O.P Vacqueyras", ""],
|
||||||
|
["Domaine de Montvac 2017", "9 €"],
|
||||||
|
["A.O.P Languedoc", ""],
|
||||||
|
["Domaine de Joncas « Nébla » 2015", "8 €"],
|
||||||
|
["Villa Tempora « L’arroseur arrosé » 2015", "9 €"],
|
||||||
|
["A.O.P Côtes de Provence", ""],
|
||||||
|
["Château Grand Boise « Sainte Victoire » 2017", "9 €"],
|
||||||
|
["Château Léoube 2016", "10 €"],
|
||||||
|
["Les Rouges", "12,CL"],
|
||||||
|
["A.O.P Côtes du Rhône", ""],
|
||||||
|
["Domaine de Dionysos « La Cigalette »", "8 €"],
|
||||||
|
["Château Saint Estève d’Uchaux « Grande Réserve » 2014", "9 €"],
|
||||||
|
["Domaine de la Guicharde « Cuvée Massillan » 2016", "9 €"],
|
||||||
|
["Domaine de la Florane « Terre Pourpre » 2014", "10 €"],
|
||||||
|
["L’Oratoire St Martin « Réserve des Seigneurs » 2015", "11 €"],
|
||||||
|
["A.O.P Saint Joseph", ""],
|
||||||
|
["Domaine Monier Perréol « Châtelet » 2015", "13 €"],
|
||||||
|
["A.O.P Châteauneuf du Pape", ""],
|
||||||
|
["Domaine de Beaurenard 2011", "15 €"],
|
||||||
|
["A.O.P Cornas", ""],
|
||||||
|
["Domaine Lionnet « Terre Brûlée » 2012", "15 €"]
|
||||||
]
|
]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -81,7 +81,7 @@ def test_stream_columns():
|
|||||||
|
|
||||||
filename = os.path.join(testdir, "mexican_towns.pdf")
|
filename = os.path.join(testdir, "mexican_towns.pdf")
|
||||||
tables = camelot.read_pdf(
|
tables = camelot.read_pdf(
|
||||||
filename, flavor="stream", columns=["67,180,230,425,475"], row_close_tol=10)
|
filename, flavor="stream", columns=["67,180,230,425,475"], row_tol=10)
|
||||||
assert df.equals(tables[0].df)
|
assert df.equals(tables[0].df)
|
||||||
|
|
||||||
|
|
||||||
@@ -102,6 +102,31 @@ def test_stream_flag_size():
|
|||||||
assert df.equals(tables[0].df)
|
assert df.equals(tables[0].df)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_strip_text():
|
||||||
|
df = pd.DataFrame(data_stream_strip_text)
|
||||||
|
|
||||||
|
filename = os.path.join(testdir, "detect_vertical_false.pdf")
|
||||||
|
tables = camelot.read_pdf(filename, flavor="stream", strip_text="\n")
|
||||||
|
assert df.equals(tables[0].df)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_edge_tol():
|
||||||
|
df = pd.DataFrame(data_stream_edge_tol)
|
||||||
|
|
||||||
|
filename = os.path.join(testdir, "edge_tol.pdf")
|
||||||
|
tables = camelot.read_pdf(filename, flavor="stream", edge_tol=500)
|
||||||
|
assert df.equals(tables[0].df)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_layout_kwargs():
|
||||||
|
df = pd.DataFrame(data_stream_layout_kwargs)
|
||||||
|
|
||||||
|
filename = os.path.join(testdir, "detect_vertical_false.pdf")
|
||||||
|
tables = camelot.read_pdf(
|
||||||
|
filename, flavor="stream", layout_kwargs={"detect_vertical": False})
|
||||||
|
assert df.equals(tables[0].df)
|
||||||
|
|
||||||
|
|
||||||
def test_lattice():
|
def test_lattice():
|
||||||
df = pd.DataFrame(data_lattice)
|
df = pd.DataFrame(data_lattice)
|
||||||
|
|
||||||
@@ -179,7 +204,15 @@ def test_repr():
|
|||||||
tables = camelot.read_pdf(filename)
|
tables = camelot.read_pdf(filename)
|
||||||
assert repr(tables) == "<TableList n=1>"
|
assert repr(tables) == "<TableList n=1>"
|
||||||
assert repr(tables[0]) == "<Table shape=(7, 7)>"
|
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>"
|
assert repr(tables[0].cells[0][0]) == "<Cell x1=120.48 y1=218.43 x2=164.64 y2=233.77>"
|
||||||
|
|
||||||
|
|
||||||
|
def test_url():
|
||||||
|
url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf"
|
||||||
|
tables = camelot.read_pdf(url)
|
||||||
|
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.43 x2=164.64 y2=233.77>"
|
||||||
|
|
||||||
|
|
||||||
def test_arabic():
|
def test_arabic():
|
||||||
|
|||||||