跳至內容

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 方法。我把它留作練習。

華夏公益教科書