Не открывается книжка Excel на Python Windows

Разрабатываю бота, который должен отсылать в ответ пользователю скриншот определенной области из excel файла.
Сейчас проблема в том, что он не может открыть книжку.

Стек: Python, win32com, PIL, gc.

Код получения скриншота:

import win32com.client as win32
import os
import time
from PIL import ImageGrab
import gc


def screenshot_excel_range(output_file, sheet_name, range_address=None):
    file_path = output_file.replace(".png", ".xlsx")

    print(f"Открываю файл: {file_path}")
    abs_path = os.path.abspath(file_path)
    print(f"Абсолютный путь: {abs_path}")

    if not os.path.exists(file_path):
        print(f"ОШИБКА: Файл не существует: {file_path}")
        return

    try:
        excel = win32.Dispatch('Excel.Application')
        excel.Visible = True 
        excel.DisplayAlerts = False

        print("Excel запущен, пытаюсь открыть книгу...")

        try:
            workbooks = excel.Workbooks
            workbook = workbooks.Open(r"{}".format(abs_path), ReadOnly=1)

            print("Книга успешно открыта")

            try:
                sheet = workbook.Worksheets(sheet_name)
                print(f"Лист '{sheet_name}' найден")

                if range_address is None:
                    print("Используем видимую область")
                    excel.ActiveWindow.View = 1 
                    sheet.Activate()
                    visible_range = excel.ActiveWindow.VisibleRange
                    rng = visible_range
                else:
                    print(f"Используем указанный диапазон: {range_address}")
                    rng = sheet.Range(range_address)

                print("Копирую изображение в буфер обмена...")
                rng.CopyPicture(Appearance=1, Format=2)  
                time.sleep(2.0) 

                print("Получаю изображение из буфера обмена...")
                img = ImageGrab.grabclipboard()
                if img:
                    print(f"Сохраняю изображение в {output_file}")
                    img.save(output_file)
                    print(f"Изображение успешно сохранено в {output_file}")
                else:
                    print("Не удалось получить изображение из буфера обмена.")
            except Exception as e:
                print(f"Ошибка при работе с листом: {e}")
            finally:
                print("Закрываю книгу...")
                workbook.Close(SaveChanges=False)
                print("Книга закрыта")
        except Exception as e:
            print(f"Ошибка при открытии книги: {e}")
        finally:
            print("Закрываю Excel...")
            excel.Quit()
            print("Excel закрыт")
    except Exception as e:
        print(f"Общая ошибка: {e}")
    finally:
        print("Очистка COM-объектов...")
        gc.collect()
        print("Готово")

Код хенделор:

import os
import asyncio
from datetime import datetime, timedelta
from aiogram import Router, types, F
from aiogram.filters import Command
from aiogram.types import InlineKeyboardMarkup
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.utils.formatting import as_list, as_marked_section
from aiogram.types import BotCommand, BotCommandScopeDefault
from aiogram.types import FSInputFile
from assets.get_data import screenshot_excel_range
from assets.extract_excel_data import update_excel_file_for_roof_1, update_excel_file_for_roof_2, \
    update_excel_file_for_fence_1, update_excel_file_for_fence_2
from assets.googleAPI import download_file
from create_bot import bot

router = Router()

# Укажите ID администратора, которому будут отправляться сообщения
# ADMIN_CHAT_ID = 1734794410

# Переменная для хранения количества пользователей за день
user_count_today = 0

sheet_name = "стоимость металической кровли "


class GetLenghtRoof(StatesGroup):
    roof_length = State()


class GetLenghtRamp(StatesGroup):
    ramp_length1 = State()
    ramp_length2 = State()


class GetLenghtFence(StatesGroup):
    fence_lenght = State()


class GetHightFence(StatesGroup):
    fence_height = State()


class GetBeetwenPicket(StatesGroup):
    beetwen_picket = State()


async def setup_bot_commands():
    bot_commands = [
        BotCommand(command="getroofcost", description="рассчитать стоимость кровли"),
        BotCommand(command="getfencecost", description="рассчитать стоимость забора"),
    ]
    await bot.set_my_commands(bot_commands, BotCommandScopeDefault())


def get_detailing() -> types.InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()
    builder.row(types.InlineKeyboardButton(text="Получить детализацию", callback_data="get_detailing"),
                types.InlineKeyboardButton(text="Нет", callback_data="no"))
    return builder


