如何使用elisp将十六进制字符串转换为ASCII?

8
今天我收到了一封回复邮件,内容是一串十六进制字节:
"686170707920333974682068617665206120676f6f64206f6e6521"

我正在考虑一种将字符串转换为其ASCII等效项的最有效和干净的方法。我会在问题中添加我的答案,但我觉得它可能不够优雅。


有很多有效的解决方案。我选择@Inaimathi作为答案的“获胜者”,因为它提供了最完整的多个替代方案。然而,如果您想要elisp代码示例,所有回答都值得研究。 - stsquad
7个回答

11

这是一种迭代解决方案

(defun decode-hex-string (hex-string)
  (let ((res nil))
    (dotimes (i (/ (length hex-string) 2) (apply #'concat (reverse res)))
      (let ((hex-byte (substring hex-string (* 2 i) (* 2 (+ i 1)))))
        (push (format "%c" (string-to-number hex-byte 16)) res)))))

如果您想避免副作用操作,可以使用一个使用 loop 的方法(您可能需要(require 'cl)来使用它):

(defun decode-hex-string (hex-string)
  (apply #'concat 
     (loop for i from 0 to (- (/ (length hex-string) 2) 1) 
           for hex-byte = (substring hex-string (* 2 i) (* 2 (+ i 1)))
           collect (format "%c" (string-to-number hex-byte 16)))))

一般情况下,在Elisp和Common Lisp中最好避免使用递归;如果输入足够大,您的堆栈会翻倒,而且两种语言都不保证尾递归(虽然您没有使用)。 对Scheme来说,情况就不同了。

顺便说一句,39周年快乐。


感谢您发布了两个替代方案。我看到了在elisp中使用cl的古老困境再次出现。我猜想cl的东西更容易理解。 - stsquad
@stsquad - 在这种情况下,我同意。我认为CL的人们通过loop DSL完美地解决了一般迭代问题,而其他人似乎要么忽略了这个问题,要么没有深入思考,或者决定map是唯一正确的迭代方式™©(在具有单一函数/变量命名空间的惰性/函数式语言中,这是有道理的)。 - Inaimathi

4

对于那些在这里搜索的人……

在Inaimathi的答案上进行详细说明,下面是用于用解码后的十六进制替换所选区域的代码:

(defun decode-hex-string (hex-string)
  (apply #'concat 
         (loop for i from 0 to (- (/ (length hex-string) 2) 1) 
               for hex-byte = (substring hex-string (* 2 i) (* 2 (+ i 1)))
               collect (format "%c" (string-to-number hex-byte 16)))))

(defun hex-decode-region (start end) 
  "Decode a hex string in the selected region."
  (interactive "r")
  (save-excursion
    (let* ((decoded-text 
            (decode-hex-string 
             (buffer-substring start end))))
      (delete-region start end)
      (insert decoded-text))))

  (provide 'decode-hex-string)
  (provide 'hex-decode-region)

将其保存到文件中,然后执行 M-x load-file 命令。或者放置在 ~/emacs.d 或其他位置。然后选择包含十六进制内容的区域并执行 M-x hex-decode-region 命令。享受吧!


4
如果您使用 Magnar Sveen 的 dash.el 列表API(并且您应该这样做),请尝试:
(concat (--map (string-to-number (concat it) 16) (-partition 2 (string-to-list "686170707920333974682068617665206120676f6f64206f6e6521"))))

该解决方案使用了Emacs函数string-to-numberstring-to-list以及concat,同时还使用了dash.el-partition和句法版本的-map函数。concat的好处在于它不仅可以连接字符串,还可以连接字符列表或向量。我们可以使用->>线程宏来重写此代码。它将第一个参数的结果应用于第二个,第三个等参数,就像Unix管道一样。
(->> (string-to-list "686170707920333974682068617665206120676f6f64206f6e6521")
  (-partition 2)
  (--map (string-to-number (concat it) 16))
  concat)

线程宏看起来很整洁,但在查看宏展开方式之前,我发现很难理解。 - stsquad

2

在Inaimathi和Shrein提供的答案基础上,我也添加了一个编码函数。以下是对字符串和区域参数实现编码和解码的代码:

;; ASCII-HEX converion
(defun my/hex-decode-string (hex-string)
  (let ((res nil))
    (dotimes (i (/ (length hex-string) 2) (apply #'concat (reverse res)))
      (let ((hex-byte (substring hex-string (* 2 i) (* 2 (+ i 1)))))
        (push (format "%c" (string-to-number hex-byte 16)) res)))))

(defun my/hex-encode-string (ascii-string)
  (let ((res nil))
    (dotimes (i (length ascii-string) (apply #'concat (reverse res)))
      (let ((ascii-char (substring ascii-string i  (+ i 1))))
        (push (format "%x" (string-to-char ascii-char)) res)))))

(defun my/hex-decode-region (start end) 
  "Decode a hex string in the selected region."
  (interactive "r")
  (save-excursion
    (let* ((decoded-text 
            (my/hex-decode-string
             (buffer-substring start end))))
      (delete-region start end)
      (insert decoded-text))))

(defun my/hex-encode-region (start end) 
  "Encode a hex string in the selected region."
  (interactive "r")
  (save-excursion
    (let* ((encoded-text 
            (my/hex-encode-string
             (buffer-substring start end))))
      (delete-region start end)
      (insert encoded-text))))

1
一开始我没有看到必须使用Elisp的要求,所以我进行了交互式操作,下面的代码就是我按照交互式过程写出来的。
   (defun decode-hex-string (hex-string)
      (with-temp-buffer
        (insert-char 32 (/ (length hex-string) 2))
        (beginning-of-buffer)
        (hexl-mode)        
        (hexl-insert-hex-string hex-string 1)
        (hexl-mode-exit)
        (buffer-string)))

1
这是我的翻译。我并不保证它特别地通顺或优雅,可能有点老派。
(defun hex-string-decode (str)
  "Decode STR of the form \"4153434949\" to corresponding \"ASCII\"."
  (let (decoded sub)
    (while (> (length str) 0)
      (setq sub (substring str 0 2)
            decoded (cons (string-to-number sub 16) decoded)
            str (substring str 2) ) )
    (when (not (zerop (length str))) (error "residue %s" str))
    (mapconcat #'char-to-string (nreverse decoded) "") ) )

我对使用setq而不是let表达式感到好奇。这是你所指的老派风格吗? - stsquad
setqlet 表达式内部。 - tripleee
抱歉,我有点糊涂了。我看到很多使用let语句并包含很多计算的代码风格。当然它们并不是这样迭代的。 - stsquad

0

这是我想出来的解决方案,但我觉得有点丑陋:

(defun decode-hex-string(string)
  "Decode a hex string into ASCII"
  (let* ((hex-byte (substring string 0 2))
     (rest (substring string 2))
     (rest-as-string (if (> (length rest) 2)
                 (decode-hex-string rest)
               "")))
    (format "%c%s" (string-to-number hex-byte 16) rest-as-string)))

2
在elisp中,O(n)递归是一个不好的想法--没有递归优化,所以当字符串足够长时,你会用完堆栈空间。最好在单个函数调用中简单地迭代字符串。 - phils
最有可能的是,尽管以函数式风格编写似乎是最简单的方法,但我很欢迎使用单个函数调用的示例。 - stsquad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接