|
|
|||||||||||||||||||||||||||||
|
Регулярные выражения облегчают поиск и выборку данныхИсточник: MSDN Magazine Дэвид Банистер
Оглавление
Хотя T-SQL является исключительно мощным языком обработки данных, он плохо приспособлен для анализа текста и манипуляций с ним. Попытки проведения анализа сколько-нибудь сложного текста с помощью встроенных строковых функций приводят к необходимости включения в код чрезмерно большого количества этих функций и хранимых процедур, вследствие чего код становится трудно отлаживать и поддерживать. Но, может быть, существует более подходящий путь? На самом деле гораздо более эффективное и элегантное решение предлагают регулярные выражения. Легко убедиться в том, как они полезны при сравнении текста для идентификации записей, хотя при этом они способны на гораздо большее. Я покажу, как решать разнообразные задачи, для которых средства SQL Server 2000 считались неудобными или неприменимыми, а теперь с появлением SQL Server 2005 стали возможными благодаря поддержке хостинга CLR. Регулярные выражения для SQL - не новость. Компания Oracle встроила регулярные выражения еще в базу данных 10g, и многие решения для баз данных с открытым исходным кодом также используют различные библиотеки регулярных выражений. По сути, регулярные выражения могли бы применяться и в более ранних версиях SQL Server, но механизм их обработки был неэффективен. С помощью хранимой процедуры sp_OACreate можно задействовать любой объект OLE Automation, реализующий регулярные выражения, но сначала нужно создать COM-объект, затем сделать минимум один вызов IDispatch и уничтожить этот объект. Для большинства задач это совершенно неэффективно и было причиной многих проблем с производительностью. Единственной альтернативой было создание расширенной хранимой процедуры. Однако сегодня существует SQLCLR, пользовательская CLR-функция, позволяющая создавать эффективный и менее подверженный ошибкам набор функций на основе Microsoft .NET Framework. Пользовательские CLR-функцииТакие функции являются попросту статическими методами (общими функциями в Visual Basic), определенными внутри .NET-сборки. Чтобы использовать объекты SQLCLR, необходимо зарегистрировать сборку в SQL Server новым оператором CREATE ASSEMBLY, а затем создавать каждый объект, указывая на его реализацию в сборке. Для поддержки пользовательских CLR-функций усовершенствован оператор CREATE FUNCTION. При использовании SQL Server Project среда разработки Visual Studio 2005 берет на себя весь процесс регистрации. Этот тип проекта отличается от большинства проектов Visual Studio, поскольку при попытке отладки (или запуска без отладки) проект компилируется заново, и получаемая сборка, как и все определенные в ней объекты SQLCLR, развертываются и регистрируются в SQL Server. Затем интегрированная среда разработки запускает тестовый сценарий, предназначенный для данного проекта. Для упрощения отладки в SQL-сценарий и в .NET-код можно ставить точки прерывания. Добавление функции аналогично добавлению нового класса к проекту любого другого типа. Вы просто включаете в проект новый элемент и при появлении запроса выбираете User-Defined Function (UDF). Новый метод добавляется к частичному классу, в котором содержатся все пользовательские функции. К новому методу также применяется атрибут SqlFunction. Он используется средой Visual Studio для создания SQL-операторов, необходимых при регистрации функции. Поля IsDeterministic, IsPrecise, DataAccess и SystemDataAccess атрибута SqlFunction используются SQL Server. Поиск по шаблонамОпределить, соответствует ли строка шаблону, - простейшее применение регулярных выражений и, как видно в листинге 1, реализуется очень легко. Листинг 1. Поиск строки по шаблону
Сначала я использую поле Options для сохранения параметров регулярного выражения применительно к функциям. В данном случае я выбрал RegexOptions.SingleLine и RegexOptions.IgnorePatternWhitespace. Первый из них устанавливает однострочный режим, а второй исключает пробелы, не помеченные Escape-символом, из регулярного выражения и разрешает отмечать комментарии знаком #. Другой параметр, который вам, возможно, понадобится после долгих раздумий, - RegexOption.Compiled. Этот параметр при интенсивном использовании регулярных выражений, пока их количество не слишком велико, дает заметный прирост производительности. Компилировать следует выражения, применяемые многократно. Но не используйте параметр Compiled с регулярными выражениями, к которым вы обращаетесь лишь изредка, поскольку это приведет к издержкам и использованию лишней памяти. Вероятно, вы захотите дополнить мою универсальную функцию RegexMatch еще одним параметром, определяющим, надо ли компилировать данное выражение; таким образом, в каждом конкретном случае можно было бы решать, насколько оправданы издержки ради увеличения производительности. После задания параметров RegexOptions я определяю функцию RegexMatch, используя тип данных SqlChars вместо SqlString. Тип данных SqlString преобразуется в тип nvarchar(4,000), тогда как SqlChars преобразуется в тип nvarchar(max). Новая возможность задания максимального размера допускает работу со строками, длина которых превышает предел для SQL Server 2000, равный 8000 байт. Для большей гибкости nvarchar(max) в настоящей статье используется в как можно более обобщенном виде. Однако, если соответствующие строки содержат менее 4000 символов, эффективность при использовании nvarchar(4,000) может быть гораздо выше. Поэтому сначала оцените конкретные потребности и пишите код с их учетом. Остальная часть кода данного метода довольно проста. Создается экземпляр Regex с определенными параметрами и заданным шаблоном, после чего для определения, соответствует ли шаблону введенное значение, применяется метод IsMatch. Теперь необходимо добавить к тестовому сценарию простой запрос:
Шаблоном в этом выражении является простая проверка на номер в системе страхования США. Установите точку прерывания в новом запросе и, запустив отладчик, начинайте пошагово выполнять функцию. Эта функция позволяет делать массу различных проверок, но здесь мы рассмотрим лишь некоторые особенности, которые многие не принимают во внимание. Например, очень важно придерживаться соглашения об именовании, действующего в пределах базы данных, а написание запроса для проверки того, что все хранимые процедуры удовлетворяют нормативам данной организации, - дело непростое. Функция RegexMatch значительно упрощает эту задачу. Например, следующий тест запроса выполняет эту задачу:
Этот запрос проверяет, каждая ли хранимая процедура имеет префикс «usp_», за которым следует «Insert», «Update», «Delete» или «Select». Он также проверяет, начинается ли каждое слово в названии объекта с заглавной буквы. Сравните те строки с этой крайне упрощенной версией, использующей только встроенные функции:
Хотя этот запрос в большей мере является кодом, в нем фактически не хватает нескольких функций, присутствующих в версии с регулярными выражениями. Во-первых, он нечувствителен к регистру букв, и поиск по шаблону может дать неверные результаты. Во-вторых, он не проверяет фактическое имя сущности, содержащееся в имени процедуры. В-третьих, все четыре строки, тестируемые в запросе, имеют длину в шесть символов, что позволило упростить код извлечением отдельной подстроки длиной в шесть символов и использовать ее во всех допустимых операциях сравнения. Это не проблема в данном примере, поскольку все имена команд состоят из шести символов, но представьте себе стандарт с более сложными командами вроде Get, List или Find. Этими командами легко манипулирует функция RegexMatch, поскольку они являются лишь дополнительными вариантами в списке. Проверка значений на допустимость - очень распространенное применение регулярных выражений в отношении, например, телефонных номеров, почтовых индексов и т. д. Как показывает следующее определение таблицы, ограничение CHECK идеально подходит для этой цели:
Содержимое столбца AccountNumber проверяется на соответствие произвольному соглашению о том, что оно должно начинаться с трех-пяти букв, за которыми следуют пять цифр, затем тире и еще три цифры. Как телефонные номера, так и почтовые индексы проверяются на соответствие стандартам США для форматов телефонных номеров и почтовых индексов. Функция RegexMatch предоставляет массу возможностей для SQL Server, но реализация регулярных выражений в .NET, как будет показано, дает гораздо больше. Извлечение данныхДля извлечения данных из строки могут использоваться средства группирования регулярных выражений. Разработанная мной функция RegexGroup обеспечивает эту возможность для T-SQL:
Она, как и функция RegexMatch, создает объект Regex. Однако, вместо того чтобы проводить проверку на соответствие, для первого же найденного во входной строке совпадения создается объект Match. Этот объект используется для извлечения указанной группы. Если во входной строке соответствия не обнаружено, функция возвращает null. Функция будет работать и в том случае, если вместо именованных групп вы предпочтете использовать нумерованные. Просто передавайте целое значение функции в SQL-коде, и оно будет неявно приводиться к типу nvarchar, в результате чего будет возвращаться соответствующая группа. Для извлечения одних фрагментов данных из других можно использовать функцию RegexGroup в списке SELECT. Например, для поля, в котором содержится URL, можно легко провести его синтаксический разбор для получения отдельных частей. Данный запрос использует группирование для нахождения имен всех серверов в поле Url таблицы UrlTable:
Эта функция применима и в вычисляемых полях. Следующее определение таблицы обеспечивает разделение электронных адресов на почтовые ящики и домены:
Поле почтового ящика будет возвращать почтовый ящик или имя пользователя этого адреса, а поле домена - домен. Хранилище шаблоновВсе шаблоны (patterns), используемые этими функциями, - просто строки, и любая из них может храниться в таблице базы данных. Большинство баз данных с информацией международного характера имеет таблицу, представляющую страны. Добавив к такой таблице несколько дополнительных полей, можно хранить в ней шаблоны проверки на конкретную страну. Это позволяет ввести ограничение для адресной строки, автоматически изменяемое в зависимости от страны. В базах данных, хранящих информацию о клиентах, как правило, есть таблица, представляющая клиента. Такую таблицу можно использовать для хранения группирующих шаблонов, описывающих способ, с помощью которого необработанные данные о клиенте сохраняются в базе данных; это позволяет создать вычисляемые поля для выборки лишь действительно необходимых данных. Например, если номера банковских счетов клиентов имеют уникальную структуру и нужны только отдельные фрагменты номеров, можно легко создать выражение, позволяющее извлечь корректный фрагмент информации из номера счета каждого клиента. СовпаденияВместо того чтобы определять, совпадает ли строка с шаблоном, иногда предпочтительнее извлечь каждое совпадение. Раньше для этого потребовались бы курсоры с перемещением в цикле. Этот процесс достаточно медленный, а сам код труден для понимания и поддержки. Регулярные выражения - гораздо более подходящее средство для выполнения такой операции. Главная проблема здесь в том, как возвратить все требуемые данные в рамках SQL-конструкции. Решение - табличные функции. Табличные функции отчасти похожи на функции, описанные выше, но имеют два существенных отличия. Во-первых, атрибуты, применяемые к методу, должны полностью определять структуру возвращаемой таблицы. Во-вторых, в процессе участвуют два метода. Первый - возвращает перечисляемый объект (enumerable object) вместо фактического результата функции. Второй передает перечисляемые объекты для заполнения полей каждой строки. Каждое значение, получаемое от перечислителя (enumerator), должно соответствовать одной строке набора результатов. Интерфейс ICollection в .NET Framework реализует IEnumerable, что означает возможность возврата любого набора первым методом. Класс Regex содержит метод Matches, возвращающий объект MatchCollection, который вы могли бы использовать. Проблема с этим объектом в том, что вся строка должна быть обработана до возврата управления из метода Matches. Из-за оптимизаций в SQL Server я предпочитаю писать собственный перечислитель, который возвращает каждое совпадение по требованию вместо того, чтобы заранее возвращать весь набор. На самом деле окончательное решение следует принимать с учетом того, как используется функция, и должно быть тщательно проверено до оптимизации перечислителя. В листинге 2 показан код перечислителя. Класс MatchNode служит оболочкой для индивидуального совпадения, найденного в строке, и параллельно отслеживает его позицию в наборе возвращаемых совпадений. Класс MatchIterator является перечисляемым и отвечает за обработку регулярного выражения. Он использует новое ключевое слово yield, которое значительно упрощает создание перечислителя. Каждое совпадение, обнаруженное внутри строки, он будет возвращать по требованию. Листинг 2. Собственный перечисляемый объект для совпадений
В коде листинга 3 определена табличная функция CLR UDF. Метод RegexMatches возвращает новый MatchIterator. Арибут SqlFunctionAttribute метода RegexMatches включает несколько дополнительных свойств. В свойство TableDefinition помещается определение таблицы функции, в свойство FillRowMethodName - имя метода, который должен вызываться на каждой итерации возвращаемого перечисляемого объекта. В данном случае этот метод называется FillMatchRow. Листинг 3. Табличная функция CLR UDF для совпадений
На каждой итерации MatchIterator объект MatchNode передается методу FillMatchRow в качестве первого аргумента. Остальные параметры метода FillMatchRow должны быть объявлены как выходные и должны соответствовать описанию таблицы, определенной в первой функции. Функция FillMatchRow просто использует свойства MatchNode для заполнения полей данными. Наконец, с помощью этой функции легко извлекать из строки разнообразные фрагменты данных. Чтобы проиллюстрировать применение функции RegexMatches, обработаем строку с помощью приведенного ниже запроса и определим, сколько различных слов в ней содержится:
Приведенный пример довольно прямолинеен. Он демонстрирует некоторые возможности применения данной функции, а если вы удалите ключевое слово distinct, функция будет возвращать число слов в строке. На многих веб-сайтах ввод текста ограничивают весьма произвольной длиной. С помощью показанной мной проверки в сочетании с новой нотацией nvarchar(max) становится возможным ограничение ввода определенным числом слов. Этот тип запроса пригодится при выполнении различных задач аналитической обработки, но функцию RegexMatches можно использовать и для более общих задач. К сожалению, этот тип запроса также отражает излишне усердное использование регулярных выражений. Операцию разбиения, выполняемую в данном случае выражением "\w+", можно было бы легко выполнить методом String.Split, который работает гораздо быстрее. Регулярные выражения - мощное средство, но, прежде чем их применять, следует разобраться, насколько это оправданно. На форумах MSDN я часто вижу вопросы о том, как передавать список значений в хранимую процедуру. Кроме того, мне доводилось встречать различные запутанные способы разбора такого списка для идентификации соответствующих записей. Должен сказать, что функция RegexMatches позволяет делать это гораздо аккуратнее и понятнее:
Этот шаблон совпадает с любой группой символов, не содержащей запятые. Для таблицы с именем Data, содержащей целочисленное поле ID, этот запрос будет возвращать каждую найденную в списке запись. Его польза становится еще очевиднее, если учесть неявное приведение типов в SQL Server. Один и тот же запрос можно использовать для целочисленных данных, для «даты/время», GUID или типов данных с плавающей точкой. Другие способы обработки списка значений потребовали бы применения множества функций или усложнения хранимых процедур. Эта функция применима и для списков, где в качестве разделителя запятая не используется. Она позволяет обрабатывать списки, разделяемые пробелами, точками с запятой, табуляторами, символами «возврат каретки» или любыми другими идентифицируемыми символами. Извлечение данных из совпаденийПо аналогии с возвратом совпадений вы можете извлекать данные из каждого совпадения. Использовать для этого SQL довольно затруднительно. Обычно этот вид задач выполняется приложениями, а не СУБД, что вызывает определенные проблемы, так как каждое приложение, использующее базу данных, должно реализовать соответствующую обработку. В таких случаях разумным является подход, позволяющий добиться требуемой функциональности с помощью хранимых процедур. Как и с реализацией RegexMatches, я предпочитаю получать информацию о группах через собственный перечисляемый объект. Группирование лишь чуть сложнее, поскольку в пределах каждого совпадения нужно выполнять итерации по группам. В листинге 4 класс GroupNode выглядит почти так же, как класс MatchNode, с тем исключением, что он включает и имя группы, которую представляет. Класс GroupIterator аналогичен классу MatchIterator, но включает дополнительный цикл для возврата каждой группы. Теперь, когда у меня есть перечисляемый объект, я могу точно так же определить табличную функцию, как и для функции RegexMatches. Листинг 4. Собственный перечисляемый объект для групп
В листинге 5 функция RegexGroups определяется аналогично RegexMatches за исключением того, что она возвращает дополнительное поле данных, содержащее имя группы внутри совпадения. Теперь с помощью этой функции можно находить в пределах строки различные совпадения и извлекать из каждого совпадения нужные фрагменты информации. Листинг 5. Табличная UDF-функция для групп
Импорт данных в различных форматах при работе с базами данных - задача весьма распространенная. Импорт файлов с форматом, где разделителями являются запятые, представляет собой гораздо более утомительное дело, чем следовало бы. Большинство разработчиков в таких случаях создает программу, которая обрабатывает каждую строку, извлекает из нее данные и для каждой строки запускает хранимую процедуру. И хотя этот процесс работает, я бы предложил другое решение. Что если б мы могли передать весь файл хранимой процедуре и позволить ей управлять всем процессом? Эта идея обычно считается слишком сложной в реализации, но благодаря функции RegexGroups все можно сделать фактически в рамках одного запроса. Рассмотрим, к примеру, следующие данные о клиентах:
Из каждой строки надо получить три фрагмента информации: семизначный номер клиента, фамилию и тип клиента, указываемый одной буквой. Все три фрагмента можно извлечь с помощью выражения:
Теперь возникает проблема, заключающаяся в том, что результаты, возвращенные функцией RegexGroups, нельзя использовать напрямую. Вместо курсора для прохода по результатам можно задействовать функциональность SQL Server 2005. Заключив все это в хранимую процедуру, мы получим то, что надо. Хранимая процедура в листинге 6 принимает текстовый файл с запятыми в качестве разделителей, содержащий до 2 Гб данных в кодировке Unicode. Она обрабатывает файл целиком и каждую строку этого файла вставляет в таблицу Customer. Таким же образом может быть обработан любой текстовый файл с разделителями. Внеся в шаблон некоторые несущественные изменения, вы могли бы добавить управляющие последовательности для поддержки запятых в строках. Листинг 6. Обработка файла с запятыми в качестве разделителей
И вновь эта процедура демонстрирует возможность решать одну и ту же задачу разными способами, причем не всегда регулярные выражения являются лучшим выбором. В приведенном примере использование сводной таблицы фактически отменяет всю работу, которую выполняла функция RegexGroups для возврата данных в специальном группирующем формате. Можно было бы вставлять данные напрямую в таблицу, применяя гораздо более простую и быструю табличную функцию, просто считывающую каждую строку, метод String.Split (ориентируясь на запятые) и возвращать каждую строку. ЗаключениеХотя функции поиска совпадений очень мощные, они недостаточно полны. Есть масса вариантов поиска совпадений. Если база данных нечувствительна к регистру букв, могут потребоваться функции, которые обеспечивают нахождение совпадений и без учета регистра. Для уменьшения набора результатов может понадобиться критерий отбора. Многострочный режим позволяет для некоторых задач создавать более точные шаблоны поиска. Можно даже создать пользовательский тип для передачи соответствующих «настроечных» значений каждой из функций, чтобы при вызовах они использовали нужный вам набор параметров. Помните и о том, что при обработке текста могут возникать некоторые проблемы, связанные с локализацией. Например, класс Regex из .NET Framework распознает гораздо больше букв, чем входит в латиницу, поэтому следует быть внимательнее при разработке шаблонов для глобальных баз данных. И, конечно же, как неоднократно упоминалось в данной статье, поскольку регулярные выражения - чрезвычайно мощный инструмент, следует четко представлять, насколько необходима его мощь для решения стоящих перед вами задач. Некоторые задачи можно решить быстрее, используя более простые средства. В приведенных примерах для упрощения картины опущены блоки кода, необходимые для верификации данных и обработки ошибок. Все входные параметры функции должны проверяться на допустимость, а для реакции на нулевые или пустые значения вводимых строк должны быть разработаны соответствующие правила. Класс Regex может генерировать исключения, если не удается разобрать шаблон или если переданы недопустимые параметры. Такие исключения должны корректно обрабатываться. Комбинирование регулярных выражений с возможностями SQL обуславливает множество альтернативных вариантов обработки данных. Применение этих функций может сократить время, необходимое для расширения функциональности базы данных, а также сделать ее более удобной в сопровождении. Регулярные выражения применимы в любой базе данных, и я советую поэкспериментировать с ними. Ссылки по теме
|
|