跳轉至內容

Common Lisp/高階主題/字串

來自 Wikibooks,開放世界中的開放書籍

關於 Common Lisp 中字串,最重要的可能就是它們是陣列,因此也是序列。這意味著所有適用於陣列和序列的概念也適用於字串。如果您找不到特定的字串函式,請確保您也搜尋了更通用的陣列或序列函式。這裡只涵蓋了可以用字串做的事情中的一小部分。

訪問子字串

[編輯 | 編輯原始碼]

由於字串是序列,您可以使用 SUBSEQ 函式訪問子字串。字串中的索引,和以往一樣,是從零開始的。第三個可選引數是第一個不屬於子字串的字元的索引,它不是子字串的長度。

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(subseq *my-string* 8)
 "Marx"
(subseq *my-string* 0 7)
 "Groucho"
(subseq *my-string* 1 5)
 "rouc"

如果將 SUBSEQ 與 SETF 一起使用,您還可以操作子字串。

(defparameter *my-string* (string "Harpo Marx"))
 *MY-STRING*
(subseq *my-string* 0 5)
 "Harpo"
(setf (subseq *my-string* 0 5) "Chico")
 "Chico"
*my-string*
 "Chico Marx"

但請注意,字串不是“可伸縮”的。引用 HyperSpec: “如果子序列和新序列長度不等,則較短的長度決定被替換元素的數量”。例如

(defparameter *my-string* (string "Karl Marx"))
 *MY-STRING*
(subseq *my-string* 0 4)
 "Karl"
(setf (subseq *my-string* 0 4) "Harpo")
 "Harpo"
*my-string*
 "Harp Marx"
(subseq *my-string* 4)
 " Marx"
(setf (subseq *my-string* 4) "o Marx")
 "o Marx"
*my-string*
 "Harpo Mar"

訪問單個字元

[編輯 | 編輯原始碼]

您可以使用 CHAR 函式訪問字串的單個字元。CHAR 也可以與 SETF 一起使用。

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(char *my-string* 11)
 #\x
(char *my-string* 7)
 #\Space
(char *my-string* 6)
 #\o
