Страницы

Поиск по вопросам

воскресенье, 29 декабря 2019 г.

C#: Получение коллекции данных с помощью регулярного выражения в многострочном режиме

#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.BaseDi‌​rectory,
"..\\..\\")).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

Комментариев нет:

Отправить комментарий