refactor: разбить RuleRegistry на классы по категориям правил
RuleRegistry.java вырос до 621 строки — вынесены общие regex-фрагменты в RulePatterns и группы правил в DocumentRules, FinanceRules, DateRules, FioRules, ContactRules, AddressRules. RuleRegistry теперь только собирает списки и хранит публичный API (detect, isAddressType, hasAddressContext).
This commit is contained in:
@@ -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+адрес)";
|
||||
}
|
||||
Reference in New Issue
Block a user