Common Lisp/高级主题/CLOS/示例 1
假设我们有一个文件,其中包含电影的一些字幕,例如 .srt 格式。
00:00:33,657 --> 00:00:35,852 Michael Rennie was ill 2 00:00:36,097 --> 00:00:39,055 The day the earth stood still 3 00:00:39,297 --> 00:00:44,132 But he told us where we stand 4 00:00:44,377 --> 00:00:46,447 And Flash Gordon was there 5 00:00:46,697 --> 00:00:49,609 In silver underwear
但是这些字幕对你来说没有用,因为你的电影版本由于某种原因在开头有一个 10.532 秒的暂停。手动更改所有时间戳是不可能的,而且假设没有工具可以为我们做到这一点。所以我们必须用 Common Lisp(还有什么)编写一个脚本。现在就开始吧!
我们将要使用的对象类是一组时间戳。我们需要能够在文件中找到它们,将它们加在一起,并将它们插入回去。
(defclass srt-time () ((hr :initarg :hr :initform 0 :accessor hr) (mi :initarg :mi :initform 0 :accessor mi) (se :initarg :se :initform 0 :accessor se) (ms :initarg :ms :initform 0 :accessor ms)) (:documentation "Time format for srt")) (defgeneric display (what) (:documentation "Returns string that represents the object")) (defgeneric normalise (time) (:documentation "Fix overflow of fields")) (defmethod normalise ((time srt-time)) (with-slots (hr mi se ms) time (loop until (< ms 1000) do (decf ms 1000) (incf se)) (loop until (< se 60) do (decf se 60) (incf mi)) (loop until (< mi 60) do (decf mi 60) (incf hr))) time) (defmethod display ((time srt-time)) (normalise time) (with-slots (hr mi se ms) time (format nil "~2,'0d:~2,'0d:~2,'0d,~3,'0d" hr mi se ms))) (defun make-srt-time (arglist) (destructuring-bind (hr mi se ms) arglist (make-instance 'srt-time :hr hr :mi mi :se se :ms ms)))
display 方法将返回 srt-time 对象的文本表示。normalise 是一个辅助函数,它修复所有插槽的“溢出”(不能超过 60 秒,等等)。make-srt-time 是围绕 make-instance 的包装器,它允许更容易地创建 srt-time 对象。
现在,我们编写了两种添加时间的方法。
(defgeneric add (t1 t2)) (defmethod add ((t1 srt-time) (t2 srt-time)) "Adds two srt-times" (normalise (make-srt-time (mapcar #'+ (list (hr t1) (mi t1) (se t1) (ms t1)) (list (hr t2) (mi t2) (se t2) (ms t2)))))) (defmethod add ((t1 srt-time) (t2 integer)) "Adds some number of seconds" (normalise (make-srt-time (list (hr t1) (mi t1) (+ (se t1) t2) (ms t1)))))
添加另一种添加时间的方法看起来并不多。但请记住,每个调用 add 的函数都可能为第二个参数传递整数而不是 srt-time。正如我们稍后将看到的,这种功能扩展会传播到程序的上层,包括用户打算调用的函数。
现在让我们考虑一下任务的第二部分。给定一个文本字符串,我们必须用修改后的时间戳替换所有时间戳实例。幸运的是,CL-PPCRE 可以做到这一点。我们只需要找到一个合适的 正则表达式。正则表达式不是本维基百科的主题,但如果你不熟悉它们,有很多好的网站可以学习这个概念。我只写下来:“([0-9]{2,}):([0-9]{2}):([0-9]{2}),([0-9]{3})”。至少尝试弄清楚它如何对应于一个特定的时间戳,例如 00:00:44,132。请注意,与 "(" 和 ")" 之间的正则表达式部分匹配的内容将被 CL-PPCRE 记住,我们将使用这个事实。现在,让我们生成一个与该正则表达式相对应的扫描器
(defparameter *find-time* (cl-ppcre:create-scanner "([0-9]{2,}):([0-9]{2}):([0-9]{2}),([0-9]{3})"))
这个扫描器实际上是一个编译后的函数,但如果一切按预期工作,则无需了解实现细节。下一步是使用此扫描器查找和替换字符串的某些子字符串。
(defun modify-times (str fun) "Modify all instances of srt-time being hidden in the given string using a given function" (cl-ppcre:regex-replace-all *find-time* str fun :simple-calls t))
该函数只接受一个任意字符串和一个任意函数,并使用该函数来转换扫描器 *find-time* 在该字符串中找到的所有时间戳。现在我们将编写一个函数,为 modify-times 提供正确的函数。
(defun apply-line-add (str delta) (labels ((adder (match hr mi se ms) (declare (ignore match)) ;;match is needed for CL-PPCRE (display (add (make-srt-time (mapcar #'parse-integer (list hr mi se ms))) delta)))) (modify-times str #'adder)))
现在很有趣吧?我们在运行时构建了所需的函数,因为我们还不知道用户想要添加多少时间!regex-replace-all 将使用 5 个参数调用 adder。第一个参数 match 用于整个匹配 - 我们不需要它。我们需要的是这些部分(用括号括起来的部分)。这些对应于小时、分钟、秒和毫秒。我们使用 parse-integer 将它们从字符串转换为整数。然后,从这些数字生成一个 srt-time 对象,然后向它添加delta(请注意,delta 可以是 srt-time 或整数,我们不知道,我们也不关心)。然后使用 display 方法将结果转换回字符串。这就是 CL-PPCRE 从该函数中想要的,现在我们可以忘记 CL-PPCRE 并专注于其他事情。
下一个函数 mapline 将文件切片成行,将这些行馈送给某个函数,并将这些行打印到输出文件。
(defun mapline (fun input output) "Applies function to lines of input file and outputs the result" (with-open-file (in input) (with-open-file (out output :direction :output :if-exists :supersede) (loop for str = (read-line in nil nil) while str do (princ (funcall fun str) out) (terpri out)))))
你不喜欢 with-open-file 吗?简洁明了。
现在,最后的函数,它将结合 mapline 和 modify-times 的强大功能。
(defun delay (delay input output) "Adjusts all srt-times in file by adding delay to them. Delay can be either integer (number of seconds) or srt-time instance." (mapline (lambda (str) (apply-line-add str delay)) input output))
现在,为什么这个示例归类在 CLOS 下?嗯,它显示了为什么 CLOS 很好。它使你的程序非常可扩展。假设你需要添加一个功能,这样延迟就可以是浮点数秒。只需编写一个合适的 add 方法。我把它留作练习。