跳至内容

Common Lisp/高级主题/CLOS/示例 1

来自 Wikibooks,开放世界中的开放书籍

假设我们有一个文件,其中包含电影的一些字幕,例如 .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 吗?简洁明了。

现在,最后的函数,它将结合 maplinemodify-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 方法。我把它留作练习。

华夏公益教科书