428 lines
12 KiB
Python
428 lines
12 KiB
Python
#!/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)
|