147 lines
4.6 KiB
Python
147 lines
4.6 KiB
Python
"""
|
|
# Summary
|
|
|
|
XLSX parser implementations for OmniRead.
|
|
|
|
This module provides a concrete, generic parser for Office Open XML
|
|
spreadsheets. It exposes workbook sheets as lists of string rows so
|
|
downstream consumers can interpret tabular content without depending on
|
|
openpyxl directly.
|
|
|
|
The parser is intentionally statement-agnostic: it performs no header
|
|
detection or column interpretation beyond basic cell normalization.
|
|
"""
|
|
|
|
import datetime
|
|
from io import BytesIO
|
|
from typing import List, Optional, Union
|
|
|
|
import openpyxl
|
|
|
|
from omniread.core.content import Content
|
|
from .parser_base import XlsxParserBase
|
|
|
|
|
|
class XlsxParser(XlsxParserBase):
|
|
"""
|
|
Generic xlsx parser producing string rows from a worksheet.
|
|
|
|
Notes:
|
|
**Responsibilities:**
|
|
|
|
- Lazily load the workbook owned by the parser's content.
|
|
- Normalize cells (including dates and numeric values) into
|
|
deterministic string representations.
|
|
- Expose sheet discovery and row extraction helpers.
|
|
|
|
**Constraints:**
|
|
|
|
- Cells are rendered with ``str(value)`` after trimming; date and
|
|
datetime values are rendered in ISO format. Consumers requiring
|
|
locale-specific formatting must convert on their side.
|
|
"""
|
|
|
|
def __init__(self, content: Content, *, data_only: bool = True, read_only: bool = True):
|
|
"""
|
|
Initialize the parser.
|
|
|
|
Args:
|
|
content (Content):
|
|
XLSX content to parse; its type must be supported.
|
|
data_only (bool):
|
|
Passed to openpyxl: when True, formula cells yield their last
|
|
computed value instead of the formula string.
|
|
read_only (bool):
|
|
Passed to openpyxl: streaming mode for lower memory usage.
|
|
"""
|
|
super().__init__(content)
|
|
self._data_only = data_only
|
|
self._read_only = read_only
|
|
self._workbook: Optional[openpyxl.Workbook] = None
|
|
|
|
@property
|
|
def workbook(self) -> openpyxl.Workbook:
|
|
"""
|
|
The lazily loaded workbook backing this parser's content.
|
|
"""
|
|
if self._workbook is None:
|
|
self._workbook = openpyxl.load_workbook(
|
|
BytesIO(self.content.raw),
|
|
data_only=self._data_only,
|
|
read_only=self._read_only,
|
|
)
|
|
return self._workbook
|
|
|
|
@property
|
|
def sheet_names(self) -> List[str]:
|
|
"""
|
|
Names of all worksheets contained in the workbook.
|
|
"""
|
|
return list(self.workbook.sheetnames)
|
|
|
|
def parse(self) -> List[List[str]]:
|
|
"""
|
|
Parse the first worksheet into normalized string rows.
|
|
|
|
Returns:
|
|
List[List[str]]:
|
|
Rows of the default (first) worksheet.
|
|
"""
|
|
return self.rows()
|
|
|
|
def rows(
|
|
self,
|
|
sheet: Optional[Union[int, str]] = None,
|
|
*,
|
|
skip_empty: bool = True,
|
|
) -> List[List[str]]:
|
|
"""
|
|
Extract normalized string rows from a worksheet.
|
|
|
|
Args:
|
|
sheet (Optional[Union[int, str]]):
|
|
Worksheet index or title; defaults to the first worksheet.
|
|
skip_empty (bool):
|
|
When True (default), rows whose cells are all blank are omitted.
|
|
|
|
Returns:
|
|
List[List[str]]:
|
|
Normalized rows; trailing blank cells are trimmed per row.
|
|
|
|
Raises:
|
|
ValueError:
|
|
If the requested sheet does not exist.
|
|
"""
|
|
ws = self._resolve_sheet(sheet)
|
|
out: List[List[str]] = []
|
|
for row in ws.iter_rows(values_only=True):
|
|
cells = [self._cell_str(v) for v in row]
|
|
while cells and not cells[-1].strip():
|
|
cells.pop()
|
|
if skip_empty and not any(c.strip() for c in cells):
|
|
continue
|
|
out.append(cells)
|
|
return out
|
|
|
|
def _resolve_sheet(self, sheet: Optional[Union[int, str]]):
|
|
names = self.workbook.sheetnames
|
|
if not names:
|
|
raise ValueError("Workbook contains no worksheets")
|
|
if sheet is None:
|
|
index = 0
|
|
elif isinstance(sheet, int):
|
|
index = sheet
|
|
else:
|
|
index = names.index(sheet) if sheet in names else -1
|
|
if not (0 <= index < len(names)):
|
|
raise ValueError(f"Worksheet not found: {sheet!r} (available: {names})")
|
|
return self.workbook[names[index]]
|
|
|
|
@staticmethod
|
|
def _cell_str(value) -> str:
|
|
if value is None:
|
|
return ""
|
|
if isinstance(value, (datetime.datetime, datetime.date)):
|
|
return value.isoformat()
|
|
return str(value).strip()
|