Разбор конкретных JSON-подобных данных (NextSTEP PList) из Ruby
Я пишу клиента в сторонний API, и они предоставляют данные в странном формате. Сначала это может выглядеть как JSON, но это не так, и я немного озадачен тем, как мне это делать.
Это формат, основанный на значениях ключей (очень похожий на JSON).
- Ключи отделяются символом "=" от своих значений.
- Ключи и значения заключены в двойные кавычки.
- Словари начинаются с "{" и заканчиваются на "}".
- Массивы начинаются с '(' и заканчиваются на ')'
- Строки заканчиваются на ";" (Исключается для содержимого массивов) и символа конца строки (\r i think).
- Иногда кажется, что в строках есть Unicode (например, \U2623 для знака BioHazard).
Что может быть в этом формате? Должен ли я использовать готовый драгоценный камень, чтобы разобрать его, или я должен создать свой собственный парсер?
{ "anArray" = (
"100",
"200",
"300"
);
"aDictionary" = {
"aString" = "Something";
};
}
РЕДАКТИРОВАТЬ Этот формат, кажется, список свойств Apple, но это не XML и не Binary... Это имеет смысл, поскольку API от веб-службы WebObjects. Я попытаюсь использовать гем CFPropertyList для его анализа, если есть лучшее решение, пожалуйста, дайте мне знать.
РЕДАКТИРОВАТЬ 2 Это список свойств NextSTEP.
2 ответа
Вот надежный ответ с использованием собственного анализатора на основе StringScanner. Это позволяет использовать пробелы в качестве необязательного, позволяет использовать запятые после последнего элемента в списке и позволяет пропустить точку с запятой после последней пары ключ / значение словаря. Это позволяет внешнему элементу быть словарем, массивом или строкой. И это позволяет действительно любой вид легального содержимого строки, в том числе скобки и фигурные скобки и экранированный текст, как \n
,
Видно в действии:
p parse('{ "array" = ( "1", "2", ( "3", "4" ) ); "hash"={ "key"={ "more"="oh}]yes;!"; }; }; }')
#=> {"array"=>["1", "2", ["3", "4"]], "hash"=>{"key"=>{"more"=>"oh}]yes;!"}}}
puts parse('("Escaped \"Quotes\" Allowed", "And Unicode \u2623 OK")')
#=> Escaped "Quotes" Allowed
#=> And Unicode ☣ OK
Код:
require 'strscan'
def parse(str)
ss, getstr, getary, getdct = StringScanner.new(str)
getvalue = ->{
if ss.scan /\s*\{\s*/ then getdct[]
elsif ss.scan /\s*\(\s*/ then getary[]
elsif str = getstr[] then str
elsif ss.scan /\s*[)}]\s*/ then nil end
}
getstr = ->{
if str=ss.scan(/\s*"(?:[^"\\]|\\u\d+|\\.)*"\s*/i)
eval str.gsub(/([^\\](?:\\\\)*)#(?=[{@$])/,'\1\#')
end
}
getary = ->{
[].tap do |a|
while v=getvalue[]
a << v
ss.scan /\s*,\s*/
end
end
}
getdct = ->{
{}.tap do |h|
while key = getstr[]
ss.scan /\s*=\s*/
if value=getvalue[] then h[key]=value; ss.scan(/\s*;\s*/) end
end
end
end
}
getvalue[]
end
В качестве альтернативы созданию собственного парсера с нуля в будущем вы также можете обратиться к библиотеке Treetop Ruby.
Изменить: я заменил реализацию getstr
выше с тем, который должен предотвратить запуск произвольного кода Ruby внутри eval
, Для получения дополнительной информации см. "Оценить строку без интерполяции". Видно в действии:
@secret = "OH NO!"
$secret = "OH NO!"
@@secret = "OH NO!"
puts parse('"\"#{:NOT&&:very}\" bad. \u262E\n#@secret \\#$secret \\\\#@@secret"')
Вот очень быстрый и грязный хак, который преобразует синтаксис в действительный Ruby, а затем пробует его. Обратите внимание, что это может быть опасно. Что еще более важно, это преобразует все скобки внутри ключей и значений в квадратные скобки.
def parse(str)
eval(
str
.gsub( /" = (?=[({"])/, '" => ' ) # Dictionary separators become =>
.gsub( /(?<=[)}"]); (?=[)}"])/, ', ' ) # Dictionary semicolons become ,
.tr( '()', '[]' ) # ALL parens become square brackets
)
end
p parse('{ "anArray" = ( "100", "200", "300" ); "aDictionary" = { "aString" = "Something"; }; }')
#=> {"anArray"=>["100", "200", "300"], "aDictionary"=>{"aString"=>"Something"}}