# Copyright Stanislav Mironov from abc import ABC, abstractmethod from datetime import time 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, is_time=False): self.value = value self.is_time = isinstance(value, time) or is_time self._is_empty = is_empty def is_nospace_nocase_same(self, query): try: if self.value.lower().replace(" ", "").strip() == query.lower().replace(" ", "").strip(): return True except: pass return False 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 get_sheet_name(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) -> TranschendentnostCell: """Возвращает абстрактную клетку""" pass def find(self, query = None, startswith=False, nospace=False): return self.find_any([query], startswith=startswith, nospace=nospace) def find_any(self, query = None, startswith=False, nospace=False): for rx in range(self.get_row_count()): i = 0 for x in self.get_row_values(rx): if nospace: x = str(x).replace(" ", "").strip() for query_selected in query: if x == query_selected: return Coord(rx, i) elif startswith: try: if str(x).lower().startswith(query_selected.lower()): return Coord(rx, i) except: pass 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 get_sheet_name(self): return self.sheet.name 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) is_empty = c.ctype in EMPTY_CTYPES is_time = c.ctype == xlrd.XL_CELL_DATE value = c.value if is_empty: value = "" elif is_time: if isinstance(value, float): if value <= 1: seconds = round(value * 86400) minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) value = time(hour=hours, second=seconds, minute=minutes) else: print(f"TODO: value is {value} its unix? not 0.xxxxxxxx") else: is_time = False print("IsTime but not float!") return TranschendentnostCell(value, is_empty, is_time=is_time) 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 get_sheet_name(self): return self.workbook.sheetnames[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, is_time=isinstance(c.value, time)) 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'): return OpenpyxlSheetReader(file_path, **kwargs) elif file_path.lower().endswith('.xls'): return XlrdSheetReader(file_path, **kwargs) else: raise ValueError("Неподдерживаемый формат файла. Используйте .xls или .xlsx")