def type_roof_buttons() -> types.InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()
    builder.row(types.InlineKeyboardButton(text="Односкатная", callback_data="single_pitched"),
                types.InlineKeyboardButton(text="Двухскатная", callback_data="double_pitched"))
    return builder.as_markup()


def type_fence_buttons() -> types.InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()
    builder.row(types.InlineKeyboardButton(text="Из профнастила", callback_data="corrugated_sheet"),
                types.InlineKeyboardButton(text="Из Штакетника", callback_data="picket_fence"))
    return builder.as_markup()


@router.message(Command("start"))
async def send_welcome(message: types.Message):
    photo = FSInputFile('assets/photo_start.jpg')
    await message.answer_photo(photo=photo, caption="Привет! Я Матвей, что тебе рассчитать?")


@router.message(Command("getfencecost"))
async def fence_cost(message: types.Message):
    await message.answer("Для начала выберите тип забора:", reply_markup=type_fence_buttons())


@router.message(Command("getroofcost"))
async def roof_cost(message: types.Message):
    await message.answer("Для начала выберите тип кровли:", reply_markup=type_roof_buttons())


@router.callback_query(F.data == 'get_detailing')
async def get_detailing(call: types.CallbackQuery):
    pass


@router.callback_query(F.data.in_(["corrugated_sheet", "picket_fence"]))
async def process_fence_type(call: types.CallbackQuery, state: FSMContext):
    fence_type = call.data
    await state.update_data(fence_type=fence_type)
    await call.message.answer("Укажите длину забора в метрах в формате 00.00 ЧЕРЕЗ ТОЧКУ:\nнапример 12.3")
    await state.set_state(GetLenghtFence.fence_lenght)
    await call.answer()


@router.message(GetLenghtFence.fence_lenght)
async def process_fence_length(message: types.Message, state: FSMContext):
    fence_lenght = message.text.strip()
    if not fence_lenght:
        await message.answer("Пожалуйста, введите корректное значение.")
        return

    await state.update_data(fence_lenght=fence_lenght)
    await message.answer("Укажите высоту забора в метрах В ФОРМАТЕ 00.00 ЧЕРЕЗ ТОЧКУ:")
    await state.set_state(GetHightFence.fence_height)


@router.message(GetHightFence.fence_height)
async def process_fence_height(message: types.Message, state: FSMContext):
    fence_height = message.text.strip()
    if not fence_height:
        await message.answer("Пожалуйста, введите корректное значение.")
        return

    await state.update_data(fence_height=fence_height)
    data = await state.get_data()
    fence_type = data.get("fence_type")

    if fence_type == "picket_fence":
        await message.answer("Укажите расстояние между штакетниками в САНТИМЕТРАХ:")
        await state.set_state(GetBeetwenPicket.beetwen_picket)
    else:
        # Если тип забора - профнастил, сразу считаем и отправляем результат
        await calculate_and_send_fence_results(message, state)


@router.message(GetBeetwenPicket.beetwen_picket)
async def process_between_picket(message: types.Message, state: FSMContext):
    between_picket = message.text.strip()
    if not between_picket:
        await message.answer("Пожалуйста, введите корректное значение.")
        return

    await state.update_data(between_picket=between_picket)
    await calculate_and_send_fence_results(message, state)


@router.callback_query(F.data.in_(["single_pitched", "double_pitched"]))
async def single_pitched(call: types.CallbackQuery, state: FSMContext):
    roof_type = call.data
    await state.update_data(roof_type=roof_type)
    await call.message.answer("Введите длину кровли по коньку в метрах В ФОРМАТЕ 00.00 ЧЕРЕЗ ТОЧКУ:")
    await state.set_state(GetLenghtRoof.roof_length)
    await call.answer()


@router.message(GetLenghtRoof.roof_length)
async def process_get_roof_length(message: types.Message, state: FSMContext):
    roof_length = message.text.strip()
    if not roof_length:
        await message.answer("Пожалуйста, введите корректное значение.")
        return

    await state.update_data(roof_length=roof_length)
    data = await state.get_data()
    roof_type = data.get("roof_type")

    if roof_type == "double_pitched":
        await message.answer("Введите длину первого ската в метрах В ФОРМАТЕ 00.00 ЧЕРЕЗ ТОЧКУ:")
    else:
        await message.answer("Введите длину ската в метрах В ФОРМАТЕ 00.00 ЧЕРЕЗ ТОЧКУ:")
    await state.set_state(GetLenghtRamp.ramp_length1)


