【问题标题】:How to convert a huge Elisp data structure to JSON?如何将庞大的 Elisp 数据结构转换为 JSON?
【发布时间】:2015-05-12 07:36:57
【问题描述】:

org-element-parse-buffer 即使对于一个小的 Org 文件也会返回一个巨大的树。我想把这棵树转换成 JSON。显然,json.el 使用递归函数来遍历 cons 单元格,并且由于 Elisp 不支持 tail recursion,json-encode 的调用很快就会耗尽堆栈。如果我增加max-lisp-eval-depth 和max-specpdl-size,Emacs 就会崩溃。

如何解决这个问题并将巨大的树结构转换为 JSON?一般来说,当我有一个庞大的数据结构和一个可能会耗尽堆栈的递归函数时,我该如何解决?

【问题讨论】:

  • 我会提交一份关于 json.el 的错误报告——在 elisp 中的任意数据结构中递归是一个坏主意,因为你已经指出了。
  • 无需将代码重写为非递归,您可以也许破解它以观察递归深度,并将太深的节点推入队列以进行后续处理,并在这些点在 JSON 中生成令牌(以便在您遍历队列以独立处理这些子树时进行后续替换)。这可能会也可能不会比听起来更复杂。

标签: json recursion emacs elisp org-mode


【解决方案1】:

tl;博士

以下是在将组织树编码为 json 之前删除对父级和结构的引用的方法:

 (let* ((tree (org-element-parse-buffer 'object nil)))
    (org-element-map tree (append org-element-all-elements
                                  org-element-all-objects '(plain-text))
      (lambda (x)
        (if (org-element-property :parent x)
            (org-element-put-property x :parent "none"))
        (if (org-element-property :structure x)
            (org-element-put-property x :structure "none"))))
    (json-encode tree))

【讨论】:

    【解决方案2】:

    是的,json.el 函数是递归的,但是在 Org-Element 上调用的递归函数导致堆栈溢出不是因为 org-element-parse-buffer 返回一个巨大的 AST,而是因为它返回一个 circular list。循环列表上的树递归函数就像笼子里的松鼠。

    我想,在 AST 返回中使用自引用背后的想法是,如果您遍历它,您可以随时通过在关键字 :parent 上运行 plist-get 来返回父级。我想象这种用于上下遍历 AST 的用法:

    (let ((xs '#1=(:text "foo" :child (:text "bar" :parent #1#))))
      (plist-get
       (plist-get
        xs
        :child)  ; (:text "bar" :parent (:text "foo" :child #0))
       :parent)) ; (:text "foo" :child (:text "bar" :parent #0))
    

    但 JSON 不支持循环列表,因此您需要在尝试转换为任何数据序列化格式之前从 AST 中删除这些自引用。我还没有找到优雅地删除 AST 中的循环引用的方法,所以我求助于一个肮脏的 hack:

    1. 将 AST 转换为字符串
    2. 删除带有regular expressions 的引用
    3. 将字符串转换回 Elisp 数据结构

    假设我有一个名为test.org 的组织文件,其内容如下:

    * Heading
    ** Subheading
    Text
    

    然后变量tree 包含来自此缓冲区的已解析组织数据:(setq tree (with-current-buffer "test.org" (org-element-parse-buffer)))。然后为 JSON 导出准备这些数据,我只需运行:

    (car (read-from-string (replace-regexp-in-string ":parent #[0-9]+?" "" (prin1-to-string tree)))))
    

    即使删除了所有提到的:parent,新的AST仍然有效,所以如果新的AST在变量tree2中,那么以下3个表达式是等价的:

    (org-element-interpret-data tree2)
    (with-current-buffer "test.org" (buffer-substring-no-properties 1 (buffer-end 1)))
    "* Heading\n** Subheading\nText\n"
    

    请注意,由于某种原因,org-element-interpret-data 会删除前面的空格,因此当您的 Org 文件中有 text 之类的行时,上述内容在技术上并不正确。

    现在您需要做的就是将新的非循环 AST 编码为 JSON 并将其写入文件:

    (f-write (json-encode tree2) 'utf-8 "test.json")
    

    注意事项

    1. Elisp 的 cons cells 是一对 2 个插槽:car 和 cdr。如果每个单元格的cdr 包含指向另一个 cons 单元格的链接,我们将得到一个链表。如果car 和cdr 都指向 2 个值,我们会得到一个点对。因此(1 . (2 . (3 . nil))) 等价于(1 2 3)。但是cdr(或car)可能指向任何其他缺点单元格,包括已经在列表中较早的那个单元格,从而产生circular linked list。

    2. 练习: 创建一个复杂的树数据结构,其中包含对不同子树的多个自引用。然后尝试遍历这棵树并通过自引用跳跃来获得这个想法。

    3. 使用来自dash 列表操作库的->> 线程宏,表达式相当于:

      (->> tree prin1-to-string (replace-regexp-in-string ":parent #[0-9]+?" "") read-from-string car)
      
    4. (buffer-substring-no-properties 1 (buffer-end 1)) 类似于(buffer-string),但没有附加恼人的text properties。

    5. f-write 是一个将文本从f 第三方文件操作库写入文件的函数。

    【讨论】:

    • “就像笼子里的松鼠”?
    猜你喜欢
    • 1970-01-01
    • 2013-06-06
    • 1970-01-01
    • 1970-01-01
    • 2021-10-18
    • 2020-03-07
    • 1970-01-01
    • 2021-06-10
    • 2015-02-19
    相关资源
    最近更新 更多