Соответствие] (закрывающая квадратная скобка) с PATINDEX с использованием подстановочного знака «[]»

9

Я пишу пользовательский анализатор JSON в T-SQL .

Для моего парсера я использую PATINDEXфункцию, которая вычисляет позицию токена из списка токенов. В моем случае все токены состоят из отдельных символов и включают в себя:

{} []:,

Обычно, когда мне нужно найти (первую) позицию любого из нескольких заданных символов, я использую такую PATINDEXфункцию:

PATINDEX('%[abc]%', SourceString)

Затем функция выдаст мне первую позицию aили bили c- в зависимости от того, что окажется первым - в SourceString.

Теперь проблема в моем случае, кажется, связана с ]персонажем. Как только я укажу это в списке символов, например, вот так:

PATINDEX('%[[]{}:,]%', SourceString)

мой предполагаемый шаблон, очевидно, становится неработающим, потому что функция никогда не находит соответствия. Похоже, мне нужен способ избежать первого, ]чтобы PATINDEXон воспринимался как один из символов поиска, а не как специальный символ.

Я нашел этот вопрос, спрашивая о похожей проблеме:

Однако в этом случае ]просто не нужно указывать в скобках, потому что это всего лишь один символ, и его можно указывать без скобок вокруг них. Альтернативное решение, которое использует экранирование, работает только для, LIKEа не для PATINDEX, потому что оно использует ESCAPEподпункт, поддерживаемый первым, а не последним.

Итак, мой вопрос, есть ли способ найти ]с PATINDEXиспользованием [ ]подстановочного знака? Или есть способ эмулировать эту функциональность, используя другие инструменты Transact-SQL?

Дополнительная информация

Ниже приведен пример запроса , когда мне нужно использовать PATINDEXс […]рисунком , как указаны выше. Шаблон здесь работает (хотя и несколько ), потому что он не включает в себя ]символ. Мне нужно, чтобы он ]тоже работал:

WITH
  data AS (SELECT CAST('{"f1":["v1","v2"],"f2":"v3"}' AS varchar(max)) AS ResponseJSON),
  parser AS
  (
    SELECT
      Level         = 1,
      OpenClose     = 1,
      P             = p.P,
      S             = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
      C             = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1),
      ResponseJSON  = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
    FROM
      data AS d
      CROSS APPLY (SELECT PATINDEX('%[[{]%', d.ResponseJSON)) AS p (P)
    UNION ALL
    SELECT
      Level         = ISNULL(d.OpenClose - 1, 0) + d.Level + ISNULL(oc.OpenClose, 0),
      OpenClose     = oc.OpenClose,
      P             = d.P + p.P,
      S             = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
      C             = c.C,
      ResponseJSON  = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
    FROM
      parser AS d
      CROSS APPLY (SELECT PATINDEX('%[[{}:,]%' COLLATE Latin1_General_BIN2, d.ResponseJSON)) AS p (P)
      CROSS APPLY (SELECT SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1)) AS c (C)
      CROSS APPLY (SELECT CASE WHEN c.C IN ('[', '{') THEN 1 WHEN c.C IN (']', '}') THEN 0 END) AS oc (OpenClose)
    WHERE 1=1
      AND p.P <> 0
  )
SELECT
  *
FROM
  parser
OPTION
  (MAXRECURSION 0)
;

Я получаю вывод:

Level  OpenClose  P   S      C   ResponseJSON
-----  ---------  --  -----  --  ---------------------------
1      1          1          {   "f1":["v1","v2"],"f2":"v3"}
1      null       6   "f1"   :   ["v1","v2"],"f2":"v3"}
2      1          7          [   "v1","v2"],"f2":"v3"}
2      null       12  "v1"   ,   "v2"],"f2":"v3"}
2      null       18  "v2"]  ,   "f2":"v3"}
2      null       23  "f2"   :   "v3"}
2      0          28  "v3"   }   

Вы можете видеть, что ]он включен как часть Sодной из строк. LevelКолонка показывает уровень вложенности, то есть кронштейн и брекеты вложенности. Как вы можете видеть, когда уровень становится 2, он никогда не возвращается к 1. Это было бы, если бы я мог сделать PATINDEXраспознавание ]в качестве токена.

Ожидаемый результат для приведенного выше примера:

Level  OpenClose  P   S     C   ResponseJSON
-----  ---------  --  ----  --  ---------------------------
1      1          1         {   "f1":["v1","v2"],"f2":"v3"}
1      NULL       6   "f1"  :   ["v1","v2"],"f2":"v3"}
2      1          7         [   "v1","v2"],"f2":"v3"}
2      NULL       12  "v1"  ,   "v2"],"f2":"v3"}
2      0          17  "v2"  ]   ,"f2":"v3"}
1      NULL       18        ,   "f2":"v3"}
1      NULL       23  "f2"  :   "v3"}
1      0          28  "v3"  }

