#c_sharp #регулярные_выражения
Задача
В C#-программе получить из текстового файла телефонные коды городов и их названия
с помощью регулярного выражения.
Для простоты в данной задаче будем брать только названия городов, состоящие из одного
слова без дефиса.
Логично будет, если в файле данных сначала шло бы название города, а потом код, но
я по определённым причинам сделал наоборот.
В идеале, если в файле данных строка будет начинаться с кода города, а потом через
один пробел будет идти его название, но идеология юзабилити требует адаптации к криворуким
пользователям, потому регулярное выражение должно допускать пробелы до кода, лишние
пробелы после него, а также лишние пробелы после названия города. Недопустимо лишь
отсутствие пробела между кодом и городом.
Файл данных
39032 Абакан
39042 Саяногорск
39031 Черногорск
39036Копьево
42722 Анадыр ь
81831145 Березник
81 856 Карпогоры
Составление регулярного выражения ##
На Regex Storm .NET с регулярным выражением ^\s*(\d{4,5})\s+(\w)+\s*$ и опцией Multiline
я получил 3 совпадения (первые три города), а остальные 4 паттерну не соответствуют:
Копьево - Отсутствует пробел между кодом и именем
Анадырь - Имеется ошибочный пробел в имени (повторюсь, что в этом вопросе мы рассматриваем
только имена собственные, состоящие из одного слова).
Березинск - Слишком много цифер в коде (несоответствие стандарту)
Карпогоры - Пробел в номере
Программная реализация
Как я узнал, в C# режим Multiline является режимом по умолчанию. С точки зрения логики,
в паттерне нужно указать начало и конец строки. Тем не менее с паттерном
string pattern = @"^\s*(\d{4,5})\s*(\w+)\s$";
в программе я не получил ни одного совпадения. С паттерном @"^\s*(\d{4,5})\s*(\w+)\s"
(без указания конца строки) я получил единственное совпадение: пустой код города и
имя первого города. Наконец с паттерном @"\s*(\d{4,5})\s*(\w+)\s" (без указания начала
и конца строки) я получил невалидные данные:
Код первого города - пустой.
Копьево, Анадырь - получено несмотря на невалидность
Березинск: Получен код города 81831; следующие три цифры зачтены как название города.
Как следует исправить код, чтобы совпадения были те же, что и на Regex Storm?
class Program {
static void Main(string[] args) {
// Указание пути к файлу данных
String currentProjectPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"..\\..\\")).ToString();
String dataFolderPath = currentProjectPath + "/Data/";
String dataFileNameWithExtension = "Data.txt";
String fullPathToDataFile = dataFolderPath + dataFileNameWithExtension;
// Чтение файла
string[] lines = System.IO.File.ReadAllLines(fullPathToDataFile);
string fileContains = "";
foreach (string line in lines) {
fileContains = fileContains + line + "\n";
}
// Применение регулярного выражения
string pattern = @"\s*(\d{4,5})\s*(\w+)\s";
MatchCollection matches = Regex.Matches(fileContains, pattern);
foreach (Match match in matches) {
Console.WriteLine("Код города:"+ match.Groups[1].Value);
Console.WriteLine("Имя города:" + match.Groups[2].Value);
}
}
}
Если для ответа на данный вопрос Вам нужно поэкспериментировать с кодом, то в целях
экономии Вашего времени я подготовил проект для Visual Studio (ссылка на Яндекс Диск;
возможно станет недоступной после получения ответа на вопрос).
Ответы
Ответ 1
Ваша изначальная регулярка неверна, она возвращает код города и последовательность по одной букве: Правильная регулярка: ^\s*(\d{4,5})\s+(\w+)\s*$: Рабочий код: var lines = @"39032 Абакан 39042 Саяногорск 39031 Черногорск 39036Копьево 42722 Анадыр ь 81831145 Березник 81 856 Карпогоры"; string pattern = @"^\s*(\d{4,5})\s+(\w+)\s*$"; MatchCollection matches = Regex.Matches(lines, pattern, RegexOptions.Multiline); foreach (Match match in matches) { Console.WriteLine("Код города:" + match.Groups[1].Value); Console.WriteLine("Имя города:" + match.Groups[2].Value); } Обратите внимание, на параметр RegexOptions.Multiline, он: Изменяет ^ и $ так, чтобы они соответствовали началу/концу строки текста, а не началу/концу всей строки регулярного выражения PS: На сколько я понял .NET Regex Tester как раз работает на движке регулярных выражений .NET, поэтому раз уж вы включили там галочку Multiline, то должны и в своем коде указать соответствующий флагОтвет 2
var lines = @"39032 Абакан 39042 Саяногорск 39031 Черногорск 39036Копьево 42722 Анадыр ь 81831145 Березник 81 856 Карпогоры"; lines = lines.Replace(" ", ""); string pattern = @"(\d{4,5})\d*(\w+)"; MatchCollection matches = Regex.Matches(lines, pattern); foreach (Match match in matches) { Console.WriteLine("Код города:" + match.Groups[1].Value); Console.WriteLine("Имя города:" + match.Groups[2].Value); } Код города:39032 Имя города:Абакан Код города:39042 Имя города:Саяногорск Код города:39031 Имя города:Черногорск Код города:39036 Имя города:Копьево Код города:42722 Имя города:Анадырь Код города:81831 Имя города:Березник Код города:81856 Имя города:КарпогорыОтвет 3
Ваша изначальная регулярка, ^\s*(\d{4,5})\s+(\w)+\s*$, верна, и ничуть не хуже, чем ^\s*(\d{4,5})\s+(\w+)\s*$. Да, из нее неудобно достать результат, но это вполне возможно кодом вида: Console.WriteLine("Имя города:" + String.Join("", match.Groups[2].Captures.Cast().Select(c=>c.Value))); Дело в только в том, что доставать результат парсинга регекса, опираясь на индексы групп - не слишком надежный способ. Надежный способ - именованные группы. Тогда все три выражения дадут вам одинаковый результат, вне зависимисти от того, как вы поставите скобки: ^\s*(?'code'\d{4,5})\s+(?'city'(\w)+)\s*$ ^\s*(?'code'\d{4,5})\s+(?'city'\w+)\s*$ ^\s*(?'code'\d{4,5})\s+(?'city'(\w+))\s*$ Имена групп можно обрамлять в <>, если одиночные кавычки режут глаз: ^\s*(? \d{4,5})\s+(?\w+)\s*$ Пример кода с вашей изначальной регуляркой + именами групп var lines = @"39032 Абакан 39042 Саяногорск 39031 Черногорск 39036Копьево 42722 Анадыр ь 81831145 Березник 81 856 Карпогоры"; string pattern = @"^\s*(?'code'\d{4,5})\s+(?'city'(\w)+)\s*$"; MatchCollection matches = Regex.Matches(lines, pattern, RegexOptions.Multiline); foreach (Match match in matches) { Console.WriteLine("Код города:" + match.Groups["code"].Value); Console.WriteLine("Имя города:" + match.Groups["city"].Value); } При этом неименованные группы можно вообще убрать из результатов, задав флаг RegexOptions.ExplicitCapture
Комментариев нет:
Отправить комментарий