Ознакомься с кодом, ничего менять не нужно
from lxml import etree
import pandas as pd
import re
import os
import openpyxl

os.chdir(r"C:\VSProjects\PythonProjects\parse-report-signal")


def find_signals(log, signals_map, logs_map, visited_logs):
    """Рекурсивно находит сигналы, связанные с указанным log."""
    related_signals = set()

    # Проверка существования атрибута vsigid1_signalitem_signame
    vsigid_signame = log.get("{http://bmi.ch.abb.com/kbs/rr}vsigid1_signalitem_signame")
    if vsigid_signame and vsigid_signame in signals_map:
        related_signals.add(signals_map[vsigid_signame])

    # Проверка существования атрибутов csigid?_signalitem_signame
    for attr in log.attrib:
        if re.match(
            r"{http://bmi.ch.abb.com/kbs/rr}csigid[1-3]?_signalitem_signame", attr
        ):  # csigid1_signalitem_signame или csigid2_signalitem_signame
            signal_name = log.get(attr)
            if signal_name and signal_name in signals_map:
                related_signals.add(signals_map[signal_name])

    # Проверка дочернего тега PLCREPITEMEXT
    plcrepitemext = log.find("PLCREPITEMEXT")
    if plcrepitemext is not None:
        plcsigid_signalitem_signame = plcrepitemext.get(
            "{http://bmi.ch.abb.com/kbs/rr}plcsigid_signalitem_signame"
        )
        if plcsigid_signalitem_signame and plcsigid_signalitem_signame in signals_map:
            related_signals.add(signals_map[plcsigid_signalitem_signame])

    # Проверка атрибутов plcricode?_repitem_ritext для поиска других log
    for attr in log.attrib:
        if re.match(
            r"{http://bmi.ch.abb.com/kbs/rr}plcricode[2-3]?_repitem_ritext", attr
        ):  # plcricode2_repitem_ritext и т.д.
            related_log_name = log.get(attr)
            if related_log_name and related_log_name not in visited_logs:
                # Помечаем лог как посещенный
                visited_logs.add(related_log_name)
                # Получаем объект log по имени
                related_log = logs_map.get(related_log_name)
                if related_log is not None:
                    # Осуществляем рекурсивный поиск сигналов
                    related_signals.update(
                        find_signals(related_log, signals_map, logs_map, visited_logs)
                    )

    # Используем регулярные выражения для поиска атрибутов vsigid?_repitem_ritext
    for attr in log.attrib:
        if re.match(
            r"{http://bmi.ch.abb.com/kbs/rr}vsigid([1-9]|10)_repitem_ritext", attr
        ):  # vsigid1_repitem_ritext до vsigid10_repitem_ritext
            related_log_name = log.get(attr)
            if related_log_name and related_log_name not in visited_logs:
                # Помечаем лог как посещенный
                visited_logs.add(related_log_name)
                # Получаем объект log по имени
                related_log = logs_map.get(related_log_name)
                if related_log is not None:
                    # Осуществляем рекурсивный поиск сигналов
                    related_signals.update(
                        find_signals(related_log, signals_map, logs_map, visited_logs)
                    )

    return related_signals


def find_reports_with_signals(xml_file, output_file):
    # Парсинг XML файла
    with open(xml_file, "r", encoding="utf-8") as f:
        tree = etree.parse(f)

    # Создаем список для хранения результатов
    results = []

    # Создаем мапу сигналов для быстрого доступа
    signals_map = {
        signal.get("{http://bmi.ch.abb.com/kbs/nm}signame"): signal.get(
            "{http://bmi.ch.abb.com/kbs/nm}signame"
        )
        for signal in tree.xpath(".//SIGNALITEM")
    }

    # Создаем мапу логов для быстрого доступа
    logs_map = {
        log.get("{http://bmi.ch.abb.com/kbs/nm}ritext"): log
        for log in tree.xpath(".//REPITEM")
    }

    # Ищем все reports
    reports = tree.xpath(".//REPORT30")

    for report in reports:
        repname = report.get("{http://bmi.ch.abb.com/kbs/nm}repname")
        repdescr = report.get("{http://bmi.ch.abb.com/kbs/db}description")
        tmplname = report.get("{http://bmi.ch.abb.com/kbs/rr}tplid_template_tplname")

        # Находим все property на любом уровне вложенности в report
        property_elements = report.xpath(".//PROPERTY")

        visited_logs = set()  # Для предотвращения зацикливания

        for property_element in property_elements:
            log_name = property_element.get(
                "{http://bmi.ch.abb.com/kbs/rr}value_repitem_ritext"
            )
            log = logs_map.get(log_name)

            if log is not None:
                # Добавляем текущий log в посещенные
                visited_logs.add(log_name)
                # Находим сигналы, связанные с текущим log
                signals = find_signals(log, signals_map, logs_map, visited_logs)

                # Для каждого связанного сигнала добавляем отдельную запись
                for signal in signals:
                    results.append(
                        {
                            "Report Name": repname,
                            "Related Signal": signal,
                            "Report Description": repdescr,
                            "Template Name": tmplname,
                        }
                    )

    # Создаем DataFrame и записываем в Excel
    df = pd.DataFrame(results)
    df.to_excel(output_file, index=False)


# Используем функцию
xml_file = "1.LIMS.xml"  # Замените на путь к вашему XML файлу
output_file = "Reports_LIMS_Test.xlsx"  # Задайте имя файла для выхода
find_reports_with_signals(xml_file, output_file)

excel_file = output_file

# Настройка ширины столбцов
workbook = openpyxl.load_workbook(excel_file)
sheet = workbook.active

# Adjust column widths
sheet.column_dimensions["A"].width = 40
sheet.column_dimensions["B"].width = 40
sheet.column_dimensions["C"].width = 40
sheet.column_dimensions["D"].width = 40

# Save the workbook
workbook.save(excel_file)
print(f"Data has been written to {excel_file}")