@router.message(GetLenghtRamp.ramp_length1)
async def process_ramp_length1(message: types.Message, state: FSMContext):
    ramp_length1 = message.text.strip()
    if not ramp_length1:
        await message.answer("Пожалуйста, введите корректное значение.")
        return

    await state.update_data(ramp_length1=ramp_length1)
    data = await state.get_data()
    roof_type = data.get("roof_type")

    if roof_type == "double_pitched":
        await message.answer("Введите длину второго ската в метрах В ФОРМАТЕ 00.00 ЧЕРЕЗ ТОЧКУ:")
        await state.set_state(GetLenghtRamp.ramp_length2)
    else:
        await calculate_and_send_roof_results(message, state)


@router.message(GetLenghtRamp.ramp_length2)
async def process_ramp_length2(message: types.Message, state: FSMContext):
    ramp_length2 = message.text.strip()
    if not ramp_length2:
        await message.answer("Пожалуйста, введите корректное значение.")
        return

    await state.update_data(ramp_length2=ramp_length2)
    await calculate_and_send_roof_results(message, state)


async def calculate_and_send_roof_results(message: types.Message, state: FSMContext):
    """Функция выполняет расчет и отправку данных для кровли."""
    data = await state.get_data()
    roof_length = data.get("roof_length")
    ramp_length1 = data.get("ramp_length1")
    ramp_length2 = data.get("ramp_length2", None)
    roof_type = data.get("roof_type")

    user_id = message.from_user.id
    global user_count_today
    username = message.from_user.username or "Не указан"

    local_file = None
    output_file = None

    try:
        local_file = download_file(user_id)
        base_name = os.path.splitext(os.path.basename(local_file))[0]
        output_file = os.path.join(os.path.dirname(os.path.abspath(local_file)), base_name + ".png")

        if roof_type == "double_pitched":
            update_excel_file_for_roof_2(local_file, roof_length, ramp_length1, ramp_length2, sheet_name)
        else:
            update_excel_file_for_roof_1(local_file, roof_length, ramp_length1, sheet_name)

        # Используем новую функцию для скриншота
        # Можно настроить диапазон в зависимости от типа крыши или оставить None для автоматического определения
        screenshot_excel_range(output_file, sheet_name, "A15:O31")

        await message.answer_photo(photo=types.FSInputFile(output_file))
        await message.answer(
            "Отлично! Вот твой расчет. В стоимость включены саморезы, коньки и ветровые планки.\nХочешь получить детализацию заказа?",
            reply_markup=get_detailing().as_markup())

        user_count_today += 1

        # Отправка данных администратору
        report_text = f"""? Новый пользователь рассчитал кровлю:
Пользователь: {'@' + username if username else 'Не указан'}
Длина кровли по коньку: {roof_length} м
Первый скат: {ramp_length1} м
{f'Второй скат: {ramp_length2} м' if ramp_length2 else ''}"""

    except Exception as e:
        print(f"Ошибка: {e}")
        await message.answer(
            f"Произошла ошибка при расчете. Пожалуйста, попробуйте еще раз или обратитесь к администратору.")
    finally:
        # На Windows мы не удаляем файлы, так как они могут быть заблокированы Excel
        pass

    await state.clear()


