clojure.spec库入门学习
此文已由作者张佃鹏授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
clojure是一门动态类型的语言,在类型检查方面并没有c++/java这种静态类型语言好用,所以多个模块之间进行接口参数传递时,由于接口文档设计不严谨等原因,总会发生接口参数类型错误,参数个数不正确等问题,给代码调试带来很大的挑战,因此在clojure中,对接口参数的进行类型和范围的检查是非常必要的。为此,我们找到了clojure.spec这个库( https://clojure.github.io/clojure/branch-master/clojure.spec-api.html ),正好可以解决以上问题。
1.clojure.spec库介绍:
spec库主要是用来定义数据的结构和类型,并对其数据类型进行校验,并且可以根据spec生成对应的校验数据。要使用clojure.spec库,必须依赖最新的alpha版本的clojure:
[org.clojure/clojure "1.9.0-alpha10"]
因为clojure.spec库中有很多函数与clojure.core库中的函数重名(+、melt、def),所以不要使用use将整个库中的函数导入到当前命名空间下,而是要使用require,如下:
(require '[clojure.spec :as s])
2.specification的定义:
可以使用s/def函数来给一个specification定义名字,由s/def定义的specification必须是namespace keyword(其中关于命名空间的关键字在这篇博客中有介绍:http://blog.csdn.net/zdplife/article/details/52304258 )。最简单定义spec的方式就是:一个普通的谓词函数(接收一个参数,返回boolean类型)就可以作为一个specification:
;;定义一个specification,只有偶数才能满足条件
(s/def ::even-val even?)
;;=> :my-clj.core/even-val
specification也可以是多个specification的组合,最简单的组合函数是:s/and和s/or。s/and必须满足and下的所有specification的条件,组合的specification才会校验成功,如下:
;; ::and-test表示大于10的偶数
(s/def ::and-test (s/and
::even-val
#(> % 10)
))
;;=> :my-clj.core/and-test
s/or函数表示只要满足其中一个条件则成立,但是s/or函数必须在每一个条件前面加一个:tag,这主要是为了在使用s/conform解析数据时,让数据更容易理解,便于确定是满足那个条件。s/or的用法如下:
;;如下在谓词int?和string?前加了tag,tag主要是为了告诉我们是满足哪个条件
(s/def ::or-test
(s/or :int-number int? :string-number string?))
;;=> :my-clj.core/or-test
3.常用解析spec的函数:
使用spec来解析和判断数据的函数主要有:s/valid? ,s/conform ,s/explain ,s/explain-str ,s/explain-data:
s/valid?函数:
s/valid?用来判断给定数据是否满足当前的specification,如果满足则返回true,否则返回false:
(s/valid? ::even-val 10)
;;=> true
(s/valid? ::even-val 11)
;;=> false
s/conform函数:
s/conform函数根据给定的specification和data参数,如果data符合specification校验条件,则根据specification的格式返回解析后的data,否则返回s/invalid:
;;如果满足条件则返回对应的解析数据,否则返回s/invalid
(s/conform ::even-val 10)
;;=> 10
(s/conform ::even-val 11)
;;=> :clojure.spec/invalid ;;conform并非一定会返回与原有数据一模一样的数据,它会根据对应specification会有所变化,比如or-test有:tag标志,所以返回的conform-data是一个vector,告诉我们“hello world”是满足string条件,而不是int条件。
(s/conform ::or-test "hello world")
;;=> [:string-number "hello world"]
s/explain系列函数:
根据explain的字面意思,很容易理解其作用是用来解释匹配失败的原因,共有三个相关函数:s/explain函数将错误信息的原因打印出来,并返回nil;s/explain-str会将s/explian打印出来的内容转换为字符串并返回;s/explain-data会将错误信息作为一个map数据结构返回。如果匹配成功,三个函数则都返回对应的success。
;;val=11不满足偶数的判断条件,根据返回结果,我们很容易找到数据校验错误的位置
(s/explain ::even-val 11)
;;打印结果:val: 11 fails spec: :my-clj.core/even-val predicate: even?
;;=> nil (s/explain-str ::even-val 11)
;;=> "val: 11 fails spec: :my-clj.core/even-val predicate: even?\r\n" ;;explain-data会把错误信息作为一个map返回
(s/explain-data ::even-val 11)
;;=> #:clojure.spec{:problems [{:path [], :pred even?, :val 11, :via [:my-clj.core/even-val], :in []}]}
4.集合spec的生成:
我们经常会遇到某个类型字段是属于某个集合中的任意一个元素,比如我们定义一个聚合字段类型,该字段类型可能是SUM/MAX/MIN/AVG/COUNT中的一个。为此我们可以定义该字段的spec如下:
;;因为clojure中的set可以当作谓词使用,所以我们很容易实现该字段spec的定义
(s/def ::aggregator #{"SUM","AVG","CNT","CNTD","MAX","MIN"}) (s/valid? ::aggregator "SUM")
;;=> true
(s/valid? ::aggregator "SUM1")
;;=> false
5.数组spec的生成:
对于数组spec经常使用的生成函数主要有s/coll-of ,s/every ,s/tuple这三个函数:
s/coll-of函数:
coll-of函数生成的数组spec的特点是:该数组中的所有元素必须满足同一个条件。
(s/def ::even-val even?)
;;=> :my-clj.core/even-val
;;定义一个数组的spec,该数组中的所有元素都必须是偶数
(s/def ::coll-of-test (s/coll-of ::even-val))
;;=> :my-clj.core/coll-of-test (s/valid? ::coll-of-test [2 4 6])
;;=> true
(s/valid? ::coll-of-test [1 4 6])
;;=> false
;;其中数组为空也满足条件
(s/valid? ::coll-of-test [])
;;=> truecoll-of函数还接收可选的参数,用来对数组中的元素进行限制,可选参数有如下:
(1):kind- - - -可以指定数组的类型,vector,set,list等;
(2):count- - - -可以限定数组中元素的个数;
(3):min-count- - - -限定数组中元素个数的最小值
(4):max-count- - - -限定数组中元素个数的最大值
(5):distinct- - - -数组没有重复的元素
(6):into- - - -可以将数组的元素插入到[],(),{},#{}这些其中之一,主要是为了改变conform函数的返回结果
关于以上可选参数使用举例如下:
;;定义一个数组的spec,其中所有元素是偶数,这个数组是vector类型,共有4个元素,元素不能重复,conform结果放在set中:
(s/def ::coll-of-test (s/coll-of even? :kind vector? :count 4 :distinct true :into #{}))
;;=> :my-clj.core/coll-of-test ;;满足条件
(s/valid? ::coll-of-test [2 4 6 8])
;;=> true ;;用explain函数解释错误的原因
(s/explain ::coll-of-test [2 4 6 7])
;;In: [3] val: 7 fails spec: :my-clj.core/coll-of-test predicate: even? ;;list的数组不满足条件
(s/explain ::coll-of-test '(2 4 6 8))
;;val: (2 4 6 8) fails spec: :my-clj.core/coll-of-test predicate: vector? ;;conform解析结果放在了一个set中
(s/conform ::coll-of-test [2 4 6 8])
;;=> #{4 6 2 8}
s/every函数:
s/every函数与s/coll-of函数的作用相同,并且参数类型也相同。不同的是:在有不满足条件元素的情况下,s/coll-of函数依然会检查每一个元素并返回错误信息,而s/every函数只检查部分元素。所以s/every函数更适合数据量比较大的情况:
;;用s/coll-of和s/every函数定义作用相同的spec,都是检查数组是否全部为string类型的元素
(s/def ::coll-of-test (s/coll-of string?))
(s/def ::every-test (s/every string?)) ;;在校验错误的情况下,every-test只返回前20个元素的错误信息
(s/explain ::every-test (range 50))
;;In: [0] val: 0 fails spec: :my-clj.core/every-test predicate: string?
;;In: [1] val: 1 fails spec: :my-clj.core/every-test predicate: string?
;;...
;;In: [18] val: 18 fails spec: :my-clj.core/every-test predicate: string?
;;In: [19] val: 19 fails spec: :my-clj.core/every-test predicate: string? ;;而coll-of-test会返回所有不满足元素的校验错误信息
(s/explain ::coll-of-test (range 50))
;;In: [0] val: 0 fails spec: :my-clj.core/coll-of-test predicate: string?
;;In: [1] val: 1 fails spec: :my-clj.core/coll-of-test predicate: string?
;;...
;;In: [48] val: 48 fails spec: :my-clj.core/coll-of-test predicate: string?
;;In: [49] val: 49 fails spec: :my-clj.core/coll-of-test predicate: string?
所以在需要确定所有元素错误信息的情况下使用coll-of,而在大量数据的情况下,为了保证效率,使用every函数效果会更好。
s/tuple函数:
tuple函数与前两个函数不同,它可以指定数组中每个元素的类型,检查要求更严格苛刻,适合数组元素比较少且元素类型不是全部相同的校验:
;;可以指定一个数组中三个元素,类型分别为int,string,vector
(s/def ::tuple-test (s/tuple int? string? vector?)) (s/valid? ::tuple-test [1 "hello" [1 2 3]])
;;=> true
(s/valid? ::tuple-test [1 "hello" 3])
;;=> false
6.map的spec生成:
对于map的spec生成函数主要有s/map-of, s/every-kv ,s/keys ,s/keys*
s/map-of函数:
s/map-of函数与s/coll-of函数作用类似,前两个参数分别是对key和value进行校验的spec,后面还可以跟着可选参数,与coll-of的可选参数一样,有:kind/count/min-count/max-count/distinct/into,其中kind默认情况下是map,如果是list和vector,则校验数据是[k v]的数组,map-of的用法如下:
;;用map-of定义一个spec,类型是[[k v]],k是关键字,v是字符串
(s/def ::map-of-test (s/map-of keyword? string? :kind vector?)) ;;因为不是vector形式的map,所以校验失败
(s/explain ::map-of-test {:a 1 :b 2}) val: {:a 1, :b 2} fails spec: :my-clj.core/map-of-test predicate: vector? ;;满足条件返回success
(s/explain ::map-of-test [[:a "hello"] [:b "world"]])
;;Success!
s/every-kv函数:
s/every-kv函数与s/map-of的区别就像s/every与s/coll-of函数的区别,s/every-kv函数适合数据量比较大的情况下,其参数格式与map-of函数的参数格式完全一样,用法如下:
;;用every-kv定义一个spec,校验key为关键字,value为数字的map
(s/def ::every-kv-test (s/every-kv keyword? number?)) (s/explain ::every-kv-test {:a 1 :b 3.14})
;;Success!
(s/explain ::every-kv-test {:a "hello" :b 3.14})
;;In: [:a 1] val: "hello" fails spec: :my-clj.core/every-kv-test at: [1] predicate: number?
s/keys函数:
s/keys函数比map-of函数和every-kv函数的限制更加强烈,可以分别对map中的每一个value的进行限制,但是map的key必须是关键字。 假设我们定义一个person的数据结构如下:
person:
{ :name string类型 :age int类型且满足(< 0 age 100) :gender boolean类型 :spouse (optional)string类型
}
以上定义的person数据结构有3个必选的字段(:name :age :gender)和1个可选择的字段(:spouse),为了用keys定义person数据结构的spec,首先我们需要根据person的key(key必须是关键字)来定义其对应value的校验spec,如下:
;;所定义的spec的名字必须与person中key的名字的一致
(s/def ::name string?)
(s/def ::age (s/and int? #(<= 0 % 100)))
(s/def ::gender boolean?)
(s/def ::spouse string?)
接下来我们就可以使用keys来定义person的spec了,用keys定义spec时,需要使用:req和:opt来指定哪些字段是必须的,哪些字段可选的,如下:
;;所有必选字段和可选字段的spec都必须在[]中列出来
(s/def ::person1
(s/keys :req [::name ::age ::gender]
:opt [::spouse]
))
这时如果使用::person1去校验任意一个满足条件的person数据结构都会出错:
(s/explain ::person1 {:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"
})
;;val: {:name "xiao ming", :age 25, :gender false, :spouse "xiao hua"} fails spec: :my-clj.core/person1 predicate: (contains? % :my-clj.core/name)
根据以上错误信息我们可以找到原因,是因为该数据结构中不包含:my-clj.core/name字段。这是因为使用:req和:opt指定的字段要求被校验的map的key必须是namespace keyword,也就是说person数据结构中的key必须是namespace keyword。如下如果改成namespace keyword,返回结果则会成功:
(s/explain ::person1 {::name "xiao ming"
::age 25
::gender false
::spouse "xiao hua"
})
;;Success!
如果必须使用namespace keyword做map中的key会给我们书写代码时带来很多困惑,幸好keys函数还提供了另外两个字段:req-un/opt-un用来替换对应的req/opt,这样数据结构中的key就可以是全局的keyword了,如下:
;;使用标志req-un/opt-un来区别必须字段和可选字段
(s/def ::person2
(s/keys :req-un [::name ::age ::gender] :opt-un [::spouse]
)) ;;这种情况下,普通的keyword就可以校验成功了。
(s/explain ::person2 {:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"
})
;;Success!
;;因为:spouse字段是可选的,没有:spouse字段,依然会成功
(s/explain ::person2 {:name "xiao ming"
:age 25
:gender false
})
;;Success!
;; :gender字段是必须有的,如果没有则会失败
(s/explain ::person2 {:name "xiao ming"
:age 25
:spouse "xiao hua"
})
;;val: {:name "xiao ming", :age 25, :spouse "xiao hua"} fails spec: :my-clj.core/person2 predicate: (contains? % :gender)
需要注意的是,s/keys函数并没有对不在req和opt中字段作限制。也就是说,如果person数据中多了一些其它没必要的字段,校验也会成功:
;;虽然person的数据结构中多了一个color字段依然会成功,keys函数只会校验存在req和opt中的字段,会无视其他字段
(s/explain ::person2 {:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"
:color "yellow"
})
;;Success!
这样设计的目的可能是给予程序员更多选择的机会吧,但是在项目中,多余的字段总是会给我们带来困惑和bug,所以为了将其字段限制在一定范围内,我们可以定义一个对其keys进行限制的谓词函数如下:
;;定义一个谓词函数,mmap中的每个key都必须在数组mkeys中
(defn keys-validator? [mmap mkeys]
(clojure.set/subset? (set (keys mmap)) (set mkeys)))
这样我们就可以使用keys-validator函数和s/keys一起对一个map数据结构的spec作更加严格的限制:
;;重新定义一个::person3,有三个必选字段和一个可选字段,以及所有字段必须在[:name :age :gender :spouse]中
(s/def ::person3
(s/and
(s/keys :req-un [::name ::age ::gender] :opt-un [::spouse]
)
(fn [x] (keys-validator? x [:name :age :gender :spouse]))
)) ;;成功校验用例
(s/explain ::person3 {:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"
})
;;Success! ;;如果多了一个color字段会失败,提示不满足keys-validator这个条件
(s/explain ::person3 {:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"
:color "yellow"
})
;;val: {:name "xiao ming", :age 25, :gender false, :spouse "xiao hua", :color "yellow"} fails spec: :my-clj.core/person3 predicate: (fn [x] (keys-validator? x [:name :age :gender :spouse]))
s/keys*函数:
s/keys函数与s/keys函数功能基本完全一致,只是验证的数据格式不一样而已,s/keys验证的是以{key value}形式表示的map,而s/keys验证的是数组形式[key value],s/keys*的使用如下:
;;以keys*定义一个::person4.格式与::person2一致,只是keys换成keys*
(s/def ::person4
(s/keys* :req-un [::name ::age ::gender] :opt-un [::spouse]
)) ;;这时候如果去校验一个{}形式的map会报错
(s/explain ::person4
{:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"
}) ;;In: [0] val: [:name "xiao ming"] fails spec: :my-clj.core/person4 at: [:clojure.spec/k] predicate: keyword? ;;如果解释数组形式的key value则返回success!
(s/explain ::person4
[:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"]
)
;;Success!
7.spec中空值(null)的处理:
在定义数据接口时,经常遇到某个字段无值的特殊情况,在clojure空值用nil表示,对于某个可能为nil的字段的校验,clojure.spec中提供了s/nilable这个函数,接收一个spec参数,返回一个spec,表示该spec可以接收空值特殊情况:
;;如果我们给person2中name字段为nil,校验则会出错
(s/explain ::person2
{:name nil
:age 25
:gender false
:spouse "xiao hua"
})
;;In: [:name] val: nil fails spec: :my-clj.core/name at: [:name] predicate: string? ;;当我们把定义::name时,容许其为nilable,
(s/def ::name (s/nilable string?))
(s/def ::person5
(s/keys :req-un [::name ::age ::gender] :opt-un [::spouse]
)) ;;此时,即使name为nil,也会返回success
(s/explain ::person2
{:name nil
:age 25
:gender false
:spouse "xiao hua"
})
;;Success!
所以我们可以根据接口定义要求合理的使用s/nilable来满足我们的需求。
8.spec中命名冲突的处理:
当我们使用"::"定义spec时,如果两个不同的数据结构下某个字段名字相同,就会发生命名冲突的问题,比如我们在定义person数据结构的基础上又定义了一个dog结构:
dog:
{
:name string?
:age int? (< 0 age 20)
}
对于dog中的:name字段,可以和person中的:name字段用同一个spec,因为他们描述一致,但是对于两个结构中age字段的范围不一致,这样就会导致命名冲突的问题。因为clojure.spec库中每定义一个spec的名字必须是namespace keyword,而这正是使用namespace keyword而不使用keyword的原因,因为我们可以在定义spec时选择为其指定命名空间的方式定义(http://blog.csdn.net/zdplife/article/details/52304258 ),其命名空间可以随意指定,该命名空间不一定存在也可以,因此就可以很好的解决该问题了:
;; ::person的定义
(s/def :person/name string?)
(s/def :person/age (s/and int? #(<= 0 % 100)))
(s/def :person/gender boolean?)
(s/def :person/spouse string?)
(s/def ::person
(s/keys :req-un [:person/name :person/age :person/gender] :opt-un [:person/spouse]
)) ;; ::dog的定义
(s/def :dog/name string?)
(s/def :dog/age (s/and int? #(<= 0 % 20)))
(s/def ::dog
(s/keys :req-un [:dog/name :dog/age]
)) ;; 成功解决命名冲突,校验成功
(s/explain ::person {:name "xiao ming"
:age 25
:gender false
:spouse "xiao hua"
})
;; Success!
(s/explain ::dog {:name "du du"
:age 19
})
;; Success!
9.正则表达式在spec中的使用:
正则表达式在校验数据格式方面,有着其独特的优势。正好clojure中也有相关正则表达式的处理函数,经常使用的re-matches函数,可以构造一个正则表达式适配器(http://blog.csdn.net/zdplife/article/details/51868499),而该正则表达式适配器正好可以当作clojure.spec使用:
;;定义一个匹配移动电话号码的正则表达式
(def reg-phone-num #"^1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\d{8}$") ;;使用上述定义的正则表达式来定义一个spec
(s/def ::phone-num #(re-matches reg-phone-num %)) ;;号码正确,返回success
(s/explain ::phone-num "15977765765")
;;Success! ;;号码错误,错误信息返回
(s/explain ::phone-num "14988865765")
;;val: "14988865765" fails spec: :my-clj.core/phone-num predicate: (re-matches reg-phone-num %)
10.在接口函数中使用spec:
在函数中使用spec,最好的方式是利用在定义函数时的的输入输出条件检查(pre-condition/post-condition),假设我们定义一个接口函数transform-person,函数输入是一个person数据结构,输出是一个字符串,定义如下:
;;定义一个函数,用s/valid?函数对其输入和输出参数进行校验
(defn transform-person [person]
{:pre [(s/valid? ::person person)] :post [(s/valid? string? %)]
}
(let [gender-str (if (:gender person) "a boy" "a girl")
his-or-her (if (:gender person) "his" "her")
]
(str (:name person) " is " gender-str ",and " his-or-her " age is " (:age person))
))
;;定义一个满足条件的person1
(def person1
{:name "xiao ming", :age 25, :gender false, :spouse "xiao hua"}
)
;;定义一个不满足条件的person2
(def person2
{:name "liu wei", :age -10, :gender false, :spouse "xiao hua"}
) ;;调用接口函数成功
(transform-person person1)
;;=> "xiao ming is a girl,and her age is 25" ;;接口参数校验失败,抛出异常
(transform-person person2)
;;AssertionError Assert failed: (s/valid? :my-clj.core/person person) my-clj.core/transform-person (form-init1838625064402666216.clj:1)
11.总结:
本文主要介绍了clojure.spec库的一些基本函数的用法,其实这些函数对于普通的接口参数检测已经足够了,clojure.spec库中还有一些其它函数:alt函数/cat函数/melt函数,还可以使用正则中的*/+/?等,这些函数也蛮好玩的,有兴趣的可以去试着玩一下,这边有介绍:http://clojure.org/guides/spec
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 一文看尽Raft一致性协议的关键点
【推荐】 Question|网站被黑客扫描撞库该怎么应对防范?
clojure.spec库入门学习的更多相关文章
- pandas库学习笔记(二)DataFrame入门学习
Pandas基本介绍——DataFrame入门学习 前篇文章中,小生初步介绍pandas库中的Series结构的创建与运算,今天小生继续“死磕自己”为大家介绍pandas库的另一种最为常见的数据结构D ...
- libevent的入门学习-库的安装【转】
转自:https://blog.csdn.net/lookintosky/article/details/61658067 libevent的入门学习-库的安装最近开始接触Linux应用层的东西,发现 ...
- opengl入门学习
OpenGL入门学习 说起编程作图,大概还有很多人想起TC的#include <graphics.h>吧? 但是各位是否想过,那些画面绚丽的PC游戏是如何编写出来的?就靠TC那可怜的640 ...
- git入门学习(二):新建分支/上传代码/删除分支
一.git新建分支,上传代码到新的不同分支 我要实现的效果,即是多个内容的平行分支:这样做的主要目的是方便统一管理属于同一个内容的不同的项目,互不干扰.如图所示: 前提是我的github上已经有we ...
- Aho-Corasick算法、多模正则匹配、Snort入门学习
希望解决的问题 . 在一些高流量.高IO的WAF中,是如何对规则库(POST.GET)中的字符串进行多正则匹配的,是单条轮询执行,还是多模式并发执行 . Snort是怎么组织.匹配高达上千条的正则规则 ...
- OpenGL入门学习(转)
OpenGL入门学习 http://www.cppblog.com/doing5552/archive/2009/01/08/71532.html 说起编程作图,大概还有很多人想起TC的#includ ...
- OpenCV入门学习笔记
OpenCV入门学习笔记 参照OpenCV中文论坛相关文档(http://www.opencv.org.cn/) 一.简介 OpenCV(Open Source Computer Vision),开源 ...
- Asp.Net MVC5入门学习系列④
原文:Asp.Net MVC5入门学习系列④ 添加Model且简单的使用EF 对于EF(EntityFramework)不了解的朋友可以去百度文科或者在园子里搜一些简资源看下,假如和我一样知道EF的概 ...
- 数据分析与展示——NumPy库入门
这是我学习北京理工大学嵩天老师的<Python数据分析与展示>课程的笔记.嵩老师的课程重点突出.层次分明,在这里特别感谢嵩老师的精彩讲解. NumPy库入门 数据的维度 维度是一组数据的组 ...
随机推荐
- bzoj 3907 网格 bzoj2822 [AHOI2012]树屋阶梯——卡特兰数(阶乘高精度模板)
题目:https://www.lydsy.com/JudgeOnline/problem.php?id=3907 https://www.lydsy.com/JudgeOnline/problem.p ...
- python3之编码
这个符号(#!)的名称,叫做"Shebang"或者"Sha-bang"Shebang这个符号通常在Unix系统的脚本中第一行开头中写到,它指明了执行这个脚本文件 ...
- POJ3249(DAG上的dfs)
Test for Job Time Limit: 5000MS Memory Limit: 65536K Total Submissions: 10567 Accepted: 2482 Des ...
- VS编译linux项目生成静态库并在另一个项目中静态链接的方法
VS2017也推出很久了,在单位的时候写linux的服务端程序只能用vim,这让用惯了IDE的我很难受. 加上想自己撸一套linux上的轮子,决定用VS开工远程编写调试linux程序. 在window ...
- python-xlrd api
1.导入模块 import xlrd from xlrd import open_workbook 2.打开Excel文件读取数据 data = xlrd.open_workbook('excelFi ...
- PowerDesignerPDM中搜寻表名或字段名
Option Explicit ValidationMode = True InteractiveMode = im_Batch Dim mdl '当前model '获取当前活 ...
- 如何创建和配置Solaris10 zones (ZT)
http://thegeekdiary.com/how-to-create-and-configure-solaris-10-zones/ Solaris zones enables a softwa ...
- Android Tombstone 分析
1.什么是tombstone 当一个动态库(native 程序)开始执行时,系统会注册一些连接到 debuggerd 的 signal handlers,当系统 crash 的时候,会保存一个 tom ...
- matlab图片高清复制到visio
编辑→复制图窗→在visio中粘贴
- 一些API的用法
//1.init初始化 NSString * str1 = [[NSString alloc] init]; NSLog(@"str1 = %@",str1); //2.initW ...