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+адрес)"; }