supersam/anonymize_xml.py

428 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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'<?xml version="1.0" encoding="UTF-8"?>\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)