278 lines
10 KiB
Python
278 lines
10 KiB
Python
# --- Абстрактный базовый класс (Контракт) ---
|
||
|
||
from abc import ABC, abstractmethod
|
||
|
||
import openpyxl
|
||
import xlrd
|
||
|
||
from coord import Coord, Merged
|
||
|
||
EMPTY_CTYPES = [xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK]
|
||
|
||
class TranschendentnostCell:
|
||
def __init__(self, value, is_empty):
|
||
self.value = value
|
||
self._is_empty = is_empty
|
||
|
||
def is_empty(self):
|
||
return self._is_empty
|
||
|
||
class ExcelSheetReader(ABC):
|
||
"""
|
||
Абстрактный базовый класс, определяющий интерфейс для чтения данных из листа Excel.
|
||
Использует 0-индексированную систему координат, как в xlrd.
|
||
"""
|
||
def __init__(self, file_path):
|
||
self.file_path = file_path
|
||
|
||
@abstractmethod
|
||
def get_sheet_index(self):
|
||
pass
|
||
|
||
@abstractmethod
|
||
def has_next_sheet(self):
|
||
pass
|
||
|
||
@abstractmethod
|
||
def next_sheet(self):
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_cell_value(self, row, col):
|
||
"""Возвращает значение ячейки по 0-индексированным координатам."""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_border_style(self, coord: Coord, side):
|
||
"""
|
||
Возвращает числовой стиль границы (как в xlrd).
|
||
side: 'left', 'right', 'top', 'bottom'
|
||
"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_merged_cells(self):
|
||
"""Возвращает список объединенных ячеек в формате xlrd: [(rlo, rhi, clo, chi), ...]."""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_row_count(self):
|
||
"""Возвращает общее количество строк на листе."""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def get_row_values(self, row_index):
|
||
"""Возвращает список значений всех ячеек в строке."""
|
||
pass
|
||
|
||
#@abstractmethod
|
||
def info(self):
|
||
"""Возвращает строку информации"""
|
||
return "TODO: info"
|
||
|
||
@abstractmethod
|
||
def cell(self, row, col):
|
||
"""Возвращает абстрактную клетку"""
|
||
pass
|
||
|
||
def find(self, query = None):
|
||
for rx in range(self.get_row_count()):
|
||
i = 0
|
||
for x in self.get_row_values(rx):
|
||
if x == query:
|
||
return Coord(rx, i)
|
||
i += 1
|
||
|
||
return None
|
||
|
||
def get_merged(self, rowx: int, colx: int):
|
||
"""Даём ей координаты ячейки таблицы а она выдаёт её границы если переданные координаты находятся 'внутри' объединённой ячейки"""
|
||
for crange in self.get_merged_cells():
|
||
rlo, rhi, clo, chi = crange
|
||
chi -= 1
|
||
rhi -= 1
|
||
if rlo <= rowx <= rhi and chi >= colx >= clo:
|
||
return rlo, clo, rhi, chi
|
||
|
||
# если ячейка не часть объединённых то начала и концы у неё равны.
|
||
return rowx, colx, rowx, colx
|
||
|
||
def get_merged_coord(self, coord: "Coord") -> Merged:
|
||
merged = self.get_merged(coord.row, coord.col)
|
||
return Merged(coord1=Coord(merged[0], merged[1]), coord2=Coord(merged[2], merged[3]))
|
||
|
||
|
||
|
||
# --- Реализация №1: Обертка для xlrd ---
|
||
|
||
class XlrdSheetReader(ExcelSheetReader):
|
||
def __init__(self, file_path, sheet_index=0):
|
||
super().__init__(file_path)
|
||
self.sheet_index = sheet_index
|
||
self.book = xlrd.open_workbook(file_path, formatting_info=True)
|
||
self.init_sheet()
|
||
|
||
def get_sheet_index(self):
|
||
return self.sheet_index
|
||
|
||
def init_sheet(self):
|
||
self.sheet = self.book.sheet_by_index(self.sheet_index)
|
||
|
||
def has_next_sheet(self):
|
||
return self.sheet_index < len(self.book.sheet_names())-1
|
||
|
||
def next_sheet(self):
|
||
if self.has_next_sheet():
|
||
self.sheet_index += 1
|
||
self.init_sheet()
|
||
|
||
def get_cell_value(self, row, col):
|
||
# Проверка на выход за пределы таблицы, чтобы избежать ошибок
|
||
if row < self.sheet.nrows and col < self.sheet.ncols:
|
||
return self.sheet.cell_value(row, col)
|
||
return None
|
||
|
||
def info(self):
|
||
return """[XLRD (.xls)] The number of worksheets is {0}
|
||
Worksheet name(s): {1}
|
||
'{2}': size: {3}x{4}""".format(self.book.nsheets, self.book.sheet_names(), self.sheet.name, self.sheet.nrows, self.sheet.ncols)
|
||
|
||
def cell(self, row, col):
|
||
"""Возвращает абстрактную клетку"""
|
||
c = self.sheet.cell(row, col)
|
||
return TranschendentnostCell(c.value, c.ctype in EMPTY_CTYPES)
|
||
|
||
def get_border_style(self, coord: Coord, side):
|
||
row = coord.row
|
||
col = coord.col
|
||
if row >= self.sheet.nrows or col >= self.sheet.ncols:
|
||
return 0 # Нет границы за пределами листа
|
||
|
||
cell = self.sheet.cell(row, col)
|
||
xf = self.book.xf_list[cell.xf_index]
|
||
border = xf.border
|
||
|
||
style_map = {
|
||
'left': border.left_line_style,
|
||
'right': border.right_line_style,
|
||
'top': border.top_line_style,
|
||
'bottom': border.bottom_line_style,
|
||
}
|
||
return style_map.get(side, 0)
|
||
|
||
def get_merged_cells(self):
|
||
return self.sheet.merged_cells
|
||
|
||
def get_row_count(self):
|
||
return self.sheet.nrows
|
||
|
||
def get_row_values(self, row_index):
|
||
if row_index < self.sheet.nrows:
|
||
return self.sheet.row_values(row_index)
|
||
return []
|
||
|
||
|
||
# --- Реализация №2: Обертка-транслятор для openpyxl ---
|
||
|
||
class OpenpyxlSheetReader(ExcelSheetReader):
|
||
def __init__(self, file_path, sheet_name=None):
|
||
super().__init__(file_path)
|
||
self.sheet_index = 0
|
||
self.workbook = openpyxl.load_workbook(file_path, data_only=True)
|
||
self.init_sheet()
|
||
|
||
# Словарь для трансляции стилей границ openpyxl в числовые коды xlrd
|
||
self.BORDER_STYLE_MAP = {
|
||
'thin': 1, 'medium': 2, 'dashed': 3, 'dotted': 4, 'thick': 5,
|
||
'double': 6, 'hair': 7, 'mediumDashed': 8, 'dashDot': 9,
|
||
'mediumDashDot': 10, 'dashDotDot': 11, 'mediumDashDotDot': 12,
|
||
'slantDashDot': 13
|
||
}
|
||
|
||
def get_sheet_index(self):
|
||
return self.sheet_index
|
||
|
||
def has_next_sheet(self):
|
||
return self.sheet_index < len(self.workbook.sheetnames)-1
|
||
|
||
def next_sheet(self):
|
||
if self.has_next_sheet():
|
||
self.sheet_index += 1
|
||
self.init_sheet()
|
||
|
||
def init_sheet(self):
|
||
self.sheet = self.workbook[self.workbook.sheetnames[self.sheet_index]]
|
||
|
||
def info(self):
|
||
return """[OpenPyXL (.xlsx)] The number of worksheets is {0}
|
||
Worksheet name(s): {1}
|
||
'{2}': size: {3}x{4}""".format(len(self.workbook.sheetnames), self.workbook.sheetnames, self.sheet, self.sheet.max_row, self.sheet.max_column)
|
||
|
||
|
||
def _get_cell(self, row, col):
|
||
"""Внутренний метод для получения ячейки с преобразованием координат."""
|
||
# openpyxl использует 1-индексированную систему
|
||
if row < self.sheet.max_row and col < self.sheet.max_column:
|
||
return self.sheet.cell(row=row + 1, column=col + 1)
|
||
return None
|
||
|
||
def cell(self, row, col):
|
||
"""Возвращает абстрактную клетку"""
|
||
c = self._get_cell(row, col)
|
||
|
||
is_empty = (c.value is None)
|
||
return TranschendentnostCell("" if is_empty else c.value, is_empty)
|
||
|
||
def get_cell_value(self, row, col):
|
||
cell = self._get_cell(row, col)
|
||
return cell.value if cell else None
|
||
|
||
def get_border_style(self, coord: Coord, side):
|
||
cell = self._get_cell(coord.row, coord.col)
|
||
if not cell:
|
||
return 0
|
||
|
||
border_side = getattr(cell.border, side, None)
|
||
if border_side:
|
||
return self.BORDER_STYLE_MAP.get(border_side.style, 0)
|
||
return 0
|
||
|
||
def get_merged_cells(self):
|
||
# Преобразуем формат объединенных ячеек openpyxl в формат xlrd
|
||
merged_list = []
|
||
for merged_range in self.sheet.merged_cells.ranges:
|
||
# Преобразуем 1-индексированные границы в 0-индексированный формат xlrd (rlo, rhi, clo, chi)
|
||
# rhi и chi в xlrd являются эксклюзивными (до, но не включая)
|
||
rlo = merged_range.min_row - 1
|
||
rhi = merged_range.max_row
|
||
clo = merged_range.min_col - 1
|
||
chi = merged_range.max_col
|
||
merged_list.append((rlo, rhi, clo, chi))
|
||
return merged_list
|
||
|
||
def get_row_count(self):
|
||
return self.sheet.max_row
|
||
|
||
def get_row_values(self, row_index):
|
||
if row_index < self.sheet.max_row:
|
||
# Получаем значения из генератора
|
||
return [cell.value for cell in self.sheet[row_index + 1]]
|
||
return []
|
||
|
||
|
||
# --- Фабричная функция (Ваша единственная точка входа) ---
|
||
|
||
def create_reader(file_path, **kwargs) -> ExcelSheetReader:
|
||
"""
|
||
Создает и возвращает подходящий экземпляр ридера в зависимости от расширения файла.
|
||
"""
|
||
if file_path.lower().endswith('.xlsx'):
|
||
print("Используется движок openpyxl для .xlsx")
|
||
return OpenpyxlSheetReader(file_path, **kwargs)
|
||
|
||
elif file_path.lower().endswith('.xls'):
|
||
print("Используется движок xlrd для .xls")
|
||
return XlrdSheetReader(file_path, **kwargs)
|
||
|
||
else:
|
||
raise ValueError("Неподдерживаемый формат файла. Используйте .xls или .xlsx") |