跳转至内容

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]

华夏公益教科书