Mencocokkan a] (menutup braket persegi) dengan PATINDEX menggunakan wildcard "[]"

9

Saya menulis parser JSON khusus di T-SQL .

Untuk keperluan parser saya, saya menggunakan PATINDEXfungsi yang menghitung posisi token dari daftar token. Token dalam kasus saya semuanya adalah karakter tunggal dan termasuk:

{} []:,

Biasanya, ketika saya perlu menemukan posisi (pertama) dari beberapa karakter yang diberikan, saya menggunakan PATINDEXfungsi seperti ini:

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

Fungsi kemudian akan memberikan posisi pertama aatau batau c- mana kebetulan ditemukan pertama - dalam SourceString.

Sekarang masalah dalam kasus saya tampaknya terhubung dengan ]karakter. Segera setelah saya tentukan dalam daftar karakter, misal seperti ini:

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

Pola yang saya maksudkan ternyata menjadi rusak, karena fungsi tidak pernah menemukan kecocokan. Sepertinya saya perlu cara untuk melarikan diri yang pertama ]sehingga PATINDEXmemperlakukannya sebagai salah satu karakter pencarian daripada simbol khusus.

Saya menemukan pertanyaan ini menanyakan tentang masalah yang serupa:

Namun, dalam hal ]itu tidak perlu ditentukan dalam tanda kurung, karena itu hanya satu karakter dan dapat ditentukan tanpa tanda kurung di sekitarnya. Solusi alternatif, yang menggunakan melarikan diri, hanya berfungsi untuk LIKEdan bukan untuk PATINDEX, karena menggunakan ESCAPEsub - klausa, didukung oleh yang pertama dan bukan oleh yang terakhir.

Jadi, pertanyaan saya adalah, apakah ada cara untuk mencari ]dengan PATINDEXmenggunakan [ ]wildcard? Atau adakah cara untuk meniru fungsi itu menggunakan alat Transact-SQL lainnya?

informasi tambahan

Berikut adalah contoh query di mana saya harus menggunakan PATINDEXdengan […]pola seperti di atas. Pola di sini berfungsi (meskipun agak ) karena tidak termasuk ]karakter. Saya membutuhkannya untuk bekerja dengan ]:

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

Output yang saya dapatkan adalah:

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

Anda dapat melihat bahwa ]ini dimasukkan sebagai bagian dari Sdalam salah satu baris. The Levelkolom menunjukkan tingkat bersarang, yang berarti bracket dan kawat gigi bersarang. Seperti yang Anda lihat, begitu levelnya menjadi 2, ia tidak pernah kembali ke 1. Ini akan terjadi jika saya bisa PATINDEXmengenali ]sebagai token.

Output yang diharapkan untuk contoh di atas adalah:

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

Anda dapat bermain dengan kueri ini di db <> fiddle .


Kami menggunakan SQL Server 2014 dan tidak mungkin untuk segera memutakhirkan ke versi yang mendukung penguraian JSON secara asli. Saya bisa menulis aplikasi untuk melakukan pekerjaan itu tetapi hasil parsing perlu diproses lebih lanjut, yang menyiratkan lebih banyak pekerjaan dalam aplikasi daripada hanya parsing - jenis pekerjaan yang akan jauh lebih mudah, dan mungkin lebih efisien, dilakukan dengan skrip T-SQL, kalau saja saya bisa menerapkannya langsung ke hasil.

Sangat tidak mungkin saya bisa menggunakan SQLCLR sebagai solusi untuk masalah ini. Namun, saya tidak keberatan jika seseorang memutuskan untuk mengirim solusi SQLCLR, karena itu bisa berguna bagi orang lain.

Andriy M
sumber
Bagaimana dengan json yang terlihat seperti itu ["foo]bar”]?
Salman A
@SalmanA: Skenario seperti itu dapat diabaikan dengan aman.
Andriy M

Jawaban:

6

Solusi saya sendiri, yang lebih merupakan solusi, terdiri dalam menentukan rentang karakter yang termasuk ]dan menggunakan rentang itu bersama dengan karakter lain di [ ]wildcard. Saya menggunakan rentang berdasarkan tabel ASCII. Menurut tabel itu, ]karakter terletak di lingkungan berikut:

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

Jangkauan saya, oleh karena itu, mengambil bentuk [-^, yaitu itu termasuk empat karakter: [, \, ], ^. Saya juga menentukan bahwa polanya menggunakan Binary collation, untuk mencocokkan kisaran ASCII dengan tepat. PATINDEXEkspresi yang dihasilkan akhirnya tampak seperti ini:

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

Masalah yang jelas dengan pendekatan ini adalah bahwa rentang di awal pola mencakup dua karakter yang tidak diinginkan, \dan ^. Solusinya bekerja untuk saya hanya karena karakter tambahan tidak pernah dapat terjadi di string JSON tertentu yang saya butuhkan untuk menguraikan. Secara alami, ini tidak mungkin benar secara umum, jadi saya masih tertarik pada metode lain, semoga lebih universal daripada metode saya.

Andriy M
sumber
4

Saya mungkin memiliki pandangan buruk tentang ini dari belakang ketika saya harus melakukan banyak pemisahan tali.

Jika Anda memiliki serangkaian karakter yang diketahui, buatlah tabelnya.

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)

Kemudian gunakan sihir itu CROSS APPLYbersama dengan 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

Jika saya melewatkan sesuatu yang jelas tentang apa yang perlu Anda lakukan, saya tahu.

Erik Darling
sumber
4

Saya telah melihat pendekatan di masa lalu untuk menggantikan karakter yang menyinggung sebelum mencari, dan memasukkannya kembali setelahnya.

Dalam hal ini kita dapat melakukan sesuatu seperti:

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

SET @test = 'Test[]@String'

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

Kode ini kembali dengan benar 5. Saya menggunakan karakter ¬ karena tidak mungkin muncul - jika tidak ada karakter ASCII yang tidak akan Anda gunakan, solusi ini tidak akan berfungsi.

Namun anehnya, jawaban langsung untuk pertanyaan Anda adalah tidak - Saya juga tidak bisa mendapatkan PATINDEX untuk mencari ']', tetapi jika Anda menggantinya, Anda tidak perlu melakukannya.

Contoh yang sama tetapi tanpa penggunaan variabel:

DECLARE @test NVARCHAR(MAX);

SET @test = 'Test[]@String'

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

Menggunakan solusi di atas dalam kode Anda menghasilkan hasil yang Anda perlukan:

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

Karena ]hanya khusus di [...], Anda dapat menggunakan PATINDEXdua kali, bergerak di ]luar [...]. Mengevaluasi keduanya PATINDEX('%[[{}:,]%', SourceString)dan PATINDEX('%]%', SourceString). Jika satu hasilnya nol, ambil yang lainnya. Jika tidak, ambil yang lebih rendah dari dua nilai.

Dalam contoh Anda:

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

Untuk sebelah kiri '[':

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

Untuk hak ']':

PATINDEX('%]%',expression)
Seni
sumber
1
Ini menentukan cara mencari braket persegi pembuka, atau braket penutup; OP sedang mencari salah satu dari beberapa karakter (dicatat dengan melampirkan karakter yang dimaksud dalam tanda kurung siku), termasuk braket kotak penutup.
RDFozz