Regex для проверки JSON

Я ищу Regex, который позволяет мне проверить JSON.

Я очень плохо знаком с Regex и знаю, что синтаксический анализ с Regex - это плохо, но можно ли его использовать для проверки?

11 ответов

Решение

Да, возможна полная проверка регулярных выражений.

Большинство современных реализаций регулярных выражений допускают рекурсивные выражения регулярных выражений, которые могут проверить полную сериализованную структуру JSON. Спецификация http://json.org/ делает это довольно простым.

$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   
';

Это работает довольно хорошо в PHP с функциями PCRE. Должен работать без изменений в Perl; и, безусловно, может быть адаптирован для других языков. Также это успешно с тестовыми случаями JSON.

Более простая проверка RFC4627

Более простым подходом является минимальная проверка согласованности, как указано в RFC4627, раздел 6. Тем не менее, он предназначен только для проверки безопасности и основных мер предосторожности:

  var my_JSON_object = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
         text.replace(/"(\\.|[^"\\])*"/g, ''))) &&
     eval('(' + text + ')');

Да, это распространенное заблуждение, что регулярные выражения могут соответствовать только обычным языкам. Фактически, функции PCRE могут соответствовать гораздо большему, чем обычные языки, они могут соответствовать даже некоторым неконтекстно-свободным языкам! В статье Википедии о RegExps есть специальный раздел об этом.

JSON можно распознать с помощью PCRE несколькими способами! @mario показал одно отличное решение с использованием именованных шаблонов и обратных ссылок. Затем он отметил, что должно быть решение с использованием рекурсивных шаблонов (?R), Вот пример такого регулярного выражения, написанного на 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';

я использую (?1) вместо (?R) потому что последний ссылается на весь шаблон, но мы имеем \A а также \Z последовательности, которые не должны использоваться внутри подшаблона. (?1) ссылки на регулярное выражение, помеченные самыми внешними скобками (вот почему самые внешние ( ) не начинается с ?:). Итак, RegExp становится длиной 268 символов:)

/\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

В любом случае, это следует рассматривать как "демонстрацию технологии", а не как практическое решение. В PHP я проверю строку JSON с вызовом json_decode() функция (так же, как отметил @Epcylon). Если я собираюсь использовать этот JSON (если он проверен), то это лучший метод.

Из-за рекурсивной природы JSON (вложенный {...}-s), регулярное выражение не подходит для проверки. Конечно, некоторые разновидности регулярных выражений могут рекурсивно соответствовать шаблонам* (и, следовательно, могут соответствовать JSON), но результирующие шаблоны ужасно смотреть и никогда не должны использоваться в производственном коде IMO!

* Остерегайтесь, однако, многие реализации регулярных выражений не поддерживают рекурсивные шаблоны. Из популярных языков программирования они поддерживают рекурсивные шаблоны: Perl, .NET, PHP и Ruby 1.9.2.

Глядя на документацию по JSON, кажется, что регулярное выражение может просто состоять из трех частей, если целью является просто проверка на пригодность:

  1. Строка начинается и заканчивается либо [] или же {}
    • [{\[]{1}... [}\]]{1}
  2. а также
    1. Этот символ является допустимым управляющим символом JSON (только один)
      • ... [,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]...
    2. или Набор символов, содержащихся в ""
      • ... ".*?"...

Все вместе: [{\[]{1}([,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]|".*?")+[}\]]{1}

Если строка JSON содержит newline символы, то вы должны использовать singleline включите свой регулярное выражение так, чтобы . Матчи newline, Обратите внимание, что это не сработает на всех плохих JSON, но сработает, если базовая структура JSON будет недопустимой, что является простым способом выполнить базовую проверку работоспособности перед передачей ее парсеру.

Я попробовал ответ @mario, но у меня он не сработал, потому что я скачал набор тестов с JSON.org ( архив) и было 4 неудачных теста (fail1.json, fail18.json, fail25.json, fail27. JSON).

Я исследовал ошибки и выяснил, что fail1.json на самом деле правильно (согласно примечанию руководства, а допустимая строка RFC-7159 также является допустимым JSON). файл fail18.json Это также не так, потому что он содержит действительно правильный глубоко вложенный JSON:

[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]

Итак, осталось два файла: fail25.json а также fail27.json:

["  tab character   in  string  "]

а также

["line
break"]

Оба содержат недопустимые символы. Итак, я обновил шаблон так (строка обновлена):

$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';

Так что теперь все юридические тесты с json.org можно пройти.

Я создал Ruby-реализацию решения Mario, которая работает:

# 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

Завершающая запятая в массиве JSON приводила к зависанию моего Perl 5.16, возможно, потому, что он продолжал возвращаться. Я должен был добавить директиву возврата назад:

(?<json>   \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) )(*PRUNE) \s* )
                                                                                   ^^^^^^^^

Таким образом, как только он идентифицирует конструкцию, которая не является "необязательной" (* или же ?), он не должен пытаться вернуться назад, чтобы идентифицировать его как что-то еще.


Регулярное выражение, которое проверяет простой JSON, а не JSONArray

он проверяет ключ (строка): значение (строка, целое число,[{ключ: значение},{ключ: значение}],{ключ: значение})

^\{(\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)*\}$

образцы данных, которые проверяются этим JSON

{
"key":"string",
"key": 56,
"key":{
        "attr":"integer",
        "attr": 12
        },
"key":{
        "key":[
            {
                "attr": 4,
                "attr": "string"
            }
        ]
     }
}

Для "строк и чисел" я думаю, что частичное регулярное выражение для чисел:

-?(?:0|[1-9]\d*)(?:\.\d+)(?:[eE][+-]\d+)?

должно быть вместо:

-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+\-]?\d+)?

поскольку десятичная часть числа является необязательной, а также, вероятно, безопаснее избежать - символ в [+-] так как он имеет особое значение в скобках

Как было написано выше, если в используемом вами языке есть JSON-библиотека, используйте его, чтобы попытаться расшифровать строку и перехватить исключение / ошибку, если она потерпит неудачу! Если у языка нет (только что был такой случай с FreeMarker), следующее регулярное выражение может, по крайней мере, обеспечить некоторую базовую проверку (он написан для PHP/PCRE, чтобы его можно было тестировать / использовать для большего количества пользователей). Это не так надежно, как принятое решение, но и не так страшно =)

~^\{\s*\".*\}$|^\[\n?\{\s*\".*\}\n?\]$~s

краткое объяснение:

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

если я пропустил что-то, что непреднамеренно сломало бы это, я благодарен за комментарии!

Вот мое регулярное выражение для проверки строки:

^\"([^\"\\]*|\\(["\\\/bfnrt]{1}|u[a-f0-9]{4}))*\"$

Была написана оригинальная синтаксическая диаграмма.

Я понимаю, что это более 6 лет назад. Тем не менее, я думаю, что есть решение, которое никто здесь не упомянул, которое намного проще, чем регулярное выражение

function isAJSON(string) {
    try {
        JSON.parse(string)  
    } catch(e) {
        if(e instanceof SyntaxError) return false;
    };  
    return true;
}
Другие вопросы по тегам