Correspondance d'un] (crochet de fermeture) avec PATINDEX en utilisant le caractère générique "[]"

9

J'écris un analyseur JSON personnalisé en T-SQL .

Aux fins de mon analyseur, j'utilise la PATINDEXfonction qui calcule la position d'un jeton à partir d'une liste de jetons. Les jetons dans mon cas sont tous des caractères uniques et ils comprennent ceux-ci:

{} []:,

Habituellement, lorsque j'ai besoin de trouver la (première) position de l'un des caractères donnés, j'utilise la PATINDEXfonction comme ceci:

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

La fonction me donnera alors la première position de aou bou c- selon ce qui se trouve en premier - dans SourceString.

Maintenant, le problème dans mon cas semble être lié au ]personnage. Dès que je le précise dans la liste des personnages, par exemple comme ceci:

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

mon modèle prévu devient apparemment cassé, car la fonction ne trouve jamais de correspondance. Il semble que j'ai besoin d'un moyen d'échapper au premier, ]ce qui le PATINDEXtraite comme l'un des caractères de recherche plutôt que comme un symbole spécial.

J'ai trouvé cette question posant sur un problème similaire:

Cependant, dans ce cas, le ]simplement n'a pas besoin d'être spécifié entre crochets, car il s'agit d'un seul caractère et il peut être spécifié sans crochets autour d'eux. La solution alternative, qui utilise l'échappement, ne fonctionne que pour LIKEet non pour PATINDEX, car elle utilise un ESCAPEsous-paragraphe, pris en charge par le premier et non par le second.

Donc, ma question est, existe-t-il un moyen de rechercher un ]avec l' PATINDEXutilisation du [ ]caractère générique? Ou existe-t-il un moyen d'émuler cette fonctionnalité à l'aide d'autres outils Transact-SQL?

Information additionnelle

Voici un exemple de requête que je dois utiliser PATINDEXavec le […]modèle ci-dessus. Le modèle ici fonctionne (quoique quelque peu ) car il n'inclut pas le ]personnage. J'en ai aussi besoin pour travailler avec ]:

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)
;

La sortie que j'obtiens est:

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"   }   

Vous pouvez voir que le ]est inclus dans le cadre de l' Sune des lignes. La Levelcolonne indique le niveau d'imbrication, c'est-à-dire l'imbrication des crochets et des accolades. Comme vous pouvez le voir, une fois que le niveau devient 2, il ne revient jamais à 1. Il l'aurait fait si je pouvais faire PATINDEXreconnaître ]comme un jeton.

La sortie attendue pour l'exemple ci-dessus est:

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"  }

Vous pouvez jouer avec cette requête sur db <> fiddle .


Nous utilisons SQL Server 2014 et il est peu probable que nous procédions à une mise à niveau prochaine vers une version qui prend en charge l'analyse JSON en natif. Je pourrais écrire une application pour faire le travail mais les résultats de l'analyse doivent être traités plus loin, ce qui implique plus de travail dans l'application que simplement l'analyse - le type de travail qui serait beaucoup plus facile, et probablement plus efficace, effectué avec un script T-SQL, si seulement je pouvais l'appliquer directement aux résultats.

Il est très peu probable que je puisse utiliser SQLCLR comme solution à ce problème. Cependant, cela ne me dérange pas si quelqu'un décide de publier une solution SQLCLR, car cela pourrait être utile pour d'autres.

Andriy M
la source
Qu'en est-il de json qui ressemble ["foo]bar”]?
Salman A
@SalmanA: De tels scénarios peuvent être ignorés en toute sécurité.
Andriy M

Réponses:

6

Ma propre solution, qui est plus une solution de contournement, consistait à spécifier une plage de caractères qui incluait le ]et à utiliser cette plage avec les autres caractères du [ ]caractère générique. J'ai utilisé une plage basée sur la table ASCII. Selon ce tableau, le ]personnage se situe dans le quartier suivant:

Hex Dec Char
--- --- ----
…
5A 90 Z
5B 91 [
5C 92 \
5D 93]
5E 94 ^
5F 95 _
…

Ma gamme donc, a pris la forme de [-^, à savoir qu'il comprenait quatre caractères: [, \, ], ^. J'ai également spécifié que le modèle utilise un classement binaire, pour correspondre exactement à la plage ASCII. L' PATINDEXexpression résultante a fini par ressembler à ceci:

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

Le problème évident de cette approche est que la plage au début du modèle comprend deux caractères indésirables, \et ^. La solution a fonctionné pour moi simplement parce que les caractères supplémentaires ne pouvaient jamais apparaître dans les chaînes JSON spécifiques que j'avais besoin d'analyser. Naturellement, cela ne peut pas être vrai en général, donc je suis toujours intéressé par d'autres méthodes, espérons-le plus universelles que les miennes.

Andriy M
la source
4

J'ai une prise de position probablement terrible de l'arrière quand j'ai dû faire beaucoup de fractionnement de cordes.

Si vous avez un jeu de caractères connu, faites-en un tableau.

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)

Ensuite, utilisez cette magie CROSS APPLYavec 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

Si je manque quelque chose d'évident sur ce que vous devez faire, laissez-moi le savoir.

Erik Darling
la source
4

J'ai vu des approches dans le passé pour remplacer le personnage incriminé avant de chercher et de le remettre après.

Dans ce cas, nous pourrions faire quelque chose comme:

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

SET @test = 'Test[]@String'

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

Ce code renvoie correctement 5. J'utilise le caractère ¬ car il est peu probable qu'il apparaisse - s'il n'y a pas de caractères ASCII que vous n'utiliserez pas, cette solution ne fonctionnera pas.

Curieusement, la réponse directe à votre question serait non - je ne peux pas non plus demander à PATINDEX de rechercher «]», mais si vous le remplacez, vous n'en avez pas besoin.

Même exemple mais sans l'utilisation variable:

DECLARE @test NVARCHAR(MAX);

SET @test = 'Test[]@String'

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

L'utilisation de la solution ci-dessus dans votre code donne les résultats requis:

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
la source
4

Étant donné ]que ce n'est spécial que dans [...], vous pouvez l'utiliser PATINDEXdeux fois, en vous déplaçant à l' ]extérieur du [...]. Évaluez les deux PATINDEX('%[[{}:,]%', SourceString)et PATINDEX('%]%', SourceString). Si un résultat est nul, prenez l'autre. Sinon, prenez la moindre des deux valeurs.

Dans votre exemple:

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
la source
-4

Pour un '[' gauche:

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

Pour un droit ']':

PATINDEX('%]%',expression)
Art
la source
1
Ceci spécifie comment rechercher un crochet carré ouvrant ou fermant; l'OP recherche l'un des nombreux caractères (notés en mettant les caractères en question entre crochets), y compris un crochet de fermeture.
RDFozz