#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Скрипт для анонимизации персональных данных в XML-файле. Заменяет ФИО и телефоны на сгенерированные, сохраняя соответствие дублей. Использование: python3 anonymize_xml.py <входной_xml> [выходной_xml] """ import sys import random import xml.etree.ElementTree as ET from pathlib import Path # --- Генераторы фейковых данных --- MALE_SURNAMES = [ "Иванов", "Смирнов", "Кузнецов", "Попов", "Васильев", "Петров", "Соколов", "Михайлов", "Новиков", "Федоров", "Морозов", "Волков", "Алексеев", "Лебедев", "Семенов", "Егоров", "Павлов", "Козлов", "Степанов", "Николаев", "Орлов", "Андреев", "Макаров", "Зайцев", "Соловьев", "Борисов", "Яковлев", "Григорьев", "Романов", "Воробьев", "Антонов", "Захаров", "Максимов", "Фролов", "Дмитриев", "Калинин", "Беляев", "Гусев", "Назаров", "Карпов", "Афанасьев", "Тихонов", "Марков", "Комаров", "Шестаков", "Артемьев", "Кукушкин", "Плотников", "Дорофеев", ] FEMALE_SURNAMES = [ "Иванова", "Смирнова", "Кузнецова", "Попова", "Васильева", "Петрова", "Соколова", "Михайлова", "Новикова", "Федорова", "Морозова", "Волкова", "Алексеева", "Лебедева", "Семенова", "Егорова", "Павлова", "Козлова", "Степанова", "Николаева", "Орлова", "Андреева", "Макарова", "Зайцева", "Соловьева", "Борисова", "Яковлева", "Григорьева", "Романова", "Воробьева", "Атонова", "Захарова", "Максимова", "Фролова", "Дмитриева", "Калинина", "Беляева", "Гусева", "Назарова", "Карпова", "Афанасьева", "Тихонова", "Маркова", "Комарова", "Шестакова", "Артемьева", "Кукушкина", "Плотникова", "Дорофеева", ] MALE_NAMES = [ "Александр", "Дмитрий", "Максим", "Сергей", "Андрей", "Алексей", "Артем", "Илья", "Кирилл", "Михаил", "Никита", "Матвей", "Роман", "Егор", "Арсений", "Иван", "Денис", "Евгений", "Даниил", "Тимофей", "Владимир", "Павел", "Руслан", "Марк", "Константин", "Тимур", "Олег", "Ярослав", "Степан", "Глеб", "Николай", "Петр", "Виктор", "Семен", "Леонид", "Григорий", "Богдан", "Захар", "Тарас", "Федор", ] FEMALE_NAMES = [ "Анна", "Мария", "Елена", "Алиса", "Виктория", "Полина", "Александра", "Елизавета", "Дарья", "Анастасия", "Ксения", "Валерия", "Варвара", "Вероника", "София", "Арина", "Марина", "Юлия", "Татьяна", "Наталья", "Ольга", "Светлана", "Ирина", "Екатерина", "Оксана", "Алина", "Кристина", "Людмила", "Галина", "Надежда", "Любовь", "Маргарита", "Софья", "Яна", "Диана", "Алла", "Инна", "Эмилия", "Лариса", "Злата", ] MALE_PATRONYMICS = [ "Александрович", "Дмитриевич", "Максимович", "Сергеевич", "Андреевич", "Алексеевич", "Артемович", "Ильич", "Кириллович", "Михайлович", "Никитич", "Матвеевич", "Романович", "Егорович", "Арсеньевич", "Иванович", "Денисович", "Евгеньевич", "Даниилович", "Тимофеевич", "Владимирович", "Павлович", "Константинович", "Тимурович", "Олегович", "Ярославович", "Степанович", "Глебович", "Николаевич", "Петрович", "Викторович", "Семенович", "Леонидович", "Григорьевич", "Богданович", "Захарович", "Тарасович", "Федорович", ] FEMALE_PATRONYMICS = [ "Александровна", "Дмитриевна", "Максимовна", "Сергеевна", "Андреевна", "Алексеевна", "Артемовна", "Ильинична", "Кирилловна", "Михайловна", "Никитична", "Матвеевна", "Романовна", "Егоровна", "Арсеньевна", "Ивановна", "Денисовна", "Евгеньевна", "Данииловна", "Тимофеевна", "Владимировна", "Павловна", "Константиновна", "Тимуровна", "Олеговна", "Ярославовна", "Степановна", "Глебовна", "Николаевна", "Петровна", "Викторовна", "Семеновна", "Леонидовна", "Григорьевна", "Богдановна", "Захаровна", "Тарасовна", "Федоровна", ] def generate_fake_fio(): """Генерирует случайное русское ФИО с инициалами или полным отчеством.""" if random.choice([True, False]): # Полное ФИО surname = random.choice(MALE_SURNAMES + FEMALE_SURNAMES) # Попытка согласовать род фамилии и имя/отчество if surname.endswith("а") and not surname.endswith( ("ина", "ова", "ева", "ына", "ая") ): # Может быть мужская фамилия типа Буря, но проще — выбираем случайно is_female = random.choice([True, False]) else: is_female = surname.endswith( ("ина", "ова", "ева", "ына", "ая", "ска", "цка", "ёва") ) if is_female: name = random.choice(FEMALE_NAMES) patronymic = random.choice(FEMALE_PATRONYMICS) else: name = random.choice(MALE_NAMES) patronymic = random.choice(MALE_PATRONYMICS) return f"{surname} {name} {patronymic}" else: # Фамилия с инициалами surname = random.choice(MALE_SURNAMES + FEMALE_SURNAMES) is_female = surname.endswith( ("ина", "ова", "ева", "ына", "ая", "ска", "цка", "ёва") ) if is_female: name_initial = random.choice(FEMALE_NAMES)[0] patronymic_initial = random.choice(FEMALE_PATRONYMICS)[0] else: name_initial = random.choice(MALE_NAMES)[0] patronymic_initial = random.choice(MALE_PATRONYMICS)[0] return f"{surname} {name_initial}.{patronymic_initial}." def generate_fake_phone(): """Генерирует случайный российский мобильный номер (10 цифр, начинается с 9).""" return f"9{random.randint(100000000, 999999999)}" def anonymize_xml(input_path: str, output_path: str = None): input_file = Path(input_path) if not input_file.exists(): print(f"Ошибка: файл не найден: {input_path}") sys.exit(1) # Определяем выходной путь if output_path is None: output_file = input_file.with_suffix(".anonymized.xml") else: output_file = Path(output_path) # Читаем исходный файл # Пробуем cp1251 (WINDOWS-1251), если ошибка — utf-8 raw_bytes = input_file.read_bytes() try: text = raw_bytes.decode("cp1251") except UnicodeDecodeError: text = raw_bytes.decode("utf-8", errors="replace") # Парсим XML try: root = ET.fromstring(text) except ET.ParseError as e: print(f"Ошибка парсинга XML: {e}") sys.exit(1) # Собираем уникальные значения fio_map = {} tel_map = {} fio_values = set() tel_values = set() for account in root.findall("Account"): fio = account.get("fio") tel = account.get("tel") if fio: fio_values.add(fio) if tel: tel_values.add(tel) # Генерируем маппинги random.seed(42) # Для воспроизводимости (можно убрать или изменить) for fio in sorted(fio_values): fio_map[fio] = generate_fake_fio() for tel in sorted(tel_values): tel_map[tel] = generate_fake_phone() # Заменяем в дереве for account in root.findall("Account"): original_fio = account.get("fio") original_tel = account.get("tel") if original_fio and original_fio in fio_map: account.set("fio", fio_map[original_fio]) if original_tel and original_tel in tel_map: account.set("tel", tel_map[original_tel]) # Сохраняем результат # Используем собственную сериализацию для сохранения формата # ElementTree.write может слегка изменять формат, поэтому используем tostring tree = ET.ElementTree(root) # Добавляем XML declaration xml_bytes = ET.tostring(root, encoding="unicode") result = f'\n{xml_bytes}' output_file.write_text(result, encoding="utf-8") print(f"Готово! Результат сохранён в: {output_file.resolve()}") print(f" Уникальных ФИО заменено: {len(fio_map)}") print(f" Уникальных телефонов заменено: {len(tel_map)}") # Печатаем часть маппинга для информации print("\nПримеры замен (первые 10):") count = 0 for k, v in fio_map.items(): if count >= 10: break tel_k = list(tel_map.keys())[count] if count < len(tel_map) else None tel_v = tel_map[tel_k] if tel_k else "" print(f" ФИО: '{k}' -> '{v}'") if tel_k: print(f" Тел: '{tel_k}' -> '{tel_v}'") count += 1 if __name__ == "__main__": if len(sys.argv) < 2: script_name = Path(sys.argv[0]).name print(f"Использование: python3 {script_name} <входной_xml> [выходной_xml]") print(f"Пример: python3 {script_name} data.xml data_anon.xml") sys.exit(1) input_path = sys.argv[1] output_path = sys.argv[2] if len(sys.argv) > 2 else None anonymize_xml(input_path, output_path)