mirror of
https://github.com/jsvine/pdfplumber.git
synced 2026-08-29 16:40:24 +08:00
Allow *all* extract_text kwargs for tables
Any `text_`-prefixed table extraction setting now automatically gets passed to `.extract_text(...)` via `table.extract(...)`. This introduces one minor but breaking change, which is that `keep_blank_chars` (previously, a valid table extraction setting) now needs to be passed as `text_keep_blank_chars`.
This commit is contained in:
@@ -384,9 +384,9 @@ By default, `extract_tables` uses the page's vertical and horizontal lines (or r
|
||||
|`"edge_min_length"`| Edges shorter than `edge_min_length` will be discarded before attempting to reconstruct the table.|
|
||||
|`"min_words_vertical"`| When using `"vertical_strategy": "text"`, at least `min_words_vertical` words must share the same alignment.|
|
||||
|`"min_words_horizontal"`| When using `"horizontal_strategy": "text"`, at least `min_words_horizontal` words must share the same alignment.|
|
||||
|`"keep_blank_chars"`| When using the `text` strategy, consider `" "` chars to be *parts* of words and not word-separators.|
|
||||
|`"text_tolerance"`, `"text_x_tolerance"`, `"text_y_tolerance"`| When the `text` strategy searches for words, it will expect the individual letters in each word to be no more than `text_tolerance` pixels apart.|
|
||||
|`"intersection_tolerance"`, `"intersection_x_tolerance"`, `"intersection_y_tolerance"`| When combining edges into cells, orthogonal edges must be within `intersection_tolerance` pixels to be considered intersecting.|
|
||||
|`"text_*"`| All settings prefixed with `text_` are then used when extracting text from each discovered table. All possible arguments to `Page.extract_text(...)` are also valid here.|
|
||||
|`"text_x_tolerance"`, `"text_y_tolerance"`| These `text_`-prefixed settings *also* apply to the table-identification algorithm when the `text` strategy is used. I.e., when that algorithm searches for words, it will expect the individual letters in each word to be no more than `text_[x|y]_tolerance` pixels apart.|
|
||||
|
||||
### Table-extraction strategies
|
||||
|
||||
|
||||
+2
-11
@@ -273,12 +273,7 @@ class Page(Container):
|
||||
) -> List[List[List[Optional[str]]]]:
|
||||
tset = TableSettings.resolve(table_settings)
|
||||
tables = self.find_tables(tset)
|
||||
|
||||
extract_kwargs = {
|
||||
k: getattr(tset, "text_" + k) for k in ["x_tolerance", "y_tolerance"]
|
||||
}
|
||||
|
||||
return [table.extract(**extract_kwargs) for table in tables]
|
||||
return [table.extract(**(tset.text_settings or {})) for table in tables]
|
||||
|
||||
def extract_table(
|
||||
self, table_settings: Optional[T_table_settings] = None
|
||||
@@ -295,11 +290,7 @@ class Page(Container):
|
||||
|
||||
largest = list(sorted(tables, key=sorter))[0]
|
||||
|
||||
extract_kwargs = {
|
||||
k: getattr(tset, "text_" + k) for k in ["x_tolerance", "y_tolerance"]
|
||||
}
|
||||
|
||||
return largest.extract(**extract_kwargs)
|
||||
return largest.extract(**(tset.text_settings or {}))
|
||||
|
||||
def _get_textmap(self, **kwargs: Any) -> TextMap:
|
||||
defaults = dict(x_shift=self.bbox[0], y_shift=self.bbox[1])
|
||||
|
||||
+30
-25
@@ -396,11 +396,7 @@ class Table(object):
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
def extract(
|
||||
self,
|
||||
x_tolerance: T_num = utils.DEFAULT_X_TOLERANCE,
|
||||
y_tolerance: T_num = utils.DEFAULT_Y_TOLERANCE,
|
||||
) -> List[List[Optional[str]]]:
|
||||
def extract(self, **kwargs: Any) -> List[List[Optional[str]]]:
|
||||
|
||||
chars = self.page.chars
|
||||
table_arr = []
|
||||
@@ -426,11 +422,12 @@ class Table(object):
|
||||
]
|
||||
|
||||
if len(cell_chars):
|
||||
cell_text = utils.extract_text(
|
||||
cell_chars,
|
||||
x_tolerance=x_tolerance,
|
||||
y_tolerance=y_tolerance,
|
||||
).strip()
|
||||
kwargs["x_shift"] = cell[0]
|
||||
kwargs["y_shift"] = cell[1]
|
||||
if "layout" in kwargs:
|
||||
kwargs["layout_width"] = cell[2] - cell[0]
|
||||
kwargs["layout_height"] = cell[3] - cell[1]
|
||||
cell_text = utils.extract_text(cell_chars, **kwargs)
|
||||
else:
|
||||
cell_text = ""
|
||||
arr.append(cell_text)
|
||||
@@ -450,9 +447,6 @@ NON_NEGATIVE_SETTINGS = [
|
||||
"edge_min_length",
|
||||
"min_words_vertical",
|
||||
"min_words_horizontal",
|
||||
"text_tolerance",
|
||||
"text_x_tolerance",
|
||||
"text_y_tolerance",
|
||||
"intersection_tolerance",
|
||||
"intersection_x_tolerance",
|
||||
"intersection_y_tolerance",
|
||||
@@ -481,13 +475,10 @@ class TableSettings:
|
||||
edge_min_length: T_num = 3
|
||||
min_words_vertical: int = DEFAULT_MIN_WORDS_VERTICAL
|
||||
min_words_horizontal: int = DEFAULT_MIN_WORDS_HORIZONTAL
|
||||
keep_blank_chars: bool = False
|
||||
text_tolerance: T_num = 3
|
||||
text_x_tolerance: T_num = UNSET
|
||||
text_y_tolerance: T_num = UNSET
|
||||
intersection_tolerance: T_num = 3
|
||||
intersection_x_tolerance: T_num = UNSET
|
||||
intersection_y_tolerance: T_num = UNSET
|
||||
text_settings: Optional[Dict[str, Any]] = None
|
||||
|
||||
def __post_init__(self) -> "TableSettings":
|
||||
"""Clean up user-provided table settings.
|
||||
@@ -517,9 +508,19 @@ class TableSettings:
|
||||
f'{{{",".join(TABLE_STRATEGIES)}}}'
|
||||
)
|
||||
|
||||
if self.text_settings is None:
|
||||
self.text_settings = {}
|
||||
|
||||
# This next section is for backwards compatibility
|
||||
for attr in ["x_tolerance", "y_tolerance"]:
|
||||
if attr not in self.text_settings:
|
||||
self.text_settings[attr] = self.text_settings.get("tolerance", 3)
|
||||
|
||||
if "tolerance" in self.text_settings:
|
||||
del self.text_settings["tolerance"]
|
||||
# End of that section
|
||||
|
||||
for attr, fallback in [
|
||||
("text_x_tolerance", "text_tolerance"),
|
||||
("text_y_tolerance", "text_tolerance"),
|
||||
("snap_x_tolerance", "snap_tolerance"),
|
||||
("snap_y_tolerance", "snap_tolerance"),
|
||||
("join_x_tolerance", "join_tolerance"),
|
||||
@@ -539,7 +540,15 @@ class TableSettings:
|
||||
elif isinstance(settings, cls):
|
||||
return settings
|
||||
elif isinstance(settings, dict):
|
||||
return cls(**settings)
|
||||
core_settings = {}
|
||||
text_settings = {}
|
||||
for k, v in settings.items():
|
||||
if k[:5] == "text_":
|
||||
text_settings[k[5:]] = v
|
||||
else:
|
||||
core_settings[k] = v
|
||||
core_settings["text_settings"] = text_settings
|
||||
return cls(**core_settings)
|
||||
else:
|
||||
raise ValueError(f"Cannot resolve settings: {settings}")
|
||||
|
||||
@@ -588,11 +597,7 @@ class TableFinder(object):
|
||||
h_strat = settings.horizontal_strategy
|
||||
|
||||
if v_strat == "text" or h_strat == "text":
|
||||
words = self.page.extract_words(
|
||||
x_tolerance=settings.text_x_tolerance,
|
||||
y_tolerance=settings.text_y_tolerance,
|
||||
keep_blank_chars=settings.keep_blank_chars,
|
||||
)
|
||||
words = self.page.extract_words(**(settings.text_settings or {}))
|
||||
|
||||
v_explicit = []
|
||||
for desc in settings.explicit_vertical_lines or []:
|
||||
|
||||
+11
-1
@@ -133,6 +133,16 @@ class Test(unittest.TestCase):
|
||||
]
|
||||
assert t_tol[-1] == t_tol_from_tables[-1]
|
||||
|
||||
def test_text_layout(self):
|
||||
path = os.path.join(HERE, "pdfs/issue-53-example.pdf")
|
||||
with pdfplumber.open(path) as pdf:
|
||||
table = pdf.pages[0].extract_table(
|
||||
{
|
||||
"text_layout": True,
|
||||
}
|
||||
)
|
||||
assert table[3][0] == " FY2013 \n FY2014 "
|
||||
|
||||
def test_text_without_words(self):
|
||||
assert table.words_to_edges_h([]) == []
|
||||
assert table.words_to_edges_v([]) == []
|
||||
@@ -195,7 +205,7 @@ class Test(unittest.TestCase):
|
||||
"edge_min_length": 3,
|
||||
"min_words_vertical": 3,
|
||||
"min_words_horizontal": 1,
|
||||
"keep_blank_chars": False,
|
||||
"text_keep_blank_chars": False,
|
||||
"text_tolerance": 3,
|
||||
"intersection_tolerance": 3,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user