Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c209c4a366 | ||
|
|
0b9371e4f2 |
+12
-1
@@ -2,7 +2,7 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"demask": true,
|
"demask": true,
|
||||||
"maskMode": "MASK",
|
"maskMode": "TOKEN",
|
||||||
"types": ["*"],
|
"types": ["*"],
|
||||||
"requireCompanion": [
|
"requireCompanion": [
|
||||||
"CVV", "PIN", "DATE", "BIRTH_PLACE", "ADDRESS_COUNTRY",
|
"CVV", "PIN", "DATE", "BIRTH_PLACE", "ADDRESS_COUNTRY",
|
||||||
@@ -27,5 +27,16 @@
|
|||||||
"demask": false,
|
"demask": false,
|
||||||
"maskMode": "MASK",
|
"maskMode": "MASK",
|
||||||
"types": ["*"]
|
"types": ["*"]
|
||||||
|
},
|
||||||
|
"strict": {
|
||||||
|
"enabled": true,
|
||||||
|
"demask": true,
|
||||||
|
"maskMode": "STRICT",
|
||||||
|
"types": ["*"],
|
||||||
|
"requireCompanion": [
|
||||||
|
"CVV", "PIN", "DATE", "BIRTH_PLACE", "ADDRESS_COUNTRY",
|
||||||
|
"ACCOUNT_NUMBER", "BIK", "OGRN", "OGRNIP", "KPP",
|
||||||
|
"INCOME", "BIOMETRIC"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,10 +158,6 @@ public class Pipeline {
|
|||||||
|
|
||||||
recordLatency("mask", policy.name(), started);
|
recordLatency("mask", policy.name(), started);
|
||||||
logFindings(policy.name(), payloadId, payload.length(), spans);
|
logFindings(policy.name(), payloadId, payload.length(), spans);
|
||||||
// Только для отладки формата входных данных на DEBUG — на INFO не активен.
|
|
||||||
// На боевом прогоне (нагрузочное тестирование) уровень DEBUG должен быть выключен:
|
|
||||||
// сюда попадают сырые ПД, что напрямую нарушает требование ТЗ не логировать значения.
|
|
||||||
LOG.debug("payload_id={} raw={}", payloadId, payload);
|
|
||||||
return masked;
|
return masked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static ru.pdguard.detect.RulePatterns.CITIZENSHIP_GAP;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.CITIZENSHIP_VALUE;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.ORGANISATION_NEARBY;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.ROLE_GAP;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.STREET_NAME;
|
||||||
|
import static ru.pdguard.detect.RuleRegistry.ADDRESS_NEARBY;
|
||||||
|
|
||||||
|
/** Правила распознавания органа выдачи паспорта, места рождения, гражданства и адреса. */
|
||||||
|
final class AddressRules {
|
||||||
|
|
||||||
|
private AddressRules() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static final List<Rule> RULES = List.of(
|
||||||
|
|
||||||
|
// «выдан ОУФМС России по г. Москве 12.05.2015» — дата в состав органа не входит,
|
||||||
|
// её забирает отдельное правило. Приоритет выше городского, иначе от органа
|
||||||
|
// осталась бы замаскированной только его часть.
|
||||||
|
// Перечень форм, не голая основа «выда»: она зацепила бы и «выдающийся»
|
||||||
|
// (обычное слово, не про выдачу документа).
|
||||||
|
Rule.of(PdTypes.PASSPORT_ISSUER, "(?iu:выдан|выдал[аио]?|выдали|выдач[аи]|выдаче)"
|
||||||
|
+ "\\W{0,3}([^,;\\n]{3,90}?)"
|
||||||
|
+ "(?=\\s*\\d{1,2}[.\\-/]\\d{1,2}[.\\-/]\\d{2,4}|[,;\\n]|\\s*$)", 78)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("выдан", "выдал", "выдач"),
|
||||||
|
|
||||||
|
// «совпадает с указанным в анкете: X» — второе упоминание органа выдачи
|
||||||
|
// под собственным якорем, без бэкреференса на первое.
|
||||||
|
Rule.of(PdTypes.PASSPORT_ISSUER, "(?iu:указанн\\w*\\s+в\\s+анкете)\\W{0,5}([^,;.\\n]{3,90}?)"
|
||||||
|
+ "(?=[,;.\\n]|\\s*$)", 78)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("указанн"),
|
||||||
|
|
||||||
|
// «Орган выдачи УФМС России по Республике Татарстан» — орган после якоря,
|
||||||
|
// до слова «совпадает» или конца фразы.
|
||||||
|
Rule.of(PdTypes.PASSPORT_ISSUER, "(?iu:орган\\s+выдачи)\\W{0,5}([^,;:\\n]{3,90}?)"
|
||||||
|
+ "(?=\\s*(?iu:совпадает|указанн)|[,;:\\n]|\\s*$)", 78)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("орган выдачи"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.BIRTH_PLACE, "(?iu:мест\\w*\\s+рождения)\\W{0,5}([^,;\\n]{3,60}?)(?=\\s*[,;\\n]|\\s*$)", 76)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("рождения"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.BIRTH_PLACE, "(?iu:родил(?:ся|ась))[^,;\\n]{0,40}?\\s+в\\s+"
|
||||||
|
+ "([^,;\\n]{3,40}?)(?=\\s*[,;\\n]|\\s*$)", 76)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("родил"),
|
||||||
|
|
||||||
|
// ROLE_GAP, не \W{0,5}: «Гражданство бенефициара по договору страхования: Х» —
|
||||||
|
// между якорем и значением бывает несколько слов, не только пунктуация.
|
||||||
|
// Список через запятую/слэш — вторая опциональная группа тем же шаблоном.
|
||||||
|
Rule.of(PdTypes.CITIZENSHIP, "(?iu:гражданств)\\w*" + CITIZENSHIP_GAP
|
||||||
|
+ "(" + CITIZENSHIP_VALUE + ")(?:\\s*[,/]\\s*(" + CITIZENSHIP_VALUE + "))?", 80)
|
||||||
|
.groups(1, 2)
|
||||||
|
.validatedBy(CountryDictionary::isKnownCountry)
|
||||||
|
.anchoredBy("гражданств"),
|
||||||
|
|
||||||
|
// ин/ка/ина/ки — именительный/родительный; ином/кой — творительный
|
||||||
|
// («гражданином», «гражданкой»).
|
||||||
|
Rule.of(PdTypes.CITIZENSHIP, "(?iu:граждан(?:ин|ка|ина|ки|ином|кой))\\b\\s+"
|
||||||
|
+ "(" + CITIZENSHIP_VALUE + ")(?:\\s*[,/]\\s*(" + CITIZENSHIP_VALUE + "))?", 75)
|
||||||
|
.groups(1, 2)
|
||||||
|
.validatedBy(CountryDictionary::isKnownCountry)
|
||||||
|
.anchoredBy("граждан"),
|
||||||
|
|
||||||
|
// --- Адрес: каждая составляющая настраивается отдельно ---
|
||||||
|
|
||||||
|
Rule.of(PdTypes.ADDRESS_POSTCODE, "(?iu:индекс)\\w*" + ROLE_GAP + "(\\d{6})\\b", 74)
|
||||||
|
.groups(1)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY)
|
||||||
|
.anchoredBy("индекс"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.ADDRESS_POSTCODE,
|
||||||
|
"\\b(\\d{6})(?=\\s*,?\\s*(?iu:г\\.|город|обл\\.|область|респ|край))", 74)
|
||||||
|
.groups(1)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY),
|
||||||
|
|
||||||
|
// Не только «г.»: перепись, на которой проверяется словарь, покрывает
|
||||||
|
// сёла, посёлки, деревни, хутора и станицы — «рп. Ильинское», «с. Кукуево»
|
||||||
|
// из ТЗ без этих якорей не нашлись бы вообще, город там ни при чём.
|
||||||
|
Rule.of(PdTypes.ADDRESS_CITY, "(?iu:\\bг\\.|\\bгор\\.|\\bгород|\\bрп\\.|\\bпгт\\.?|\\bп\\.|\\bс\\.|\\bсело\\b"
|
||||||
|
+ "|\\bд\\.|\\bдеревня\\b|\\bдер\\.|\\bх\\.|\\bхутор\\b|\\bст-ца|\\bстаница|\\bаул\\b"
|
||||||
|
+ "|\\bсл\\.|\\bслобода\\b|\\bаал\\b)\\s?(\\p{Lu}[\\p{L}-]{1,30})\\b", 73)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(ToponymDictionary::isKnownSettlement)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY)
|
||||||
|
.anchoredBy("г.", "гор", "город", "рп.", "пгт", "п.", "с.", "село", "д.", "деревня",
|
||||||
|
"дер.", "х.", "хутор", "ст-ца", "станица", "аул", "сл.", "слобода", "аал"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.ADDRESS_STREET,
|
||||||
|
"(?iu:\\bул\\.|\\bулиц\\p{L}*|\\bпр-т|\\bпроспект\\p{L}*|\\bпер\\.|\\bпереул\\p{L}*"
|
||||||
|
+ "|\\bш\\.|\\bшоссе|\\bб-р|\\bбульвар\\p{L}*|\\bнаб\\.|\\bнабережн\\p{L}*)"
|
||||||
|
+ "\\W{0,3}(" + STREET_NAME + ")", 73)
|
||||||
|
.groups(1)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY)
|
||||||
|
.requiringNear(ADDRESS_NEARBY)
|
||||||
|
.anchoredBy("ул", "просп", "пр-т", "пер.", "шоссе", "ш.", "бульвар", "б-р", "наб"),
|
||||||
|
|
||||||
|
// «Невский пр-т» — указатель после названия. Форма слишком общая, поэтому
|
||||||
|
// принимается только рядом с другими частями адреса: иначе под маску попал бы
|
||||||
|
// любой рассказ про Невский проспект.
|
||||||
|
Rule.of(PdTypes.ADDRESS_STREET, "\\b(\\p{Lu}[\\p{L}-]{2,30})\\s+"
|
||||||
|
+ "(?iu:пр-т|проспект|улиц\\p{L}*|шоссе|бульвар|переул\\p{L}*|набережн\\p{L}*)\\b", 73)
|
||||||
|
.groups(1)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY)
|
||||||
|
.requiringNear(ADDRESS_NEARBY)
|
||||||
|
.anchoredBy("пр-т", "проспект", "улиц", "шоссе", "бульвар", "переул", "набережн"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.ADDRESS_HOUSE,
|
||||||
|
"(?iu:\\bд\\.|\\bдом)\\s?(\\d+\\p{L}?(?:\\s?(?iu:к\\.|корп\\.?|стр\\.)\\s?\\d+)?)\\b", 72)
|
||||||
|
.groups(1)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY)
|
||||||
|
.anchoredBy("д.", "дом"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.ADDRESS_FLAT, "(?iu:\\bкв\\.|\\bквартир\\p{L}*)\\s?(\\d+\\p{L}?)\\b", 72)
|
||||||
|
.groups(1)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY)
|
||||||
|
.anchoredBy("кв"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.ADDRESS_COUNTRY,
|
||||||
|
"(?iu:стран\\p{L}*(?:\\s+(?:регистрации|проживания|гражданства))?)"
|
||||||
|
+ "\\W{0,5}(\\p{Lu}[\\p{L}-]{2,30})\\b", 71)
|
||||||
|
.groups(1)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY)
|
||||||
|
.anchoredBy("стран")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static ru.pdguard.detect.RulePatterns.ROLE_GAP;
|
||||||
|
|
||||||
|
/** Правила распознавания контактных и идентификационных данных: телефон, email, ИНН, СНИЛС. */
|
||||||
|
final class ContactRules {
|
||||||
|
|
||||||
|
private ContactRules() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static final List<Rule> RULES = List.of(
|
||||||
|
|
||||||
|
Rule.of(PdTypes.INN, "(?iu)\\bИНН\\b" + ROLE_GAP + "(\\d{12}|\\d{10})\\b", 84)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("инн"),
|
||||||
|
|
||||||
|
// «ИНН/КПП 7712345671/771201001» — ИНН юрлица перед КПП через слэш.
|
||||||
|
Rule.of(PdTypes.INN, "(?iu)\\bИНН\\s*/\\s*КПП\\b\\W{0,5}(\\d{10})\\b", 84)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("инн/кпп"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.SNILS, "(?iu)(?:\\bСНИЛС\\b\\D{0,10})?(\\d{3}[ -]\\d{3}[ -]\\d{3}[ -]\\d{2})\\b", 84)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(Validators::snils),
|
||||||
|
|
||||||
|
// \b7, не только +7: номер без плюса («79031119955») тоже встречается.
|
||||||
|
Rule.of(PdTypes.PHONE, "(?:\\+7|\\b7|\\b8)[ ()-]{0,3}\\d{3}[ ()-]{0,3}\\d{3}[ -]{0,2}\\d{2}[ -]{0,2}\\d{2}\\b", 82),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.EMAIL, "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b", 80)
|
||||||
|
.anchoredBy("@"),
|
||||||
|
|
||||||
|
// ИНН физлица без якорного слова — только с верной контрольной суммой.
|
||||||
|
Rule.of(PdTypes.INN, "\\b\\d{12}\\b", 62)
|
||||||
|
.validatedBy(Validators::inn)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static ru.pdguard.detect.RulePatterns.DATE_ANY;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.DATE_GAP;
|
||||||
|
|
||||||
|
/** Правила распознавания дат: рождения, выдачи документа и дат без якорного слова. */
|
||||||
|
final class DateRules {
|
||||||
|
|
||||||
|
private DateRules() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static final List<Rule> RULES = List.of(
|
||||||
|
|
||||||
|
Rule.of(PdTypes.BIRTH_DATE, "(?iu:дат\\p{L}*\\s+рождения|дата\\s+рожд\\.)"
|
||||||
|
+ DATE_GAP + "(" + DATE_ANY + ")", 87)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(Validators::date)
|
||||||
|
.anchoredBy("рожден"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.BIRTH_DATE, "(?iu:родил(?:ся|ась))"
|
||||||
|
+ DATE_GAP + "(" + DATE_ANY + ")", 87)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(Validators::date)
|
||||||
|
.anchoredBy("родил"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.BIRTH_DATE, "(" + DATE_ANY + ")\\s*(?iu:г\\.\\s?р\\.|г/р|года\\s+рождения)", 87)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(Validators::date)
|
||||||
|
.anchoredBy("г.р", "г/р", "года рождения"),
|
||||||
|
|
||||||
|
// «дата выдачи 12.05.2015» и «дата выдачи паспорта 12.05.2015»
|
||||||
|
Rule.of(PdTypes.PASSPORT_DATE, "(?iu:дат\\p{L}*\\s+выдачи)"
|
||||||
|
+ DATE_GAP + "(" + DATE_ANY + ")", 87)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(Validators::date)
|
||||||
|
.anchoredBy("выдач"),
|
||||||
|
|
||||||
|
// Дата без якорного слова персональными данными сама по себе не является:
|
||||||
|
// маскируется, только если в тексте есть ПД другого типа.
|
||||||
|
Rule.of(PdTypes.DATE, DATE_ANY, 58)
|
||||||
|
.validatedBy(Validators::date)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static ru.pdguard.detect.RulePatterns.ROLE_GAP;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.SERIES_AND_NUMBER;
|
||||||
|
|
||||||
|
/** Правила распознавания документов, удостоверяющих личность, и кодов подразделений. */
|
||||||
|
final class DocumentRules {
|
||||||
|
|
||||||
|
private DocumentRules() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static final List<Rule> RULES = List.of(
|
||||||
|
|
||||||
|
// CVV: латиница, кириллическая транслитерация («цвв», «сививи») и
|
||||||
|
// описательные якоря («код на обороте карты»). Между якорем и числом
|
||||||
|
// допускаются слова («CVV код 321», «CVV указан код 123») и длинные
|
||||||
|
// разделители («код на обороте карты 789»).
|
||||||
|
Rule.of(PdTypes.CVV, "(?iu:\\b(?:cvv2?|cvc2?|цвв|сививи|код\\p{L}*\\s+на\\s+обороте\\s+карты"
|
||||||
|
+ "|код\\s+проверки|защитный\\s+код)\\b)"
|
||||||
|
+ "(?:\\s+\\p{L}+){0,2}\\W{0,30}(\\d{3,4})\\b", 92)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("cvv", "cvc", "цвв", "сививи", "код на обороте", "код проверки", "защитный код"),
|
||||||
|
|
||||||
|
// PIN: «пин-код», «пин код», «ПИН:», «пин 3456». Между якорем и числом
|
||||||
|
// допускаются слова («ПИН-код карты 2468») и длинные разделители
|
||||||
|
// («Пин Код: 1234»).
|
||||||
|
Rule.of(PdTypes.PIN, "(?iu:\\b(?:пин[\\s-]?кода?|pin[\\s-]?code|пин|pin)\\b)"
|
||||||
|
+ "(?:\\s+\\p{L}+){0,2}\\W{0,30}(\\d{4,6})\\b", 92)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("пин", "pin"),
|
||||||
|
|
||||||
|
// «паспорт 4509 123456», «паспорт гражданина РФ 45 09 123456»
|
||||||
|
Rule.of(PdTypes.PASSPORT, "(?iu:паспорт)\\w*(?:\\W+(?iu:гражданина\\s+РФ|РФ|России|Российской\\s+Федерации))?"
|
||||||
|
+ "\\W{0,10}(" + SERIES_AND_NUMBER + ")\\b", 90)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("паспорт"),
|
||||||
|
|
||||||
|
// «серия 4509 номер 123456», «серии 45 09 № 123456»
|
||||||
|
// Между серией и номером помещается слово: «серия 4509 номер 123456»,
|
||||||
|
// «серии 4509 за номером 123456», «серия 4509 № 123456».
|
||||||
|
Rule.of(PdTypes.PASSPORT, "(?iu:сери)\\w{0,3}\\W{0,5}(\\d{2}\\s?\\d{2})[^\\d]{0,20}(\\d{6})\\b", 90)
|
||||||
|
.groups(1, 2)
|
||||||
|
.anchoredBy("сери"),
|
||||||
|
|
||||||
|
// Необязательное «серия»/«серии» между якорем и цифрами: «ВУ серия 12 34 номер 567890».
|
||||||
|
Rule.of(PdTypes.DRIVER_LICENSE, "(?iu:водительск\\w+\\s+удостоверени\\w+|в/у|вод\\.\\s?удост\\w*|\\bВУ)\\b"
|
||||||
|
+ "\\W{0,15}(?:(?iu:сери\\w{0,3})\\W{0,5})?(" + SERIES_AND_NUMBER + ")\\b", 89)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("водительск", "в/у", "вод.", "ву "),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.FOREIGN_PASSPORT, "(?iu:загранпаспорт|заграничн\\p{L}*\\s+паспорт)\\p{L}*"
|
||||||
|
+ "\\W{0,10}(\\d{2}\\s?\\d{7})\\b", 89)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("загранпаспорт", "заграничн"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.MILITARY_ID, "(?iu:военн\\p{L}*\\s+билет)\\p{L}*"
|
||||||
|
+ "\\W{0,10}(\\p{Lu}{2}\\s?\\d{7})\\b", 89)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("военн"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.BIRTH_CERTIFICATE, "(?iu:свидетельств\\p{L}*\\s+о\\s+рождении)"
|
||||||
|
+ "\\W{0,15}([IVXLC]{1,4}[- ]?\\p{Lu}{2}\\s?(?:№\\s?)?\\d{6})\\b", 89)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("свидетельств"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.MEDICAL_POLICY, "(?iu:полис\\p{L}*(?:\\s+ОМС)?)\\W{0,10}(\\d{16})\\b", 89)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("полис"),
|
||||||
|
|
||||||
|
// ROLE_GAP, не \W{0,5}: «код подразделения стоит 001-000» — между якорем и
|
||||||
|
// значением есть слово («стоит»/«объекта»), не только пунктуация.
|
||||||
|
Rule.of(PdTypes.DEPT_CODE, "(?iu:код\\w*\\s+подразделения|к/п)" + ROLE_GAP
|
||||||
|
+ "(\\d{3}\\s?-?\\s?\\d{3})\\b", 88)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("подразделени", "к/п"),
|
||||||
|
|
||||||
|
// «770-001 — таков код подразделения» — значение перед якорем.
|
||||||
|
Rule.of(PdTypes.DEPT_CODE, "\\b(\\d{3}\\s?-?\\s?\\d{3})\\b\\s*[—-]\\s*(?:\\p{L}+\\s+){0,3}"
|
||||||
|
+ "(?iu:код\\w*\\s+подразделения)", 88)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("подразделени")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static ru.pdguard.detect.RulePatterns.HOLDER_STEM;
|
||||||
|
|
||||||
|
/** Правила распознавания банковских реквизитов, карты, ОГРН/КПП и держателя карты. */
|
||||||
|
final class FinanceRules {
|
||||||
|
|
||||||
|
private FinanceRules() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static final List<Rule> RULES = List.of(
|
||||||
|
|
||||||
|
// Расчётный счёт — ровно 20 цифр после якоря, группировка пробелами не важна.
|
||||||
|
Rule.of(PdTypes.ACCOUNT_NUMBER, "(?iu:р/с|расчетн\\w*\\s+счет|расчётн\\w*\\s+счёт|лицев\\w*\\s+счет|"
|
||||||
|
+ "лицев\\w*\\s+счёт)\\W{0,5}((?:\\d[ ]?){19}\\d)\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("р/с", "расчетн", "расчётн", "лицев"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.BIK, "(?iu:бик)\\W{0,5}(\\d{9})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("бик"),
|
||||||
|
|
||||||
|
// «действительна до 09/27», «exp 09/27» — срок действия карты, не дата рождения.
|
||||||
|
Rule.of(PdTypes.CARD_EXPIRY, "(?iu:срок\\s+действия|действительна?\\s+до|\\bexp\\w*)\\W{0,5}"
|
||||||
|
+ "(\\d{2}\\s?/\\s?\\d{2})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("срок действия", "действительн", "exp"),
|
||||||
|
|
||||||
|
// ОГРНИП раньше ОГРН: без отрицательного просмотра «ОГРНИП» частично ловился бы
|
||||||
|
// ещё и правилом ОГРН. Контрольная сумма отсекает случайные 13/15-значные
|
||||||
|
// числа рядом со словом — раньше якоря было достаточно самого по себе.
|
||||||
|
Rule.of(PdTypes.OGRNIP, "(?iu:огрнип)\\W{0,5}(\\d{15})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(Validators::ogrnip)
|
||||||
|
.anchoredBy("огрнип"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.OGRN, "(?iu:огрн(?!ип))\\W{0,5}(\\d{13})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(Validators::ogrn)
|
||||||
|
.anchoredBy("огрн"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.KPP, "(?iu:кпп)\\W{0,5}(\\d{9})\\b", 83)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("кпп"),
|
||||||
|
|
||||||
|
// Доход/зарплата: сумма с разделителями тысяч. Между якорем и суммой может
|
||||||
|
// стоять слово («доход клиента», «доход за год») — без этого якорь ловил
|
||||||
|
// бы только «доход 85000», вплотную.
|
||||||
|
Rule.of(PdTypes.INCOME, "(?iu:доход|заработн\\w*\\s+плат\\w*|зарплат\\w*)(?:\\s+\\p{L}+){0,3}?"
|
||||||
|
+ "\\W{0,5}(\\d{1,3}(?:[\\s.]?\\d{3})*(?:,\\d{2})?)\\s?(?iu:руб\\p{L}*|₽)?\\b", 76)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("доход", "заработн", "зарплат"),
|
||||||
|
|
||||||
|
// Биометрия — сама фраза уже говорит, что дальше персональные данные, отдельного
|
||||||
|
// значения для захвата нет: маскируется якорная фраза целиком.
|
||||||
|
Rule.of(PdTypes.BIOMETRIC, "(?iu:биометрическ\\w*\\s+(?:данны\\w*|образц\\w*|шаблон\\w*)"
|
||||||
|
+ "|слепок\\s+голоса|отпечаток\\s+пальца|скан\\s+лица|\\bЕБС\\b)", 81)
|
||||||
|
.anchoredBy("биометри", "слепок голоса", "отпечаток пальца", "скан лица", "ебс"),
|
||||||
|
|
||||||
|
Rule.of(PdTypes.CARDHOLDER, "(?iu:держател\\w*(?:\\s+карты)?|cardholder|на\\s+имя)"
|
||||||
|
+ "\\W{0,10}([A-Z]{2,20}\\s+[A-Z]{2,20})\\b", 86)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy(HOLDER_STEM, "cardholder", "на имя"),
|
||||||
|
|
||||||
|
// --- Уровень 1: подтверждается контрольной суммой ---
|
||||||
|
|
||||||
|
Rule.of(PdTypes.CARD, "\\b\\d(?:[ -]?\\d){11,18}\\b", 85)
|
||||||
|
.validatedBy(Validators::luhn)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static ru.pdguard.detect.RulePatterns.CAPITALISED;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.HOLDER_STEM;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.ORGANISATION_NEARBY;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.PATRONYMIC;
|
||||||
|
import static ru.pdguard.detect.RulePatterns.SURNAME;
|
||||||
|
|
||||||
|
/** Правила распознавания ФИО — от полной тройки с ролевым словом до одиночного имени по словарю. */
|
||||||
|
final class FioRules {
|
||||||
|
|
||||||
|
private FioRules() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static final List<Rule> RULES = List.of(
|
||||||
|
|
||||||
|
// Фамилия Имя Отчество: первое слово опознаётся по словообразованию фамилии.
|
||||||
|
// Свободная тройка «любое слово с заглавной + имя + отчество» здесь
|
||||||
|
// сознательно не используется: она захватывает глагол в начале
|
||||||
|
// предложения («Пригласите Ивана Сергеевича») и заметно дороже по времени.
|
||||||
|
// Фамилии без привычного окончания — Ким, Цой — ловятся по ролевому слову.
|
||||||
|
Rule.of(PdTypes.FIO, "\\b" + SURNAME + "\\s+" + CAPITALISED + "\\s+" + PATRONYMIC + "\\b", 79),
|
||||||
|
|
||||||
|
// Имя Отчество Фамилия — второй распространённый порядок слов.
|
||||||
|
Rule.of(PdTypes.FIO, "\\b" + CAPITALISED + "\\s+" + PATRONYMIC + "\\s+" + SURNAME + "\\b", 79),
|
||||||
|
|
||||||
|
// Иванов И.И. и И.И. Иванов
|
||||||
|
Rule.of(PdTypes.FIO, "\\b" + SURNAME + "\\s+\\p{Lu}\\.\\s?\\p{Lu}\\.", 79),
|
||||||
|
Rule.of(PdTypes.FIO, "\\b\\p{Lu}\\.\\s?\\p{Lu}\\.\\s?" + SURNAME + "\\b", 79),
|
||||||
|
|
||||||
|
// Имя Отчество без фамилии
|
||||||
|
Rule.of(PdTypes.FIO, "\\b" + CAPITALISED + "\\s+" + PATRONYMIC + "\\b", 77),
|
||||||
|
|
||||||
|
// «ФИО: иванов иван иванович» — явный якорь снимает требование к регистру
|
||||||
|
Rule.of(PdTypes.FIO, "(?iu:\\bФИО|\\bф\\.\\s?и\\.\\s?о\\.|\\bна\\s+имя)"
|
||||||
|
+ "(?:\\s+\\p{L}+)?\\W{0,5}(\\p{L}{2,}(?:\\s+\\p{L}{2,}){0,2})\\b", 77)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("фио", "ф.и.о", "на имя"),
|
||||||
|
|
||||||
|
// «клиент Иванов Иван», «плательщик Петрова»
|
||||||
|
Rule.of(PdTypes.FIO, "(?iu:\\bклиент|\\bзаказчик|\\bпациент|\\bсотрудник|\\bвладел|\\bплательщик"
|
||||||
|
+ "|\\bполучател|\\bабонент|\\bв\\s+лице|\\bпредставител|\\bпоручител"
|
||||||
|
+ "|\\bсозаёмщик|\\bсозаемщик|\\bзаёмщик|\\bзаемщик|\\bзаявител|\\bдоверител"
|
||||||
|
+ "|\\bвкладчик|\\bответственн|\\bконтактное\\s+лицо|\\bисполнител|\\bдержател)\\p{L}*"
|
||||||
|
+ "\\W{0,5}(\\p{Lu}\\p{Ll}+(?:\\s+\\p{Lu}\\p{Ll}+){0,2})\\b", 77)
|
||||||
|
.groups(1)
|
||||||
|
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
||||||
|
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
||||||
|
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
||||||
|
"контактное лицо", "исполнител", HOLDER_STEM),
|
||||||
|
|
||||||
|
// «клиент иван иванов», «поручитель петрович» — строчные имена после
|
||||||
|
// ролевого слова. Регистр снимает требование к заглавной букве, а словарь
|
||||||
|
// имён отсекает «клиент пришёл в офис».
|
||||||
|
Rule.of(PdTypes.FIO, "(?iu:\\bклиент|\\bзаказчик|\\bпациент|\\bсотрудник|\\bвладел|\\bплательщик"
|
||||||
|
+ "|\\bполучател|\\bабонент|\\bв\\s+лице|\\bпредставител|\\bпоручител"
|
||||||
|
+ "|\\bсозаёмщик|\\bсозаемщик|\\bзаёмщик|\\bзаемщик|\\bзаявител|\\bдоверител"
|
||||||
|
+ "|\\bвкладчик|\\bответственн|\\bконтактное\\s+лицо|\\bисполнител|\\bдержател"
|
||||||
|
+ "|\\bотправител|\\bбенефициар|\\bдоверенное\\s+лицо|\\bнаследник|\\bсозаемщик"
|
||||||
|
// \p{L}*+ (possessive), не \p{L}*: без possessive откат назад позволял
|
||||||
|
// движку «отдать» уже съеденное падежное окончание ролевого слова и
|
||||||
|
// захватить его как будто отдельное имя — «пациентов» ловилось бы как «ов».
|
||||||
|
+ "|\\bпоручител)\\p{L}*+"
|
||||||
|
+ "(?:\\s+\\p{L}+){0,3}\\W{0,5}(\\p{L}{2,}(?:\\s+\\p{L}{2,}){0,2}(?:\\s+\\p{Lu}\\.){0,2})(?![\\p{L}.])", 77)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(NameDictionary::containsNamePart)
|
||||||
|
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
||||||
|
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
||||||
|
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
||||||
|
"контактное лицо", "исполнител", HOLDER_STEM, "отправител", "бенефициар",
|
||||||
|
"доверенное лицо", "наследник", "созаемщик"),
|
||||||
|
|
||||||
|
// Фамилия рядом с личным именем из словаря: без словаря правило ловило бы
|
||||||
|
// «Тверская улица» и тому подобное. Имя проверяется по множеству уже
|
||||||
|
// после совпадения — чередование из ста веток в шаблоне обходится дорого.
|
||||||
|
// Самое слабое основание среди правил ФИО — ни ролевого слова, ни явного
|
||||||
|
// якоря, — поэтому именно здесь нужно вето на адресный контекст: «Великие
|
||||||
|
// Луки» (реальный город) распознаётся как имя «Лука» в падеже плюс
|
||||||
|
// случайное слово, «Богдана Хмельницкого» — улица в честь исторической
|
||||||
|
// фигуры. Найдено на реальных адресах отделений из реестра ЦБ.
|
||||||
|
Rule.of(PdTypes.FIO, "\\b" + SURNAME + "\\s+" + CAPITALISED + "\\b", 74)
|
||||||
|
.validatedBy(NameDictionary::containsGivenName)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY),
|
||||||
|
Rule.of(PdTypes.FIO, "\\b" + CAPITALISED + "\\s+" + SURNAME + "\\b", 74)
|
||||||
|
.validatedBy(NameDictionary::containsGivenName)
|
||||||
|
.vetoedBy(ORGANISATION_NEARBY),
|
||||||
|
|
||||||
|
// Одиночное имя, фамилия или отчество: «Иванов», «иван», «петрович».
|
||||||
|
// Самое слабое основание среди правил ФИО — ни ролевого слова, ни пары
|
||||||
|
// слов, — поэтому приоритет ниже и проверка по словарю обязательна.
|
||||||
|
// Словарь отсекает «сочи», «казань» и прочие не-имена. Первое слово текста
|
||||||
|
// не рассматривается: заглавная буква там от начала предложения, а не от
|
||||||
|
// имени, и словообразовательная эвристика ложно ловит «Магазин», «Отдел».
|
||||||
|
Rule.of(PdTypes.FIO, "(?<!^)\\b(\\p{L}{2,})\\b", 70)
|
||||||
|
.groups(1)
|
||||||
|
.validatedBy(NameDictionary::isStandaloneNameCandidate)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package ru.pdguard.detect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Общие фрагменты регулярных выражений, переиспользуемые между группами правил
|
||||||
|
* в {@link RuleRegistry}. Вынесены отдельно, чтобы не дублировать их в каждой
|
||||||
|
* группе — «серия и номер», разрывы между якорем и значением, формы дат и т.п.
|
||||||
|
* встречаются в правилах разных категорий (документы, банк, ФИО, адрес).
|
||||||
|
*/
|
||||||
|
final class RulePatterns {
|
||||||
|
|
||||||
|
private RulePatterns() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Слово с заглавной буквы; остальные буквы любого регистра, чтобы
|
||||||
|
* «ИВАНОВ» распознавался наравне с «Иванов».
|
||||||
|
*/
|
||||||
|
static final String CAPITALISED = "\\p{Lu}[\\p{Lu}\\p{Ll}]+";
|
||||||
|
|
||||||
|
/** Якорное слово-основа: держатель карты, держателем и т.п. */
|
||||||
|
static final String HOLDER_STEM = "держател";
|
||||||
|
|
||||||
|
/** Разрыв между якорем и значением, когда между ними ролевое слово («ИНН плательщика»). */
|
||||||
|
static final String ROLE_GAP = "(?:\\s+[\\p{L}-]+){0,5}\\W{0,10}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* То же самое, но только строчные слова-филлеры: ролевые слова перед значением
|
||||||
|
* гражданства всегда строчные («бенефициара», «поручителя»), а само значение —
|
||||||
|
* с заглавной («Республики», «Соединенные»). Обычный {@link #ROLE_GAP} жадно
|
||||||
|
* поглощал бы и заглавное слово значения как будто это ролевое слово, оставляя
|
||||||
|
* значение только хвостом («Республики Беларусь» → «Беларусь»).
|
||||||
|
*/
|
||||||
|
static final String CITIZENSHIP_GAP = "(?:\\s+\\p{Ll}[\\p{L}-]*){0,5}\\W{0,10}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Название улицы: от одного до трёх слов с заглавной буквы либо чисел —
|
||||||
|
* «Тверская», «Малая Никитская», «8 Марта». Ограничение по форме обязательно:
|
||||||
|
* без него правило дожёвывало строку до конца, и «Проспект Вернадского перекрыт
|
||||||
|
* до вечера» оказывался под маской целиком.
|
||||||
|
*/
|
||||||
|
static final String STREET_NAME =
|
||||||
|
"(?:\\p{Lu}[\\p{L}-]+|\\d+[\\p{L}-]*)(?:\\s+(?:\\p{Lu}[\\p{L}-]+|\\d+[\\p{L}-]*)){0,2}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фамилия по словообразованию: Иванов, Ковалёва, Троицкий, Шевченко, Мкртчян.
|
||||||
|
* Хвост из двух букв покрывает падежные окончания: Ковалёв-ой, Иванов-а.
|
||||||
|
*/
|
||||||
|
static final String SURNAME =
|
||||||
|
"\\p{Lu}[\\p{Lu}\\p{Ll}]*(?iu:ов|ев|ёв|ин|ын|ск(?:ий|ая|ого|ой|ом)|цк(?:ий|ая)"
|
||||||
|
+ "|енко|ко|ук|юк|ян|швили|дзе)\\p{L}{0,2}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отчество: признак надёжный, ни одно другое слово так не оканчивается.
|
||||||
|
* Основы даны без падежного окончания — Иванович, Ивановича, Ивановне.
|
||||||
|
*/
|
||||||
|
static final String PATRONYMIC =
|
||||||
|
"\\p{Lu}[\\p{Lu}\\p{Ll}]+(?iu:ович|евич|ьич|мич|нич|тич|лич|кич|бич|сич"
|
||||||
|
+ "|овн|евн|иничн|ичн)\\p{L}{0,2}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Серия и номер: «4509 123456», «45 09 123456», «4509123456», «45 09 № 123456»,
|
||||||
|
* а также с произвольным числом пробелов и словом «номер» между частями —
|
||||||
|
* «12 34 номер 567890» (реальный кейс из бланка).
|
||||||
|
*/
|
||||||
|
static final String SERIES_AND_NUMBER =
|
||||||
|
"\\d{2}\\s*\\d{2}(?:\\s*(?:№|N|номер)\\s*|[\\s№N]{0,3})\\d{6}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Название месяца: полная форма («январь»), сокращение («янв») и плейсхолдер
|
||||||
|
* «ммм» (в логах встречается и латинская «M»). Сокращения нужны, потому что
|
||||||
|
* в датах вида «15 ЯНВ 10» месяц записан тремя буквами.
|
||||||
|
*/
|
||||||
|
static final String MONTH =
|
||||||
|
"(?iu:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр"
|
||||||
|
+ "|янв|фев|мар|апр|авг|сен|окт|ноя|дек|[МM]мм)\\p{L}*";
|
||||||
|
|
||||||
|
/** Числовая запись при любом порядке частей: дд.мм.гггг, мм/дд/гггг, гггг-мм-дд. */
|
||||||
|
static final String DATE_DIGITS = "\\b\\d{1,4}[.\\-/]\\d{1,2}[.\\-/]\\d{1,4}\\b";
|
||||||
|
|
||||||
|
/** «15 03 1990», «15 03 10» — числовая дата с пробелами вместо разделителей. */
|
||||||
|
static final String DATE_DIGITS_SPACE = "\\b\\d{1,2}\\s+\\d{1,2}\\s+\\d{2,4}\\b";
|
||||||
|
|
||||||
|
/** «15 03», «15/03» — день и месяц без года. */
|
||||||
|
static final String DATE_DAY_MONTH = "\\b\\d{1,2}\\s*[-/.]?\\s*\\d{1,2}\\b";
|
||||||
|
|
||||||
|
/** «12 мая 1985 г.», «15-ЯНВ-10», «15 января» — месяц словом, год 2-4 цифры или без года. */
|
||||||
|
static final String DATE_MONTH_WORD =
|
||||||
|
"\\b\\d{1,2}\\s*[-/.]?\\s*" + MONTH + "\\s*[-/.]?\\s*(?:\\d{2,4})?\\b"
|
||||||
|
+ "(?:\\s*(?iu:года|г\\.|г\\b))?";
|
||||||
|
|
||||||
|
/** «двенадцатого мая тысяча девятьсот восемьдесят пятого года» */
|
||||||
|
static final String DATE_WORDS =
|
||||||
|
"\\b(?:(?iu:двадцать|тридцать)\\s+)?"
|
||||||
|
+ "(?iu:перв|втор|треть|четв[её]рт|пят|шест|седьм|восьм|девят|десят|одиннадцат|двенадцат"
|
||||||
|
+ "|тринадцат|четырнадцат|пятнадцат|шестнадцат|семнадцат|восемнадцат|девятнадцат|двадцат|тридцат)"
|
||||||
|
+ "(?iu:ьего|ого|его|ое)\\s+" + MONTH
|
||||||
|
+ "\\s+(?:\\d{4}|(?iu:тысяча)(?:\\s+\\p{L}+){1,8})\\s*(?iu:года|год\\b|г\\.)";
|
||||||
|
|
||||||
|
/** Любая из записей даты; внутри только незахватывающие группы. */
|
||||||
|
static final String DATE_ANY =
|
||||||
|
"(?:" + DATE_WORDS + "|" + DATE_MONTH_WORD + "|" + DATE_DIGITS + "|"
|
||||||
|
+ DATE_DIGITS_SPACE + "|" + DATE_DAY_MONTH + ")";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Промежуток между якорем даты («дата рождения») и самой датой: слова,
|
||||||
|
* скобочные группы («(день и месяц)») и знаки препинания. Без скобочной
|
||||||
|
* ветки «Дата рождения клиента (день и месяц): 15 января» не находилась бы:
|
||||||
|
* «день и месяц» — это слова, а не дата. Ветка со словами требует пробела
|
||||||
|
* перед словом ({@code \s+}), иначе она неоднозначна с веткой {@code \W},
|
||||||
|
* которая тоже матчит пробелы, — это приводило к катастрофическому
|
||||||
|
* возврату на длинных текстах. Отдельная ветка с дефисом нужна для слитных
|
||||||
|
* слов без пробела внутри: «клиента-нерезидента» — дефис сам по себе ловится
|
||||||
|
* веткой {@code \W}, но следующие за ним буквы без пробела перед ними не
|
||||||
|
* покрывала ни одна ветка.
|
||||||
|
*/
|
||||||
|
static final String DATE_GAP = "(?:\\s+\\([^)]*\\)|\\s+\\p{L}+|-\\p{L}+|\\W){0,30}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значение гражданства: «рф»/«росс…»(любая форма, включая строчную «российское»)/
|
||||||
|
* «республики X» — частые формы отдельным списком; последняя ветка — страна из
|
||||||
|
* 1-4 слов с заглавной буквы («Армения», «Соединенные Штаты Америки»). Хвост
|
||||||
|
* идёт после якоря «гражданств», поэтому «Двойное» перед якорем не попадёт.
|
||||||
|
*/
|
||||||
|
static final String CITIZENSHIP_VALUE =
|
||||||
|
"\\p{Lu}\\p{Ll}+(?:[\\s/]+\\p{Lu}\\p{Ll}+){0,3}|\\p{Ll}+(?:[\\s/]+\\p{Ll}+){0,3}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Слова, при которых адрес/имя принадлежит организации, а не человеку:
|
||||||
|
* адрес отделения банка персональными данными не является.
|
||||||
|
*/
|
||||||
|
static final String ORGANISATION_NEARBY =
|
||||||
|
"(?iu:отделени|филиал|банкомат|доп\\.?\\s?офис|офис|головн|юридическ\\p{L}*\\s+адрес)";
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ import ru.pdguard.config.SystemPolicy;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Реестр правил детекции и сам поиск ПД в тексте.
|
* Реестр правил детекции и сам поиск ПД в тексте.
|
||||||
@@ -23,123 +25,15 @@ import java.util.regex.Pattern;
|
|||||||
* <p>Якорные слова распознаются без учёта регистра — флаг {@code (?iu:...)} навешен
|
* <p>Якорные слова распознаются без учёта регистра — флаг {@code (?iu:...)} навешен
|
||||||
* именно на них. На захватываемое значение регистронезависимость не распространяется:
|
* именно на них. На захватываемое значение регистронезависимость не распространяется:
|
||||||
* там, где значение опознаётся по заглавной букве, это существенно.
|
* там, где значение опознаётся по заглавной букве, это существенно.
|
||||||
|
*
|
||||||
|
* <p>Сами правила сгруппированы по категориям в отдельных классах пакета —
|
||||||
|
* {@link DocumentRules}, {@link FinanceRules}, {@link DateRules}, {@link FioRules},
|
||||||
|
* {@link ContactRules}, {@link AddressRules} — чтобы каждая категория читалась
|
||||||
|
* отдельно от остальных. Здесь их списки только объединяются и используются.
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class RuleRegistry {
|
public class RuleRegistry {
|
||||||
|
|
||||||
/**
|
|
||||||
* Слово с заглавной буквы; остальные буквы любого регистра, чтобы
|
|
||||||
* «ИВАНОВ» распознавался наравне с «Иванов».
|
|
||||||
*/
|
|
||||||
private static final String CAPITALISED = "\\p{Lu}[\\p{Lu}\\p{Ll}]+";
|
|
||||||
|
|
||||||
/** Якорное слово-основа: держатель карты, держателем и т.п. */
|
|
||||||
private static final String HOLDER_STEM = "держател";
|
|
||||||
|
|
||||||
/** Разрыв между якорем и значением, когда между ними ролевое слово («ИНН плательщика»). */
|
|
||||||
private static final String ROLE_GAP = "(?:\\s+[\\p{L}-]+){0,5}\\W{0,10}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* То же самое, но только строчные слова-филлеры: ролевые слова перед значением
|
|
||||||
* гражданства всегда строчные («бенефициара», «поручителя»), а само значение —
|
|
||||||
* с заглавной («Республики», «Соединенные»). Обычный {@link #ROLE_GAP} жадно
|
|
||||||
* поглощал бы и заглавное слово значения как будто это ролевое слово, оставляя
|
|
||||||
* CITIZENSHIP_VALUE только хвост («Республики Беларусь» → «Беларусь»).
|
|
||||||
*/
|
|
||||||
private static final String CITIZENSHIP_GAP = "(?:\\s+\\p{Ll}[\\p{L}-]*){0,5}\\W{0,10}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Название улицы: от одного до трёх слов с заглавной буквы либо чисел —
|
|
||||||
* «Тверская», «Малая Никитская», «8 Марта». Ограничение по форме обязательно:
|
|
||||||
* без него правило дожёвывало строку до конца, и «Проспект Вернадского перекрыт
|
|
||||||
* до вечера» оказывался под маской целиком.
|
|
||||||
*/
|
|
||||||
private static final String STREET_NAME =
|
|
||||||
"(?:\\p{Lu}[\\p{L}-]+|\\d+[\\p{L}-]*)(?:\\s+(?:\\p{Lu}[\\p{L}-]+|\\d+[\\p{L}-]*)){0,2}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Фамилия по словообразованию: Иванов, Ковалёва, Троицкий, Шевченко, Мкртчян.
|
|
||||||
* Хвост из двух букв покрывает падежные окончания: Ковалёв-ой, Иванов-а.
|
|
||||||
*/
|
|
||||||
private static final String SURNAME =
|
|
||||||
"\\p{Lu}[\\p{Lu}\\p{Ll}]*(?iu:ов|ев|ёв|ин|ын|ск(?:ий|ая|ого|ой|ом)|цк(?:ий|ая)"
|
|
||||||
+ "|енко|ко|ук|юк|ян|швили|дзе)\\p{L}{0,2}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Отчество: признак надёжный, ни одно другое слово так не оканчивается.
|
|
||||||
* Основы даны без падежного окончания — Иванович, Ивановича, Ивановне.
|
|
||||||
*/
|
|
||||||
private static final String PATRONYMIC =
|
|
||||||
"\\p{Lu}[\\p{Lu}\\p{Ll}]+(?iu:ович|евич|ьич|мич|нич|тич|лич|кич|бич|сич"
|
|
||||||
+ "|овн|евн|иничн|ичн)\\p{L}{0,2}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Серия и номер: «4509 123456», «45 09 123456», «4509123456», «45 09 № 123456»,
|
|
||||||
* а также с произвольным числом пробелов и словом «номер» между частями —
|
|
||||||
* «12 34 номер 567890» (реальный кейс из бланка).
|
|
||||||
*/
|
|
||||||
private static final String SERIES_AND_NUMBER =
|
|
||||||
"\\d{2}\\s*\\d{2}(?:\\s*(?:№|N|номер)\\s*|[\\s№N]{0,3})\\d{6}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Название месяца: полная форма («январь»), сокращение («янв») и плейсхолдер
|
|
||||||
* «ммм» (в логах встречается и латинская «M»). Сокращения нужны, потому что
|
|
||||||
* в датах вида «15 ЯНВ 10» месяц записан тремя буквами.
|
|
||||||
*/
|
|
||||||
private static final String MONTH =
|
|
||||||
"(?iu:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр"
|
|
||||||
+ "|янв|фев|мар|апр|авг|сен|окт|ноя|дек|[МM]мм)\\p{L}*";
|
|
||||||
|
|
||||||
/** Числовая запись при любом порядке частей: дд.мм.гггг, мм/дд/гггг, гггг-мм-дд. */
|
|
||||||
private static final String DATE_DIGITS = "\\b\\d{1,4}[.\\-/]\\d{1,2}[.\\-/]\\d{1,4}\\b";
|
|
||||||
|
|
||||||
/** «15 03 1990», «15 03 10» — числовая дата с пробелами вместо разделителей. */
|
|
||||||
private static final String DATE_DIGITS_SPACE = "\\b\\d{1,2}\\s+\\d{1,2}\\s+\\d{2,4}\\b";
|
|
||||||
|
|
||||||
/** «15 03», «15/03» — день и месяц без года. */
|
|
||||||
private static final String DATE_DAY_MONTH = "\\b\\d{1,2}\\s*[-/.]?\\s*\\d{1,2}\\b";
|
|
||||||
|
|
||||||
/** «12 мая 1985 г.», «15-ЯНВ-10», «15 января» — месяц словом, год 2-4 цифры или без года. */
|
|
||||||
private static final String DATE_MONTH_WORD =
|
|
||||||
"\\b\\d{1,2}\\s*[-/.]?\\s*" + MONTH + "\\s*[-/.]?\\s*(?:\\d{2,4})?\\b"
|
|
||||||
+ "(?:\\s*(?iu:года|г\\.|г\\b))?";
|
|
||||||
|
|
||||||
/** «двенадцатого мая тысяча девятьсот восемьдесят пятого года» */
|
|
||||||
private static final String DATE_WORDS =
|
|
||||||
"\\b(?:(?iu:двадцать|тридцать)\\s+)?"
|
|
||||||
+ "(?iu:перв|втор|треть|четв[её]рт|пят|шест|седьм|восьм|девят|десят|одиннадцат|двенадцат"
|
|
||||||
+ "|тринадцат|четырнадцат|пятнадцат|шестнадцат|семнадцат|восемнадцат|девятнадцат|двадцат|тридцат)"
|
|
||||||
+ "(?iu:ьего|ого|его|ое)\\s+" + MONTH
|
|
||||||
+ "\\s+(?:\\d{4}|(?iu:тысяча)(?:\\s+\\p{L}+){1,8})\\s*(?iu:года|год\\b|г\\.)";
|
|
||||||
|
|
||||||
/** Любая из записей даты; внутри только незахватывающие группы. */
|
|
||||||
private static final String DATE_ANY =
|
|
||||||
"(?:" + DATE_WORDS + "|" + DATE_MONTH_WORD + "|" + DATE_DIGITS + "|"
|
|
||||||
+ DATE_DIGITS_SPACE + "|" + DATE_DAY_MONTH + ")";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Промежуток между якорем даты («дата рождения») и самой датой: слова,
|
|
||||||
* скобочные группы («(день и месяц)») и знаки препинания. Без скобочной
|
|
||||||
* ветки «Дата рождения клиента (день и месяц): 15 января» не находилась бы:
|
|
||||||
* «день и месяц» — это слова, а не дата. Ветка со словами требует пробела
|
|
||||||
* перед словом ({@code \s+}), иначе она неоднозначна с веткой {@code \W},
|
|
||||||
* которая тоже матчит пробелы, — это приводило к катастрофическому
|
|
||||||
* возврату на длинных текстах. Отдельная ветка с дефисом нужна для слитных
|
|
||||||
* слов без пробела внутри: «клиента-нерезидента» — дефис сам по себе ловится
|
|
||||||
* веткой {@code \W}, но следующие за ним буквы без пробела перед ними не
|
|
||||||
* покрывала ни одна ветка.
|
|
||||||
*/
|
|
||||||
private static final String DATE_GAP = "(?:\\s+\\([^)]*\\)|\\s+\\p{L}+|-\\p{L}+|\\W){0,30}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Значение гражданства: «рф»/«росс…»(любая форма, включая строчную «российское»)/
|
|
||||||
* «республики X» — частые формы отдельным списком; последняя ветка — страна из
|
|
||||||
* 1-4 слов с заглавной буквы («Армения», «Соединенные Штаты Америки»). Хвост
|
|
||||||
* идёт после якоря «гражданств», поэтому «Двойное» перед якорем не попадёт.
|
|
||||||
*/
|
|
||||||
private static final String CITIZENSHIP_VALUE =
|
|
||||||
"\\p{Lu}\\p{Ll}+(?:[\\s/]+\\p{Lu}\\p{Ll}+){0,3}|\\p{Ll}+(?:[\\s/]+\\p{Ll}+){0,3}";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Слова, при которых адрес принадлежит организации, а не человеку:
|
* Слова, при которых адрес принадлежит организации, а не человеку:
|
||||||
* адрес отделения банка персональными данными не является. Части адреса рядом:
|
* адрес отделения банка персональными данными не является. Части адреса рядом:
|
||||||
@@ -154,7 +48,7 @@ public class RuleRegistry {
|
|||||||
Pattern.compile(ADDRESS_NEARBY, Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE);
|
Pattern.compile(ADDRESS_NEARBY, Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE);
|
||||||
|
|
||||||
/** Адресные типы, которые вне адресного окружения персональными данными не являются. */
|
/** Адресные типы, которые вне адресного окружения персональными данными не являются. */
|
||||||
private static final java.util.Set<String> ADDRESS_TYPES = java.util.Set.of(
|
private static final Set<String> ADDRESS_TYPES = Set.of(
|
||||||
PdTypes.ADDRESS_COUNTRY, PdTypes.ADDRESS_REGION, PdTypes.ADDRESS_DISTRICT, PdTypes.ADDRESS_CITY,
|
PdTypes.ADDRESS_COUNTRY, PdTypes.ADDRESS_REGION, PdTypes.ADDRESS_DISTRICT, PdTypes.ADDRESS_CITY,
|
||||||
PdTypes.ADDRESS_STREET, PdTypes.ADDRESS_HOUSE, PdTypes.ADDRESS_FLAT, PdTypes.ADDRESS_POSTCODE);
|
PdTypes.ADDRESS_STREET, PdTypes.ADDRESS_HOUSE, PdTypes.ADDRESS_FLAT, PdTypes.ADDRESS_POSTCODE);
|
||||||
|
|
||||||
@@ -171,398 +65,11 @@ public class RuleRegistry {
|
|||||||
return ADDRESS_CONTEXT.matcher(surroundings(text, start, end)).find();
|
return ADDRESS_CONTEXT.matcher(surroundings(text, start, end)).find();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String ORGANISATION_NEARBY =
|
private static final List<Rule> RULES = Stream.of(
|
||||||
"(?iu:отделени|филиал|банкомат|доп\\.?\\s?офис|офис|головн|юридическ\\p{L}*\\s+адрес)";
|
DocumentRules.RULES, FinanceRules.RULES, DateRules.RULES,
|
||||||
|
FioRules.RULES, ContactRules.RULES, AddressRules.RULES)
|
||||||
private static final List<Rule> RULES = List.of(
|
.flatMap(List::stream)
|
||||||
|
.toList();
|
||||||
// --- Уровень 3: значение опознаётся только рядом с якорным словом ---
|
|
||||||
|
|
||||||
// CVV: латиница, кириллическая транслитерация («цвв», «сививи») и
|
|
||||||
// описательные якоря («код на обороте карты»). Между якорем и числом
|
|
||||||
// допускаются слова («CVV код 321», «CVV указан код 123») и длинные
|
|
||||||
// разделители («код на обороте карты 789»).
|
|
||||||
Rule.of(PdTypes.CVV, "(?iu:\\b(?:cvv2?|cvc2?|цвв|сививи|код\\p{L}*\\s+на\\s+обороте\\s+карты"
|
|
||||||
+ "|код\\s+проверки|защитный\\s+код)\\b)"
|
|
||||||
+ "(?:\\s+\\p{L}+){0,2}\\W{0,30}(\\d{3,4})\\b", 92)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("cvv", "cvc", "цвв", "сививи", "код на обороте", "код проверки", "защитный код"),
|
|
||||||
|
|
||||||
// PIN: «пин-код», «пин код», «ПИН:», «пин 3456». Между якорем и числом
|
|
||||||
// допускаются слова («ПИН-код карты 2468») и длинные разделители
|
|
||||||
// («Пин Код: 1234»).
|
|
||||||
Rule.of(PdTypes.PIN, "(?iu:\\b(?:пин[\\s-]?кода?|pin[\\s-]?code|пин|pin)\\b)"
|
|
||||||
+ "(?:\\s+\\p{L}+){0,2}\\W{0,30}(\\d{4,6})\\b", 92)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("пин", "pin"),
|
|
||||||
|
|
||||||
// «паспорт 4509 123456», «паспорт гражданина РФ 45 09 123456»
|
|
||||||
Rule.of(PdTypes.PASSPORT, "(?iu:паспорт)\\w*(?:\\W+(?iu:гражданина\\s+РФ|РФ|России|Российской\\s+Федерации))?"
|
|
||||||
+ "\\W{0,10}(" + SERIES_AND_NUMBER + ")\\b", 90)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("паспорт"),
|
|
||||||
|
|
||||||
// «серия 4509 номер 123456», «серии 45 09 № 123456»
|
|
||||||
// Между серией и номером помещается слово: «серия 4509 номер 123456»,
|
|
||||||
// «серии 4509 за номером 123456», «серия 4509 № 123456».
|
|
||||||
Rule.of(PdTypes.PASSPORT, "(?iu:сери)\\w{0,3}\\W{0,5}(\\d{2}\\s?\\d{2})[^\\d]{0,20}(\\d{6})\\b", 90)
|
|
||||||
.groups(1, 2)
|
|
||||||
.anchoredBy("сери"),
|
|
||||||
|
|
||||||
// Необязательное «серия»/«серии» между якорем и цифрами: «ВУ серия 12 34 номер 567890».
|
|
||||||
Rule.of(PdTypes.DRIVER_LICENSE, "(?iu:водительск\\w+\\s+удостоверени\\w+|в/у|вод\\.\\s?удост\\w*|\\bВУ)\\b"
|
|
||||||
+ "\\W{0,15}(?:(?iu:сери\\w{0,3})\\W{0,5})?(" + SERIES_AND_NUMBER + ")\\b", 89)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("водительск", "в/у", "вод.", "ву "),
|
|
||||||
|
|
||||||
// --- Прочие документы, удостоверяющие личность ---
|
|
||||||
|
|
||||||
Rule.of(PdTypes.FOREIGN_PASSPORT, "(?iu:загранпаспорт|заграничн\\p{L}*\\s+паспорт)\\p{L}*"
|
|
||||||
+ "\\W{0,10}(\\d{2}\\s?\\d{7})\\b", 89)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("загранпаспорт", "заграничн"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.MILITARY_ID, "(?iu:военн\\p{L}*\\s+билет)\\p{L}*"
|
|
||||||
+ "\\W{0,10}(\\p{Lu}{2}\\s?\\d{7})\\b", 89)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("военн"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.BIRTH_CERTIFICATE, "(?iu:свидетельств\\p{L}*\\s+о\\s+рождении)"
|
|
||||||
+ "\\W{0,15}([IVXLC]{1,4}[- ]?\\p{Lu}{2}\\s?(?:№\\s?)?\\d{6})\\b", 89)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("свидетельств"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.MEDICAL_POLICY, "(?iu:полис\\p{L}*(?:\\s+ОМС)?)\\W{0,10}(\\d{16})\\b", 89)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("полис"),
|
|
||||||
|
|
||||||
// ROLE_GAP, не \W{0,5}: «код подразделения стоит 001-000» — между якорем и
|
|
||||||
// значением есть слово («стоит»/«объекта»), не только пунктуация.
|
|
||||||
Rule.of(PdTypes.DEPT_CODE, "(?iu:код\\w*\\s+подразделения|к/п)" + ROLE_GAP
|
|
||||||
+ "(\\d{3}\\s?-?\\s?\\d{3})\\b", 88)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("подразделени", "к/п"),
|
|
||||||
|
|
||||||
// «770-001 — таков код подразделения» — значение перед якорем.
|
|
||||||
Rule.of(PdTypes.DEPT_CODE, "\\b(\\d{3}\\s?-?\\s?\\d{3})\\b\\s*[—-]\\s*(?:\\p{L}+\\s+){0,3}"
|
|
||||||
+ "(?iu:код\\w*\\s+подразделения)", 88)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("подразделени"),
|
|
||||||
|
|
||||||
// --- Банковские реквизиты сверх карты ---
|
|
||||||
|
|
||||||
// Расчётный счёт — ровно 20 цифр после якоря, группировка пробелами не важна.
|
|
||||||
Rule.of(PdTypes.ACCOUNT_NUMBER, "(?iu:р/с|расчетн\\w*\\s+счет|расчётн\\w*\\s+счёт|лицев\\w*\\s+счет|"
|
|
||||||
+ "лицев\\w*\\s+счёт)\\W{0,5}((?:\\d[ ]?){19}\\d)\\b", 83)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("р/с", "расчетн", "расчётн", "лицев"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.BIK, "(?iu:бик)\\W{0,5}(\\d{9})\\b", 83)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("бик"),
|
|
||||||
|
|
||||||
// «действительна до 09/27», «exp 09/27» — срок действия карты, не дата рождения.
|
|
||||||
Rule.of(PdTypes.CARD_EXPIRY, "(?iu:срок\\s+действия|действительна?\\s+до|\\bexp\\w*)\\W{0,5}"
|
|
||||||
+ "(\\d{2}\\s?/\\s?\\d{2})\\b", 83)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("срок действия", "действительн", "exp"),
|
|
||||||
|
|
||||||
// ОГРНИП раньше ОГРН: без отрицательного просмотра «ОГРНИП» частично ловился бы
|
|
||||||
// ещё и правилом ОГРН. Контрольная сумма отсекает случайные 13/15-значные
|
|
||||||
// числа рядом со словом — раньше якоря было достаточно самого по себе.
|
|
||||||
Rule.of(PdTypes.OGRNIP, "(?iu:огрнип)\\W{0,5}(\\d{15})\\b", 83)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(Validators::ogrnip)
|
|
||||||
.anchoredBy("огрнип"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.OGRN, "(?iu:огрн(?!ип))\\W{0,5}(\\d{13})\\b", 83)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(Validators::ogrn)
|
|
||||||
.anchoredBy("огрн"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.KPP, "(?iu:кпп)\\W{0,5}(\\d{9})\\b", 83)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("кпп"),
|
|
||||||
|
|
||||||
// Доход/зарплата: сумма с разделителями тысяч. Между якорем и суммой может
|
|
||||||
// стоять слово («доход клиента», «доход за год») — без этого якорь ловил
|
|
||||||
// бы только «доход 85000», вплотную.
|
|
||||||
Rule.of(PdTypes.INCOME, "(?iu:доход|заработн\\w*\\s+плат\\w*|зарплат\\w*)(?:\\s+\\p{L}+){0,3}?"
|
|
||||||
+ "\\W{0,5}(\\d{1,3}(?:[\\s.]?\\d{3})*(?:,\\d{2})?)\\s?(?iu:руб\\p{L}*|₽)?\\b", 76)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("доход", "заработн", "зарплат"),
|
|
||||||
|
|
||||||
// Биометрия — сама фраза уже говорит, что дальше персональные данные, отдельного
|
|
||||||
// значения для захвата нет: маскируется якорная фраза целиком.
|
|
||||||
Rule.of(PdTypes.BIOMETRIC, "(?iu:биометрическ\\w*\\s+(?:данны\\w*|образц\\w*|шаблон\\w*)"
|
|
||||||
+ "|слепок\\s+голоса|отпечаток\\s+пальца|скан\\s+лица|\\bЕБС\\b)", 81)
|
|
||||||
.anchoredBy("биометри", "слепок голоса", "отпечаток пальца", "скан лица", "ебс"),
|
|
||||||
|
|
||||||
// --- Даты с явным якорем ---
|
|
||||||
|
|
||||||
Rule.of(PdTypes.BIRTH_DATE, "(?iu:дат\\p{L}*\\s+рождения|дата\\s+рожд\\.)"
|
|
||||||
+ DATE_GAP + "(" + DATE_ANY + ")", 87)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(Validators::date)
|
|
||||||
.anchoredBy("рожден"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.BIRTH_DATE, "(?iu:родил(?:ся|ась))"
|
|
||||||
+ DATE_GAP + "(" + DATE_ANY + ")", 87)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(Validators::date)
|
|
||||||
.anchoredBy("родил"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.BIRTH_DATE, "(" + DATE_ANY + ")\\s*(?iu:г\\.\\s?р\\.|г/р|года\\s+рождения)", 87)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(Validators::date)
|
|
||||||
.anchoredBy("г.р", "г/р", "года рождения"),
|
|
||||||
|
|
||||||
// «дата выдачи 12.05.2015» и «дата выдачи паспорта 12.05.2015»
|
|
||||||
Rule.of(PdTypes.PASSPORT_DATE, "(?iu:дат\\p{L}*\\s+выдачи)"
|
|
||||||
+ DATE_GAP + "(" + DATE_ANY + ")", 87)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(Validators::date)
|
|
||||||
.anchoredBy("выдач"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.CARDHOLDER, "(?iu:держател\\w*(?:\\s+карты)?|cardholder|на\\s+имя)"
|
|
||||||
+ "\\W{0,10}([A-Z]{2,20}\\s+[A-Z]{2,20})\\b", 86)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy(HOLDER_STEM, "cardholder", "на имя"),
|
|
||||||
|
|
||||||
// --- ФИО ---
|
|
||||||
|
|
||||||
// Фамилия Имя Отчество: первое слово опознаётся по словообразованию фамилии.
|
|
||||||
// Свободная тройка «любое слово с заглавной + имя + отчество» здесь
|
|
||||||
// сознательно не используется: она захватывает глагол в начале
|
|
||||||
// предложения («Пригласите Ивана Сергеевича») и заметно дороже по времени.
|
|
||||||
// Фамилии без привычного окончания — Ким, Цой — ловятся по ролевому слову.
|
|
||||||
Rule.of(PdTypes.FIO, "\\b" + SURNAME + "\\s+" + CAPITALISED + "\\s+" + PATRONYMIC + "\\b", 79),
|
|
||||||
|
|
||||||
// Имя Отчество Фамилия — второй распространённый порядок слов.
|
|
||||||
Rule.of(PdTypes.FIO, "\\b" + CAPITALISED + "\\s+" + PATRONYMIC + "\\s+" + SURNAME + "\\b", 79),
|
|
||||||
|
|
||||||
// Иванов И.И. и И.И. Иванов
|
|
||||||
Rule.of(PdTypes.FIO, "\\b" + SURNAME + "\\s+\\p{Lu}\\.\\s?\\p{Lu}\\.", 79),
|
|
||||||
Rule.of(PdTypes.FIO, "\\b\\p{Lu}\\.\\s?\\p{Lu}\\.\\s?" + SURNAME + "\\b", 79),
|
|
||||||
|
|
||||||
// Имя Отчество без фамилии
|
|
||||||
Rule.of(PdTypes.FIO, "\\b" + CAPITALISED + "\\s+" + PATRONYMIC + "\\b", 77),
|
|
||||||
|
|
||||||
// «ФИО: иванов иван иванович» — явный якорь снимает требование к регистру
|
|
||||||
Rule.of(PdTypes.FIO, "(?iu:\\bФИО|\\bф\\.\\s?и\\.\\s?о\\.|\\bна\\s+имя)"
|
|
||||||
+ "(?:\\s+\\p{L}+)?\\W{0,5}(\\p{L}{2,}(?:\\s+\\p{L}{2,}){0,2})\\b", 77)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("фио", "ф.и.о", "на имя"),
|
|
||||||
|
|
||||||
// «клиент Иванов Иван», «плательщик Петрова»
|
|
||||||
Rule.of(PdTypes.FIO, "(?iu:\\bклиент|\\bзаказчик|\\bпациент|\\bсотрудник|\\bвладел|\\bплательщик"
|
|
||||||
+ "|\\bполучател|\\bабонент|\\bв\\s+лице|\\bпредставител|\\bпоручител"
|
|
||||||
+ "|\\bсозаёмщик|\\bсозаемщик|\\bзаёмщик|\\bзаемщик|\\bзаявител|\\bдоверител"
|
|
||||||
+ "|\\bвкладчик|\\bответственн|\\bконтактное\\s+лицо|\\bисполнител|\\bдержател)\\p{L}*"
|
|
||||||
+ "\\W{0,5}(\\p{Lu}\\p{Ll}+(?:\\s+\\p{Lu}\\p{Ll}+){0,2})\\b", 77)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
|
||||||
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
|
||||||
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
|
||||||
"контактное лицо", "исполнител", HOLDER_STEM),
|
|
||||||
|
|
||||||
// «клиент иван иванов», «поручитель петрович» — строчные имена после
|
|
||||||
// ролевого слова. Регистр снимает требование к заглавной букве, а словарь
|
|
||||||
// имён отсекает «клиент пришёл в офис».
|
|
||||||
Rule.of(PdTypes.FIO, "(?iu:\\bклиент|\\bзаказчик|\\bпациент|\\bсотрудник|\\bвладел|\\bплательщик"
|
|
||||||
+ "|\\bполучател|\\bабонент|\\bв\\s+лице|\\bпредставител|\\bпоручител"
|
|
||||||
+ "|\\bсозаёмщик|\\bсозаемщик|\\bзаёмщик|\\bзаемщик|\\bзаявител|\\bдоверител"
|
|
||||||
+ "|\\bвкладчик|\\bответственн|\\bконтактное\\s+лицо|\\bисполнител|\\bдержател"
|
|
||||||
+ "|\\bотправител|\\bбенефициар|\\bдоверенное\\s+лицо|\\bнаследник|\\bсозаемщик"
|
|
||||||
// \p{L}*+ (possessive), не \p{L}*: без possessive откат назад позволял
|
|
||||||
// движку «отдать» уже съеденное падежное окончание ролевого слова и
|
|
||||||
// захватить его как будто отдельное имя — «пациентов» ловилось бы как «ов».
|
|
||||||
+ "|\\bпоручител)\\p{L}*+"
|
|
||||||
+ "(?:\\s+\\p{L}+){0,3}\\W{0,5}(\\p{L}{2,}(?:\\s+\\p{L}{2,}){0,2}(?:\\s+\\p{Lu}\\.){0,2})(?![\\p{L}.])", 77)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(NameDictionary::containsNamePart)
|
|
||||||
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
|
|
||||||
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
|
|
||||||
"заемщик", "заявител", "доверител", "вкладчик", "ответственн",
|
|
||||||
"контактное лицо", "исполнител", HOLDER_STEM, "отправител", "бенефициар",
|
|
||||||
"доверенное лицо", "наследник", "созаемщик"),
|
|
||||||
|
|
||||||
// Фамилия рядом с личным именем из словаря: без словаря правило ловило бы
|
|
||||||
// «Тверская улица» и тому подобное. Имя проверяется по множеству уже
|
|
||||||
// после совпадения — чередование из ста веток в шаблоне обходится дорого.
|
|
||||||
// Самое слабое основание среди правил ФИО — ни ролевого слова, ни явного
|
|
||||||
// якоря, — поэтому именно здесь нужно вето на адресный контекст: «Великие
|
|
||||||
// Луки» (реальный город) распознаётся как имя «Лука» в падеже плюс
|
|
||||||
// случайное слово, «Богдана Хмельницкого» — улица в честь исторической
|
|
||||||
// фигуры. Найдено на реальных адресах отделений из реестра ЦБ.
|
|
||||||
Rule.of(PdTypes.FIO, "\\b" + SURNAME + "\\s+" + CAPITALISED + "\\b", 74)
|
|
||||||
.validatedBy(NameDictionary::containsGivenName)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY),
|
|
||||||
Rule.of(PdTypes.FIO, "\\b" + CAPITALISED + "\\s+" + SURNAME + "\\b", 74)
|
|
||||||
.validatedBy(NameDictionary::containsGivenName)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY),
|
|
||||||
|
|
||||||
// Одиночное имя, фамилия или отчество: «Иванов», «иван», «петрович».
|
|
||||||
// Самое слабое основание среди правил ФИО — ни ролевого слова, ни пары
|
|
||||||
// слов, — поэтому приоритет ниже и проверка по словарю обязательна.
|
|
||||||
// Словарь отсекает «сочи», «казань» и прочие не-имена. Первое слово текста
|
|
||||||
// не рассматривается: заглавная буква там от начала предложения, а не от
|
|
||||||
// имени, и словообразовательная эвристика ложно ловит «Магазин», «Отдел».
|
|
||||||
Rule.of(PdTypes.FIO, "(?<!^)\\b(\\p{L}{2,})\\b", 70)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(NameDictionary::isStandaloneNameCandidate),
|
|
||||||
|
|
||||||
// --- Уровень 1: подтверждается контрольной суммой ---
|
|
||||||
|
|
||||||
Rule.of(PdTypes.CARD, "\\b\\d(?:[ -]?\\d){11,18}\\b", 85)
|
|
||||||
.validatedBy(Validators::luhn),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.INN, "(?iu)\\bИНН\\b" + ROLE_GAP + "(\\d{12}|\\d{10})\\b", 84)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("инн"),
|
|
||||||
|
|
||||||
// «ИНН/КПП 7712345671/771201001» — ИНН юрлица перед КПП через слэш.
|
|
||||||
Rule.of(PdTypes.INN, "(?iu)\\bИНН\\s*/\\s*КПП\\b\\W{0,5}(\\d{10})\\b", 84)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("инн/кпп"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.SNILS, "(?iu)(?:\\bСНИЛС\\b\\D{0,10})?(\\d{3}[ -]\\d{3}[ -]\\d{3}[ -]\\d{2})\\b", 84)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(Validators::snils),
|
|
||||||
|
|
||||||
// --- Уровень 2: формат однозначен сам по себе ---
|
|
||||||
|
|
||||||
// \b7, не только +7: номер без плюса («79031119955») тоже встречается.
|
|
||||||
Rule.of(PdTypes.PHONE, "(?:\\+7|\\b7|\\b8)[ ()-]{0,3}\\d{3}[ ()-]{0,3}\\d{3}[ -]{0,2}\\d{2}[ -]{0,2}\\d{2}\\b", 82),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.EMAIL, "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b", 80)
|
|
||||||
.anchoredBy("@"),
|
|
||||||
|
|
||||||
// --- Уровень 3: свободный текст после якорного слова ---
|
|
||||||
|
|
||||||
// «выдан ОУФМС России по г. Москве 12.05.2015» — дата в состав органа не входит,
|
|
||||||
// её забирает отдельное правило. Приоритет выше городского, иначе от органа
|
|
||||||
// осталась бы замаскированной только его часть.
|
|
||||||
// Перечень форм, не голая основа «выда»: она зацепила бы и «выдающийся»
|
|
||||||
// (обычное слово, не про выдачу документа).
|
|
||||||
Rule.of(PdTypes.PASSPORT_ISSUER, "(?iu:выдан|выдал[аио]?|выдали|выдач[аи]|выдаче)"
|
|
||||||
+ "\\W{0,3}([^,;\\n]{3,90}?)"
|
|
||||||
+ "(?=\\s*\\d{1,2}[.\\-/]\\d{1,2}[.\\-/]\\d{2,4}|[,;\\n]|\\s*$)", 78)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("выдан", "выдал", "выдач"),
|
|
||||||
|
|
||||||
// «совпадает с указанным в анкете: X» — второе упоминание органа выдачи
|
|
||||||
// под собственным якорем, без бэкреференса на первое.
|
|
||||||
Rule.of(PdTypes.PASSPORT_ISSUER, "(?iu:указанн\\w*\\s+в\\s+анкете)\\W{0,5}([^,;.\\n]{3,90}?)"
|
|
||||||
+ "(?=[,;.\\n]|\\s*$)", 78)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("указанн"),
|
|
||||||
|
|
||||||
// «Орган выдачи УФМС России по Республике Татарстан» — орган после якоря,
|
|
||||||
// до слова «совпадает» или конца фразы.
|
|
||||||
Rule.of(PdTypes.PASSPORT_ISSUER, "(?iu:орган\\s+выдачи)\\W{0,5}([^,;:\\n]{3,90}?)"
|
|
||||||
+ "(?=\\s*(?iu:совпадает|указанн)|[,;:\\n]|\\s*$)", 78)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("орган выдачи"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.BIRTH_PLACE, "(?iu:мест\\w*\\s+рождения)\\W{0,5}([^,;\\n]{3,60}?)(?=\\s*[,;\\n]|\\s*$)", 76)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("рождения"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.BIRTH_PLACE, "(?iu:родил(?:ся|ась))[^,;\\n]{0,40}?\\s+в\\s+"
|
|
||||||
+ "([^,;\\n]{3,40}?)(?=\\s*[,;\\n]|\\s*$)", 76)
|
|
||||||
.groups(1)
|
|
||||||
.anchoredBy("родил"),
|
|
||||||
|
|
||||||
// ROLE_GAP, не \W{0,5}: «Гражданство бенефициара по договору страхования: Х» —
|
|
||||||
// между якорем и значением бывает несколько слов, не только пунктуация.
|
|
||||||
// Список через запятую/слэш — вторая опциональная группа тем же шаблоном.
|
|
||||||
Rule.of(PdTypes.CITIZENSHIP, "(?iu:гражданств)\\w*" + CITIZENSHIP_GAP
|
|
||||||
+ "(" + CITIZENSHIP_VALUE + ")(?:\\s*[,/]\\s*(" + CITIZENSHIP_VALUE + "))?", 80)
|
|
||||||
.groups(1, 2)
|
|
||||||
.validatedBy(CountryDictionary::isKnownCountry)
|
|
||||||
.anchoredBy("гражданств"),
|
|
||||||
|
|
||||||
// ин/ка/ина/ки — именительный/родительный; ином/кой — творительный
|
|
||||||
// («гражданином», «гражданкой»).
|
|
||||||
Rule.of(PdTypes.CITIZENSHIP, "(?iu:граждан(?:ин|ка|ина|ки|ином|кой))\\b\\s+"
|
|
||||||
+ "(" + CITIZENSHIP_VALUE + ")(?:\\s*[,/]\\s*(" + CITIZENSHIP_VALUE + "))?", 75)
|
|
||||||
.groups(1, 2)
|
|
||||||
.validatedBy(CountryDictionary::isKnownCountry)
|
|
||||||
.anchoredBy("граждан"),
|
|
||||||
|
|
||||||
// --- Адрес: каждая составляющая настраивается отдельно ---
|
|
||||||
|
|
||||||
Rule.of(PdTypes.ADDRESS_POSTCODE, "(?iu:индекс)\\w*" + ROLE_GAP + "(\\d{6})\\b", 74)
|
|
||||||
.groups(1)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY)
|
|
||||||
.anchoredBy("индекс"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.ADDRESS_POSTCODE,
|
|
||||||
"\\b(\\d{6})(?=\\s*,?\\s*(?iu:г\\.|город|обл\\.|область|респ|край))", 74)
|
|
||||||
.groups(1)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY),
|
|
||||||
|
|
||||||
// Не только «г.»: перепись, на которой проверяется словарь, покрывает
|
|
||||||
// сёла, посёлки, деревни, хутора и станицы — «рп. Ильинское», «с. Кукуево»
|
|
||||||
// из ТЗ без этих якорей не нашлись бы вообще, город там ни при чём.
|
|
||||||
Rule.of(PdTypes.ADDRESS_CITY, "(?iu:\\bг\\.|\\bгор\\.|\\bгород|\\bрп\\.|\\bпгт\\.?|\\bп\\.|\\bс\\.|\\bсело\\b"
|
|
||||||
+ "|\\bд\\.|\\bдеревня\\b|\\bдер\\.|\\bх\\.|\\bхутор\\b|\\bст-ца|\\bстаница|\\bаул\\b"
|
|
||||||
+ "|\\bсл\\.|\\bслобода\\b|\\bаал\\b)\\s?(\\p{Lu}[\\p{L}-]{1,30})\\b", 73)
|
|
||||||
.groups(1)
|
|
||||||
.validatedBy(ToponymDictionary::isKnownSettlement)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY)
|
|
||||||
.anchoredBy("г.", "гор", "город", "рп.", "пгт", "п.", "с.", "село", "д.", "деревня",
|
|
||||||
"дер.", "х.", "хутор", "ст-ца", "станица", "аул", "сл.", "слобода", "аал"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.ADDRESS_STREET,
|
|
||||||
"(?iu:\\bул\\.|\\bулиц\\p{L}*|\\bпр-т|\\bпроспект\\p{L}*|\\bпер\\.|\\bпереул\\p{L}*"
|
|
||||||
+ "|\\bш\\.|\\bшоссе|\\bб-р|\\bбульвар\\p{L}*|\\bнаб\\.|\\bнабережн\\p{L}*)"
|
|
||||||
+ "\\W{0,3}(" + STREET_NAME + ")", 73)
|
|
||||||
.groups(1)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY)
|
|
||||||
.requiringNear(ADDRESS_NEARBY)
|
|
||||||
.anchoredBy("ул", "просп", "пр-т", "пер.", "шоссе", "ш.", "бульвар", "б-р", "наб"),
|
|
||||||
|
|
||||||
// «Невский пр-т» — указатель после названия. Форма слишком общая, поэтому
|
|
||||||
// принимается только рядом с другими частями адреса: иначе под маску попал бы
|
|
||||||
// любой рассказ про Невский проспект.
|
|
||||||
Rule.of(PdTypes.ADDRESS_STREET, "\\b(\\p{Lu}[\\p{L}-]{2,30})\\s+"
|
|
||||||
+ "(?iu:пр-т|проспект|улиц\\p{L}*|шоссе|бульвар|переул\\p{L}*|набережн\\p{L}*)\\b", 73)
|
|
||||||
.groups(1)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY)
|
|
||||||
.requiringNear(ADDRESS_NEARBY)
|
|
||||||
.anchoredBy("пр-т", "проспект", "улиц", "шоссе", "бульвар", "переул", "набережн"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.ADDRESS_HOUSE,
|
|
||||||
"(?iu:\\bд\\.|\\bдом)\\s?(\\d+\\p{L}?(?:\\s?(?iu:к\\.|корп\\.?|стр\\.)\\s?\\d+)?)\\b", 72)
|
|
||||||
.groups(1)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY)
|
|
||||||
.anchoredBy("д.", "дом"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.ADDRESS_FLAT, "(?iu:\\bкв\\.|\\bквартир\\p{L}*)\\s?(\\d+\\p{L}?)\\b", 72)
|
|
||||||
.groups(1)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY)
|
|
||||||
.anchoredBy("кв"),
|
|
||||||
|
|
||||||
Rule.of(PdTypes.ADDRESS_COUNTRY,
|
|
||||||
"(?iu:стран\\p{L}*(?:\\s+(?:регистрации|проживания|гражданства))?)"
|
|
||||||
+ "\\W{0,5}(\\p{Lu}[\\p{L}-]{2,30})\\b", 71)
|
|
||||||
.groups(1)
|
|
||||||
.vetoedBy(ORGANISATION_NEARBY)
|
|
||||||
.anchoredBy("стран"),
|
|
||||||
|
|
||||||
// --- Значения без якоря: принимаются только вместе с другими ПД ---
|
|
||||||
|
|
||||||
// ИНН физлица без якорного слова — только с верной контрольной суммой.
|
|
||||||
Rule.of(PdTypes.INN, "\\b\\d{12}\\b", 62)
|
|
||||||
.validatedBy(Validators::inn),
|
|
||||||
|
|
||||||
// Дата без якорного слова персональными данными сама по себе не является:
|
|
||||||
// маскируется, только если в тексте есть ПД другого типа.
|
|
||||||
Rule.of(PdTypes.DATE, DATE_ANY, 58)
|
|
||||||
.validatedBy(Validators::date)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Все типы ПД, которые умеет распознавать сервис. */
|
/** Все типы ПД, которые умеет распознавать сервис. */
|
||||||
public List<String> knownTypes() {
|
public List<String> knownTypes() {
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ public enum MaskMode {
|
|||||||
/** Звёздочки с сохранением длины и разделителей: {@code 45** ****56}. */
|
/** Звёздочки с сохранением длины и разделителей: {@code 45** ****56}. */
|
||||||
MASK,
|
MASK,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Звёздочки без исключений: каждый тип закрывается целиком, даже те, что в
|
||||||
|
* {@link #MASK} частично открыты (края номера) или превращаются в инициалы
|
||||||
|
* (ФИО {@code Иванов Иван Иванович} → {@code ******* **** *********}, не
|
||||||
|
* {@code И. И. И.} — инициалы всё ещё выдают число слов и первую букву
|
||||||
|
* каждого).
|
||||||
|
*/
|
||||||
|
STRICT,
|
||||||
|
|
||||||
/** Порядковый токен: {@code [FIO_1]}. Компактно и однозначно обратимо. */
|
/** Порядковый токен: {@code [FIO_1]}. Компактно и однозначно обратимо. */
|
||||||
TOKEN,
|
TOKEN,
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ public class Masker {
|
|||||||
public String mask(String type, String value, MaskMode mode, MaskContext context) {
|
public String mask(String type, String value, MaskMode mode, MaskContext context) {
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
case MASK -> BY_TYPE.getOrDefault(type, Strategies::stars).apply(value);
|
case MASK -> BY_TYPE.getOrDefault(type, Strategies::stars).apply(value);
|
||||||
|
// STRICT игнорирует BY_TYPE целиком — ни один тип не открывает края
|
||||||
|
// и ФИО не превращается в инициалы, только сплошные звёздочки.
|
||||||
|
case STRICT -> Strategies.stars(value);
|
||||||
case TOKEN -> context.resolve(type, value, (t, n) -> "[" + t + "_" + n + "]");
|
case TOKEN -> context.resolve(type, value, (t, n) -> "[" + t + "_" + n + "]");
|
||||||
case SYNTHETIC -> context.resolve(type, value, (t, n) -> Synthetic.forType(t, value, n));
|
case SYNTHETIC -> context.resolve(type, value, (t, n) -> Synthetic.forType(t, value, n));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ class MaskModeTest {
|
|||||||
return pipeline.process(text, UUID.randomUUID().toString(), policy(mode));
|
return pipeline.process(text, UUID.randomUUID().toString(), policy(mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void strictModeHidesEverythingIncludingFio() {
|
||||||
|
String masked = mask(MaskMode.STRICT, "Клиент Иванов Иван Иванович, паспорт 4509 123456");
|
||||||
|
|
||||||
|
assertFalse(masked.contains("Иванов"), masked);
|
||||||
|
assertFalse(masked.contains("И. И. И."), "STRICT не должен превращать ФИО в инициалы: " + masked);
|
||||||
|
assertTrue(masked.contains("****** **** ********"), "ожидались звёздочки по длине каждого слова: " + masked);
|
||||||
|
assertTrue(masked.contains("**** ******"), "край паспорта не должен открываться: " + masked);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void tokenModeNumbersEachType() {
|
void tokenModeNumbersEachType() {
|
||||||
String masked = mask(MaskMode.TOKEN, "Клиент Иванов Иван Иванович, почта ivan@mail.ru");
|
String masked = mask(MaskMode.TOKEN, "Клиент Иванов Иван Иванович, почта ivan@mail.ru");
|
||||||
|
|||||||
Reference in New Issue
Block a user