ORegex: Від символів до об'єктів

Добрий вечір, хаброжители!
Сьогодні я хочу поділитися з вами таким ще молодим проектом, як ORegex або Object Regular Expressions. Я вже досить довго працюю в комп'ютерній лінгвістиці і хоч я не лінгвіст, але все ж бачу в мовах якісь усталені конструкції, шаблони.
Для тих кому цікаво, як я вирішив їх виділяти під кат.

Ці шаблони можуть бути як простими:
  • Смайли;
  • Хештеги;
  • Дати;
  • Телефони;
  • і т. д.
Так і складними:
  • Пряма мова;
  • Назви різноманітних компаній;
  • Імена;
  • Перерахування в тексті;
  • і т. д.


В основному моя робота полягала в тому, щоб зрозуміти, що і як витягти з послідовності об'єктів. Це можна робити через граматики, можна через автомати, а можна просто написати пару вкладених циклів. Але коли мені конкретно набридло писати тонни парсерів різноманітних послідовностей (токени, слова, поєднання слів і т. д.) з різною складністю і величезною кількістю помилок, мені прийшов у голову резонне питання — чи можна зробити простіше? Відповідь прийшла інтуїтивно: використовуй регулярні вирази для пошуку.

Але як? Регулярні вирази, звичайно, добре і швидко впораються із завданням пошуку за шаблоном, от тільки всі движки написані виключно під символьні послідовності, а ті, що заточені під об'єкти зовсім не радують своїми швидкісними якостями і взагалі знаходяться в інших мовних площинах. В результаті недовгих роздумів було прийнято рішення написати свій «велосипед з нормальною системою передач».

Проект я вирішив зробити з відкритим вихідним кодом по ряду причин, але зараз не про це.
Давайте поглянемо на те, як взагалі її можна використовувати в бойових умови.

How to use?

Над синтаксисом я довго не думав, було вирішено використовувати стандартну нотацію .NET + можливість додавати коментарі, писати нормальні імена для атомарних умов. Це дозволило би без проблем виносити патерни в окремі файли:

{MyPredicate1} | (?{MyPredicate2} {MyPredicate3}*)

Варто зазначити, що на даний момент деякі функції .NET Regex не включені (умовні оператори, lookahed), але в недалекому майбутньому вони обов'язково з'являться. А тепер перейдемо до прикладів. Припустимо, у нас є послідовність чисел:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13

І Ваше завдання полягає в тому, щоб знайти всі послідовні прості числа. Для цього Вам потрібно визначити функцію, яка буде відповідати на питання чи є число простим, або ні:

private static bool IsPrime(int number)
{
int boundary = (int)Math.Floor(Math.Sqrt(number));
if (number == 1) return false;
if (number == 2) return true;
for (int i = 2; i <= boundary; ++i)
{
if (number % i == 0) return false;
}
return true;
}


І визначити патерн для пошуку простих послідовностей:

{IsPrime}(.{IsPrime})*

На цьому, загалом і в цілому, складна частина закінчена. Опишемо саму процедуру виділення:

public void PrimeTest()
{
var oregex = new ORegex<int>("{0}(.{0})*", IsPrime);
var input = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
foreach (var match in oregex.Matches(input))
{
Console.WriteLine(string.Join(",", match.Values));
}

//OUTPUT:
//2
//3,4,5,6,7
//11,12,13
}


Ну от і все, але не так переконливо, так? Добре, тоді давайте наведемо приклад складніше. Уявімо, що у нас є якась послідовність слів прийшла до нас з лексико-морфологічного модуля. Питання, як по швидкому виділити найменування персон з послідовності? Досить просто.

Визначаємо класи слова і персони:

public enum SemanticType
{
Name,
FamilyName,
Other,
}

public class Word
{
public readonly string Value;
public readonly SemanticType SemType;
public Word(string value, SemanticType semType)
{
Value = value;
SemType = semType;
}
}

public class Person
{
public readonly Word[] Words;
public readonly string Name;
public Person(OMatch<Word> match)
{
Words = match.Values.ToArray();
Name = match.OCaptures["name"].First().Values.First().Value;
//Just Now normalize this name and you are good.
}
}


І додатково якусь важливу функцію, яка буде визначати рядок насправді є ініціалом:

private static bool IsInitial(string str)
{
var inp = str.Trim(new[] { '.', '', '\t', '\n', '\r' });
return inp.Length == 1 && char.IsUpper(inp[0]);
}


Без зайвих слів, складаємо таблицю предикатів, патерн і отримуємо наші об'єкти персон:

public void PersonSelectionTest()
{
//INPUT_TEXT: Пяточкова Тамара вирішила вигуляти Джека і зустрілася з Михайлом А. М.
var sentence = new Word[]
{
new Word("Пяточкова", SemanticType.FamilyName),
new Word("Тамара", SemanticType.Name),
new Word("вирішила", SemanticType.Other),
new Word("вигуляти", SemanticType.Other),
new Word("Джека", SemanticType.Name),
new Word("і", SemanticType.Other),
new Word("зустрілася", SemanticType.Other),
new Word("з", SemanticType.Other),
new Word("Михайлом", SemanticType.Name),
new Word("А", SemanticType.Other),
new Word("М", SemanticType.Other),
};

//Створюємо таблицю предикатів.
var pTable = new PredicateTable<Word>();
pTable.AddPredicate("Прізвище", x => x.SemType == SemanticType.FamilyName); //Check if word is FamilyName.
pTable.AddPredicate("Ім'я", x => x.SemType == SemanticType.Name); //Check if word is simple Name.
pTable.AddPredicate("Ініціал", x => IsInitial(x.Value)); //Complex check if Value is Inital character.

//Створюємо наше вираз з патерну і таблиці.
var oregex = new ORegex<Word>(@"
{Прізвище}(?<name>{Ім'я}) //Comments can be written inside pattern...
|
(?<name>{Ім'я})({Прізвище}|{Ініціал}{1,2})? /*...even complex ones.*/
", pTable);

//Виділяємо персон в послідовності.
var persons = oregex.Matches(sentence).Select(x => new Person(x)).ToArray();

foreach (var person in persons)
{
Console.WriteLine("Person found: {0}, length: {1}", person.Name, person.Words.Length);
}

//OUTPUT:
//Person found: Тамара, length: 2
//Person found: Джека, length: 1
//Person found: Михайлом, length: 3
}


Ну ось і все. Постарався описати все коротко і дохідливо =)
Якщо що бібліотека доступна як в nuget так і на github.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.