Corrispondenza a] (parentesi quadra di chiusura) con PATINDEX utilizzando il carattere jolly “[]”


9

Sto scrivendo un parser JSON personalizzato in T-SQL .

Ai fini del mio parser, sto usando la PATINDEXfunzione che calcola la posizione di un token da un elenco di token. I token nel mio caso sono tutti caratteri singoli e includono questi:

{} []:,

Di solito, quando ho bisogno di trovare la (prima) posizione di uno qualsiasi dei vari caratteri, uso la PATINDEXfunzione in questo modo:

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

La funzione mi darà quindi la prima posizione di ao bo c- qualunque sia la prima volta che si trova - in SourceString.

Ora il problema nel mio caso sembra essere collegato al ]personaggio. Non appena lo specifico nell'elenco dei caratteri, ad esempio in questo modo:

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

il mio modello previsto apparentemente si spezza, perché la funzione non trova mai una corrispondenza. Sembra che ho bisogno di un modo per sfuggire al primo in ]modo che lo PATINDEXtratti come uno dei personaggi di ricerca piuttosto che un simbolo speciale.

Ho trovato questa domanda chiedendo un problema simile:

Tuttavia, in tal caso ]non è necessario specificare semplicemente tra parentesi, poiché è solo un carattere e può essere specificato senza parentesi attorno ad essi. La soluzione alternativa, che utilizza l'escaping, funziona solo per LIKEe non per PATINDEX, perché utilizza un ESCAPEsottoclauso, supportato dal primo e non dal secondo.

Quindi, la mia domanda è, c'è un modo per cercare una ]con PATINDEXutilizzando il [ ]carattere jolly? O c'è un modo per emulare quella funzionalità usando altri strumenti Transact-SQL?

Informazioni aggiuntive

Ecco un esempio di una query in cui devo utilizzare PATINDEXil […]modello come sopra. Il modello qui funziona (anche se in qualche modo ) perché non include il ]personaggio. Ho bisogno che funzioni anche con ]:

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

L'output che ottengo è:

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

Puoi vedere che ]è incluso come parte di Sin una delle righe. La Levelcolonna indica il livello di annidamento, ovvero l'annidamento di parentesi quadre e parentesi graffe. Come puoi vedere, una volta che il livello diventa 2, non torna mai a 1. Avrebbe potuto farlo PATINDEXriconoscere ]come token.

L'output previsto per l'esempio sopra è:

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

Puoi giocare con questa query su db <> fiddle .


Stiamo utilizzando SQL Server 2014 e è improbabile che presto eseguiamo l'aggiornamento a una versione che supporta l'analisi JSON in modo nativo. Potrei scrivere un'applicazione per fare il lavoro, ma i risultati dell'analisi devono essere ulteriormente elaborati, il che implica più lavoro nell'applicazione rispetto al solo analisi - il tipo di lavoro che sarebbe molto più facile, e probabilmente più efficiente, fatto con uno script T-SQL, se solo potessi applicarlo direttamente ai risultati.

È molto improbabile che io possa usare SQLCLR come soluzione per questo problema. Tuttavia, non mi importa se qualcuno decide di pubblicare una soluzione SQLCLR, dal momento che potrebbe essere utile per gli altri.


E che mi dici di Json ["foo]bar”]?
Salman A

@SalmanA: tali scenari possono essere tranquillamente ignorati.
Andriy M,

Risposte:


6

La mia soluzione, che è più una soluzione alternativa, consisteva nello specificare un intervallo di caratteri che includesse ]e utilizzare quell'intervallo insieme agli altri caratteri nel carattere [ ]jolly. Ho usato un intervallo basato sulla tabella ASCII. Secondo tale tabella, il ]personaggio si trova nel seguente quartiere:

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

La mia gamma, quindi, ha preso la forma di [-^, vale a dire che comprendeva quattro personaggi: [, \, ], ^. Ho anche specificato che il modello utilizza un confronto binario, per corrispondere esattamente all'intervallo ASCII. L' PATINDEXespressione risultante è risultata così:

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

Il problema ovvio con questo approccio è che l'intervallo all'inizio del modello include due caratteri indesiderati \e ^. La soluzione ha funzionato per me semplicemente perché i caratteri extra non potevano mai comparire nelle stringhe JSON specifiche che dovevo analizzare. Naturalmente, questo non può essere vero in generale, quindi sono ancora interessato ad altri metodi, si spera più universali dei miei.


4

Probabilmente ho una visione terribile di questo da quando ho dovuto fare molte spaccature.

Se hai un set noto di personaggi, creane una tabella.

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)

Quindi usa quel magico CROSS APPLYinsieme a 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

Se mi manca qualcosa di ovvio su ciò che devi fare, fammi sapere.


4

Ho visto approcci in passato per sostituire il personaggio offensivo prima di cercare e rimetterlo in gioco in seguito.

In questo caso potremmo fare qualcosa del tipo:

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

SET @test = 'Test[]@String'

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

Questo codice restituisce correttamente 5. Sto usando il carattere ¬ poiché è improbabile che appaia - se non ci sono caratteri ASCII che non utilizzerai, questa soluzione non funzionerà.

Stranamente, però, la risposta diretta alla tua domanda sarebbe no: non riesco nemmeno a cercare PATINDEX per "]", ma se lo sostituisci non è necessario.

Stesso esempio ma senza l'utilizzo variabile:

DECLARE @test NVARCHAR(MAX);

SET @test = 'Test[]@String'

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

L'uso della soluzione sopra nel codice produce i risultati richiesti:

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

4

Dal momento che ]è speciale solo in [...], è possibile utilizzare PATINDEXdue volte, spostandosi ]al di fuori del [...]. Valuta entrambi PATINDEX('%[[{}:,]%', SourceString)e PATINDEX('%]%', SourceString). Se un risultato è zero, prendi l'altro. Altrimenti, prendi il minore dei due valori.

Nel tuo esempio:

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


-4

Per una sinistra '[':

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

Per un diritto ']':

PATINDEX('%]%',expression)

1
Questo specifica come cercare una parentesi quadra aperta o una chiusa; l'OP sta cercando uno dei numerosi caratteri (indicato racchiudendo i caratteri in questione tra parentesi quadre), inclusa una parentesi quadra di chiusura.
RDFozz
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.