从 Common Lisp 中读取文件返回空字符

Null char returning from reading a file in Common Lisp

我正在读取文件并使用此函数将它们存储为字符串:

(defun file-to-str (path)
  (with-open-file (stream path) :external-format 'utf-8
          (let ((data (make-string (file-length stream))))
            (read-sequence data stream)
            data)))

如果文件只有ASCII字符,我按预期得到了文件的内容;但是如果有超过 127 的字符,对于超过 127 的每个这样的字符,我在字符串的末尾得到一个空字符 (^@)。所以,在 $ echo "~a^?" > ~/teste 之后我得到

CL-USER> (file-to-string "~/teste")
"~a^?
"

;但是在 echo "aaa§§§" > ~/teste 之后,REPL 给了我

CL-USER> (file-to-string "~/teste")
"aaa§§§
^@^@^@"

等等。我怎样才能解决这个问题?我在 utf-8 语言环境中使用 SBCL 1.4.0。

首先,您的关键字参数 :external-format 放错了地方,没有任何效果。它应该在 streampath 的括号内。但是,这对最终结果没有影响,因为 UTF-8 是默认编码。

这里的问题是在UTF-8编码中,对不同的字符进行编码需要不同的字节数。 ASCII 字符都编码为单个字节,但其他字符需要 2-4 个字节。您现在在字符串中为输入文件的每个 byte 分配数据,而不是其中的每个 character。未使用的字符最终保持不变; make-string 将它们初始化为 ^@.

(read-sequence) 函数 returns 函数未更改的第一个元素的索引。您目前只是丢弃此信息,但您应该在知道已使用了多少元素后使用它来调整缓冲区大小:

(defun file-to-str (path)
  (with-open-file (stream path :external-format :utf-8)
    (let* ((data (make-string (file-length stream)))
           (used (read-sequence data stream)))
      (subseq data 0 used))))

这是安全的,因为文件的长度总是大于或等于其中编码的 UTF-8 字符数。然而,它并不是非常有效,因为它分配了一个不必要的大缓冲区,最后将整个输出复制到一个新的字符串中以返回数据。

虽然这对于学习实验来说很好,但对于 real-world 用例,我推荐 Alexandria 实用程序库,它有一个 ready-made 函数:

* (ql:quickload "alexandria")
To load "alexandria":
  Load 1 ASDF system:
    alexandria
; Loading "alexandria"
* (alexandria:read-file-into-string "~/teste")
"aaa§§§
"
*