Вы можете поиграть с этим запросом на db <> fiddle .


Мы используем SQL Server 2014 и вряд ли скоро обновимся до версии, которая изначально поддерживает синтаксический анализ JSON. Я мог бы написать приложение, чтобы выполнить работу, но результаты парсинга нужно обрабатывать дальше, что подразумевает больше работы в приложении, чем просто парсинг - такой вид работы, который будет гораздо проще и, вероятно, более эффективен, выполняется с помощью сценарий T-SQL, если бы я мог применить его непосредственно к результатам.

Маловероятно, что я могу использовать SQLCLR в качестве решения этой проблемы. Однако я не против, если кто-то решит опубликовать решение SQLCLR, так как это может быть полезно для других.

Андрей М
источник
Как насчет JSON, который выглядит ["foo]bar”]?
Салман А
@SalmanA: Такие сценарии можно смело игнорировать.
Андрей М

Ответы:

6

Мое собственное решение, которое является скорее обходным путем, заключалось в указании диапазона символов, который включал этот диапазон, ]и использовании этого диапазона вместе с другими символами в [ ]шаблоне. Я использовал диапазон, основанный на таблице ASCII. Согласно этой таблице, ]персонаж находится в следующем районе:

Hex Dec Char
--- --- ----
...
5A 90 Z
5В 91 [
5С 92 \
5D 93]
5E 94 ^
5F 95 _
...

Мой диапазон, поэтому принял форму [-^, т.е. включены четыре символа: [, \, ], ^. Я также указал, что шаблон использует двоичное сопоставление, чтобы точно соответствовать диапазону ASCII. Полученное PATINDEXвыражение в итоге выглядело так:

PATINDEX('%[[-^{}:,]%' COLLATE Latin1_General_BIN2, MyJSONString)

Очевидная проблема с этим подходом состоит в том, что диапазон в начале шаблона включает в себя два нежелательных символа, \и ^. Решение сработало для меня просто потому, что дополнительные символы никогда не могли появляться в определенных строках JSON, которые мне нужно было проанализировать. Естественно, это не может быть правдой вообще, поэтому я все еще интересуюсь другими методами, надеюсь, более универсальными, чем мои.

Андрей М
источник
4

Я, вероятно, ужасно воспринял это со спины, когда мне пришлось много разбивать строки.

Если у вас есть известный набор символов, составьте таблицу из них.

CREATE TABLE dbo.characters ( character CHAR(1) NOT NULL PRIMARY KEY CLUSTERED );

INSERT dbo.characters ( character )
SELECT *
FROM (
        SELECT '[' UNION ALL
        SELECT ']' UNION ALL
        SELECT '{' UNION ALL
        SELECT '}' UNION ALL
        SELECT ',' 
) AS x (v)

Тогда используйте это волшебство CROSS APPLYвместе с CHARINDEX:

SELECT TOP 1000 p.Id, p.Body, ca.*
FROM dbo.Posts AS p
CROSS APPLY (
    SELECT TOP 1 CHARINDEX(c.character, p.Body) AS first_things_first
    FROM dbo.characters AS c
    ORDER BY CHARINDEX(c.character, p.Body) ASC
) AS ca
WHERE ca.first_things_first > 0

Если я упускаю что-то очевидное в том, что тебе нужно сделать, дай мне знать.

Эрик Дарлинг
источник
4

В прошлом я видел подходы, позволяющие заменить оскорбительного персонажа перед поиском, а затем вернуть его обратно.

В этом случае мы могли бы сделать что-то вроде:

DECLARE @test NVARCHAR(MAX);
DECLARE @replacementcharacter CHAR(1) = CHAR(174);

SET @test = 'Test[]@String'

SELECT PATINDEX('%[[' + @replacementcharacter + '@]%', REPLACE(@test,']',@Replacementcharacter))

Этот код правильно возвращает 5. Я использую символ ¬, поскольку он вряд ли появится - если нет символов ASCII, которые вы не будете использовать, это решение не будет работать.

Как ни странно, прямого ответа на ваш вопрос было бы нет - я не могу заставить PATINDEX искать ']', но если вы замените его, вам не нужно.

Тот же пример, но без использования переменной:

DECLARE @test NVARCHAR(MAX);

SET @test = 'Test[]@String'

SELECT PATINDEX('%[[' + CHAR(174) + '@]%', REPLACE(@test,']',CHAR(174)))

Использование вышеуказанного решения в вашем коде дает требуемые результаты:

WITH
  data AS (SELECT CAST('{"f1":["v1","v2"],"f2":"v3"}' AS varchar(max)) AS ResponseJSON),
  parser AS
  (
    SELECT
      Level         = 1,
      OpenClose     = 1,
      P             = p.P,
      S             = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
      C             = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1),
      ResponseJSON  = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
    FROM
      data AS d
      CROSS APPLY (SELECT PATINDEX('%[[{'+ CHAR(174) + ']%', REPLACE(d.ResponseJSON,']',CHAR(174)))) AS p (P)
    UNION ALL
    SELECT
      Level         = ISNULL(d.OpenClose - 1, 0) + d.Level + ISNULL(oc.OpenClose, 0),
      OpenClose     = oc.OpenClose,
      P             = d.P + p.P,
      S             = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
      C             = c.C,
      ResponseJSON  = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
    FROM
      parser AS d
      CROSS APPLY (SELECT PATINDEX('%[[{}:,'+ CHAR(174) + ']%' COLLATE Latin1_General_BIN2, REPLACE(d.ResponseJSON,']',CHAR(174)))) AS p (P)
      CROSS APPLY (SELECT SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1)) AS c (C)
      CROSS APPLY (SELECT CASE WHEN c.C IN ('[', '{') THEN 1 WHEN c.C IN (']', '}') THEN 0 END) AS oc (OpenClose)
    WHERE 1=1
      AND p.P <> 0
  )
