首先,使用一些中间变量使您对关联列表的访问更加清晰可能就足够了。首先,让我们定义一个我们可以解析的输入字符串(以后请尝试在问题中提供这些,因为它们会帮助其他人提供答案):
(defparameter *input*
"{\"coord\":{\"lon\":-123.12,\"lat\":49.25},\"weather\":[{\"id\":500,\"main\":\"Rain\",\"description\":\"light rain\",\"icon\":\"10n\"}],\"base\":\"cmc stations\",\"main\":{\"temp\":281.56,\"pressure\":1001,\"humidity\":93,\"temp--min\":276.15,\"temp--max\":283.15},\"wind\":{\"speed\":3.1,\"deg\":100,\"clouds\":{\"all\":90}},\"dt\":1453467600,\"sys\":{\"sunrise\":1453478139,\"sunset\":1453510389},\"id\":6173331,\"name\":\"Vancouver\",\"cod\":200}")
现在,在我看来,您可以更清晰地提取天气字段的主字段的值:
(let* ((report (cl-json:decode-json-from-string *input*))
(weather (first (cdr (assoc :weather report))))
(main (cdr (assoc :main weather))))
main)
;;=> "Rain"
如果您将像这样重复调用 (cdr (assoc ...)),那么基于提供的路径执行此操作的函数将有很大帮助,如 @987654321 所示@。当然,数组索引(例如,您想要天气列表的第一个元素)会使事情变得不那么干净。
现在,您还可以解码为 CLOS 实例。 CL-JSON 可以将 JSON 解码为匿名 CLOS 类的实例。单独这样做会稍微改变访问权限,但不会改变太多。尽管如此,它仍然使该字段如何访问它的工作更加清晰。请注意,由于天气值现在是一个数组,而不是一个列表,所以我们使用 (aref array 0) 获取第一个元素,而不是 (first list)。
(json:with-decoder-simple-clos-semantics
(let ((json:*json-symbols-package* nil))
(let* ((report (json:decode-json-from-string *input*))
(weather (aref (slot-value report 'weather) 0))
(main (slot-value weather 'main)))
main)))
;;=> "Rain"
现在,我认为使用 CLOS 类的真正好处是您可以定义自己的类,然后使用change-class 将 CL-JSON 提供给您的实例更改为您自己的类的实例。
在代码中定义类对文档也有很大帮助。对于一个小例子来说,这似乎没什么大不了的,但在编写可维护的代码时,这非常重要。例如,我们现在可以记录这些插槽的预期类型及其含义。这是一个可行的类定义。请注意,人们对命名约定有不同的看法(例如,是使用 wreport-weather 作为访问器还是 weather)。
(defclass wreport ()
((coord
:accessor wreport-coord
:documentation "An object with LON and LAT slots.")
(weather
:accessor wreport-weather
:documentation "An array of objects with ID, MAIN, DESCRIPTION, and ICON slots.")
(base) ;; and so on ...
(main)
(wind)
(dy)
(sys)
(id)
(name)))
现在您可以使用 change-class 将您的对象变成 wreport,然后您可以使用 wreport-weather(连同aref,因为值还是一个数组)来获取子对象,然后你可以使用slot-value(如上),来获取主字段:
(json:with-decoder-simple-clos-semantics
(let ((json:*json-symbols-package* nil))
(let ((x (json:decode-json-from-string *input*)))
(let* ((wreport (change-class x 'wreport))
(weather (aref (wreport-weather wreport) 0))
(main (slot-value weather 'main)))
main))))
;;=> "Rain"
为天气元素定义一个子类可能是有意义的,这并不难。由于我们将顶层事物称为 wreport,因此我们可以将较低层事物称为 subreports:
(defclass subreport ()
((id
:accessor subreport-id
:documentation "")
(main
:accessor subreport-main
:documentation "A short string containing a concise description of the weather.")
(description
:accessor subreport-description
:documentation "...")
(icon
:accessor subreport-icon
:documentation "...")))
现在,剩下要做的就是在我们使用 change-class 将顶级报表更改为 wreport 的实例之后,我们需要调用 change-class 为其天气数组中的每个元素转换为 子报告。根据change-class 的文档,调用了update-instance-for-different-class。我们可以定义一个 :after 方法来为我们进行转换:
(defmethod update-instance-for-different-class :after (previous (current wreport) &rest initargs &key &allow-other-keys)
"When changing an instance of something into a WREPORT, recursively
change the elements of the WEATHER array (if bound) to elements of
SUBREPORT."
(declare (ignore initargs))
(when (slot-boundp current 'weather)
(loop for sub across (wreport-weather current)
do (change-class sub 'subreport))))
如果您对 CLOS 做的不多,那可能有点吓人,但您实际上是在说“在 change-class 完成所有工作之后,还要再进行一次转换. 现在您可以在两个级别使用域适当的访问器:
(json:with-decoder-simple-clos-semantics
(let ((json:*json-symbols-package* nil))
(let ((x (json:decode-json-from-string *input*)))
(let* ((wreport (change-class x 'wreport))
(subreport (aref (wreport-weather wreport) 0))
(main (subreport-main subreport))) ;; (slot-value subreport 'main)))
main))))
;;=> "Rain"
这似乎需要做很多工作,而对于一个快速的小脚本来说,这可能是非常好的。但是,如果您暂时需要这些结构,将文档编入代码中会很有帮助。而且,如果您需要构建任何天气报告,那么拥有一个好的域模型将大有帮助。