Sto cercando un Regex che mi consenta di convalidare JSON.
Sono molto nuovo in Regex e so abbastanza che l'analisi con Regex è un male, ma può essere usato per convalidare?
Sto cercando un Regex che mi consenta di convalidare JSON.
Sono molto nuovo in Regex e so abbastanza che l'analisi con Regex è un male, ma può essere usato per convalidare?
Risposte:
La maggior parte delle moderne implementazioni di espressioni regolari consente espressioni regex ricorsive, che possono verificare una struttura serializzata JSON completa. La specifica di json.org lo rende abbastanza semplice.
$pcre_regex = '
/
(?(DEFINE)
(?<number> -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? )
(?<boolean> true | false | null )
(?<string> " ([^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " )
(?<array> \[ (?: (?&json) (?: , (?&json) )* )? \s* \] )
(?<pair> \s* (?&string) \s* : (?&json) )
(?<object> \{ (?: (?&pair) (?: , (?&pair) )* )? \s* \} )
(?<json> \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) \s* )
)
\A (?&json) \Z
/six
';
Funziona abbastanza bene in PHP con le funzioni PCRE . Dovrebbe funzionare senza modifiche in Perl; e può certamente essere adattato per altre lingue. Inoltre riesce con i casi di test JSON .
Un approccio più semplice è il controllo di coerenza minimo come specificato in RFC4627, sezione 6 . Tuttavia, è inteso solo come test di sicurezza e precauzione di base di non validità:
var my_JSON_object = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
text.replace(/"(\\.|[^"\\])*"/g, ''))) &&
eval('(' + text + ')');
false
corrisponde mentre il valore JSON di livello superiore deve essere un array o un oggetto. Ha anche molti problemi nel set di caratteri consentito nelle stringhe o negli spazi.
Sì, è un'idea sbagliata comune che le espressioni regolari possano corrispondere solo alle lingue normali . In effetti, le funzioni PCRE possono corrispondere molto di più rispetto ai linguaggi normali , possono abbinare anche alcuni linguaggi non privi di contesto! L'articolo di Wikipedia su RegExps ha una sezione speciale a riguardo.
JSON può essere riconosciuto utilizzando PCRE in diversi modi! @mario ha mostrato un'ottima soluzione utilizzando modelli secondari denominati e riferimenti a ritroso . Quindi ha notato che dovrebbe esserci una soluzione utilizzando modelli ricorsivi (?R)
. Ecco un esempio di tale regexp scritto in PHP:
$regexString = '"([^"\\\\]*|\\\\["\\\\bfnrt\/]|\\\\u[0-9a-f]{4})*"';
$regexNumber = '-?(?=[1-9]|0(?!\d))\d+(\.\d+)?([eE][+-]?\d+)?';
$regexBoolean= 'true|false|null'; // these are actually copied from Mario's answer
$regex = '/\A('.$regexString.'|'.$regexNumber.'|'.$regexBoolean.'|'; //string, number, boolean
$regex.= '\[(?:(?1)(?:,(?1))*)?\s*\]|'; //arrays
$regex.= '\{(?:\s*'.$regexString.'\s*:(?1)(?:,\s*'.$regexString.'\s*:(?1))*)?\s*\}'; //objects
$regex.= ')\Z/is';
Sto usando (?1)
invece di(?R)
perché quest'ultimo fa riferimento all'intero pattern, ma abbiamo \A
e \Z
sequenze che non dovrebbero essere usate all'interno dei subpattern. (?1)
riferimenti alla regexp contrassegnati dalle parentesi più esterne (questo è il motivo per cui il più esterno ( )
non inizia con ?:
). Quindi, la RegExp diventa lunga 268 caratteri :)
/\A("([^"\\]*|\\["\\bfnrt\/]|\\u[0-9a-f]{4})*"|-?(?=[1-9]|0(?!\d))\d+(\.\d+)?([eE][+-]?\d+)?|true|false|null|\[(?:(?1)(?:,(?1))*)?\s*\]|\{(?:\s*"([^"\\]*|\\["\\bfnrt\/]|\\u[0-9a-f]{4})*"\s*:(?1)(?:,\s*"([^"\\]*|\\["\\bfnrt\/]|\\u[0-9a-f]{4})*"\s*:(?1))*)?\s*\})\Z/is
Ad ogni modo, questo dovrebbe essere trattato come una "dimostrazione tecnologica", non come una soluzione pratica. In PHP convaliderò la stringa JSON chiamando il filejson_decode()
funzione (proprio come ha notato @Epcylon). Se intendo utilizzare quel JSON (se convalidato), questo è il metodo migliore.
\d
è pericoloso. In molte implementazioni regexp \d
corrisponde alla definizione Unicode di una cifra che non è solo [0-9]
ma include script alternativi.
\d
che non corrisponda ai numeri Unicode nell'implementazione PHP di PCRE. Ad esempio, il ٩
simbolo (0x669 cifra nove arabo-indic) verrà abbinato utilizzando il modello #\p{Nd}#u
ma non#\d#u
/u
bandiera. JSON è codificato in UTF-8. Per una corretta regexp dovresti usare quella bandiera.
u
modificatore, per favore guarda di nuovo i pattern nel mio commento precedente :) Stringhe, numeri e booleani SONO correttamente abbinati al livello superiore. Puoi incollare la lunga espressione regolare qui quanetic.com/Regex e provare tu stesso
A causa della natura ricorsiva di JSON (nidificato {...}
-s), regex non è adatto per convalidarlo. Certo, alcune espressioni regolari possono abbinare ricorsivamente i pattern * (e quindi possono corrispondere a JSON), ma i pattern risultanti sono orribili da guardare e non dovrebbero mai essere usati nel codice di produzione IMO!
* Attenzione però, molte implementazioni di regex non supportano modelli ricorsivi. Tra i più diffusi linguaggi di programmazione, questi supportano modelli ricorsivi: Perl, .NET, PHP e Ruby 1.9.2
Ho provato la risposta di @ mario, ma non ha funzionato per me, perché ho scaricato la suite di test da JSON.org ( archivio ) e c'erano 4 test falliti (fail1.json, fail18.json, fail25.json, fail27. json).
Ho esaminato gli errori e ho scoperto che in fail1.json
realtà è corretto (secondo la nota del manuale e la stringa valida RFC-7159 è anche un JSON valido). Anche il file fail18.json
non era il caso, perché contiene effettivamente un JSON profondamente annidato corretto:
[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]
Quindi sono rimasti due file: fail25.json
e fail27.json
:
[" tab character in string "]
e
["line
break"]
Entrambi contengono caratteri non validi. Quindi ho aggiornato il modello in questo modo (modello secondario di stringa aggiornato):
$pcreRegex = '/
(?(DEFINE)
(?<number> -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? )
(?<boolean> true | false | null )
(?<string> " ([^"\n\r\t\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " )
(?<array> \[ (?: (?&json) (?: , (?&json) )* )? \s* \] )
(?<pair> \s* (?&string) \s* : (?&json) )
(?<object> \{ (?: (?&pair) (?: , (?&pair) )* )? \s* \} )
(?<json> \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) \s* )
)
\A (?&json) \Z
/six';
Quindi ora tutti i test legali da json.org possono essere superati.
Guardando la documentazione per JSON , sembra che la regex possa essere semplicemente di tre parti se l'obiettivo è solo quello di verificare l'idoneità:
[]
o{}
[{\[]{1}
...[}\]]{1}
[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]
...""
".*?"
...Tutti insieme:
[{\[]{1}([,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]|".*?")+[}\]]{1}
Se la stringa JSON contiene newline
caratteri, è necessario utilizzare l' singleline
opzione nella versione regex in modo che .
corrisponda newline
. Si noti che questo non fallirà su tutti i JSON non validi, ma fallirà se la struttura JSON di base non è valida, che è un modo semplice per eseguire una convalida di sanità di base prima di passarla a un parser.
[{\[]{1}([,:{}\[\]0-9.\-+A-zr-u \n\r\t]|".*:?")+[}\]]{1}
Ho creato un'implementazione Ruby della soluzione di Mario, che funziona:
# encoding: utf-8
module Constants
JSON_VALIDATOR_RE = /(
# define subtypes and build up the json syntax, BNF-grammar-style
# The {0} is a hack to simply define them as named groups here but not match on them yet
# I added some atomic grouping to prevent catastrophic backtracking on invalid inputs
(?<number> -?(?=[1-9]|0(?!\d))\d+(\.\d+)?([eE][+-]?\d+)?){0}
(?<boolean> true | false | null ){0}
(?<string> " (?>[^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " ){0}
(?<array> \[ (?> \g<json> (?: , \g<json> )* )? \s* \] ){0}
(?<pair> \s* \g<string> \s* : \g<json> ){0}
(?<object> \{ (?> \g<pair> (?: , \g<pair> )* )? \s* \} ){0}
(?<json> \s* (?> \g<number> | \g<boolean> | \g<string> | \g<array> | \g<object> ) \s* ){0}
)
\A \g<json> \Z
/uix
end
########## inline test running
if __FILE__==$PROGRAM_NAME
# support
class String
def unindent
gsub(/^#{scan(/^(?!\n)\s*/).min_by{|l|l.length}}/u, "")
end
end
require 'test/unit' unless defined? Test::Unit
class JsonValidationTest < Test::Unit::TestCase
include Constants
def setup
end
def test_json_validator_simple_string
assert_not_nil %s[ {"somedata": 5 }].match(JSON_VALIDATOR_RE)
end
def test_json_validator_deep_string
long_json = <<-JSON.unindent
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"id": 1918723,
"boolean": true,
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
JSON
assert_not_nil long_json.match(JSON_VALIDATOR_RE)
end
end
end
Per "stringhe e numeri", penso che l'espressione regolare parziale per i numeri:
-?(?:0|[1-9]\d*)(?:\.\d+)(?:[eE][+-]\d+)?
dovrebbe essere invece:
-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+\-]?\d+)?
poiché la parte decimale del numero è facoltativa, e probabilmente è anche più sicuro sfuggire al -
simbolo [+-]
poiché ha un significato speciale tra parentesi
\d
è pericoloso. In molte implementazioni regexp \d
corrisponde alla definizione Unicode di una cifra che non è solo [0-9]
ma include script alternativi.
Una virgola finale in un array JSON ha causato il blocco del mio Perl 5.16, probabilmente perché continuava a tornare indietro. Ho dovuto aggiungere una direttiva di terminazione del backtrack:
(?<json> \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) )(*PRUNE) \s* )
^^^^^^^^
In questo modo, una volta identificato un costrutto che non è "opzionale" ( *
o ?
), non dovrebbe tentare di tornare indietro per cercare di identificarlo come qualcos'altro.
Come è stato scritto sopra, se la lingua che usi ha una libreria JSON in dotazione, usala per provare a decodificare la stringa e catturare l'eccezione / errore se fallisce! Se il linguaggio non lo fa (ha appena avuto un caso del genere con FreeMarker) la seguente regex potrebbe almeno fornire una convalida di base (è scritta per PHP / PCRE per essere testabile / utilizzabile per più utenti). Non è così infallibile come la soluzione accettata, ma nemmeno così spaventoso =):
~^\{\s*\".*\}$|^\[\n?\{\s*\".*\}\n?\]$~s
breve spiegazione:
// we have two possibilities in case the string is JSON
// 1. the string passed is "just" a JSON object, e.g. {"item": [], "anotheritem": "content"}
// this can be matched by the following regex which makes sure there is at least a {" at the
// beginning of the string and a } at the end of the string, whatever is inbetween is not checked!
^\{\s*\".*\}$
// OR (character "|" in the regex pattern)
// 2. the string passed is a JSON array, e.g. [{"item": "value"}, {"item": "value"}]
// which would be matched by the second part of the pattern above
^\[\n?\{\s*\".*\}\n?\]$
// the s modifier is used to make "." also match newline characters (can happen in prettyfied JSON)
se mi sono perso qualcosa che potrebbe rompere questo involontariamente, sono grato per i commenti!
convalida chiave (stringa): valore (stringa, intero, [{chiave: valore}, {chiave: valore}], {chiave: valore})
^\{(\s|\n\s)*(("\w*"):(\s)*("\w*"|\d*|(\{(\s|\n\s)*(("\w*"):(\s)*("\w*(,\w+)*"|\d{1,}|\[(\s|\n\s)*(\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):(\s)*("\w*"|\d{1,}))*(\s|\n)*\})){1}(\s|\n\s)*(,(\s|\n\s)*\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):(\s)*("\w*"|\d{1,}))*(\s|\n)*\})?)*(\s|\n\s)*\]))((,(\s|\n\s)*"\w*"):(\s)*("\w*(,\w+)*"|\d{1,}|\[(\s|\n\s)*(\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):(\s)*("\w*"|\d{1,}))*(\s|\n)*\})){1}(\s|\n\s)*(,(\s|\n\s)*\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):("\w*"|\d{1,}))*(\s|\n)*\})?)*(\s|\n\s)*\]))*(\s|\n\s)*\}){1}))((,(\s|\n\s)*"\w*"):(\s)*("\w*"|\d*|(\{(\s|\n\s)*(("\w*"):(\s)*("\w*(,\w+)*"|\d{1,}|\[(\s|\n\s)*(\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):(\s)*("\w*"|\d{1,}))*(\s|\n)*\})){1}(\s|\n\s)*(,(\s|\n\s)*\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):(\s)*("\w*"|\d{1,}))*(\s|\n)*\})?)*(\s|\n\s)*\]))((,(\s|\n\s)*"\w*"):(\s)*("\w*(,\w+)*"|\d{1,}|\[(\s|\n\s)*(\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):(\s)*("\w*"|\d{1,}))*(\s|\n)*\})){1}(\s|\n\s)*(,(\s|\n\s)*\{(\s|\n\s)*(("\w*"):(\s)*(("\w*"|\d{1,}))((,(\s|\n\s)*"\w*"):("\w*"|\d{1,}))*(\s|\n)*\})?)*(\s|\n\s)*\]))*(\s|\n\s)*\}){1}))*(\s|\n)*\}$
{
"key":"string",
"key": 56,
"key":{
"attr":"integer",
"attr": 12
},
"key":{
"key":[
{
"attr": 4,
"attr": "string"
}
]
}
}
Qui la mia espressione regolare per convalidare la stringa:
^\"([^\"\\]*|\\(["\\\/bfnrt]{1}|u[a-f0-9]{4}))*\"$
È stato scritto utilizzando il diagramma di sintassi originale .