T-SQLでカスタムJSONパーサーを作成しています†。
私のパーサーのために、トークンのリストからトークンの位置を計算するPATINDEX
関数を使用しています。私の場合のトークンはすべて1文字で、次のものが含まれています。
{} []:、
通常、与えられたいくつかの文字の(最初の)位置を見つける必要があるときは、次のように PATINDEX
関数を使用します。
PATINDEX('%[abc]%', SourceString)
次に、関数はa
またはb
またはc
の最初の位置を偶然にSourceString
で取得します。
今私の場合の問題は]
文字に関連しているようです。文字リストで指定するとすぐに、例えばこのような:
PATINDEX('%[[]{}:,]%', SourceString)
関数が一致を見つけられないため、私の意図したパターンは明らかに壊れています。最初の]
をエスケープして、PATINDEX
が特殊記号ではなく検索文字の1つとして扱うようにする方法が必要なようです。
私は同様の問題について尋ねるこの質問を見つけました:
ただし、その場合、]
は1文字であり、角かっこなしで指定できるため、角かっこで指定する必要はありません。エスケープを使用する代替ソリューションは、LIKE
に対してのみ機能し、PATINDEX
に対しては機能しません。これは、前者によってサポートされ、後者によってサポートされないESCAPE
サブ句を使用するためです。
だから、私の質問は、]
ワイルドカードを使用してPATINDEX
で[ ]
を検索する方法はありますか?または、他のTransact-SQLツールを使用してその機能をエミュレートする方法はありますか?
上記の[…]
パターンでPATINDEX
を使用する必要があるクエリの例を次に示します。 ]
文字が含まれていないため、ここでのパターンは(somewhatですが)機能します。]
でも機能する必要があります。
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
の一部として行の1つに含まれていることがわかります。 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" }
このクエリ at db <> fiddle で遊ぶことができます。
† SQL Server 2014を使用しており、JSON解析をネイティブでサポートするバージョンにすぐにアップグレードすることはほとんどありません。仕事をするアプリケーションを書くことはできますが、解析の結果をさらに処理する必要があります。これは、解析だけではなく、アプリケーションでより多くの作業が必要であることを意味します。 T-SQLスクリプト(結果に直接適用できる場合のみ)。
この問題の解決策としてSQLCLRを使用できる可能性はほとんどありません。ただし、誰かがSQLCLRソリューションを投稿することを決定してもかまいません。
私の独自の解決策は、より回避策であり、]
を含む文字範囲を指定し、その範囲を[ ]
ワイルドカードの他の文字と一緒に使用することでした。 ASCIIテーブルに基づく範囲を使用しました。そのテーブルによると、]
文字は次の周辺にあります:
16進数の文字 --- --- ---- … 5A 90 Z 5B 91 5C 92\ 5D 93] 5E 94 ^ 5F 95 _ …
したがって、私の範囲は[-^
の形式をとっています。つまり、[
、\
、]
、^
の4文字が含まれています。 ASCII範囲に正確に一致させるために、パターンがバイナリ照合を使用することも指定しました。結果のPATINDEX
式は、次のようになります。
PATINDEX('%[[-^{}:,]%' COLLATE Latin1_General_BIN2, MyJSONString)
このアプローチの明らかな問題は、パターンの最初の範囲に2つの不要な文字\
と^
が含まれていることです。解析が必要だった特定のJSON文字列に余分な文字が含まれることがないため、このソリューションは私にとってはうまくいきました。当然、これは一般的に真実ではあり得ないので、私はまだ他の方法に興味があります。
多くの文字列分割を行わなければならなかったとき、私はおそらくこれを恐ろしく思います。
既知の文字セットがある場合は、それらの表を作成します。
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
私があなたがする必要があることについて明白な何かが欠けているなら、lemmeは知っています。
私は過去に、問題のある文字を検索する前に置き換えて、後で元に戻すというアプローチを見てきました。
この場合、次のようなことができます。
DECLARE @test NVARCHAR(MAX);
DECLARE @replacementcharacter CHAR(1) = CHAR(174);
SET @test = 'Test[]@String'
SELECT PATINDEX('%[[' + @replacementcharacter + '@]%', REPLACE(@test,']',@Replacementcharacter))
このコードは正しく5を返します。表示される可能性が低いため、I文字を使用しています。使用しない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)
;
_]
_は_[...]
_でのみ特別であるため、PATINDEX
を2回使用して、_]
_を_[...]
_の外に移動できます。 PATINDEX('%[[{}:,]%', SourceString)
とPATINDEX('%]%', SourceString)
の両方を評価します。一方の結果がゼロの場合は、もう一方の結果を取ります。それ以外の場合は、2つの値のうち小さい方を使用します。
あなたの例では:
_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