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