work copy
This commit is contained in:
@@ -90,7 +90,7 @@ def extract_last_name(name_str: str) -> str or None:
|
|||||||
|
|
||||||
|
|
||||||
# --- Шаг 0: Константы ---
|
# --- Шаг 0: Константы ---
|
||||||
POSITIVE_KEYWORDS = ['зал', 'ауд', 'каб', 'корп', 'кор.']
|
POSITIVE_KEYWORDS = ['зал', 'ауд', 'каб', 'корп', 'кор.', "ЛК"]
|
||||||
NEGATIVE_KEYWORDS = ['доц', 'проф', 'асс', 'лек', 'пр']
|
NEGATIVE_KEYWORDS = ['доц', 'проф', 'асс', 'лек', 'пр']
|
||||||
|
|
||||||
# Транслитерация для унификации
|
# Транслитерация для унификации
|
||||||
@@ -106,7 +106,7 @@ def is_room_number(s: str) -> bool:
|
|||||||
|
|
||||||
if ',' in s:
|
if ',' in s:
|
||||||
return False # 2. Проверка на запятые (даты)
|
return False # 2. Проверка на запятые (даты)
|
||||||
|
|
||||||
# 3. Проверка на "очевидный мусор"
|
# 3. Проверка на "очевидный мусор"
|
||||||
first_word = s.strip().lower().split()[0]
|
first_word = s.strip().lower().split()[0]
|
||||||
if first_word in NEGATIVE_KEYWORDS:
|
if first_word in NEGATIVE_KEYWORDS:
|
||||||
@@ -148,3 +148,51 @@ def is_room_number(s: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlsplit, urlunsplit, quote
|
||||||
|
|
||||||
|
def download_file_from_url(url, output_filename):
|
||||||
|
"""
|
||||||
|
Скачивает файл по URL со спецсимволами и пробелами, сохраняя его под указанным именем.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): Исходный URL, который может содержать пробелы и кириллицу.
|
||||||
|
output_filename (str): Имя файла для сохранения (например, 'calc.xls').
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# --- Шаг 1: Правильное кодирование URL ---
|
||||||
|
# Разбираем URL на части: ('https', 'www.vstu.ru', '/path/to file.xls', '', '')
|
||||||
|
parts = urlsplit(url)
|
||||||
|
|
||||||
|
# Кодируем только путь, оставляя слэши '/' безопасными
|
||||||
|
# Это превратит ' ' в '%20', 'В' в '%D0%92' и т.д.
|
||||||
|
encoded_path = quote(parts.path, safe='/-_')
|
||||||
|
|
||||||
|
# Собираем URL обратно из частей с уже закодированным путем
|
||||||
|
encoded_url = urlunsplit((parts.scheme, parts.netloc, encoded_path, parts.query, parts.fragment))
|
||||||
|
|
||||||
|
|
||||||
|
# --- Шаг 2: Скачивание файла ---
|
||||||
|
response = requests.get(encoded_url, stream=True)
|
||||||
|
|
||||||
|
# Проверяем, успешен ли запрос (код 200 OK)
|
||||||
|
# Если сервер вернет ошибку (404, 500 и т.д.), здесь возникнет исключение
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# --- Шаг 3: Сохранение файла ---
|
||||||
|
# Открываем файл для записи в бинарном режиме ('wb')
|
||||||
|
# Использование 'with' гарантирует, что файл будет закрыт автоматически
|
||||||
|
with open(output_filename, 'wb') as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
print(f"✅ Файл успешно скачан и сохранен как '{output_filename}'")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"❌ Ошибка скачивания: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Произошла непредвиденная ошибка: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
28
checkenv.py
Normal file
28
checkenv.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
print("--- ДИАГНОСТИКА ОКРУЖЕНИЯ ---")
|
||||||
|
print(f"Исполняемый файл Python: {sys.executable}")
|
||||||
|
print("-" * 20)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import xlwt
|
||||||
|
print("✅ Библиотека 'xlwt' успешно импортирована.")
|
||||||
|
print(f" Версия: {xlwt.__VERSION__}")
|
||||||
|
# Попробуем найти, где она лежит
|
||||||
|
print(f" Расположение: {os.path.dirname(xlwt.__file__)}")
|
||||||
|
except ImportError:
|
||||||
|
print("❌ ОШИБКА: Библиотека 'xlwt' НЕ НАЙДЕНА в этом окружении.")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pandas
|
||||||
|
print("✅ Библиотека 'pandas' успешно импортирована.")
|
||||||
|
print(f" Версия: {pandas.__version__}")
|
||||||
|
# Попробуем найти, где она лежит
|
||||||
|
print(f" Расположение: {os.path.dirname(pandas.__file__)}")
|
||||||
|
except ImportError:
|
||||||
|
print("❌ ОШИБКА: Библиотека 'pandas' НЕ НАЙДЕНА в этом окружении.")
|
||||||
|
|
||||||
|
|
||||||
|
print("-" * 20)
|
||||||
BIN
excels/mag1757588817[C2003].xlsx.zip
Normal file
BIN
excels/mag1757588817[C2003].xlsx.zip
Normal file
Binary file not shown.
1268
groups.json
1268
groups.json
File diff suppressed because it is too large
Load Diff
141
main.py
141
main.py
@@ -1,21 +1,146 @@
|
|||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
import pandas as pd
|
||||||
|
import xlwt
|
||||||
|
|
||||||
import xlrd
|
import xlrd
|
||||||
|
import requests
|
||||||
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import aigenerated
|
||||||
import parser
|
import parser
|
||||||
import utils
|
import utils
|
||||||
import json
|
import json
|
||||||
# Общее правило проекта, сначала в координатах идёт ROW а потом COL, нумерация с нуля
|
# Общее правило проекта, сначала в координатах идёт ROW а потом COL, нумерация с нуля
|
||||||
|
|
||||||
|
FACULTETS = [
|
||||||
|
"asp", "mag", "fastiv", "fat", "ftkm", "ftpp", "feu", "fevt", "htf", "vkf", "mmf", "fpik"
|
||||||
|
]
|
||||||
|
BASE_URL = "https://www.vstu.ru/"
|
||||||
|
RASP_PREFIX = "https://www.vstu.ru/student/raspisaniya/zanyatiy/index.php?dep="
|
||||||
|
|
||||||
book = xlrd.open_workbook("ОН_ФЭВТ_2 курс.xls", formatting_info=True)
|
session = requests.Session()
|
||||||
print("The number of worksheets is {0}".format(book.nsheets))
|
session.headers = CaseInsensitiveDict(
|
||||||
print("Worksheet name(s): {0}".format(book.sheet_names()))
|
{
|
||||||
sh = book.sheet_by_index(0)
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||||
|
"Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
||||||
|
"Accept-Encoding": "gzip, deflate",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Referer": "http://dump.vstu.ru/",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"Priority": "u=0, i",
|
||||||
|
"Pragma": "no-cache",
|
||||||
|
"Cache-Control": "no-cach",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
EXCEL_LINKS = {}
|
||||||
|
filestime = str(round(time.time()))
|
||||||
|
for facultet in FACULTETS:
|
||||||
|
url = RASP_PREFIX + facultet
|
||||||
|
r = session.get(url)
|
||||||
|
print(f"GET {url}")
|
||||||
|
soup = BeautifulSoup(r.text, 'html.parser')
|
||||||
|
excel_pattern = re.compile(r'\.xlsx?$')
|
||||||
|
|
||||||
|
# Ищем все теги <a>, у которых атрибут href соответствует нашему паттерну
|
||||||
|
excel_tags = soup.find_all('a', href=excel_pattern)
|
||||||
|
excel_links = [tag.get('href') for tag in excel_tags]
|
||||||
|
|
||||||
|
# Предположим, вы уже получили excel_links из одного из методов выше
|
||||||
|
# excel_links = ['../../../upload/raspisanie/z/ОН_ХТФ_1 курс.xlsx', ...]
|
||||||
|
|
||||||
|
absolute_links = [urljoin(BASE_URL, relative_link) for relative_link in excel_links]
|
||||||
|
|
||||||
|
if facultet not in EXCEL_LINKS.keys():
|
||||||
|
EXCEL_LINKS[facultet] = set()
|
||||||
|
|
||||||
|
for excel_url in absolute_links:
|
||||||
|
EXCEL_LINKS[facultet].add(excel_url)
|
||||||
|
print(f"+url {excel_url}")
|
||||||
|
|
||||||
|
|
||||||
prs = parser.Parser(sh)
|
result = {}
|
||||||
prs.parse()
|
faileds = []
|
||||||
|
counter = 0
|
||||||
|
for facultet in FACULTETS:
|
||||||
|
counter += 1000
|
||||||
|
print(f"\n\n-- Факультет '{facultet}' --")
|
||||||
|
facultet_urls = EXCEL_LINKS[facultet]
|
||||||
|
for excel_url in facultet_urls:
|
||||||
|
counter += 1
|
||||||
|
print(f"\n\n-- Ссылка --")
|
||||||
|
print(f"{excel_url}")
|
||||||
|
xlsx = excel_url.endswith(".xlsx")
|
||||||
|
|
||||||
json.dump(prs.groups, open('groups.json', 'w'), indent=2, ensure_ascii=False)
|
try:
|
||||||
print("Saved to groups.json")
|
filename = "excels/" + facultet + filestime + f"[C{counter}]" + ".xls"
|
||||||
|
|
||||||
|
# Download a file
|
||||||
|
if not xlsx:
|
||||||
|
aigenerated.download_file_from_url(excel_url, filename)
|
||||||
|
else:
|
||||||
|
aigenerated.download_file_from_url(excel_url, filename+"x")
|
||||||
|
|
||||||
|
excel_file = pd.ExcelFile(filename + "x")
|
||||||
|
|
||||||
|
# Создаем "писателя" для формата .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
|
||||||
|
prs = parser.Parser(sh)
|
||||||
|
prs.parse()
|
||||||
|
for group_name in prs.groups.keys():
|
||||||
|
if group_name in result.keys():
|
||||||
|
print(f" -- WTF -- Doubled groups -- name: {group_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
gr = result[group_name] = prs.groups[group_name]
|
||||||
|
gr['facultet'] = facultet
|
||||||
|
gr['data_source'] = excel_url.split("/")[-1] + " SHEET: " + str(sh.name)
|
||||||
|
gr['parser_debug'] = {
|
||||||
|
"C_COUNTER": counter,
|
||||||
|
"timestime": filestime,
|
||||||
|
"excel_url": excel_url,
|
||||||
|
"sheet": sh.name,
|
||||||
|
"filename": filename
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while {excel_url}")
|
||||||
|
print(e)
|
||||||
|
traceback.print_exc()
|
||||||
|
faileds.append({
|
||||||
|
"ex": e,
|
||||||
|
"fac": facultet,
|
||||||
|
"url": excel_url
|
||||||
|
})
|
||||||
|
|
||||||
|
json.dump(result, open('result.json', 'w'), indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print("Faileds:")
|
||||||
|
print(faileds)
|
||||||
|
|
||||||
|
print("Saved to result.json")
|
||||||
|
|||||||
53
parser.py
53
parser.py
@@ -6,6 +6,11 @@ import aigenerated
|
|||||||
from coord import Coord, Merged
|
from coord import Coord, Merged
|
||||||
import utils
|
import utils
|
||||||
|
|
||||||
|
LOGGING = True
|
||||||
|
|
||||||
|
def pprint(*args, **kwargs):
|
||||||
|
if LOGGING:
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
@@ -14,27 +19,32 @@ class Parser:
|
|||||||
self.groups = {}
|
self.groups = {}
|
||||||
self.teachers = set()
|
self.teachers = set()
|
||||||
self.places = set()
|
self.places = set()
|
||||||
print("Parser created for '{0}': size: {1}x{2}".format(self.sh.name, self.sh.nrows, self.sh.ncols))
|
pprint("Parser created for '{0}': size: {1}x{2}".format(self.sh.name, self.sh.nrows, self.sh.ncols))
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
monday = utils.find(self.sh, "ПОНЕДЕЛЬНИК")
|
monday = utils.find(self.sh, "ПОНЕДЕЛЬНИК")
|
||||||
|
if monday is None:
|
||||||
|
print(" -- Failed parse! -- ")
|
||||||
|
print("ПОНЕДЕЛЬНИК НЕ НАЙДЕН!")
|
||||||
|
return
|
||||||
|
|
||||||
head_rx = monday[0] - 1 # выше первого понидельника
|
head_rx = monday[0] - 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.sh.row(head_rx) # get all ROW (months, groups)
|
||||||
print(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.sh, head, monday, head_rx) # parse groups to self.groups
|
||||||
print(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)}')
|
||||||
|
|
||||||
print("\n\n\n")
|
pprint("\n\n\n")
|
||||||
|
|
||||||
for group in self.groups.values():
|
for group in self.groups.values():
|
||||||
print("\nSTART OF PROCESS GROUP\n")
|
pprint("\nSTART OF PROCESS GROUP\n")
|
||||||
self.process_group(group, monday)
|
self.process_group(group, monday)
|
||||||
print("\nEND OF PROCESS GROUP\n")
|
pprint("\nEND OF PROCESS GROUP\n")
|
||||||
|
|
||||||
print(self.teachers)
|
pprint(self.teachers)
|
||||||
|
|
||||||
def parse_potokoviy(self, merged: Merged):
|
def parse_potokoviy(self, merged: Merged):
|
||||||
speaker = None
|
speaker = None
|
||||||
@@ -48,16 +58,16 @@ class Parser:
|
|||||||
# location
|
# location
|
||||||
location = merged.high.shift(down=1).cell(self.sh).value
|
location = merged.high.shift(down=1).cell(self.sh).value
|
||||||
|
|
||||||
return {"loc": location, "leader": speaker, "name": merged.cell(self.sh).value}
|
return {"loc": str(location), "leader": str(speaker), "name": str(merged.cell(self.sh).value)}
|
||||||
|
|
||||||
def process_group(self, group, monday):
|
def process_group(self, group, monday):
|
||||||
"""
|
"""
|
||||||
Обработать группы, выполняется для каждой группы, после того как они распарены (parse_groups)
|
Обработать группы, выполняется для каждой группы, после того как они распарены (parse_groups)
|
||||||
group = {'name': 'ИВТ-260', 'position': [5, 6], 'position_human': 'G6:J6'}
|
group = {'name': 'ИВТ-260', 'position': [5, 6], 'position_human': 'G6:J6'}
|
||||||
"""
|
"""
|
||||||
print(f"process_group group={group}")
|
pprint(f"process_group group={group}")
|
||||||
group_name = group['name']
|
group_name = group['name']
|
||||||
print(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.sh.nrows: # maybe условие чтобы не уйти ниже чем есть строк
|
||||||
@@ -71,7 +81,7 @@ class Parser:
|
|||||||
cv = merged_cell.value
|
cv = merged_cell.value
|
||||||
# В конце (12 пара:>) название группы, можно использовать как якорь
|
# В конце (12 пара:>) название группы, можно использовать как якорь
|
||||||
if utils.unspace(cv) == group_name:
|
if utils.unspace(cv) == group_name:
|
||||||
print("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 = utils.get_merged_coord(self.sh, weekday_pos)
|
||||||
@@ -83,7 +93,7 @@ class Parser:
|
|||||||
if weekday == "":
|
if weekday == "":
|
||||||
if weeknum == 1:
|
if weeknum == 1:
|
||||||
weeknum += 1
|
weeknum += 1
|
||||||
print("------")
|
pprint("------")
|
||||||
skip = 1
|
skip = 1
|
||||||
row += 1
|
row += 1
|
||||||
else:
|
else:
|
||||||
@@ -104,17 +114,20 @@ class Parser:
|
|||||||
dispname = "<no lesson>"
|
dispname = "<no lesson>"
|
||||||
|
|
||||||
if not is_empty_lesson:
|
if not is_empty_lesson:
|
||||||
|
may_prepod = merged.low.shift(down=2)
|
||||||
|
if utils.has_no_bottom_border(self.sh, may_prepod):
|
||||||
|
next = 6
|
||||||
|
is_2pair = True
|
||||||
|
|
||||||
if is_wide_maybe_potokoviy:
|
if is_wide_maybe_potokoviy:
|
||||||
ret = self.parse_potokoviy(merged)
|
ret = self.parse_potokoviy(merged)
|
||||||
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))
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
may_prepod = merged.low.shift(down=2)
|
|
||||||
if utils.border_bottom(self.sh, may_prepod) == 0 and utils.border_top(self.sh, may_prepod.shift(down=1)) == 0:
|
|
||||||
next = 6
|
|
||||||
is_2pair = True
|
|
||||||
|
|
||||||
if (is_solid):
|
if (is_solid):
|
||||||
parsed_discipline_name = cv
|
parsed_discipline_name = cv
|
||||||
|
|
||||||
@@ -128,7 +141,7 @@ class Parser:
|
|||||||
if parsed_leader: dispname += f" [{parsed_leader}]"
|
if parsed_leader: dispname += f" [{parsed_leader}]"
|
||||||
if parsed_location: dispname += f" [{parsed_location}]"
|
if parsed_location: dispname += f" [{parsed_location}]"
|
||||||
dispname = dispname.replace("\n", "\\n")
|
dispname = dispname.replace("\n", "\\n")
|
||||||
print(f"[{group_name}] row={row}; {pos} {pos_right} {pair} {weekday}: {'[ПОТОКОВЫЙ] ' if is_wide_maybe_potokoviy else ''}{dispname} {parsed_uncotigorized}")
|
pprint(f"[{group_name}] row={row}; {pos} {pos_right} {pair} {weekday}: {'[ПОТОКОВЫЙ] ' if is_wide_maybe_potokoviy else ''}{dispname} {parsed_uncotigorized}")
|
||||||
|
|
||||||
# пытаемся из некотегорезированных данных выцепить место и лидера (препода)
|
# пытаемся из некотегорезированных данных выцепить место и лидера (препода)
|
||||||
prepods = set()
|
prepods = set()
|
||||||
@@ -187,14 +200,14 @@ def parse_groups(sh, head, monday, head_rx):
|
|||||||
i = 0
|
i = 0
|
||||||
while i < len(head):
|
while i < len(head):
|
||||||
x = head[i]
|
x = head[i]
|
||||||
print(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 = utils.get_merged_coord(sh, Coord(head_rx, i))
|
||||||
if i > monday[1] + 1:
|
if i > monday[1] + 1:
|
||||||
if merged is None or x.value == "":
|
if merged is None or x.value == "":
|
||||||
break
|
break
|
||||||
|
|
||||||
if merged.width() != 4:
|
if merged.width() != 4:
|
||||||
print(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.value)
|
||||||
|
|||||||
87265
result.json
Normal file
87265
result.json
Normal file
File diff suppressed because it is too large
Load Diff
3
utils.py
3
utils.py
@@ -22,6 +22,9 @@ def border_bottom(sh, cell):
|
|||||||
def border_top(sh, cell):
|
def border_top(sh, cell):
|
||||||
return border(sh, cell).top_line_style
|
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):
|
def parse_all_dirt(sh, min_pos, right, down):
|
||||||
RET = set()
|
RET = set()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user