[求组] json-encode 嵌套列表不如预期

我的预期

希望通过 Emacs 生产如下文本内容:

{
  version: 1,
  mediaFileName: '十月围城-粤语.mp4',
  cutSegments: [
    {
      start: 4317.84,
      end: 4472.12,
      name: '',
    }
  ]
}

注意里面 cutSegments 内的形式: 如果只有一组数据时是:

  cutSegments: [
    {
      start: 4317.84,
      end: 4472.12,
      name: '',
    }
  ]

有多组数据时是;

cutSegments: [
    {
      start: 4317.84,
      end: 4472.12,
      name: '',
    },
      {
        start: 6803.04,
        end: 6846.48,
        name: '',
    },
      {
        start: 7033,
        end: 7772.56,
        name: '',
    }
  ]

我的实现

(setq video-source "十月围城-粤语.mp4")
(setq video-timestamp-start 4317.84)
(setq video-timestamp-stop 4472.12)
(setq result (make-hash-table :test 'equal))
(puthash `,video-source `(:version 1 :mediaFileName ,video-source :cutSegments ((:start ,video-timestamp-start :end ,video-timestamp-stop))) result )

(print (json-encode (gethash `,video-source result)))

得到结果却是:

{"version":1,
"mediaFileName":"十月围城-粤语.mp4",
"cutSegments":{"start":[4317.84,"end",4472.12]}
}

注意 cutSegments 的内容在只有一个时,不是我期望的(类似 Python 列表中嵌套字典):

cutSegments: [
    {
      start: 4317.84,
      end: 4472.12,
      name: '',
    }
  ]

却是:

"cutSegments":{
"start":
[4317.84,"end",4472.12]
}

后来我在

(puthash `,video-source `(:version 1 :mediaFileName ,video-source :cutSegments ((:start ,video-timestamp-start :end ,video-timestamp-stop))) result )

中手动加入一个空列表变成:

(puthash `,video-source `(:version 1 :mediaFileName ,video-source :cutSegments ((:start ,video-timestamp-start :end ,video-timestamp-stop) ())) result ))

输出结果变成:

(:version 1 
:mediaFileName 十月围城-粤语.mp4 
:cutSegments ((:start 4317.84 :end 4472.12) nil))

注意 cutSegments 的形式才变成我预期的。

但是结果 cutSegments 中多了 nil

而 nil 在其他软件无法识别。

问题

如何解决在 cutSegment 的数据只有一组时,输出结果形式和预期不一致?

json-encode 文档说入参数据的结构是类似 json-read 返回的结果,json-read 的文档中有描述

对象应该是 ((key1 . value1) (key2 . value2))

数组应该是 [value1 value2 value3 ...]

你这里应该修改为

(puthash `,video-source
         `((:version . 1)
           (:mediaFileName . ,video-source)
           (:cutSegments . [((:start . ,video-timestamp-start)
                             (:end . ,video-timestamp-stop))]))
         result)

谢谢解答。

但是我发现了一个新问题。

例如 result 这个 hash-table 的内容首次 puthash 为:

#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8125 data (<<十月围城>> ((:version . 1) (:mediaFileName . <<十月围城>>) (:cutSegments . [((:start . 2337.4) (:end . 4317.64))]))))

在下一个遍历循环中碰到 《十月围城》star: 12 end: 34 这个新的数据。 想要把新数据中的 star: 12 end: 34 加入到 result 这个 hash-table 的 key 为 <<十月围城>> 的 value 的 cutSegments

cutSegments 的数据格式很明显是 Vector,而 elisp 中 Vector 是无法像 list 一样 push,add-to-list 的操作吧。

有方便的操作能把新 star: 12 end: 34 加入到 cutSegments 然后更新 result hash-table 么?

将 array 改成 list 即可,另外最外层可以简化,如下:

(json-encode `(:version 1 :mediaFileName ,video-source :cutSegments (((:start . ,video-timestamp-start)
                                                                      (:end   . ,video-timestamp-stop)))))

下面是我看源码的过程,也可以解答最初的一些问题,我记录一下,有点乱,见谅

源码中,json-encode 中生成数组的是 json--print-array

所以想要最终输出成数组,arrayp 或者 listp 都可以,但不能是 json-alist-p: ((key1 . value1)) 以及 json-plist-p: (:key1 value1),所以, array 改成 list 就行

另外通过它的判断逻辑,可以解答你最初的一些问题,(:version 1) 形式的确实是可以作为对象,但是 ((:start ,video-timestamp-start)) 多一层,就会优先通过 json-alist-p 检测,导致作为对象输出,第一个元素是key,后面会被组成一个数组,即 {"start":[4317.84,"end",4472.12]}

而你后面加了 () 后,即 ((:start 4317.84 :end 4472.12) ()) 无法通过 json-alist-p 检测,所以走了后面 listp 的判断,就会输出为对象,而 (:start 4317.84 :end 4472.12) 还是可以单独作为对象解析,() 解析为 null,最终结果就是 [{"start":4317.84,"end":4472.12},null]

2 个赞

谢谢解答。

按照您对我最初问题的解答。我来尝试对

'(((:start . ,video-timestamp-start)
   (:end   . ,video-timestamp-stop)))

为什么解析成:

[{"start":4317.84,"end":4472.12}]

进行理解。

首先:

'(((:start . ,video-timestamp-start)
   (:end   . ,video-timestamp-stop)))

是不能通过 json-alist-p,json-plist-p 检测,只能通过 list-p,所以

'(((:start . ,video-timestamp-start)
   (:end   . ,video-timestamp-stop)))

最外面的(解析成

[{"start":4317.84,"end":4472.12}]

最外面的[

紧接着:

'(((:start . ,video-timestamp-start)
   (:end   . ,video-timestamp-stop)))

被剥去最外面一层,变成:

'((:start . ,video-timestamp-start)
   (:end   . ,video-timestamp-stop))

一样是不能通过 json-alist-p,json-plist-p 检测,只能通过 list-p, 所以不应该也是,( 解析成 [? 即:

[[(:start . ,video-timestamp-start)
   (:end . ,video-timestamp-stop)]]

最后最里层:

(:start . ,video-timestamp-start)
   (:end . ,video-timestamp-stop)

被各自解析成:

{"start": 123}
{"end": 456}

组合以上,最终不是应该变成:

[[{"start": 123}
{"end": 456}]]

但是结果却是:

[{"start":4317.84,"end":4472.12}]

我的理解完全错的。

然而这是一个alist。

(json-alist-p
  '((:start . 4317.84)
    (:end . 4472.12)))
;;; => t

所以顺理成章地被encode为一个object。


另外,一般而言,JSON array对应的就是vector。虽然json-encode把list也encode为JSON array,然而如你所见,即使plist被encode为object,list of plist并不是array of object,而是当成一个alist被encode为一个object。

这方面json-serialize会比较严格,list只处理alist和plist,array就只能是vector。

关于你的需求,用plist是可以的,array的话其实还是可以用vector。你可以先用list of plist,然后也能push新的value,在最后要encode之前再把这个list vconcat一下变成vector。这样的话json-serialize也能用。

如果不想做额外处理的话,其实也可以用alist,甚至hashtable来代表每个curSegment。list of alist,或者list of hashtable都能被encode为array of object。