Обработка ленивого JSON в Python - "Ожидание названия свойства"
Используя модуль Pythons (2.7) 'json', я ищу для обработки различных каналов JSON. К сожалению, некоторые из этих каналов не соответствуют стандартам JSON - в некоторых некоторых ключах не завертываются двойные речевые метки ("). Это приводит к ошибке Python.
Прежде чем писать фрагмент кода с уродливым адским кодом для анализа и исправления входящих данных, я подумал, что попрошу - есть ли способ разрешить Python либо анализировать этот искаженный JSON, либо "ремонтировать" данные, чтобы было бы действительно JSON?
Рабочий пример
import json
>>> json.loads('{"key1":1,"key2":2,"key3":3}')
{'key3': 3, 'key2': 2, 'key1': 1}
Неисправный пример
import json
>>> json.loads('{key1:1,key2:2,key3:3}')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Python27\lib\json\__init__.py", line 310, in loads
return _default_decoder.decode(s)
File "C:\Python27\lib\json\decoder.py", line 346, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "C:\Python27\lib\json\decoder.py", line 362, in raw_decode
obj, end = self.scan_once(s, idx)
ValueError: Expecting property name: line 1 column 1 (char 1)
Я написал небольшой REGEX, чтобы исправить JSON, исходящий от этого конкретного провайдера, но я предвижу, что это будет проблемой в будущем. Ниже я придумал.
>>> import re
>>> s = '{key1:1,key2:2,key3:3}'
>>> s = re.sub('([{,])([^{:\s"]*):', lambda m: '%s"%s":'%(m.group(1),m.group(2)),s)
>>> s
'{"key1":1,"key2":2,"key3":3}'
Ответы
Ответ 1
Вы пытаетесь использовать парсер JSON для анализа того, что не является JSON. Ваш лучший выбор - заставить создателя фидов исправить их.
Я понимаю, что это не всегда возможно. Возможно, вы сможете исправить данные с помощью регулярных выражений, в зависимости от того, насколько они нарушены:
j = re.sub(r"{\s*(\w)", r'{"\1', j)
j = re.sub(r",\s*(\w)", r',"\1', j)
j = re.sub(r"(\w):", r'\1":', j)
Ответ 2
Другой вариант - использовать модуль demjson, который может анализировать json в нестрогом режиме.
Ответ 3
Регулярные выражения, отмеченные Ned и cheeseinvert, не учитываются, когда совпадение находится внутри строки.
См. следующий пример (с использованием решения для сыворотки):
>>> fixLazyJsonWithRegex ('{ key : "a { a : b }", }')
'{ "key" : "a { "a": b }" }'
Проблема заключается в том, что ожидаемый результат:
'{ "key" : "a { a : b }" }'
Поскольку токены JSON являются подмножеством токенов python, мы можем использовать python tokenize module.
Пожалуйста, исправьте меня, если я ошибаюсь, но следующий код исправит ленивую строку json во всех случаях:
import tokenize
import token
from StringIO import StringIO
def fixLazyJson (in_text):
tokengen = tokenize.generate_tokens(StringIO(in_text).readline)
result = []
for tokid, tokval, _, _, _ in tokengen:
# fix unquoted strings
if (tokid == token.NAME):
if tokval not in ['true', 'false', 'null', '-Infinity', 'Infinity', 'NaN']:
tokid = token.STRING
tokval = u'"%s"' % tokval
# fix single-quoted strings
elif (tokid == token.STRING):
if tokval.startswith ("'"):
tokval = u'"%s"' % tokval[1:-1].replace ('"', '\\"')
# remove invalid commas
elif (tokid == token.OP) and ((tokval == '}') or (tokval == ']')):
if (len(result) > 0) and (result[-1][1] == ','):
result.pop()
# fix single-quoted strings
elif (tokid == token.STRING):
if tokval.startswith ("'"):
tokval = u'"%s"' % tokval[1:-1].replace ('"', '\\"')
result.append((tokid, tokval))
return tokenize.untokenize(result)
Итак, чтобы разобрать json-строку, вы можете инкапсулировать вызов fixLazyJson после сбоя json.loads(чтобы избежать штрафных санкций за корректный json):
import json
def json_decode (json_string, *args, **kwargs):
try:
json.loads (json_string, *args, **kwargs)
except:
json_string = fixLazyJson (json_string)
json.loads (json_string, *args, **kwargs)
Единственная проблема, которую я вижу при установке lazy json, заключается в том, что если json неверен, ошибка, вызванная вторым json.loads, не будет ссылаться на строку и столбец из исходной строки, а на измененную.
В качестве заключительной заметки я просто хочу отметить, что было бы легко обновить любой из методов для принятия файлового объекта вместо строки.
BONUS: Кроме того, людям обычно нравится включать комментарии C/С++, когда json используется для
файлы конфигурации, в этом случае вы можете удалить комментарии с помощью обычного выражения или использовать расширенную версию и исправить строку json за один проход:
import tokenize
import token
from StringIO import StringIO
def fixLazyJsonWithComments (in_text):
""" Same as fixLazyJson but removing comments as well
"""
result = []
tokengen = tokenize.generate_tokens(StringIO(in_text).readline)
sline_comment = False
mline_comment = False
last_token = ''
for tokid, tokval, _, _, _ in tokengen:
# ignore single line and multi line comments
if sline_comment:
if (tokid == token.NEWLINE) or (tokid == tokenize.NL):
sline_comment = False
continue
# ignore multi line comments
if mline_comment:
if (last_token == '*') and (tokval == '/'):
mline_comment = False
last_token = tokval
continue
# fix unquoted strings
if (tokid == token.NAME):
if tokval not in ['true', 'false', 'null', '-Infinity', 'Infinity', 'NaN']:
tokid = token.STRING
tokval = u'"%s"' % tokval
# fix single-quoted strings
elif (tokid == token.STRING):
if tokval.startswith ("'"):
tokval = u'"%s"' % tokval[1:-1].replace ('"', '\\"')
# remove invalid commas
elif (tokid == token.OP) and ((tokval == '}') or (tokval == ']')):
if (len(result) > 0) and (result[-1][1] == ','):
result.pop()
# detect single-line comments
elif tokval == "//":
sline_comment = True
continue
# detect multiline comments
elif (last_token == '/') and (tokval == '*'):
result.pop() # remove previous token
mline_comment = True
continue
result.append((tokid, tokval))
last_token = tokval
return tokenize.untokenize(result)
Ответ 4
Развернув предложение Ned, мне было полезно следующее:
j = re.sub(r"{\s*'?(\w)", r'{"\1', j)
j = re.sub(r",\s*'?(\w)", r',"\1', j)
j = re.sub(r"(\w)'?\s*:", r'\1":', j)
j = re.sub(r":\s*'(\w+)'\s*([,}])", r':"\1"\2', j)
Ответ 5
В аналогичном случае я использовал ast.literal_eval
. AFAIK, это не будет работать, только когда константа null
(соответствующая Python None
) появится в JSON.
Учитывая, что вы знаете о затруднительном положении null/None
, вы можете:
import ast
decoded_object= ast.literal_eval(json_encoded_text)
Ответ 6
В дополнение к предложениям Neds и cheeseinvert добавление (?!/)
должно избегать указанной проблемы с URL-адресами
j = re.sub(r"{\s*'?(\w)", r'{"\1', j)
j = re.sub(r",\s*'?(\w)", r',"\1', j)
j = re.sub(r"(\w)'?\s*:(?!/)", r'\1":', j)
j = re.sub(r":\s*'(\w+)'\s*([,}])", r':"\1"\2', j)
j = re.sub(r",\s*]", "]", j)