Files
pd-guard/src/main/java/ru/pdguard/detect/RulePatterns.java
T
Максименко Никита Владимирович 573d94cca8 refactor: подтянуть Legal NER, переформатировать код и добавить тесты
Слияние с 685ec97 (третья ступень NER для юридических реквизитов),
код приведён к google-java-format, добавлены юнит-тесты
AdaptiveConcurrencyLimiter/SystemsConfig/PayloadCipher.
2026-09-23 22:02:45 +03:00

141 lines
9.2 KiB
Java
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.
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+адрес)";
}