added openpyxl support

This commit is contained in:
2025-09-11 15:42:41 +03:00
parent 414907a929
commit babf491c8e
6 changed files with 224470 additions and 41307 deletions

230
translations.py Normal file
View File

@@ -0,0 +1,230 @@
# --- Абстрактный базовый класс (Контракт) ---
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):
self._is_empty
class ExcelSheetReader(ABC):
"""
Абстрактный базовый класс, определяющий интерфейс для чтения данных из листа Excel.
Использует 0-индексированную систему координат, как в xlrd.
"""
def __init__(self, file_path):
self.file_path = file_path
@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.book = xlrd.open_workbook(file_path, formatting_info=True)
self.sheet = self.book.sheet_by_index(sheet_index)
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):
print("The number of worksheets is {0}".format(self.book.nsheets))
print("Worksheet name(s): {0}".format(self.book.sheet_names()))
return "'{0}': size: {1}x{2} names: ".format(self.sheet.name, self.sheet.nrows, self.sheet.ncols, " ".join(self.book.sheet_names()))
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.workbook = openpyxl.load_workbook(file_path, data_only=True)
self.sheet = self.workbook[sheet_name] if sheet_name else self.workbook.active
# Словарь для трансляции стилей границ 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_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")