async def calculate_and_send_fence_results(message: types.Message, state: FSMContext):
    """Функция выполняет расчет и отправку данных для забора."""
    data = await state.get_data()
    fence_type = data.get("fence_type")
    fence_lenght = data.get("fence_lenght")
    fence_height = data.get("fence_height")
    between_picket = data.get("between_picket", None)

    user_id = message.from_user.id
    global user_count_today
    username = message.from_user.username or "Не указан"

    local_file = None
    output_file = None

    try:
        local_file = download_file(user_id)
        base_name = os.path.splitext(os.path.basename(local_file))[0]
        output_file = os.path.join(os.path.dirname(os.path.abspath(local_file)), base_name + ".png")

        if fence_type == "picket_fence":
            update_excel_file_for_fence_2(local_file, fence_lenght, fence_height, between_picket, sheet_name)
        else:
            update_excel_file_for_fence_1(local_file, fence_lenght, fence_height, sheet_name)

        # Используем функцию для скриншота с соответствующим диапазоном для забора
        screenshot_excel_range(output_file, sheet_name, "A15:O31")

        await message.answer_photo(photo=types.FSInputFile(output_file))
        await message.answer(
            "Отлично! Вот твой расчет стоимости забора.\nХочешь получить детализацию заказа?",
            reply_markup=get_detailing().as_markup())

        user_count_today += 1

        # Отправка данных администратору
        report_text = f"""? Новый пользователь рассчитал забор:
Пользователь: {'@' + username if username else 'Не указан'}
Тип забора: {"Штакетник" if fence_type == "picket_fence" else "Профнастил"}
Длина забора: {fence_lenght} м
Высота забора: {fence_height} м
{f'Расстояние между штакетниками: {between_picket} см' if between_picket else ''}"""

    except Exception as e:
        print(f"Ошибка: {e}")
        await message.answer(
            f"Произошла ошибка при расчете. Пожалуйста, попробуйте еще раз или обратитесь к администратору.")
    finally:
        # На Windows мы не удаляем файлы, так как они могут быть заблокированы Excel
        pass

    await state.clear()


async def send_daily_report():
    """Функция отправляет отчет администратору в 21:00 по МСК и сбрасывает счетчик."""
    global user_count_today
    while True:
        now = datetime.now()
        next_run = datetime(now.year, now.month, now.day, 21, 0, 0)  # 21:00
        if now > next_run:
            next_run += timedelta(days=1)

        wait_time = (next_run - now).total_seconds()
        await asyncio.sleep(wait_time)

        # Отправка отчета администратору
        report_message = f"? Количество пользователей за сегодня: {user_count_today}"
        # await router.bot.send_message(ADMIN_CHAT_ID, report_message)

        # Сброс счетчика
        user_count_today = 0


# Запуск задачи по отправке отчета
async def on_startup():
    asyncio.create_task(send_daily_report())

Код подключения и скачивания файла с Google Таблиц:

import io
import uuid
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload
from decouple import config

SCOPES = ['https://www.googleapis.com/auth/spreadsheets',
          'https://www.googleapis.com/auth/drive']


def download_file(user_id):
    creds = Credentials.from_service_account_file(config('CREDENTIALS_FILE'), scopes=SCOPES)
    service = build('drive', 'v3', credentials=creds)

    # If this is a native Google Sheet you want to download as Excel: # You'll need to define this check
    request = service.files().export_media(
            fileId=config('CREDENTIALS_ID'),  # Use a variable name that reflects what this ID represents
            mimeType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )
    # If this is already an Excel file in Drive:

    fh = io.BytesIO()
    downloader = MediaIoBaseDownload(fh, request)
    done = False
    while not done:
        status, done = downloader.next_chunk()

    fh.seek(0)
    unique_filename = f"таблица_{user_id}_{uuid.uuid4().hex}.xlsx"
    with open(unique_filename, "wb") as f:
        f.write(fh.read())
    return unique_filename

Код вставки данных в скачанную таблицу:

import os, gc
from openpyxl import load_workbook


def update_excel_file_for_roof_1(file, roof_lenght, ramp_lenght1, sheet_name):
    filepath = os.path.abspath(file)
    workbook = load_workbook(filepath)
    try:
        sheet = workbook[sheet_name]
        sheet['C4'].value = roof_lenght
        sheet['C5'].value = ramp_lenght1
        workbook.save(filepath)
    finally:
        if hasattr(workbook, 'close'):
            workbook.close()
        gc.collect()

def update_excel_file_for_fence_1(file, roof_lenght, ramp_lenght1, sheet_name):
    filepath = os.path.abspath(file)
    workbook = load_workbook(filepath)
    try:
        sheet = workbook[sheet_name]
        sheet['B3'].value = roof_lenght
        sheet['C3'].value = ramp_lenght1
        workbook.save(filepath)
    finally:
        if hasattr(workbook, 'close'):
            workbook.close()
        gc.collect()