SELECT
  *
FROM
  parser
OPTION
  (MAXRECURSION 0)
;
George.Palacios
источник
4

Так как ]это только особый [...], вы можете использовать PATINDEXдважды, выходя ]за пределы [...]. Оцените и то PATINDEX('%[[{}:,]%', SourceString)и другое PATINDEX('%]%', SourceString). Если один результат равен нулю, возьмите другой. В противном случае возьмите меньшее из двух значений.

В вашем примере:

WITH
  data AS (SELECT CAST('{"f1":["v1","v2"],"f2":"v3"}' AS varchar(max)) AS ResponseJSON),
  parser AS
  (
    SELECT
      Level         = 1,
      OpenClose     = 1,
      P             = p.P,
      S             = SUBSTRING(d.ResponseJSON, 1, NULLIF(p.P, 0) - 1),
      C             = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0), 1),
      ResponseJSON  = SUBSTRING(d.ResponseJSON, NULLIF(p.P, 0) + 1, 999999)
    FROM
      data AS d
      CROSS APPLY (SELECT PATINDEX('%[[{]%', d.ResponseJSON)) AS p (P)
    UNION ALL
    SELECT
      Level         = ISNULL(d.OpenClose - 1, 0) + d.Level + ISNULL(oc.OpenClose, 0),
      OpenClose     = oc.OpenClose,
      P             = d.P + ISNULL(p.P, 0),
      S             = SUBSTRING(d.ResponseJSON, 1, p.P - 1),
      C             = c.C,
      ResponseJSON  = SUBSTRING(d.ResponseJSON, p.P + 1, 999999)
    FROM
      parser AS d
      CROSS APPLY (VALUES (NULLIF(PATINDEX('%[[{}:,]%', d.ResponseJSON), 0), NULLIF(PATINDEX('%]%', d.ResponseJSON), 0))) AS p_ (a, b)
      CROSS APPLY (VALUES (CASE WHEN p_.a < p_.b OR p_.b IS NULL THEN p_.a ELSE p_.b END)) AS p (P)
      CROSS APPLY (SELECT SUBSTRING(d.ResponseJSON, p.P, 1)) AS c (C)
      CROSS APPLY (SELECT CASE WHEN c.C IN ('[', '{') THEN 1 WHEN c.C IN (']', '}') THEN 0 END) AS oc (OpenClose)
    WHERE 1=1
      AND p.P <> 0
  )
SELECT
  *
FROM
  parser
OPTION
  (MAXRECURSION 0)
;

https://dbfiddle.uk/?rdbms=sqlserver_2014&fiddle=66fba2218d8d7d310d5a682be143f6eb

HVD
источник
-4

Для левого '[':

PATINDEX('%[[]%',expression)

За право ']':

PATINDEX('%]%',expression)
Изобразительное искусство
источник
1
Это определяет, как искать либо открывающую квадратную скобку, либо закрывающую; OP ищет один из нескольких символов (отмеченных заключением соответствующих символов в квадратные скобки), включая закрывающую квадратную скобку.
RDFozz