import argparse import asyncio import pandas as pd import asyncpg import re import os def clean_name(name: str) -> str: if not isinstance(name, str): return "" name = re.sub(r'\s+[СC]$', '', name.strip()) name = re.sub(r'\s*\d+\s*$', '', name) return name.strip() def parse_excel_raw(file_path: str): excel_file = pd.ExcelFile(file_path) resident_hours = {} room_hours = {} for sheet_name in excel_file.sheet_names: df = pd.read_excel(file_path, sheet_name=sheet_name) # --- Парсинг жильцов --- if 'ФИО' in df.columns and 'Часы' in df.columns: for _, row in df.iterrows(): fio = clean_name(str(row['ФИО'])) if not fio or str(fio).lower() == 'nan': continue try: hours_raw = str(row['Часы']).split('(')[0].strip() amount = int(float(hours_raw)) except ValueError: continue if fio in resident_hours: resident_hours[fio] += amount else: resident_hours[fio] = amount # --- Парсинг комнат --- elif df.iloc[0].astype(str).str.contains('Комната', case=False, na=False).any(): header_idx = df[df.apply(lambda r: r.astype(str).str.contains('Комната', case=False).any(), axis=1)].index[0] df.columns = df.iloc[header_idx] df = df.iloc[header_idx + 1:].reset_index(drop=True) for _, row in df.iterrows(): room_str = str(row['Комната']) if not room_str or room_str.lower() == 'nan': continue room_match = re.search(r'\b(\d{3,4})\b', room_str) if not room_match: continue room_number = int(room_match.group(1)) try: punishment_raw = str(row['Наказание']).split('(')[0].replace('часов', '').replace(',', ';').strip() amounts = [int(x.strip()) for x in re.findall(r'\d+', punishment_raw)] amount = sum(amounts) if amounts else 0 except (ValueError, AttributeError): continue if amount <= 0: continue if room_number in room_hours: room_hours[room_number] += amount else: room_hours[room_number] = amount return resident_hours, room_hours async def run_parse(database_url: str, file_path: str): raw_resident_hours, raw_room_hours = parse_excel_raw(file_path) async with asyncpg.create_pool(database_url) as pool: async with pool.acquire() as conn: resident_rows = await conn.fetch("SELECT real_name FROM residents WHERE real_name IS NOT NULL;") db_names = [row['real_name'] for row in resident_rows] room_rows = await conn.fetch("SELECT number FROM rooms;") db_rooms = {row['number'] for row in room_rows} # --- Сопоставление жильцов --- db_names_lower = {name.lower(): name for name in db_names} matched_residents = {} unmatched_residents = {} for raw_fio, amount in raw_resident_hours.items(): fio_lower = raw_fio.lower() matched_name = None if fio_lower in db_names_lower: matched_name = db_names_lower[fio_lower] else: parts = fio_lower.split() if len(parts) >= 2: search_key = f"{parts[0]} {parts[1]}" if search_key in db_names_lower: matched_name = db_names_lower[search_key] if not matched_name: for db_lower, db_orig in db_names_lower.items(): if fio_lower in db_lower or db_lower in fio_lower: matched_name = db_orig break if matched_name: matched_residents[matched_name] = matched_residents.get(matched_name, 0) + amount else: unmatched_residents[raw_fio] = unmatched_residents.get(raw_fio, 0) + amount # --- Сопоставление комнат --- matched_rooms = {} unmatched_rooms = {} for room_num, amount in raw_room_hours.items(): if room_num in db_rooms: matched_rooms[room_num] = matched_rooms.get(room_num, 0) + amount else: unmatched_rooms[room_num] = unmatched_rooms.get(room_num, 0) + amount # --- Запись в файлы --- '''with open("parsed_residents.txt", "w", encoding="utf-8") as f: for name, amount in sorted(matched_residents.items()): f.write(f"{name} - {amount}\n")''' with open("parsed_rooms.txt", "w", encoding="utf-8") as f: for room, amount in sorted(matched_rooms.items()): f.write(f"{room} - {amount}\n") '''if unmatched_residents: with open("unmatched_residents.txt", "w", encoding="utf-8") as f: for name, amount in sorted(unmatched_residents.items()): f.write(f"{name} - {amount}\n")''' if unmatched_rooms: with open("unmatched_rooms.txt", "w", encoding="utf-8") as f: for room, amount in sorted(unmatched_rooms.items()): f.write(f"{room} - {amount}\n") async def run_load(database_url: str, admin_id: int): if not os.path.exists("parsed_residents.txt") or not os.path.exists("parsed_rooms.txt"): raise FileNotFoundError("Файлы parsed_residents.txt или parsed_rooms.txt не найдены. Сначала запустите parse.") resident_transactions = [] with open("parsed_residents.txt", "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split(" - ") if len(parts) == 2: resident_transactions.append((parts[0].strip(), int(parts[1].strip()))) room_transactions = [] with open("parsed_rooms.txt", "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split(" - ") if len(parts) == 2: room_transactions.append((int(parts[0].strip()), int(parts[1].strip()))) async with asyncpg.create_pool(database_url) as pool: async with pool.acquire() as conn: # Маппинги res_rows = await conn.fetch("SELECT id, real_name FROM residents WHERE real_name IS NOT NULL;") name_to_id = {row['real_name']: row['id'] for row in res_rows} rm_rows = await conn.fetch("SELECT id, number FROM rooms;") num_to_id = {row['number']: row['id'] for row in rm_rows} res_tx_params = [] res_update_params = [] for name, amount in resident_transactions: res_id = name_to_id.get(name) if res_id: res_tx_params.append((res_id, "increase", amount, admin_id, "Инициализация существующих часов")) res_update_params.append((amount, res_id)) rm_tx_params = [] rm_update_params = [] for room_num, amount in room_transactions: rm_id = num_to_id.get(room_num) if rm_id: rm_tx_params.append((rm_id, "increase", amount, admin_id, "Инициализация существующих часов")) rm_update_params.append((amount, rm_id)) async with conn.transaction(): # Транзакции резидентов if res_tx_params: await conn.executemany( """ INSERT INTO hours_transactions (resident_id, transaction_type, amount, admin_id, remark, created_at) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP); """, res_tx_params ) # Обновление баланса резидентов await conn.executemany( """ UPDATE residents SET active_hours = active_hours + $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2; """, res_update_params ) # Транзакции комнат if rm_tx_params: await conn.executemany( """ INSERT INTO room_hours_transactions (room_id, transaction_type, amount, admin_id, remark, created_at) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP); """, rm_tx_params ) # Обновление баланса комнат await conn.executemany( """ UPDATE rooms SET active_hours = active_hours + $1 WHERE id = $2; """, rm_update_params ) async def main(): parser = argparse.ArgumentParser() parser.add_argument("command", choices=["parse", "load"]) parser.add_argument("--db-url", required=True) parser.add_argument("--file", required=False) parser.add_argument("--admin-id", type=int, default=2047958833) args = parser.parse_args() if args.command == "parse": if not args.file: parser.error("Для команды parse требуется указать --file") await run_parse(args.db_url, args.file) elif args.command == "load": await run_load(args.db_url, args.admin_id) if __name__ == "__main__": asyncio.run(main())