added openpyxl support
This commit is contained in:
230
translations.py
Normal file
230
translations.py
Normal 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")
|
||||
Reference in New Issue
Block a user