ivaneye.com

Clojure教程-字符串处理

本文翻译自Strings

本文内容包括使用Clojure内建的函数和标准库来操作字符串,以及JDK中的相关操作.

版权:

This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets).

源代码在Github上.

概述

    "foo"    java.lang.String
    #"\d"    java.util.regex.Pattern (正则表达式,匹配一个数字)
    \f       java.lang.Character (表示'f')

预备知识

一些例子用到了clojure.string,clojure.edn和clojure.pprint.我们假设你的ns宏里面已经包含了如下内容:

(:require [clojure.string :as str]
          [clojure.edn :as edn]
          [clojure.pprint :as pp])

或者在REPL里面,你需要载入:

(require '[clojure.string :as str])
(require '[clojure.edn :as edn])
(require '[clojure.pprint :as pp])

方法

基础方法

;; 长度计算
(count "0123")      ;=> 4
(empty? "0123")     ;=> false
(empty? "")         ;=> true
(str/blank? "    ") ;=> true
;; 字符串连接
(str "foo" "bar")            ;=> "foobar"
(str/join ["0" "1" "2"])     ;=> "012"
(str/join "." ["0" "1" "2"]) ;=> "0.1.2"
;; 使用Java方法来匹配.
;;
;; 对于下面的情况,你应该更喜欢使用正则表达式.比如,你在进行测试,当失败时需要返回-1.
;; 或者一个字符\o需要转换成字符串或数字.
(.indexOf "foo" "oo")         ;=> 1
(.indexOf "foo" "x")          ;=> -1
(.lastIndexOf "foo" (int \o)) ;=> 2
;; 字符串截取
(subs "0123" 1)       ;=> "123"
(subs "0123" 1 3)     ;=> "12"
(str/trim "  foo  ")  ;=> "foo"
(str/triml "  foo  ") ;=> "foo  "
(str/trimr "  foo  ") ;=> "  foo"
;; 切分字符串
(seq "foo")                       ;=> (\f \o \o)
(str/split "foo/bar/quux" #"/")   ;=> ["foo" "bar" "quux"]
(str/split "foo/bar/quux" #"/" 2) ;=> ["foo" "bar/quux"]
(str/split-lines "foo
bar")                             ;=> ["foo" "bar"]
;; 大小写转换
(str/lower-case "fOo") ;=> "foo"
(str/upper-case "fOo") ;=> "FOO"
(str/capitalize "fOo") ;=> "Foo"
;; 转换
(str/escape "foo|bar|quux" {\| "||"}) ;=> "foo||bar||quux"
;; 获得给定编码的字节数组
;; (输出可能和下面的"3c3660"不同)
(.getBytes "foo" "UTF-8") ;=> #<byte[] [B@3c3660>
;; 解析为keyword
(keyword "foo")    ;=> :foo
;; 解析为数字
(bigint "20000000000000000000000000000") ;=> 20000000000000000000000000000N
(bigdec "20000000000000000000.00000000") ;=> 20000000000000000000.00000000M
(Integer/parseInt "2")                   ;=> 2
(Float/parseFloat "2")                   ;=> 2.0
;; 解析为edn,它是Clojure形式的一个子集
(edn/read-string "0xffff") ;=> 65535
;; 读取Clojure形式的方法.
;;
;; 安全警告:当你无法确保你能100%信任要处理的字符串时,请将*read-eval*置为false.
;; 从Clojure1.5开始*read-eval*默认为false,但为了安全起见,
;; 在处理字符串时还是先手动置一下false
;; 因为可能有操作将其置为了true.
(binding [*read-eval* false]
  (read-string "#\"[abc]\""))
;=> #"[abc]"

解析复杂字符串

正则表达式

正则表达式增强了字符串匹配.你可以重复匹配,匹配其中之一,等等等等

Groups:当我们想匹配多于一个子串时,正则表达式的组就起到了作用.在正则表达式#"(group-1) (group-2)"中,第0个组为整个匹配表达式.第1个组从最左边的(开始,第2个组从左起第二个(开始,依次类推.你还可以嵌套组.也可以后续通过\$0,\$1等来引用组.

匹配

;; 简单匹配
(re-find #"\d+" "foo 123 bar") ;=> "123"
;; 匹配不到时会怎样?
(re-find #"\d+" "foobar") ;=> nil
;; 对每个组返回第一个满足匹配条件的内容
(re-matches #"(@\w+)\s([.0-9]+)%"
            "@shanley 19.8%")
;=>["@shanley 19.8%" "@shanley" "19.8"]
;; 返回所有匹配的组的结果
(re-seq #"(@\w+)\s([.0-9]+)%"
        "@davidgraeber 12.3%,@shanley 19.8%")
;=> (["@davidgraeber 12.3%" "@davidgraeber" "12.3"]
;    ["@shanley 19.8%" "@shanley" "19.8"])

替换

我们使用str/replace来进行替换. 第一个参数是源字符串,下一个是匹配字符串,最后一个是替换的内容.

   match / replacement can be:
   string / string
   char / char
   pattern / (string or function of match).
;; 在替换字符串中,$0,$1等用来引用匹配字符串中的组.
(str/replace "@davidgraeber 12.3%,@shanley 19.8%"
             #"(@\S+)\s([.0-9]+)%"
             "$2 ($1)")
;=> "12.3 (@davidgraeber),19.8 (@shanley)"
;; 使用函数来替换字符串,更灵活
(println
  (str/replace "@davidgraeber 12.3%,@shanley 19.8%"
               #"(@\w+)\s([.0-9]+)%,?"
               (fn [[_ person percent]]
                   (let [points (-> percent Float/parseFloat (* 100) Math/round)]
                     (str person "'s followers grew " points " points.\n")))))
;print=> @davidgraeber's followers grew 1230 points.
;print=> @shanley's followers grew 1980 points.
;print=>

上下文无关语法

和正则表达式相比上下文无关语法提供了更具表现力的匹配方式.你能使用例如嵌套来表达想法. 我们将在JSON语法上使用Instapares.(这个例子没有经过严格的测试也没什么特色.如果想应用到实际开发中,请使用data.json)

;; 你项目中的project.clj需要包含如下依赖(你可能需要重启JVM)
;;   :dependencies [[instaparse "1.2.4"]]
;;
;;  我们假设你的ns宏包含了如下内容:
;;   (:require [instaparse.core :as insta])
;; 或你在REPL里加载了:
;;   (require '[instaparse.core :as insta])
(def barely-tested-json-parser
  (insta/parser
   "object     = <'{'> <w*> (members <w*>)* <'}'>
    <members>  = pair (<w*> <','> <w*> members)*
    <pair>     = string <w*> <':'> <w*> value
    <value>    = string | number | object | array | 'true' | 'false' | 'null'
    array      = <'['> elements* <']'>
    <elements> = value <w*> (<','> <w*> elements)*
    number     = int frac? exp?
    <int>      = '-'? digits
    <frac>     = '.' digits
    <exp>      = e digits
    <e>        = ('e' | 'E') (<'+'> | '-')?
    <digits>   = #'[0-9]+'
    (* First sketched state machine; then it was easier to figure out
       regex syntax and all the maddening escape-backslashes. *)
    string     = <'\\\"'> #'([^\"\\\\]|\\\\.)*' <'\\\"'>
    <w>        = #'\\s+'"))
(barely-tested-json-parser "{\"foo\": {\"bar\": 99.9e-9, \"quux\": [1, 2, -3]}}")
;=> [:object
;     [:string "foo"]
;     [:object
;       [:string "bar"]
;       [:number "99" "." "9" "e" "-" "9"]
;       [:string "quux"]
;       [:array [:number "1"] [:number "2"] [:number "-" "3"]]]]
;; 最后的输出有点啰嗦,我们改进一下.
(->> (barely-tested-json-parser "{\"foo\": {\"bar\": 99.9e-9, \"quux\": [1, 2, -3]}}")
     (insta/transform {:object hash-map
                       :string str
                       :array vector
                       :number (comp edn/read-string str)}))
;=> {"foo" {"quux" [1 2 -3], "bar" 9.99E-8}}
;; 所有的内容都在上面了
;;
;; =右边的语法主要用来隐藏多余信息.比如说,我们不关心空格,所以我们通过设置<w*>来隐藏
;;
;; =左边的语法只是用来避免嵌套输出的.例如,"members"仅仅是一个人为设置的实体,
;; 所以我们阻止了这个无意义的嵌套关系

构建复杂的字符串

重定向

with-out-str提供了一个简单的方法来构建字符串.它重定向标准输出(out)到StringWriter,然后返回结果字符串.这样你就可以使用print这样的函数来获得字符串(即使在嵌套函数中)

(let [shrimp-varieties ["shrimp-kabobs" "shrimp creole" "shrimp gumbo"]]
  (with-out-str
    (print "We have ")
    (doseq [name (str/join ", " shrimp-varieties)]
      (print name))
    (print "...")))
;=> "We have shrimp-kabobs, shrimp creole, shrimp gumbo..."

格式化字符串

Java里的模板能帮助你方便的构造字符串.Reference

;; %s经常会作为print的参数. 而%需要使用%%
(format "%s enjoyed %s%%." "Mozambique" 19.8) ;=> "Mozambique enjoyed 19.8%."
;; 1$前缀能引用到其后的第一个参数
(format "%1$tY-%1$tm-%1$td" #inst"2000-01-02T00:00:00") ;=> "2000-01-02"
;; 同上1$, 2$前缀可以引用其后面的参数
(format "New year: %2$tY. Old year: %1$tY"
        #inst"2000-01-02T00:00:00"
        #inst"3111-12-31T00:00:00")
;=> "New year: 3111. Old year: 2000"

CL-Format

cl-format是Common Lisp中的一个臭名昭著的功能.例如,你可以从序列来构建字符串(其怪异程度就像使用英文来表示数字,使用两种类型的罗马数字).但对于打印日期和上面的对无序参数的引用方面却又比普通的格式化功能弱.

你只需要记住cl-format就是个无人问津的不值一学的语言.但是如果你喜欢并想学习它,那么看Practical Common Lisp教程.或者Common Lisp手册

;; The first param prints to *out* if true. To string if false.
;; To a stream if it's a stream.
(pp/cl-format true "~{~{~a had ~s percentage point~:p.~}~^~%~}"
              {"@davidgraeber" 12.3
               "@shanley" 19.8
               "@tjgabbour" 1})
;print=> @davidgraeber had 12.3 percentage points.
;print=> @tjgabbour had 1 percentage point.
;print=> @shanley had 19.8 percentage points.
(def format-string "~{~#[~;~a~;~a and ~a~:;~@{~a~#[~;, and ~:;, ~]~}~]~}")
(pp/cl-format nil format-string [])
;=> ""
(pp/cl-format nil format-string ["@shanley"])
;=> "@shanley"
(pp/cl-format nil format-string ["@shanley", "@davidgraeber"])
;=> "@shanley and @davidgraeber"
(pp/cl-format nil format-string ["@shanley", "@davidgraeber", "@sarahkendzior"])
;=> "@shanley, @davidgraeber, and @sarahkendzior"

贡献者

Tj Gabbour tjg@simplevalue.de, 2013 (original author)