added openpyxl support
This commit is contained in:
13
coord.py
13
coord.py
@@ -1,9 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import xlrd
|
|
||||||
|
|
||||||
|
|
||||||
class Coord:
|
class Coord:
|
||||||
def __init__(self, row, col):
|
def __init__(self, row, col):
|
||||||
self.row = row
|
self.row = row
|
||||||
@@ -17,8 +13,8 @@ class Coord:
|
|||||||
return Coord(self.row if row is None else row,
|
return Coord(self.row if row is None else row,
|
||||||
self.col if col is None else col)
|
self.col if col is None else col)
|
||||||
|
|
||||||
def cell(self, sh) -> "xlrd.sheet.Cell":
|
def cell(self, reader: "ExcelSheetReader") -> "TranschendentnostCell":
|
||||||
return sh.cell(self.row, self.col)
|
return reader.cell(self.row, self.col)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
import utils
|
import utils
|
||||||
@@ -49,8 +45,9 @@ class Merged:
|
|||||||
def width(self):
|
def width(self):
|
||||||
return self.high.col - self.low.col + 1
|
return self.high.col - self.low.col + 1
|
||||||
|
|
||||||
def cell(self, sh) -> "xlrd.sheet.Cell":
|
def cell(self, reader: "ExcelSheetReader") -> "TranschendentnostCell":
|
||||||
return sh.cell(self.low.row, self.low.col)
|
return reader.cell(self.low.row, self.low.col)
|
||||||
|
|
||||||
|
|
||||||
def is_pseudo_merged(self):
|
def is_pseudo_merged(self):
|
||||||
"""Псевдо-мержнутая значит размеом 1x1, оно же если начало совпадает с концом"""
|
"""Псевдо-мержнутая значит размеом 1x1, оно же если начало совпадает с концом"""
|
||||||
|
|||||||
40
main.py
40
main.py
@@ -13,6 +13,7 @@ from requests.structures import CaseInsensitiveDict
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import aigenerated
|
import aigenerated
|
||||||
import parser
|
import parser
|
||||||
|
import translations
|
||||||
import utils
|
import utils
|
||||||
import json
|
import json
|
||||||
# Общее правило проекта, сначала в координатах идёт ROW а потом COL, нумерация с нуля
|
# Общее правило проекта, сначала в координатах идёт ROW а потом COL, нумерация с нуля
|
||||||
@@ -76,41 +77,16 @@ for facultet in FACULTETS:
|
|||||||
counter += 1
|
counter += 1
|
||||||
print(f"\n\n-- Ссылка --")
|
print(f"\n\n-- Ссылка --")
|
||||||
print(f"{excel_url}")
|
print(f"{excel_url}")
|
||||||
xlsx = excel_url.endswith(".xlsx")
|
is_xlsx = excel_url.endswith(".xlsx")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filename = "excels/" + facultet + filestime + f"[C{counter}]" + ".xls"
|
filename = "excels/" + facultet + filestime + f"[C{counter}]" + ".xls" + ("x" if is_xlsx else "")
|
||||||
|
|
||||||
# Download a file
|
|
||||||
if not xlsx:
|
|
||||||
aigenerated.download_file_from_url(excel_url, filename)
|
aigenerated.download_file_from_url(excel_url, filename)
|
||||||
else:
|
|
||||||
aigenerated.download_file_from_url(excel_url, filename+"x")
|
|
||||||
|
|
||||||
excel_file = pd.ExcelFile(filename + "x")
|
reader = translations.create_reader(filename)
|
||||||
|
|
||||||
# Создаем "писателя" для формата .xls с помощью движка xlwt
|
|
||||||
# Использование 'with' гарантирует, что файл будет корректно сохранен и закрыт
|
|
||||||
with pd.ExcelWriter(filename, engine='xlwt') as writer:
|
|
||||||
print("Начинаю конвертацию...")
|
|
||||||
# Проходим по каждому листу в исходном файле
|
|
||||||
for sheet_name in excel_file.sheet_names:
|
|
||||||
print(f" - Обрабатываю лист: {sheet_name}")
|
|
||||||
# Читаем лист в DataFrame
|
|
||||||
df = excel_file.parse(sheet_name)
|
|
||||||
# Записываем этот DataFrame в новый .xls файл с тем же именем листа
|
|
||||||
# index=False чтобы не добавлять лишнюю колонку с индексами pandas
|
|
||||||
df.to_excel(writer, sheet_name=sheet_name, index=False)
|
|
||||||
|
|
||||||
print(f"✅ Успешно! Файл конвертирован как: {filename}")
|
|
||||||
|
|
||||||
book = xlrd.open_workbook(filename, formatting_info=True)
|
|
||||||
print("The number of worksheets is {0}".format(book.nsheets))
|
|
||||||
print("Worksheet name(s): {0}".format(book.sheet_names()))
|
|
||||||
sh = book.sheet_by_index(0)
|
|
||||||
|
|
||||||
parser.LOGGING = False
|
parser.LOGGING = False
|
||||||
prs = parser.Parser(sh)
|
prs = parser.Parser(reader)
|
||||||
prs.parse()
|
prs.parse()
|
||||||
for group_name in prs.groups.keys():
|
for group_name in prs.groups.keys():
|
||||||
if group_name in result.keys():
|
if group_name in result.keys():
|
||||||
@@ -119,15 +95,17 @@ for facultet in FACULTETS:
|
|||||||
|
|
||||||
gr = result[group_name] = prs.groups[group_name]
|
gr = result[group_name] = prs.groups[group_name]
|
||||||
gr['facultet'] = facultet
|
gr['facultet'] = facultet
|
||||||
gr['data_source'] = excel_url.split("/")[-1] + " SHEET: " + str(sh.name)
|
gr['data_source'] = excel_url.split("/")[-1]
|
||||||
gr['parser_debug'] = {
|
gr['parser_debug'] = {
|
||||||
"C_COUNTER": counter,
|
"C_COUNTER": counter,
|
||||||
"timestime": filestime,
|
"timestime": filestime,
|
||||||
"excel_url": excel_url,
|
"excel_url": excel_url,
|
||||||
"sheet": sh.name,
|
"reader_info": reader.info(),
|
||||||
"filename": filename
|
"filename": filename
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print(f"Populates {len(prs.groups)} groups to result")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error while {excel_url}")
|
print(f"Error while {excel_url}")
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
57
parser.py
57
parser.py
@@ -4,6 +4,7 @@ import xlrd
|
|||||||
|
|
||||||
import aigenerated
|
import aigenerated
|
||||||
from coord import Coord, Merged
|
from coord import Coord, Merged
|
||||||
|
from translations import ExcelSheetReader
|
||||||
import utils
|
import utils
|
||||||
|
|
||||||
LOGGING = True
|
LOGGING = True
|
||||||
@@ -14,27 +15,27 @@ def pprint(*args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def __init__(self, sheet: "xlrd.sheet.Sheet"):
|
def __init__(self, reader: ExcelSheetReader):
|
||||||
self.sh: "xlrd.sheet.Sheet" = sheet
|
self.reader = reader
|
||||||
self.groups = {}
|
self.groups = {}
|
||||||
self.teachers = set()
|
self.teachers = set()
|
||||||
self.places = set()
|
self.places = set()
|
||||||
pprint("Parser created for '{0}': size: {1}x{2}".format(self.sh.name, self.sh.nrows, self.sh.ncols))
|
pprint("Parser created for '{0}'".format(reader.info()))
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
monday = utils.find(self.sh, "ПОНЕДЕЛЬНИК")
|
monday = self.reader.find("ПОНЕДЕЛЬНИК")
|
||||||
if monday is None:
|
if monday is None:
|
||||||
print(" -- Failed parse! -- ")
|
print(" -- Failed parse! -- ")
|
||||||
print("ПОНЕДЕЛЬНИК НЕ НАЙДЕН!")
|
print("ПОНЕДЕЛЬНИК НЕ НАЙДЕН!")
|
||||||
return
|
return
|
||||||
|
|
||||||
head_rx = monday[0] - 1 # выше первого понидельника
|
head_rx = monday.row - 1 # выше первого понидельника
|
||||||
if head_rx < 0:
|
if head_rx < 0:
|
||||||
raise Exception("head_rx < 0: Программа пыталась найти 'ПОНЕДЕЛЬНИК', но по всей видимости не нашла.")
|
raise Exception("head_rx < 0: Программа пыталась найти 'ПОНЕДЕЛЬНИК', но по всей видимости не нашла.")
|
||||||
|
|
||||||
head = self.sh.row(head_rx) # get all ROW (months, groups)
|
head = self.reader.get_row_values(head_rx) # get all ROW (months, groups)
|
||||||
pprint(f"head={head}")
|
pprint(f"head={head}")
|
||||||
self.groups = parse_groups(self.sh, head, monday, head_rx) # parse groups to self.groups
|
self.groups = parse_groups(self.reader, head, monday, head_rx) # parse groups to self.groups
|
||||||
pprint(f'self.groups={json.dumps(self.groups, indent=2, ensure_ascii=False)}')
|
pprint(f'self.groups={json.dumps(self.groups, indent=2, ensure_ascii=False)}')
|
||||||
|
|
||||||
pprint("\n\n\n")
|
pprint("\n\n\n")
|
||||||
@@ -53,12 +54,12 @@ class Parser:
|
|||||||
# speaker
|
# speaker
|
||||||
low = merged.low
|
low = merged.low
|
||||||
speaker_pos = low.shift(down=merged.height())
|
speaker_pos = low.shift(down=merged.height())
|
||||||
speaker = speaker_pos.cell(self.sh).value
|
speaker = speaker_pos.cell(self.reader).value
|
||||||
|
|
||||||
# location
|
# location
|
||||||
location = merged.high.shift(down=1).cell(self.sh).value
|
location = merged.high.shift(down=1).cell(self.reader).value
|
||||||
|
|
||||||
return {"loc": str(location), "leader": str(speaker), "name": str(merged.cell(self.sh).value)}
|
return {"loc": str(location), "leader": str(speaker), "name": str(merged.cell(self.reader).value)}
|
||||||
|
|
||||||
def process_group(self, group, monday):
|
def process_group(self, group, monday):
|
||||||
"""
|
"""
|
||||||
@@ -70,24 +71,24 @@ class Parser:
|
|||||||
pprint(group_name)
|
pprint(group_name)
|
||||||
row = group['position'][0] + 1 # counter for while, +1 for shift down; также номер строки в таблице (вроде с нуля)
|
row = group['position'][0] + 1 # counter for while, +1 for shift down; также номер строки в таблице (вроде с нуля)
|
||||||
weeknum = 1 # номер недели, щёлкнет +1 при каком-то условии.
|
weeknum = 1 # номер недели, щёлкнет +1 при каком-то условии.
|
||||||
while row < self.sh.nrows: # maybe условие чтобы не уйти ниже чем есть строк
|
while row < self.reader.get_row_count(): # maybe условие чтобы не уйти ниже чем есть строк
|
||||||
pos = Coord(row, group['position'][1]) # текущая позиция, верхний правый угол (=low)
|
pos = Coord(row, group['position'][1]) # текущая позиция, верхний левый угол (=low)
|
||||||
pos_right = pos.shift(right=3)
|
pos_right = pos.shift(right=3)
|
||||||
pair_pos = pos.replace(col=5)
|
pair_pos = pos.replace(col=5)
|
||||||
weekday_pos = pos.replace(col=4)
|
weekday_pos = pos.replace(col=4)
|
||||||
merged = utils.get_merged_coord(self.sh, pos)
|
merged = self.reader.get_merged_coord(pos)
|
||||||
right_cell = pos_right.cell(self.sh)
|
right_cell = pos_right.cell(self.reader)
|
||||||
merged_cell = merged.cell(self.sh)
|
merged_cell = merged.cell(self.reader)
|
||||||
cv = merged_cell.value
|
cv = merged_cell.value
|
||||||
# В конце (12 пара:>) название группы, можно использовать как якорь
|
# В конце (12 пара:>) название группы, можно использовать как якорь
|
||||||
if utils.unspace(cv) == group_name:
|
if utils.unspace(cv) == group_name:
|
||||||
pprint("Lesson == group name; ending group loop.")
|
pprint("Lesson == group name; ending group loop.")
|
||||||
break
|
break
|
||||||
|
|
||||||
weekday_mr = utils.get_merged_coord(self.sh, weekday_pos)
|
weekday_mr = self.reader.get_merged_coord(weekday_pos)
|
||||||
weekday = utils.unspace(weekday_mr.cell(self.sh).value)
|
weekday = utils.unspace(weekday_mr.cell(self.reader).value)
|
||||||
pair_mr = utils.get_merged_coord(self.sh, pair_pos)
|
pair_mr = self.reader.get_merged_coord(pair_pos)
|
||||||
pair = utils.unspace(pair_mr.cell(self.sh).value)
|
pair = utils.unspace(pair_mr.cell(self.reader).value)
|
||||||
|
|
||||||
skip = 0
|
skip = 0
|
||||||
if weekday == "":
|
if weekday == "":
|
||||||
@@ -101,7 +102,7 @@ class Parser:
|
|||||||
if not skip:
|
if not skip:
|
||||||
next = 3 # на сколько пыгнуть для следующего шага?
|
next = 3 # на сколько пыгнуть для следующего шага?
|
||||||
|
|
||||||
is_empty_lesson = right_cell.ctype in utils.EMPTY_CTYPES and merged_cell.ctype in utils.EMPTY_CTYPES
|
is_empty_lesson = right_cell.is_empty() and merged_cell.is_empty()
|
||||||
dispname = ""
|
dispname = ""
|
||||||
parsed_discipline_name = None
|
parsed_discipline_name = None
|
||||||
parsed_location = None
|
parsed_location = None
|
||||||
@@ -115,7 +116,7 @@ class Parser:
|
|||||||
|
|
||||||
if not is_empty_lesson:
|
if not is_empty_lesson:
|
||||||
may_prepod = merged.low.shift(down=2)
|
may_prepod = merged.low.shift(down=2)
|
||||||
if utils.has_no_bottom_border(self.sh, may_prepod):
|
if utils.has_no_bottom_border(self.reader, may_prepod):
|
||||||
next = 6
|
next = 6
|
||||||
is_2pair = True
|
is_2pair = True
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ class Parser:
|
|||||||
parsed_location = ret['loc']
|
parsed_location = ret['loc']
|
||||||
parsed_leader = ret['leader']
|
parsed_leader = ret['leader']
|
||||||
parsed_discipline_name = ret['name']
|
parsed_discipline_name = ret['name']
|
||||||
parsed_uncotigorized = list(utils.parse_all_dirt(self.sh, merged.low, merged.width(), next))
|
parsed_uncotigorized = list(utils.parse_all_dirt(self.reader, merged.low, merged.width(), next))
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -135,7 +136,7 @@ class Parser:
|
|||||||
dispname += (" SOLD" if is_solid else " SPLIT")
|
dispname += (" SOLD" if is_solid else " SPLIT")
|
||||||
dispname += (" [ДВУПАРНЫЙ]" if is_2pair else "")
|
dispname += (" [ДВУПАРНЫЙ]" if is_2pair else "")
|
||||||
|
|
||||||
parsed_uncotigorized = list(utils.parse_all_dirt(self.sh, merged.low, 4, next))
|
parsed_uncotigorized = list(utils.parse_all_dirt(self.reader, merged.low, 4, next))
|
||||||
|
|
||||||
|
|
||||||
if parsed_leader: dispname += f" [{parsed_leader}]"
|
if parsed_leader: dispname += f" [{parsed_leader}]"
|
||||||
@@ -194,23 +195,23 @@ class Parser:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def parse_groups(sh, head, monday, head_rx):
|
def parse_groups(reader: "ExcelSheetReader", head, monday: Coord, head_rx):
|
||||||
"""Распознать список групп и метаданные к ним, по сути получить список названий группы и координат её верхнего header-а (AQ6:AT6)"""
|
"""Распознать список групп и метаданные к ним, по сути получить список названий группы и координат её верхнего header-а (AQ6:AT6)"""
|
||||||
groups = {}
|
groups = {}
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(head):
|
while i < len(head):
|
||||||
x = head[i]
|
x = head[i]
|
||||||
pprint(f"while i={i} head[i]={x}")
|
pprint(f"while i={i} head[i]={x}")
|
||||||
merged = utils.get_merged_coord(sh, Coord(head_rx, i))
|
merged = reader.get_merged_coord(Coord(head_rx, i))
|
||||||
if i > monday[1] + 1:
|
if i > monday.col + 1:
|
||||||
if merged is None or x.value == "":
|
if merged is None or x == "":
|
||||||
break
|
break
|
||||||
|
|
||||||
if merged.width() != 4:
|
if merged.width() != 4:
|
||||||
pprint(f"WARNING: group header witdh !=4 (found: {merged.width()}); blocks !=4 not supported by parser.")
|
pprint(f"WARNING: group header witdh !=4 (found: {merged.width()}); blocks !=4 not supported by parser.")
|
||||||
break
|
break
|
||||||
|
|
||||||
name = utils.unspace(x.value)
|
name = utils.unspace(x)
|
||||||
groups[name] = {
|
groups[name] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"position": [head_rx, i],
|
"position": [head_rx, i],
|
||||||
|
|||||||
265381
result.json
265381
result.json
File diff suppressed because it is too large
Load Diff
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")
|
||||||
46
utils.py
46
utils.py
@@ -2,30 +2,14 @@
|
|||||||
# gemini generated
|
# gemini generated
|
||||||
import xlrd
|
import xlrd
|
||||||
from coord import Coord, Merged
|
from coord import Coord, Merged
|
||||||
|
from translations import ExcelSheetReader
|
||||||
|
|
||||||
EMPTY_CTYPES = [xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK]
|
EMPTY_CTYPES = [xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK]
|
||||||
|
|
||||||
def border(sh, coord):
|
def has_no_bottom_border(reader: "ExcelSheetReader", coord):
|
||||||
cell = sh.cell(coord.row, coord.col)
|
return reader.get_border_style(coord, 'bottom') == 0 and reader.get_border_style(coord.shift(down=1), 'top') == 0
|
||||||
xf_style: "xlrd.formatting.XF" = sh.book.xf_list[cell.xf_index]
|
|
||||||
return xf_style.border
|
|
||||||
|
|
||||||
def border_right(sh, cell):
|
def parse_all_dirt(reader: "ExcelSheetReader", min_pos, right, down):
|
||||||
return border(sh, cell).right_line_style
|
|
||||||
|
|
||||||
def border_left(sh, cell):
|
|
||||||
return border(sh, cell).left_line_style
|
|
||||||
|
|
||||||
def border_bottom(sh, cell):
|
|
||||||
return border(sh, cell).bottom_line_style
|
|
||||||
|
|
||||||
def border_top(sh, cell):
|
|
||||||
return border(sh, cell).top_line_style
|
|
||||||
|
|
||||||
def has_no_bottom_border(sh, coord):
|
|
||||||
return border_bottom(sh, coord) == 0 and border_top(sh, coord.shift(down=1)) == 0
|
|
||||||
|
|
||||||
def parse_all_dirt(sh, min_pos, right, down):
|
|
||||||
RET = set()
|
RET = set()
|
||||||
|
|
||||||
row = min_pos.row
|
row = min_pos.row
|
||||||
@@ -33,7 +17,7 @@ def parse_all_dirt(sh, min_pos, right, down):
|
|||||||
col = min_pos.col
|
col = min_pos.col
|
||||||
while col < min_pos.col + right:
|
while col < min_pos.col + right:
|
||||||
#print(excel_coordinate(row, col))
|
#print(excel_coordinate(row, col))
|
||||||
value = str(sh.cell(row, col).value)
|
value = str(reader.get_cell_value(row, col))
|
||||||
if value is not None and len(value) > 0:
|
if value is not None and len(value) > 0:
|
||||||
RET.add(value)
|
RET.add(value)
|
||||||
col += 1
|
col += 1
|
||||||
@@ -116,24 +100,6 @@ def excel_coordinate(row, col):
|
|||||||
|
|
||||||
return col_str + str(row + 1) # Добавляем номер строки (Excel начинается с 1)
|
return col_str + str(row + 1) # Добавляем номер строки (Excel начинается с 1)
|
||||||
|
|
||||||
|
|
||||||
def get_merged(sh, rowx, colx):
|
|
||||||
"""Даём ей координаты ячейки таблицы а она выдаёт её границы если переданные координаты находятся 'внутри' объединённой ячейки"""
|
|
||||||
for crange in sh.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(sh, coord):
|
|
||||||
merged = get_merged(sh, coord.row, coord.col)
|
|
||||||
return Merged(coord1=Coord(merged[0], merged[1]), coord2=Coord(merged[2], merged[3]))
|
|
||||||
|
|
||||||
|
|
||||||
def merged_humanize(crange):
|
def merged_humanize(crange):
|
||||||
"""Получить из 4 цифр границ AA:BB координаты как в Excel"""
|
"""Получить из 4 цифр границ AA:BB координаты как в Excel"""
|
||||||
row_low, col_low, row_high, col_high = crange # see order!
|
row_low, col_low, row_high, col_high = crange # see order!
|
||||||
@@ -141,6 +107,8 @@ def merged_humanize(crange):
|
|||||||
|
|
||||||
def unspace(s: str):
|
def unspace(s: str):
|
||||||
"""Убрать пробелы из текста"""
|
"""Убрать пробелы из текста"""
|
||||||
|
if s is None:
|
||||||
|
return "!!!Python None!!!"
|
||||||
return s.strip().replace(" ", "").replace("\t", "")
|
return s.strip().replace(" ", "").replace("\t", "")
|
||||||
|
|
||||||
def find(sh, query = None):
|
def find(sh, query = None):
|
||||||
|
|||||||
Reference in New Issue
Block a user