def update_excel_file_for_fence_2(file, roof_lenght, ramp_lenght1, sheet_name):
    filepath = os.path.abspath(file)
    workbook = load_workbook(filepath)
    try:
        sheet = workbook[sheet_name]
        sheet['B2'].value = roof_lenght
        sheet['C2'].value = ramp_lenght1
        sheet['D2'].value = ramp_lenght1
        workbook.save(filepath)
    finally:
        if hasattr(workbook, 'close'):
            workbook.close()
        gc.collect()


def update_excel_file_for_roof_2(file, roof_lenght, ramp_lenght1, ramp_lenght2, sheet_name):
    filepath = os.path.abspath(file)
    workbook = load_workbook(filepath)
    try:
        sheet = workbook[sheet_name]
        sheet['F4'].value = roof_lenght
        sheet['F5'].value = ramp_lenght1
        sheet['H5'].value = ramp_lenght2
        workbook.save(filepath)
    finally:
        if hasattr(workbook, 'close'):
            workbook.close()
        gc.collect()

логи:

Открываю файл: C:\Users\Administrator\Desktop\Bot_Count_Roof_Cost\таблица_998992278_a951318df67b429faec3bf26486b9416.xlsx
Абсолютный путь: C:\Users\Administrator\Desktop\Bot_Count_Roof_Cost\таблица_998992278_a951318df67b429faec3bf26486b9416.xlsx
Excel запущен, пытаюсь открыть книгу...
Ошибка при открытии книги: (-2147352567, 'Exception occurred.', (0, 'Microsoft Excel', 'Open method of Workbooks class failed', 'xlmain11.chm', 0, -2146827284), None)
Закрываю Excel...
Excel закрыт
Очистка COM-объектов...
Готово

Может кто-то с таким сталкивался? Как исправить? Функционал отправки именно скриншота обязателен.


Ответы (1 шт):

Автор решения: S. Nick

Вы не показали как и откуда вы запускаете свой код.
Если вы сделаете это правильно, то у вас все получится.

q1609135.py

import win32com.client as win32
import os
import time
from PIL import ImageGrab
import gc


def screenshot_excel_range(output_file, sheet_name, range_address=None):
    file_path = output_file.replace(".png", ".xlsx")

    print(f"Открываю файл: {file_path}")
    abs_path = os.path.abspath(file_path)
    print(f"Абсолютный путь: {abs_path}")

    if not os.path.exists(file_path):
        print(f"ОШИБКА: Файл не существует: {file_path}")
        return

    try:
        excel = win32.Dispatch('Excel.Application')
        excel.Visible = True 
        excel.DisplayAlerts = False

        print("Excel запущен, пытаюсь открыть книгу...")

        try:
            workbooks = excel.Workbooks
            workbook = workbooks.Open(r"{}".format(abs_path), ReadOnly=1)

            print("Книга успешно открыта")

            try:
                sheet = workbook.Worksheets(sheet_name)
                print(f"Лист '{sheet_name}' найден")

                if range_address is None:
                    print("Используем видимую область")
                    excel.ActiveWindow.View = 1 
                    sheet.Activate()
                    visible_range = excel.ActiveWindow.VisibleRange
                    rng = visible_range
                else:
                    print(f"Используем указанный диапазон: {range_address}")
                    rng = sheet.Range(range_address)

                print("Копирую изображение в буфер обмена...")
                rng.CopyPicture(Appearance=1, Format=2)  
                time.sleep(2.0) 

                print("Получаю изображение из буфера обмена...")
                img = ImageGrab.grabclipboard()
                if img:
                    print(f"Сохраняю изображение в {output_file}")
                    img.save(output_file)
                    print(f"Изображение успешно сохранено в {output_file}")
                else:
                    print("Не удалось получить изображение из буфера обмена.")
            except Exception as e:
                print(f"Ошибка при работе с листом: {e}")
            finally:
                print("Закрываю книгу...")
                workbook.Close(SaveChanges=False)
                print("Книга закрыта")
        except Exception as e:
            print(f"Ошибка при открытии книги: {e}")
        finally:
            print("Закрываю Excel...")
            excel.Quit()
            print("Excel закрыт")
    except Exception as e:
        print(f"Общая ошибка: {e}")
    finally:
        print("Очистка COM-объектов...")
#        gc.collect()
        print("Готово")
        
screenshot_excel_range(output_file="Excel_1.png", sheet_name="Sheet1")

введите сюда описание изображения

→ Ссылка