что делает данный код и как можно его улучшить?
import pandas as pd
import re
import pyperclip
import json
from bs4 import BeautifulSoup
from tkinter import *
from tkinter import ttk, filedialog, messagebox
from collections import defaultdict, Counter

class ToolTip:
    def __init__(self, widget, text):
        self.widget = widget
        self.text = text
        self.tooltip_window = None
        widget.bind("<Enter>", self.show_tooltip)
        widget.bind("<Leave>", self.hide_tooltip)

    def show_tooltip(self, event=None):
        if self.tooltip_window or not self.text:
            return
        x = y = 0
        x = self.widget.winfo_pointerx() + 10
        y = self.widget.winfo_pointery() + 10
        self.tooltip_window = tw = Toplevel(self.widget)
        tw.wm_overrideredirect(True)
        tw.wm_geometry(f"+{x}+{y}")
        label = Label(tw, text=self.text, background="#ffffe0", relief='solid', borderwidth=1,
                      font=("tahoma", "8", "normal"))
        label.pack(ipadx=1)

    def hide_tooltip(self, event=None):
        tw = self.tooltip_window
        if tw:
            tw.destroy()
            self.tooltip_window = None

class AttributeEditor:
    MAPPINGS_FILE = 'mappings.json'  # Файл для хранения сопоставлений

    def __init__(self, root):
        self.root = root
        self.root.title("Редактор атрибутов товаров")
        self.root.geometry("1200x700")
        self.df = None
        self.param_mapping = {}
        self.original_values = {}  # Хранение оригинальных строк из HTML
        self.manual_split_values = {}  # Ручные изменения "Результат разделения"
        self.exclusions = []  # Список исключений
        self.mappings = {}  # Таблица сопоставлений
        self.sort_directions = {}  # Для хранения направления сортировки
        self.load_mappings()  # Загрузка сохранённых сопоставлений
        self.setup_ui()

    def setup_ui(self):
        main_frame = Frame(self.root)
        main_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)

        # Управление файлами
        file_frame = Frame(main_frame)
        file_frame.pack(fill=X, pady=2)
        self.btn_load = Button(file_frame, text="Загрузить файл", command=self.load_file)
        self.btn_load.pack(side=LEFT, padx=2)
        ToolTip(self.btn_load, "Загрузите Excel-файл с HTML-характеристиками")

        # Поля для ввода столбцов
        columns_frame = Frame(main_frame)
        columns_frame.pack(fill=X, pady=2)
        Label(columns_frame, text="Столбец характеристик:").pack(side=LEFT)
        self.features_col = Entry(columns_frame, width=4)
        self.features_col.pack(side=LEFT, padx=2)
        ToolTip(self.features_col, "Номер столбца с HTML-характеристиками")
        Label(columns_frame, text="Доп. столбцы:").pack(side=LEFT, padx=2)
        self.extra_cols = Entry(columns_frame, width=10)
        self.extra_cols.pack(side=LEFT, padx=2)
        ToolTip(self.extra_cols, "Дополнительные столбцы для сохранения")
        Label(columns_frame, text="Исключения:").pack(side=LEFT, padx=2)
        self.exclusion_entry = Entry(columns_frame, width=20)
        self.exclusion_entry.pack(side=LEFT, padx=2)
        ToolTip(self.exclusion_entry, "Значения, которые не должны разделяться (через запятую)")

        # Кнопка обработки
        self.btn_process = Button(main_frame, text="Обработать таблицу", command=self.process_table)
        self.btn_process.pack(pady=2)
        ToolTip(self.btn_process, "Обработать таблицу и извлечь параметры")

        # Таблица параметров
        self.tree = ttk.Treeview(main_frame,
                                 columns=('orig_param', 'orig_values', 'new_param', 'delimiter', 'split_result'),
                                 show='headings', height=12)
        self.tree.pack(fill=BOTH, expand=True, pady=2)
        
        # Настройка колонок
        self.tree.heading('orig_param', text='Исходный параметр')
        self.tree.heading('orig_values', text='Исходные значения')
        self.tree.heading('new_param', text='Новый параметр')
        self.tree.heading('delimiter', text='Разделитель')
        self.tree.heading('split_result', text='Результат разделения')
        
        self.tree.column('orig_param', width=130)
        self.tree.column('orig_values', width=250)
        self.tree.column('new_param', width=130)
        self.tree.column('delimiter', width=70)
        self.tree.column('split_result', width=250)
        
        # Привязка событий
        self.tree.bind("<Double-1>", self.on_double_click)
        self.tree.bind("<Button-3>", self.show_context_menu)  # ПКМ
        self.tree.bind("<Button-1>", self.sort_column, True)  # Сортировка
        self.tree.bind("<Control-c>", self.copy_selected_value)  # Ctrl+C

        # Контекстное меню
        self.context_menu = Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="Копировать", command=self.copy_selected_value)

        # Таблица сопоставлений
        mapping_frame = LabelFrame(main_frame, text="Таблица сопоставлений")
        mapping_frame.pack(fill=X, pady=5)
        self.mapping_tree = ttk.Treeview(mapping_frame, columns=('original', 'replacement'), show='headings', height=5)
        self.mapping_tree.heading('original', text='Исходное значение')
        self.mapping_tree.heading('replacement', text='Замена')
        self.mapping_tree.column('original', width=250)
        self.mapping_tree.column('replacement', width=250)
        self.mapping_tree.pack(side=LEFT, fill=BOTH, expand=True)

        mapping_buttons = Frame(mapping_frame)
        mapping_buttons.pack(side=LEFT, padx=5)
        Button(mapping_buttons, text="Добавить", command=self.add_mapping).pack(fill=X, pady=2)
        Button(mapping_buttons, text="Удалить", command=self.delete_mapping).pack(fill=X, pady=2)
        Button(mapping_buttons, text="Импорт", command=self.import_mappings).pack(fill=X, pady=2)
        Button(mapping_buttons, text="Экспорт", command=self.export_mappings).pack(fill=X, pady=2)
        ToolTip(Button(mapping_buttons, text="Импорт", command=self.import_mappings), "Импортировать сопоставления из Excel")
        ToolTip(Button(mapping_buttons, text="Экспорт", command=self.export_mappings), "Экспортировать сопоставления в Excel")

        # Кнопка сохранения
        self.btn_save = Button(main_frame, text="Сохранить результат", command=self.save_results)
        self.btn_save.pack(pady=2)
        ToolTip(self.btn_save, "Сохранить обработанные данные в Excel")

        # Горячие клавиши
        self.root.bind("<Control-o>", lambda e: self.load_file())
        self.root.bind("<Control-p>", lambda e: self.process_table())
        self.root.bind("<Control-s>", lambda e: self.save_results())
        self.root.bind("<Control-i>", lambda e: self.import_mappings())
        self.root.bind("<Control-e>", lambda e: self.export_mappings())

        # Обновляем таблицу сопоставлений
        self.refresh_mapping_table()

    def sort_column(self, event):
        """Сортировка по клику на заголовок столбца"""
        region = self.tree.identify("region", event.x, event.y)
        if region != "heading":
            return
        
        column = self.tree.identify_column(event.x)
        col_idx = int(column.replace('#', '')) - 1
        
        # Получаем текущее направление сортировки
        current_reverse = self.sort_directions.get(col_idx, False)
        
        # Собираем данные для сортировки
        data = []
        for item in self.tree.get_children(''):
            values = self.tree.item(item, 'values')
            key = values[col_idx] if col_idx < len(values) else ''
            data.append((key, item))
        
        # Сортируем
        data.sort(reverse=current_reverse, key=lambda x: x[0])
        
        # Перемещаем элементы в новом порядке
        for index, (_, item) in enumerate(data):
            self.tree.move(item, '', index)
        
        # Меняем направление сортировки
        self.sort_directions[col_idx] = not current_reverse

    def refresh_mapping_table(self):
        """Обновляет таблицу сопоставлений"""
        self.mapping_tree.delete(*self.mapping_tree.get_children())
        for orig, repl in self.mappings.items():
            self.mapping_tree.insert('', 'end', values=(orig, repl))

    def add_mapping(self):
        """Добавление нового сопоставления через диалоговое окно"""
        dialog = Toplevel(self.root)
        dialog.title("Добавить сопоставление")
        dialog.geometry("400x100")
        Label(dialog, text="Исходное значение:").pack()
        entry_orig = Entry(dialog)
        entry_orig.pack(fill=X)
        Label(dialog, text="Новое значение:").pack()
        entry_repl = Entry(dialog)
        entry_repl.pack(fill=X)
        def save():
            orig = entry_orig.get().strip()
            repl = entry_repl.get().strip()
            if orig:
                self.mappings[orig] = repl
                self.save_mappings()
                self.refresh_mapping_table()
                dialog.destroy()
        Button(dialog, text="Сохранить", command=save).pack(pady=5)

    def delete_mapping(self):
        """Удаление выделенного сопоставления"""
        selected = self.mapping_tree.selection()
        if selected:
            orig = self.mapping_tree.item(selected[0], 'values')[0]
            if messagebox.askyesno("Подтверждение", f"Удалить сопоставление '{orig}'?"):
                del self.mappings[orig]
                self.save_mappings()
                self.refresh_mapping_table()

    def save_mappings(self):
        """Сохраняет сопоставления в JSON-файл"""
        with open(self.MAPPINGS_FILE, 'w', encoding='utf-8') as f:
            json.dump(self.mappings, f, ensure_ascii=False, indent=2)

    def load_mappings(self):
        """Загружает сопоставления из JSON-файла"""
        try:
            with open(self.MAPPINGS_FILE, 'r', encoding='utf-8') as f:
                self.mappings = json.load(f)
        except FileNotFoundError:
            self.mappings = {}

    def import_mappings(self, event=None):
        """Импорт сопоставлений из Excel"""
        file_path = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx")])
        if file_path:
            try:
                df = pd.read_excel(file_path)
                if len(df.columns) < 2:
                    raise ValueError("Файл должен содержать как минимум 2 столбца.")
                for _, row in df.iterrows():
                    orig = str(row.iloc[0]).strip()
                    repl = str(row.iloc[1]).strip()
                    if orig:
                        self.mappings[orig] = repl
                self.save_mappings()
                self.refresh_mapping_table()
                messagebox.showinfo("Успех", "Сопоставления импортированы")
            except Exception as e:
                messagebox.showerror("Ошибка импорта", str(e))

    def export_mappings(self, event=None):
        """Экспорт сопоставлений в Excel"""
        df = pd.DataFrame(list(self.mappings.items()), columns=["Исходное значение", "Замена"])
        save_path = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx")])
        if save_path:
            try:
                df.to_excel(save_path, index=False)
                messagebox.showinfo("Успех", "Сопоставления экспортированы")
            except Exception as e:
                messagebox.showerror("Ошибка экспорта", str(e))

    def replace_mapped_values(self, value):
        """Заменяет значения по таблице сопоставлений"""
        for orig, repl in self.mappings.items():
            if orig in value:
                value = value.replace(orig, repl)
        return value

    def load_file(self):
        file_path = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx")])
        if file_path:
            try:
                self.df = pd.read_excel(file_path)
                self.auto_fill_columns()
                messagebox.showinfo("Успех", f"Загружено {len(self.df)} записей")
            except Exception as e:
                messagebox.showerror("Ошибка", str(e))

    def auto_fill_columns(self):
        if self.df is None or self.features_col.get():
            return
        for i, col in enumerate(self.df.columns):
            sample = self.df[col].iloc[0] if len(self.df) > 0 else ""
            if isinstance(sample, str) and ("<tr>" in sample or "<table>" in sample):
                self.features_col.delete(0, END)
                self.features_col.insert(0, str(i + 1))
                break
        features_col = int(self.features_col.get()) - 1 if self.features_col.get() else 0
        extra_cols = [i for i in range(len(self.df.columns)) if i != features_col]
        self.extra_cols.delete(0, END)
        self.extra_cols.insert(0, ', '.join(str(i + 1) for i in extra_cols))

    def parse_html_table(self, html):
        soup = BeautifulSoup(html, 'html.parser')
        params = {}
        current_param = None
        for tr in soup.find_all('tr'):
            tds = tr.find_all('td')
            if len(tds) == 2:
                param = tds[0].get_text(strip=True)
                value = tds[1].get_text(strip=True)
                if param and value:
                    params[param] = self.replace_mapped_values(value)
                    current_param = param
            elif len(tds) == 1 and current_param:
                params[current_param] += f"; {tds[0].get_text(strip=True)}"
        return params

    def split_values(self, value, delimiter):
        if not value or not delimiter:
            return []
        if value.strip() in self.exclusions:
            return [value.strip()]
        try:
            parts = re.split(fr'\s*{re.escape(delimiter)}\s*', value)
            return [self.replace_mapped_values(p.strip()) for p in parts if p.strip()]
        except Exception:
            return []

    def process_table(self):
        try:
            all_values = []
            features_col_input = self.features_col.get().strip()
            if not features_col_input:
                raise ValueError("Пожалуйста, укажите номер столбца с характеристиками.")
            features_col = int(features_col_input) - 1
            for _, row in self.df.iterrows():
                html = row.iloc[features_col]
                params = self.parse_html_table(html)
                for param, value in params.items():
                    all_values.extend(value.split(','))
            value_counts = Counter([v.strip() for v in all_values if len(v.strip().split()) > 1])
            self.exclusions = list(value_counts.keys())[:5]
            self.exclusion_entry.delete(0, END)
            self.exclusion_entry.insert(0, ', '.join(self.exclusions))
            self.exclusions = [x.strip() for x in self.exclusion_entry.get().split(',') if x.strip()]
            extra_cols_input = self.extra_cols.get().strip()
            extra_cols = []
            if extra_cols_input:
                extra_cols = [int(x.strip()) - 1 for x in extra_cols_input.split(',') if x.strip()]
            all_params = defaultdict(list)
            for _, row in self.df.iterrows():
                html = row.iloc[features_col]
                params = self.parse_html_table(html)
                for param, value in params.items():
                    all_params[param].append(value)
            self.tree.delete(*self.tree.get_children())
            self.original_values = {}
            self.manual_split_values = {}
            for param, values in all_params.items():
                original_value = values[0] if values else ""
                self.original_values[param] = original_value
                delimiter = ', '
                split_result = self.split_values(original_value, delimiter)
                self.tree.insert('', 'end', values=(
                    param,
                    original_value,
                    param,
                    delimiter,
                    ', '.join(split_result)
                ))
        except ValueError as e:
            messagebox.showerror("Ошибка", str(e))

    def on_double_click(self, event):
        region = self.tree.identify("region", event.x, event.y)
        if region != "cell":
            return
        column = self.tree.identify_column(event.x)
        item = self.tree.identify_row(event.y)
        if not item:
            return
        col_index = int(column.replace('#', '')) - 1
        values = list(self.tree.item(item, 'values'))
        orig_param = values[0]
        original_value = self.original_values.get(orig_param, '')
        current_value = values[col_index]
        if col_index not in [2, 3, 4]:
            return
        entry = Entry(self.tree)
        entry.insert(0, current_value)
        entry.select_range(0, END)
        entry.focus_set()
        x, y, width, height = self.tree.bbox(item, column)
        entry.place(x=x, y=y, width=width, height=height)
        def save_and_destroy(event=None):
            new_value = entry.get()
            values[col_index] = new_value
            if col_index == 3:
                delimiter = new_value
                split_result = self.split_values(original_value, delimiter)
                values[4] = ', '.join(split_result)
                self.manual_split_values[orig_param] = False
            elif col_index == 4:
                split_result = [v.strip() for v in new_value.split(',') if v.strip()]
                values[4] = ', '.join(split_result)
                self.manual_split_values[orig_param] = new_value
            self.tree.item(item, values=values)
            entry.destroy()
        entry.bind("<Return>", save_and_destroy)
        entry.bind("<FocusOut>", save_and_destroy)
        def on_keypress(event_key):
            if event_key.state == 4 and event_key.keysym == "v":  # Ctrl+V
                try:
                    clipboard = pyperclip.paste()
                    entry.insert(INSERT, clipboard)
                except Exception as e:
                    messagebox.showerror("Ошибка вставки", str(e))
        entry.bind("<KeyPress>", on_keypress)

    def show_context_menu(self, event):
        try:
            self.context_menu.tk_popup()
            self.context_menu_x = self.tree.identify_column(event.x)
            self.context_menu_y = self.tree.identify_row(event.y)
        finally:
            self.context_menu.grab_release()

    def copy_selected_value(self, event=None):
        """Копирует значение выбранной ячейки в буфер обмена"""
        selected = self.tree.selection()
        if not selected:
            return
        
        item = selected[0]
        values = self.tree.item(item, 'values')
        
        # Если вызвано через ПКМ - используем ранее определённый столбец
        if hasattr(self, 'context_menu_x'):
            column = self.context_menu_x
            col_idx = int(column.replace('#', '')) - 1
            if 0 <= col_idx < len(values):
                pyperclip.copy(values[col_idx])
                messagebox.showinfo("Копирование", "Значение скопировано")
                return
        
        # Если вызвано через Ctrl+C - копируем всю строку
        pyperclip.copy('\t'.join(values))
        messagebox.showinfo("Копирование", "Строка скопирована целиком")

    def save_results(self):
        try:
            features_col_input = self.features_col.get().strip()
            if not features_col_input:
                raise ValueError("Пожалуйста, укажите номер столбца с характеристиками.")
            features_col = int(features_col_input) - 1
            extra_cols_input = self.extra_cols.get().strip()
            extra_cols = []
            if extra_cols_input:
                extra_cols = [int(x.strip()) - 1 for x in extra_cols_input.split(',') if x.strip()]
            self.param_mapping = {}
            for item in self.tree.get_children():
                values = self.tree.item(item)['values']
                if len(values) >= 5:
                    self.param_mapping[values[0]] = {
                        'new_name': values[2],
                        'delimiter': values[3]
                    }
            result = []
            for _, row in self.df.iterrows():
                record = {}
                for col in extra_cols:
                    record[self.df.columns[col]] = row.iloc[col]
                html = row.iloc[features_col]
                params = self.parse_html_table(html)
                attr_counter = 1
                for orig_param, value in params.items():
                    if orig_param in self.param_mapping:
                        settings = self.param_mapping[orig_param]
                        delimiter = re.escape(settings['delimiter'])
                        if orig_param in self.manual_split_values:
                            manual = self.manual_split_values[orig_param]
                            if isinstance(manual, str):
                                clean_values = [v.strip() for v in manual.split(',') if v.strip()]
                            else:
                                clean_values = re.split(fr'\s*{delimiter}\s*', value)
                                clean_values = [v.strip() for v in clean_values if v.strip()]
                        else:
                            clean_values = re.split(fr'\s*{delimiter}\s*', value)
                            clean_values = [v.strip() for v in clean_values if v.strip()]
                        param_name = settings['new_name'] if settings['new_name'] else orig_param
                        record[f'Attribute {attr_counter} name'] = param_name
                        record[f'Attribute {attr_counter} value(s)'] = ', '.join(clean_values)
                        attr_counter += 1
                result.append(record)
            output_df = pd.DataFrame(result)
            save_path = filedialog.asksaveasfilename(
                defaultextension=".xlsx",
                filetypes=[("Excel files", "*.xlsx")]
            )
            if save_path:
                output_df.to_excel(save_path, index=False)
                messagebox.showinfo("Успех", "Файл успешно сохранён!")
        except ValueError as e:
            messagebox.showerror("Ошибка", str(e))

if __name__ == "__main__":
    root = Tk()
    app = AttributeEditor(root)
    root.mainloop()