Matching a] (schließende eckige Klammer) mit PATINDEX unter Verwendung des Platzhalters „[]“

9

Ich schreibe einen benutzerdefinierten JSON-Parser in T-SQL .

Für den Zweck meines Parsers verwende ich die PATINDEXFunktion, die die Position eines Tokens aus einer Liste von Token berechnet. Die Token in meinem Fall sind alle Einzelzeichen und enthalten Folgendes:

{} []:,

Wenn ich die (erste) Position eines von mehreren gegebenen Zeichen finden muss, verwende ich normalerweise die folgende PATINDEXFunktion:

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

Die Funktion gibt mir dann die erste Position von aoder boder c- je nachdem, was zuerst gefunden wird - in SourceString.

Jetzt scheint das Problem in meinem Fall mit dem ]Charakter verbunden zu sein. Sobald ich es in der Zeichenliste spezifiziere, zB so:

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

Mein beabsichtigtes Muster wird anscheinend gebrochen, weil die Funktion nie eine Übereinstimmung findet. Es sieht ]so aus, PATINDEXals ob ich einen Weg brauche, um dem ersten zu entkommen, damit er als eines der Suchzeichen und nicht als spezielles Symbol behandelt wird.

Ich habe diese Frage zu einem ähnlichen Problem gefunden:

In diesem Fall muss das ]einfach nicht in Klammern angegeben werden, da es nur ein Zeichen ist und ohne Klammern angegeben werden kann. Die alternative Lösung, die Escape verwendet, funktioniert nur für LIKEund nicht für PATINDEX, da sie einen ESCAPEUnterabschnitt verwendet, der von der ersteren und nicht von der letzteren unterstützt wird.

Meine Frage ist also, gibt es eine Möglichkeit, ]mit PATINDEXdem [ ]Platzhalter nach einem zu suchen ? Oder gibt es eine Möglichkeit, diese Funktionalität mit anderen Transact-SQL-Tools zu emulieren?

zusätzliche Information

Hier ist ein Beispiel für eine Abfrage, bei der ich PATINDEXdas […]Muster wie oben verwenden muss. Das Muster hier funktioniert (wenn auch etwas ), weil es das ]Zeichen nicht enthält . Ich brauche es auch, um damit zu arbeiten ]:

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

Die Ausgabe, die ich bekomme, ist:

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

Sie können sehen, dass das ]als Teil von Sin einer der Zeilen enthalten ist. Die LevelSpalte gibt die Verschachtelungsebene an, dh die Verschachtelung von Klammern und Klammern. Wie Sie sehen können, sobald die Stufe 2 wird, gibt es nie zu 1. Es wäre, wenn ich machen könnte PATINDEXerkennen , ]als Zeichen.

Die erwartete Ausgabe für das obige Beispiel lautet:

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

Sie können mit dieser Abfrage bei db <> fiddle spielen .


Wir verwenden SQL Server 2014 und werden wahrscheinlich nicht bald auf eine Version aktualisieren, die JSON-Parsing nativ unterstützt. Ich könnte eine Anwendung schreiben, um die Arbeit zu erledigen, aber die Ergebnisse der Analyse müssen weiter verarbeitet werden, was mehr Arbeit in der Anwendung als nur die Analyse bedeutet - die Art von Arbeit, die viel einfacher und wahrscheinlich effizienter erledigt werden würde ein T-SQL-Skript, wenn ich es nur direkt auf die Ergebnisse anwenden könnte.

Es ist sehr unwahrscheinlich, dass ich SQLCLR als Lösung für dieses Problem verwenden kann. Es macht mir jedoch nichts aus, wenn sich jemand entscheidet, eine SQLCLR-Lösung zu veröffentlichen, da dies für andere nützlich sein könnte.

Andriy M.
quelle
Was ist mit json, das aussieht ["foo]bar”]?
Salman A
@ SalmanA: Solche Szenarien können ignoriert werden.
Andriy M

Antworten:

6

Meine eigene Lösung, die eher eine Problemumgehung darstellt, bestand darin, einen Zeichenbereich anzugeben, der den Bereich enthielt, ]und diesen Bereich zusammen mit den anderen Zeichen im [ ]Platzhalter zu verwenden. Ich habe einen Bereich verwendet, der auf der ASCII-Tabelle basiert. Gemäß dieser Tabelle befindet sich der ]Charakter in der folgenden Nachbarschaft:

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

Mein Bereich, daher nahm die Form [-^, dh es vier Zeichen enthalten: [, \, ], ^. Ich habe auch angegeben, dass das Muster eine binäre Kollatierung verwendet, um genau mit dem ASCII-Bereich übereinzustimmen. Der resultierende PATINDEXAusdruck sah folgendermaßen aus:

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

Das offensichtliche Problem bei diesem Ansatz besteht darin, dass der Bereich am Anfang des Musters zwei unerwünschte Zeichen enthält, \und ^. Die Lösung funktionierte für mich einfach, weil die zusätzlichen Zeichen in den spezifischen JSON-Zeichenfolgen, die ich zum Parsen benötigte, niemals vorkommen konnten. Natürlich kann dies im Allgemeinen nicht zutreffen, daher interessiere ich mich immer noch für andere Methoden, die hoffentlich universeller sind als meine.

Andriy M.
quelle
4

Ich habe eine wahrscheinlich schreckliche Einstellung von hinten, als ich viel Saitensplitting machen musste.

Wenn Sie einen bekannten Zeichensatz haben, erstellen Sie eine Tabelle daraus.

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)

Dann benutze diese Magie CROSS APPLYzusammen mit 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

Wenn mir etwas Offensichtliches fehlt, was Sie tun müssen, weiß ich Bescheid.

Erik Darling
quelle
4

Ich habe in der Vergangenheit Ansätze gesehen, um den beleidigenden Charakter vor der Suche zu ersetzen und ihn anschließend wieder einzufügen.

In diesem Fall könnten wir so etwas tun:

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

SET @test = 'Test[]@String'

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

Dieser Code gibt korrekt 5 zurück. Ich verwende das Zeichen ¬, da es unwahrscheinlich ist, dass es angezeigt wird. Wenn keine ASCII-Zeichen vorhanden sind, die Sie nicht verwenden, funktioniert diese Lösung nicht.

Seltsamerweise wäre die direkte Antwort auf Ihre Frage nein - ich kann PATINDEX auch nicht dazu bringen, nach ']' zu suchen, aber wenn Sie es ersetzen, müssen Sie es nicht.

Gleiches Beispiel, jedoch ohne variable Verwendung:

DECLARE @test NVARCHAR(MAX);

SET @test = 'Test[]@String'

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

Wenn Sie die obige Lösung in Ihrem Code verwenden, erhalten Sie die gewünschten Ergebnisse:

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
quelle
4

Da ]es nur etwas Besonderes ist [...], können Sie es PATINDEXzweimal verwenden und sich ]außerhalb des bewegen [...]. Bewerten Sie beide PATINDEX('%[[{}:,]%', SourceString)und PATINDEX('%]%', SourceString). Wenn ein Ergebnis Null ist, nehmen Sie das andere. Andernfalls nehmen Sie den kleineren der beiden Werte.

In Ihrem Beispiel:

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
quelle
-4

Für ein linkes '[':

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

Für ein Recht ']':

PATINDEX('%]%',expression)
Kunst
quelle
1
Dies gibt an, wie nach einer öffnenden oder einer schließenden eckigen Klammer gesucht werden soll. Das OP sucht nach einem von mehreren Zeichen (angegeben durch Einfügen der betreffenden Zeichen in eckige Klammern), einschließlich einer schließenden eckigen Klammer.
RDFozz