(setf (char *my-string* 6) #\y)
 #\y
*my-string*
 "Grouchy Marx"

請注意,還有 SCHAR。如果效率很重要,在適當的情況下,SCHAR 可能更快一點。

因為字串是陣列,因此也是序列,您也可以使用更通用的函式 AREF 和 ELT(它們更通用,而 CHAR 可能實現得更高效)。

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(aref *my-string* 3)
 #\u
(elt *my-string* 8)
 #\M

操作字串的各個部分

[編輯 | 編輯原始碼]

有一系列(序列)函式可以用來操作字串,這裡只提供一些示例。有關更多資訊,請參閱 HyperSpec 中的序列詞典。

(remove #\o "Harpo Marx")
 "Harp Marx"
(remove #\a "Harpo Marx")
 "Hrpo Mrx"
(remove #\a "Harpo Marx" :start 2)
 "Harpo Mrx"
(remove-if #'upper-case-p "Harpo Marx")
 "arpo arx"
(substitute #\u #\o "Groucho Marx")
 "Gruuchu Marx"
(substitute-if #\_ #'upper-case-p "Groucho Marx")
 "_roucho _arx"
(defparameter *my-string* (string "Zeppo Marx"))
 *MY-STRING*
(replace *my-string* "Harpo" :end1 5)
 "Harpo Marx"
*my-string*
 "Harpo Marx"

另一個可以經常使用(但不是 ANSI 標準的一部分)的函式是 replace-all。此函式提供了一個簡單的功能,用於對字串執行搜尋/替換操作,它返回一個新的字串,其中字串中所有出現的 'part' 都被 'replacement' 替換。

(replace-all "Groucho Marx Groucho" "Groucho" "ReplacementForGroucho")
 "ReplacementForGroucho Marx ReplacementForGroucho"

replace-all 的一個實現如下

(defun replace-all (string part replacement &key (test #'char=))
"Returns a new string in which all the occurences of the part 
is replaced with replacement."
    (with-output-to-string (out)
      (loop with part-length = (length part)
            for old-pos = 0 then (+ pos part-length)
            for pos = (search part string
                              :start2 old-pos
                              :test test)
            do (write-string string out
                             :start old-pos
                             :end (or pos (length string)))
            when pos do (write-string replacement out)
            while pos)))

但是,請記住,上面的程式碼沒有針對長字串進行最佳化;如果您打算對非常長的字串、檔案等執行此操作,請考慮使用 cl-ppcre 正則表示式和字串處理庫,該庫進行了大量最佳化。

連線字串

[編輯 | 編輯原始碼]

顧名思義:CONCATENATE 是您的朋友。請注意,這是一個通用的序列函式,您必須提供結果型別作為第一個引數。

(concatenate 'string "Karl" " " "Marx")
 "Karl Marx"
(concatenate 'list "Karl" " " "Marx")
 (#\K #\a #\r #\l #\Space #\M #\a #\r #\x)

但是,如果您必須用許多部分構造一個字串,所有這些對 CONCATENATE 的呼叫似乎都比較浪費。至少還有三種其他好的方法來分段構造字串,具體取決於您的資料是什麼。如果您一次構建一個字元的字串,請將其設為一個可調整的 VECTOR(一個一維 ARRAY),型別為字元,填充指標為零,然後對其使用 VECTOR-PUSH-EXTEND。這樣,如果您能估計字串的長度,您也可以為系統提供提示。(請參閱 VECTOR-PUSH-EXTEND 的第三個可選引數。)

(defparameter *my-string* (make-array 0
                                      :element-type 'character
                                      :fill-pointer 0
                                      :adjustable t))
 *MY-STRING*
*my-string*
 ""
(dolist (char '(#\Z #\a #\p #\p #\a))
  (vector-push-extend char *my-string*))
 NIL
*my-string*
 "Zappa"

如果字串將由(任意物件的打印表示)構成(符號、數字、字元、字串、...),您可以使用 FORMAT,其輸出流引數為 NIL。這會將 FORMAT 指向將指示的輸出作為字串返回。

(format nil "This is a string with a list ~A in it"
        '(1 2 3))
 "This is a string with a list (1 2 3) in it"

我們可以使用 FORMAT 小型語言的迴圈結構來模擬 CONCATENATE。

(format nil "The Marx brothers are:~{ ~A~}."
        '("Groucho" "Harpo" "Chico" "Zeppo" "Karl"))
 "The Marx brothers are: Groucho Harpo Chico Zeppo Karl."

FORMAT 可以進行更多處理,但它有一個相對晦澀的語法。在最後一個示例之後,您可以在 CLHS 中找到關於格式化輸出的部分的詳細資訊。

(format nil "The Marx brothers are:~{ ~A~^,~}."
        '("Groucho" "Harpo" "Chico" "Zeppo" "Karl"))
 "The Marx brothers are: Groucho, Harpo, Chico, Zeppo, Karl."

另一種使用各種物件的打印表示來建立字串的方法是使用 WITH-OUTPUT-TO-STRING。此方便的宏的值是一個字串,其中包含在宏體內的字串流中輸出的所有內容。這意味著您也可以使用 FORMAT 的全部功能,如果您需要的話。

(with-output-to-string (stream)
  (dolist (char '(#\Z #\a #\p #\p #\a #\, #\Space))
    (princ char stream))
  (format stream "~S - ~S" 1940 1993))
 "Zappa, 1940 - 1993"

用分隔符連線字串

[編輯 | 編輯原始碼]

儘管上一節提供了足夠的提示來說明如何做到這一點,但現在可能是強調如何使用分隔符連線字串的最佳時機和地點。假設您有一個數字或字串列表,例如 (192 168 1 1) 或 ("192" "168" "1" "1"),並且您希望使用分隔符 "." 或 ";" 連線它們以建立另一個字串。這裡有一些示例

(defparameter *my-list* '(192 168 1 1))
 *MY-LIST*
(defparameter *my-string-list* '("192" "168" "1" "1"))
 *MY-STRING-LIST*
(setf *result-string* (format nil "~{~a~^.~}" *my-list*))
 "192.168.1.1"
*result-string*
 "192.168.1.1"
(setf *result-string* (format nil "~{~a~^.~}" *my-string-list*))
 "192.168.1.1"
*result-string*
 "192.168.1.1"
(setf *result-string* (format nil "~{~a~^;~}" *my-list*))
 "192;168;1;1"
*result-string*
 "192;168;1;1"

一次處理一個字元的字串

[編輯 | 編輯原始碼]

使用 MAP 函式一次處理一個字元的字串。

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(map 'string #'(lambda (c) (print c)) *my-string*)
#\G 
#\r 
#\o 
#\u 
#\c 
#\h 
#\o 
#\Space 
#\M 
#\a 
#\r 
#\x 
 "Groucho Marx"

或者用 LOOP 做。

(loop for char across "Zeppo"
      collect char)
 (#\Z #\e #\p #\p #\o)

按單詞或字元反轉字串

[編輯 | 編輯原始碼]

使用內建的 REVERSE 函式(或其破壞性對應函式 NREVERSE)可以輕鬆地按字元反轉字串。

(defparameter *my-string* (string "DSL"))
 *MY-STRING*
(reverse *my-string*)
 "LSD"

CL 中沒有按單詞反轉字串的單行程式碼(就像您在 Perl 中使用 split 和 join 所做的那樣)。您要麼必須使用來自外部庫的函式,例如 SPLIT-SEQUENCE,要麼必須自己編寫解決方案。這裡有一個嘗試

(defun split-by-one-space (string)
    "Returns a list of substrings of string
divided by ONE space each.
Note: Two consecutive spaces will be seen as
if there were an empty string between them."
    (loop for i = 0 then (1+ j)
          as j = (position #\Space string :start i)
          collect (subseq string i j)
          while j))
 SPLIT-BY-ONE-SPACE
(split-by-one-space "Singing in the rain")
 ("Singing" "in" "the" "rain")
(split-by-one-space "Singing in the  rain")
 ("Singing" "in" "the" "" "rain")
(split-by-one-space "Cool")
 ("Cool")
(split-by-one-space " Cool ")
 ("" "Cool" "")
(defun join-string-list (string-list)
    "Concatenates a list of strings
and puts spaces between the elements."
    (format nil "~{~A~^ ~}" string-list))
 JOIN-STRING-LIST
(join-string-list '("We" "want" "better" "examples"))
 "We want better examples"
(join-string-list '("Really"))
 "Really"
(join-string-list '())
 ""
(join-string-list
   (nreverse
    (split-by-one-space
     "Reverse this sentence by word")))
 "word by sentence this Reverse"

控制大小寫

[編輯 | 編輯原始碼]

Common Lisp 有幾個函式可以控制字串的大小寫。

(string-upcase "cool")
 "COOL"
(string-upcase "Cool")
 "COOL"
(string-downcase "COOL")
 "cool"
(string-downcase "Cool")
 "cool"
(string-capitalize "cool")
 "Cool"
(string-capitalize "cool example")
 "Cool Example"

這些函式接受 :START 和 :END 關鍵字引數,因此您可以選擇性地只操作字串的一部分。它們還有破壞性對應函式,其名稱以 "N" 開頭。

(string-capitalize "cool example" :start 5)
 "cool Example"
(string-capitalize "cool example" :end 5)
 "Cool example"
(defparameter *my-string* (string "BIG"))
 *MY-STRING*
(defparameter *my-downcase-string* (nstring-downcase *my-string*))
 *MY-DOWNCASE-STRING*
*my-downcase-string*
 "big"
*my-string*
 "big"

請注意這個潛在的警告:根據 HyperSpec,“對於 STRING-UPCASE、STRING-DOWNCASE 和 STRING-CAPITALIZE,字串不會被修改。但是,如果字串中沒有字元需要轉換,則結果可能是字串本身或它的副本,具體取決於實現。”這意味著以下示例中的最後一個結果取決於實現 - 它可能是 "BIG" 或 "BUG"。如果您想確定,請使用 COPY-SEQ。

(defparameter *my-string* (string "BIG"))
 *MY-STRING*
(defparameter *my-upcase-string* (string-upcase *my-string*))
 *MY-UPCASE-STRING*
(setf (char *my-string* 1) #\U)
 #\U
*my-string*
 "BUG"
*my-upcase-string*
 "BIG"

從字串末尾修剪空格

[編輯 | 編輯原始碼]

您不僅可以修剪空格,還可以刪除任意字元。STRING-TRIM、STRING-LEFT-TRIM 和 STRING-RIGHT-TRIM 函式返回其第二個引數的子字串,其中第一個引數中所有字元都已從開頭和/或結尾刪除。第一個引數可以是任何字元序列。

(string-trim " " " trim me ")
 "trim me"
(string-trim " et" " trim me ")
 "rim m"
(string-left-trim " et" " trim me ")
 "rim me "
(string-right-trim " et" " trim me ")
 " trim m"
(string-right-trim '(#\Space #\e #\t) " trim me ")
 " trim m"
(string-right-trim '(#\Space #\e #\t #\m) " trim me ")
 " tri"

注意:關於控制大小寫的部分中提到的警告也適用於這裡。

在符號和字串之間轉換

[編輯 | 編輯原始碼]

INTERN 函式將“轉換”字串為符號。實際上,它會檢查由字串(其第一個引數)表示的符號是否已在包(其第二個可選引數,預設為當前包)中可用,並在必要時將其輸入此包。解釋所有相關概念和解決此函式的第二個返回值超出了本章的範圍。有關詳細資訊,請參閱 CLHS 中關於包的章節。

請注意,字串的大小寫是相關的。

(in-package "COMMON-LISP-USER")
 #<The COMMON-LISP-USER package, 35/44 internal, 0/9 external>
(intern "MY-SYMBOL")
 MY-SYMBOL
 NIL
(intern "MY-SYMBOL")
MY-SYMBOL
 :INTERNAL
(export 'MY-SYMBOL)
 T
(intern "MY-SYMBOL")
MY-SYMBOL
 :EXTERNAL
(intern "My-Symbol")
|My-Symbol|
 NIL
(intern "MY-SYMBOL" "KEYWORD")
:MY-SYMBOL
 NIL
(intern "MY-SYMBOL" "KEYWORD")
:MY-SYMBOL
 :EXTERNAL

要執行相反的操作,將符號轉換為字串,請使用 SYMBOL-NAME 或 STRING。

(symbol-name 'MY-SYMBOL)
 "MY-SYMBOL"
(symbol-name 'my-symbol)
 "MY-SYMBOL"
(symbol-name '|my-symbol|)
 "my-symbol"
(string 'howdy)
 "HOWDY"

字元和字串之間的轉換

[編輯 | 編輯原始碼]

可以使用 COERCE 將長度為 1 的字串轉換為字元。還可以使用 COERCE 將任何字元序列轉換為字串。但是,不能使用 COERCE 將字元轉換為字串 - 您必須使用 STRING。

(coerce "a" 'character)
 #\a
(coerce (subseq "cool" 2 3) 'character)
 #\o
(coerce "cool" 'list)
 (#\c #\o #\o #\l)
(coerce '(#\h #\e #\y) 'string)
 "hey"
(coerce (nth 2 '(#\h #\e #\y)) 'character)
 #\y
(defparameter *my-array* (make-array 5 :initial-element #\x))
 *MY-ARRAY*
*my-array*
 #(#\x #\x #\x #\x #\x)
(coerce *my-array* 'string)
 "xxxxx"
(string 'howdy)
 "HOWDY"
(string #\y)
 "y"
(coerce 'string #\y)
 Type-error in KERNEL::OBJECT-NOT-TYPE-ERROR-HANDLER:
     #\y is not of type (OR CONS CLASS SYMBOL)

查詢字串中的元素

[編輯 | 編輯原始碼]

使用 FIND、POSITION 及其 -IF 對應項在字串中查詢字元。

(find #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
 #\t
(find #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 #\T
(find #\z "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 NIL
(find-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
 #\1
(find-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :from-end t)
 #\0
(position #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
 17
(position #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 0
(position-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
 37
(position-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :from-end t)
 43

或者使用 COUNT 及其相關函式來計算字串中的字元。

(count #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
 2
(count #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 3
(count-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
 6
(count-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :start 38)
 5

查詢字串的子字串

[編輯 | 編輯原始碼]

SEARCH 函式可以查詢字串的子字串。

(search "we" "If we can't be free we can at least be cheap")
 3
(search "we" "If we can't be free we can at least be cheap" :from-end t)
 20
(search "we" "If we can't be free we can at least be cheap" :start2 4)
 20
(search "we" "If we can't be free we can at least be cheap" :end2 5 :from-end t)
 3
(search "FREE" "If we can't be free we can at least be cheap")
 NIL
(search "FREE" "If we can't be free we can at least be cheap" :test #'char-equal)
 15

將字串轉換為數字

[編輯 | 編輯原始碼]

CL 提供了 PARSE-INTEGER 函式,用於將整數的字串表示形式轉換為相應的數值。第二個返回值是解析停止的字串索引。

(parse-integer "42")
42
 2
(parse-integer "42" :start 1)
2
 2
(parse-integer "42" :end 1)
4
 1
(parse-integer "42" :radix 8)
34
 2
(parse-integer " 42 ")
42
 3
(parse-integer " 42 is forty-two" :junk-allowed t)
42
 3
(parse-integer " 42 is forty-two")

 Error in function PARSE-INTEGER:
     There's junk in this string: " 42 is forty-two".

PARSE-INTEGER 不理解 #X 這樣的基數說明符,也沒有內建函式來解析其他數值型別。在這種情況下,可以使用 READ-FROM-STRING,但請注意,如果您使用此函式,則將生效完整的讀取器。

(read-from-string "#X23")
35
 4
(read-from-string "4.5")
4.5
 3
(read-from-string "6/8")
3/4
 3
(read-from-string "#C(6/8 1)")
#C(3/4 1)
 9
(read-from-string "1.2e2")
120.00001
 5
(read-from-string "symbol")
SYMBOL
 6
(defparameter *foo* 42)
 *FOO*
(read-from-string "#.(setq *foo* \"gotcha\")")
"gotcha"
 23
*foo*
 "gotcha"

將數字轉換為字串

[編輯 | 編輯原始碼]

Common Lisp 提供了 PRINC-TO-STRING 和 PRIN1-TO-STRING 等函式,用於將數字轉換為字串。如果您想將字串和數字連線起來,可以按如下方式使用它們

(concatenate 'string "9" (princ-to-string 8))
 "98"
(concatenate 'string "9" (prin1-to-string 8))
 "98"
(concatenate 'string "9" 8)

The value 8 is not of type SEQUENCE.
   [Condition of type TYPE-ERROR]

比較字串

[編輯 | 編輯原始碼]

通用的 EQUAL 和 EQUALP 函式可用於測試兩個字串是否相等。字串逐個元素進行比較,無論是區分大小寫 (EQUAL) 還是不區分大小寫 (EQUALP)。還有一堆針對字串的比較函式。如果您正在部署字元的實現定義屬性,則需要使用這些屬性。在這種情況下,請檢視供應商的文件。

以下是一些示例。請注意,所有測試不相等的函式都將第一個不匹配的位置作為廣義布林值返回。如果您需要更多功能,也可以使用通用的序列函式 MISMATCH。

(string= "Marx" "Marx")
 T
(string= "Marx" "marx")
 NIL
(string-equal "Marx" "marx")
 T
(string< "Groucho" "Zeppo")
 0
(string< "groucho" "Zeppo")
 NIL
(string-lessp "groucho" "Zeppo")
 0
(mismatch "Harpo Marx" "Zeppo Marx" :from-end t :test #'char=)
 3

拆分字串

[編輯 | 編輯原始碼]

SPLIT-SEQUENCE 是 Common Lisp Utilities 集合的一部分,可在 http://www.cliki.net/SPLIT-SEQUENCE 獲取。

(split-sequence:SPLIT-SEQUENCE #\Space "Please split this string.") 
 ("Please" "split" "this" "string.")

版權所有 © 2002-2005 Common Lisp Cookbook 專案 [1]

華夏公益教科書