XML学习笔记6——XPath语言
在上一篇笔记的结尾,我们接触到了两个用于选择XML文档中特定范围的元素<selector>和<field>,这两个元素的取值都是XPath表达式,那么,什么是XPath呢?简单的说,XPath是用于在XML文档中查找信息的语言,可用来在XML文档中遍历元素和属性,很多XML的相关技术比如XSLT、XQuery、XPointer等都是构建于XPath的基础之上,在这一篇笔记中,就来学习一下XPath语言。
1、相关术语
(1)节点(Node):格式良好的XML文档都可以转换为一个树型结构,XPath中的节点也就是这个树型结构中的节点。概况起来,有如下所列的七种节点:
节点类型 | 说明 |
XML文档根节点 | XML文档的根称为文档节点或根节点 |
元素节点 | 一个元素的开始标签、结束标签,以及之间的全部内容整体称之为元素节点 |
属性节点 | 元素的每个属性都构成一个属性节点,包括属性名称和属性值两个部分,属性节点必须依附于元素节点 |
命名空间节点 | XML文档中的xmlns:prefix属性称之为命名空间节点,注意和属性节点的区别 |
文本节点 | XML元素中间的字符数据,包括CDATA段中的字符数据 |
注释节点 | XML文档里<!--和-->包含的注释部分构成注释节点 |
处理指令节点 | XML文档的处理指令部分构成处理指令节点 |
(2)基本值(也称为原子值,Atomic Value):专门用于表示简单的字面值,如整数值,字符串等。基本值可以当成没有父节点也没有子节点的节点。
(3)项目(Item):一个项目代表一个基本值或一个节点。
(4)节点集和序列(Sequence):XPath表达式可以表示多个节点,多个节点的组合在XPath1.0中称为节点集,而在XPath2.0中添加了一个序列的术语,即可以代表普通的项目,也可以代表节点集。
(5)节点关系:
节点关系 | 说明 |
父节点Parent | 每个元素或属性都有一个父节点 |
子节点Children | 元素节点可以有0个、1个或多个子节点 |
兄弟节点Sibling | 父节点相同的节点称之为兄弟节点 |
祖先节点Ancestor | 节点的父节点、父节点的父节点一直到根节点 |
后代节点Descendant | 节点的子节点,子节点的子节点...... |
(6)相对路径和绝对路径:与操作系统中的路径类似,XPath中也有相对路径和绝对路径,绝对路径以斜线(/)开头,总是从根节点开始匹配,而相对路径则不会以斜线开头,从当前路径开始匹配。
2、XPath语法
XPath使用路径表达式来访问XML中的节点或节点集,每个XPath表达式总是由一个或多个步(step)组成的,多个步直接使用斜线分隔。在XPath中,步的语法格式如下:
轴::节点测试[限定谓语]
也就是说,每个步都通过了3次筛选,第一次是使用“轴”选择节点方向,第二次使用“节点测试”选取在指定轴方向上的部分节点,第三次则是使用“限定谓语”来对选中的节点进一步过滤。
(1)轴:在XPath中,有下面列表中的各种轴:
轴 | 简化写法 | 说明 |
---|---|---|
ancestor | 选取当前节点的所有先辈(父、祖父等)节点 | |
ancestor-or-self | 选取当前节点的所有先辈(父、祖父等)节点以及当前节点本身 | |
attribute | @ | 选取当前节点的所有属性节点,如果当前节点不是元素节点,则attribute轴方向上的节点集为空 |
child | 省略不写 | 选取当前节点的所有子节点 |
descendant | // | 选取当前节点的所有后代节点(子、孙等) |
descendant-or-self | 选取当前节点的所有后代节点(子、孙等)以及当前节点本身 | |
following | 选取文档中当前节点的结束标签之后的所有节点,不会包含当前节点的后代节点和属性节点 | |
following-sibling | 选取文档中当前节点的结束标签之后的所有兄弟节点 | |
namespace | 选取当前节点的所有命名空间节点,当前节点不是元素节点,则namespace轴方向上的节点集为空 | |
parent | .. | 选取当前节点的父节点 |
preceding | 选取文档中当前节点的开始标签之前的所有节点,不会包含当前节点的后代节点和属性节点 | |
preceding-sibling | 选取文档中当前节点的开始标签之前的所有兄弟节点 | |
self | . | 选取当前节点 |
(2)节点测试:节点测试用于从指定轴方向上选取所匹配的特定的节点,在XPath中,常用的节点测试语法如下表所示:
节点测试 | 说明 |
nodename |
从指定轴方向上选出具有nodename的节点 如child::book选择当前节点的所有book子节点 descendant::book选择当前节点的所有book后代节点(包括book子节点、孙子节点等) |
node() | 选择指定轴匹配的所有类型的节点 |
text() |
选择指定轴匹配的所有文本类型的节点 如child::text()选择当前节点的所有文本子节点 descendant::text()选择当前节点的所有文本后代节点(包括文本子节点、文本孙子节点等) |
comment() | 选择指定轴匹配的所有注释节点 |
processing-instruction | 选择指定轴匹配的所有处理指令节点 |
* | 节点测试中的通配符,表示所有,也即不进行过滤 |
(3)限定谓语:每个步中可以接受0个或多个限定谓语,用于进一步过滤所选取的节点集,限定谓语放在方括号中,通常限定谓语返回一个boolean值。看下面的一些例子:
路径表达式 | 结果 |
---|---|
/bookstore/book[1] | 选取属于 bookstore 元素的子元素的第一个 book 元素。 |
/bookstore/book[last()] | 选取属于 bookstore 元素的子元素的最后一个 book 元素。 |
/bookstore/book[last()-1] | 选取属于 bookstore 元素的子元素的倒数第二个 book 元素。 |
/bookstore/book[position()<3] | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
//title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素。 |
//title[@lang='eng'] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
/bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
/bookstore/book[price>35.00]/title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
3、运算符
从上面的实例中可以看到,在限定谓语中,还可以使用运算符、表达式,还有很多内置的函数供使用。这一小节先看看XPath中支持的运算符:
(1)算术运算符:加(+)、减(-)、乘(*)、除(div)、取模(mod)
算术运算符非常简单,但是需要注意几点:
A、因为减号实际上也就是中划线,而中划线在XML中是合法的标识符号,从而带来了歧义,于是XPath强制规定,使用减号的时候,需要前后各加一个空格。
B、在XPath中,所有的数值都是64位的double类型,即便直接书写成0、100;另外,XPath还有几个特殊的数值:正无穷大、负无穷大、非数。
C、在运算时,如果操作数不是数值类型,会自动转换,下面的比较运算符、逻辑运算符如果有必要也会发生相应的自动类型转换。
(2)比较运算符:等于(=)、不等于(!=)、小于(<)、小于或等于(<=)、大于(>)、大于或等于(>=)
需要注意的是,不像其它编程语言,这里表示相等只需要一个等于号。
(3)逻辑运算符:与(and)、或(or)
(4)集合运算符:并集(|)
4、表达式
(1)for表达式:用于循环访问序列中的每个项,并对每项进行一次计算,最后将每项计算得到的结果组合成序列后返回,语法格式如下:
for $var in sequence return rtExpression
实际上,这里的for更类似于js中的foreach。还可以使用下面的形式遍历多个序列:
for $var1 in sequence1, $var2 in sequence2 return fn($var1,$var2)
(2)if表达式:用于处理分支,根据不同条件得到不同的返回值,语法格式如下:
if (condition1)
then rtVal1
[else if (condition2)
then rtVal2
...]
else
otherVal
(3)some表达式:迭代中只要有一项满足条件就返回true,否则返回false,语法格式如下:
some $var in sequence satisfies condition
(4)every表达式:迭代中只有有一项不满足条件就返回false,否则返回true,语法格式如下:
every $var in sequence satisfies condition
5、内置函数库
在XPath中还有大量的内置函数,用于增强相关功能,这些内置函数可以参考:XPath函数。我在下面也抄录一份供参考:
分类 | 函数 | 说明 | |
存取函数 | fn:node-name(node) | 返回参数节点的节点名称。 | |
fn:nilled(node) | 返回是否拒绝参数节点的布尔值。 | ||
fn:data(item.item,...) | 接受项目序列,并返回原子值序列。 | ||
|
返回当前节点或指定节点的 base-uri 属性的值。 | ||
|
返回当前节点或指定节点的 document-uri 属性的值。 | ||
错误和跟踪函数 |
|
例子:error(fn:QName('http://example.com/test', 'err:toohigh'), 'Error: Price is too high') 结果:向外部处理环境返回 http://example.com/test#toohigh 以及字符串 "Error: Price is too high"。 |
|
fn:trace(value,label) | 用于对查询进行 debug。 | ||
数值函数 | fn:number(arg) |
返回参数的数值。参数可以是布尔值、字符串或节点集。 例子:number('100') 结果:100 |
|
fn:abs(num) | 返回参数的绝对值。 | ||
fn:ceiling(num) | 返回大于或等于 num 参数的最小整数。 | ||
fn:floor(num) | 返回小于或等于 num 参数的最大整数。 | ||
fn:round(num) | 把 num 参数四舍五入为最接近的整数。 | ||
fn:round-half-to-even() |
返回最接近参数num的偶数 例子:round-half-to-even(0.5) 结果:0 例子:round-half-to-even(1.5) 结果:2 例子:round-half-to-even(2.5) 结果:2 |
||
字符串函数 | fn:string(arg) | 返回参数的字符串值。参数可以是数字、逻辑值或节点集。 | |
fn:codepoints-to-string(int,int,...) |
根据一个Unicode值序列序列返回字符串。 例子:codepoints-to-string((84, 104, 233, 114, 232, 115, 101)) 结果:'Thérèse' 注意:该函数的参数是一个Unicode值序列,因此必须用括号将参数括起来 |
||
fn:string-to-codepoints(string) | 根据字符串返回每个字符所对应的Unicode值的序列。 | ||
fn:codepoint-equal(comp1,comp2) | 根据 Unicode 值序列比较,如果 comp1 的值等于 comp2 的值,则返回 true,否则返回 false。 | ||
|
根据对照规则,comp1小于comp2返回 -1;comp1等于comp2,返回0;comp1大于comp2返回1。 |
||
fn:concat(string,string,...) | 返回字符串的拼接。 | ||
fn:string-join((string,string,...),sep) | 使用 sep 参数作为分隔符,来返回 string 参数拼接后的字符串。 | ||
|
返回从 start 位置开始的指定长度的子字符串。第一个字符的下标是 1。 如果省略 len 参数,则返回从位置 start 到字符串末尾的子字符串。 |
||
|
返回指定字符串的长度。如果没有 string 参数,则返回当前节点的字符串值的长度 | ||
|
删除指定字符串的首尾空白,并把内部连续空白压缩为一个,然后返回结果。没有参数,则处理当前节点。 | ||
fn:normalize-unicode() | 执行 Unicode 规格化。 | ||
fn:upper-case(string) | 把 string 参数转换为大写。 | ||
fn:lower-case(string) | 把 string 参数转换为小写。 | ||
fn:translate(string1,string2,string3) |
把 string1 中的 string2 替换为 string3。 例子:translate('12:30','30','45') 结果:'12:45' 例子:translate('12:30','03','54') 结果:'12:45' 例子:translate('12:30','0123','abcd') 结果:'bc:da' |
||
fn:escape-uri(stringURI,esc-res) |
例子:escape-uri("http://example.com/test#car", true()) 结果:"http%3A%2F%2Fexample.com%2Ftest#car" 例子:escape-uri("http://example.com/test#car", false()) 结果:http://example.com/test#car 例子:escape-uri ("http://example.com/~bébé", false()) 结果:"http://example.com/~b%C3%A9b%C3%A9" |
||
fn:contains(string1,string2) | 如果 string1 包含 string2,则返回 true,否则返回 false。 | ||
fn:starts-with(string1,string2) | 如果 string1 以 string2 开始,则返回 true,否则返回 false。 | ||
fn:ends-with(string1,string2) | 如果 string1 以 string2 结尾,则返回 true,否则返回 false。 | ||
fn:substring-before(string1,string2) | 返回 string2 在 string1 中出现之前的子字符串。 | ||
fn:substring-after(string1,string2) | 返回 string2 在 string1 中出现之后的子字符串。 | ||
fn:matches(string,pattern) | 如果 string 参数匹配指定的模式,则返回 true,否则返回 false。 | ||
fn:replace(string,pattern,replace) | 把指定的模式替换为 replace 参数,并返回结果。 | ||
fn:tokenize(string,pattern) |
例子:tokenize("XPath is fun", "\s+") 结果:("XPath", "is", "fun") |
||
anyURI函数 | fn:resolve-uri(relative,base) | ||
逻辑函数 | fn:boolean(arg) | 返回数字、字符串或节点集的布尔值。 | |
fn:not(arg) | 首先通过 boolean() 函数把参数还原为一个布尔值,然后再取反。 | ||
fn:true() | 返回布尔值 true。 | ||
fn:false() | 返回布尔值 false。 | ||
日期时间函数 | fn:dateTime(date,time) | 把参数转换为日期和时间。 | |
fn:years-from-duration(datetimedur) | 返回参数值的年份部分的整数,以标准词汇表示法来表示。 | ||
fn:months-from-duration(datetimedur) | 返回参数值的月份部分的整数,以标准词汇表示法来表示。 | ||
fn:days-from-duration(datetimedur) | 返回参数值的天部分的整数,以标准词汇表示法来表示。 | ||
fn:hours-from-duration(datetimedur) | 返回参数值的小时部分的整数,以标准词汇表示法来表示。 | ||
fn:minutes-from-duration(datetimedur) | 返回参数值的分钟部分的整数,以标准词汇表示法来表示。 | ||
fn:seconds-from-duration(datetimedur) | 返回参数值的分钟部分的十进制数,以标准词汇表示法来表示。 | ||
fn:year-from-dateTime(datetime) | 返回参数本地值的年部分的整数。 | ||
fn:month-from-dateTime(datetime) | 返回参数本地值的月部分的整数。 | ||
fn:day-from-dateTime(datetime) | 返回参数本地值的天部分的整数。 | ||
fn:hours-from-dateTime(datetime) | 返回参数本地值的小时部分的整数。 | ||
fn:minutes-from-dateTime(datetime) | 返回参数本地值的分钟部分的整数。 | ||
fn:seconds-from-dateTime(datetime) | 返回参数本地值的秒部分的十进制数。 | ||
fn:timezone-from-dateTime(datetime) | 返回参数的时区部分,如果存在。 | ||
fn:year-from-date(date) | 返回参数本地值中表示年的整数。 | ||
fn:month-from-date(date) | 返回参数本地值中表示月的整数。 | ||
fn:day-from-date(date) | 返回参数本地值中表示天的整数。 | ||
fn:timezone-from-date(date) | 返回参数的时区部分,如果存在。 | ||
fn:hours-from-time(time) | 返回参数本地值中表示小时部分的整数。 | ||
fn:minutes-from-time(time) | 返回参数本地值中表示分钟部分的整数。 | ||
fn:seconds-from-time(time) | 返回参数本地值中表示秒部分的整数。 | ||
fn:timezone-from-time(time) | 返回参数的时区部分,如果存在。 | ||
fn:adjust-dateTime-to-timezone(datetime,timezone) | 如果 timezone 参数为空,则返回没有时区的 dateTime。否则返回带有时区的 dateTime。 | ||
fn:adjust-date-to-timezone(date,timezone) | 如果 timezone 参数为空,则返回没有时区的 date。否则返回带有时区的 date。 | ||
fn:adjust-time-to-timezone(time,timezone) | 如果 timezone 参数为空,则返回没有时区的 time。否则返回带有时区的 time。 | ||
QName相关函数 | fn:QName() | ||
fn:local-name-from-QName() | |||
fn:namespace-uri-from-QName() | |||
fn:namespace-uri-for-prefix() | |||
fn:in-scope-prefixes() | |||
fn:resolve-QName() | |||
节点函数 |
|
返回当前节点的名称或指定节点集中的第一个节点。 | |
|
返回当前节点的名称或指定节点集中的第一个节点 - 不带有命名空间前缀。 | ||
|
返回当前节点或指定节点集中第一个节点的命名空间 URI。 | ||
fn:lang(lang) |
如果当前节点的语言匹配指定的语言,则返回 true。 例子:Lang("en") is true for <p xml:lang="en">...</p> 例子:Lang("de") is false for <p xml:lang="en">...</p> |
||
|
返回当前节点或指定的节点所属的节点树的根节点。通常是文档节点。 | ||
上下文函数 | fn:position() |
返回当前正在被处理的节点的 index 位置。 例子://book[position()<=3] 结果:选择前三个 book 元素 |
|
fn:last() |
返回在被处理的节点列表中的项目数目。 例子://book[last()] 结果:选择最后一个 book 元素 |
||
fn:current-dateTime() | 返回当前的 dateTime(带有时区)。 | ||
fn:current-date() | 返回当前的日期(带有时区)。 | ||
fn:current-time() | 返回当前的时间(带有时区)。 | ||
fn:implicit-timezone() | 返回隐式时区的值。 | ||
fn:default-collation() | 返回默认对照的值。 | ||
fn:static-base-uri() | 返回 base-uri 的值。 | ||
序列函数 | 一般序列函数 | fn:index-of((item,item,...),searchitem) |
返回在项目序列中等于 searchitem 参数的位置。 例子:index-of ((15, 40, 25, 40, 10), 40) 结果:(2, 4) |
fn:remove((item,item,...),position) | 返回由 item 参数构造的新序列 - 同时删除 position 参数指定的项目。 | ||
fn:empty(item,item,...) | 如果参数值是空序列,则返回 true,否则返回 false。 | ||
fn:exists(item,item,...) | 如果参数值不是空序列,则返回 true,否则返回 false。 | ||
fn:distinct-values((item,item,...),collation) |
返回唯一不同的值。 例子:distinct-values((1, 2, 3, 1, 2)) 结果:(1, 2, 3) |
||
fn:insert-before((item,item,...),pos,inserts) | 返回由 item 参数构造的新序列 - 同时在 pos 参数指定位置插入 inserts 参数的值。 | ||
fn:reverse((item,item,...)) | 返回指定的项目的颠倒顺序。 | ||
fn:subsequence((item,item,...),start,len) | 返回 start 参数指定的位置返回项目序列,序列的长度由 len 参数指定。第一个项目的位置是 1。 | ||
fn:unordered((item,item,...)) | 依据实现决定的顺序来返回项目。 | ||
容量测试函数 | fn:zero-or-one(item,item,...) | 如果参数包含零个或一个项目,则返回参数,否则生成错误。 | |
fn:one-or-more(item,item,...) | 如果参数包含一个或多个项目,则返回参数,否则生成错误。 | ||
fn:exactly-one(item,item,...) | 如果参数包含一个项目,则返回参数,否则生成错误。 | ||
比较函数 | fn:deep-equal(param1,param2,collation) | 如果 param1 和 param2 与彼此相等(deep-equal),则返回 true,否则返回 false。 | |
合计函数 | fn:count((item,item,...)) | 返回节点的数量。 | |
fn:avg((arg,arg,...)) | 返回参数值的平均数。 | ||
fn:max((arg,arg,...)) | 返回参数中的最大值。 | ||
fn:min((arg,arg,...)) | 返回参数中的最小值。 | ||
fn:sum(arg,arg,...) | 返回指定节点集中每个节点的数值的总和。 | ||
序列生成函数 | fn:id((string,string,...),node) | ||
fn:idref((string,string,...),node) | |||
fn:data((item1,item2,...)) | 返回item1、item2等各项的值所组成的序列。 | ||
fn:doc(URI) | |||
fn:doc-available(URI) | 如果 doc() 函数返回文档节点,则返回 true,否则返回 false。 | ||
|
很明显,把这些内置函数放在这里的目的并不是要强行记住,而只是需要的时候当字典查查(XQuery1.0和XPath共享了这些内置函数,有事没事看看,混个眼熟也挺好的)。
XML学习笔记6——XPath语言的更多相关文章
- delphi操作xml学习笔记 之一 入门必读
Delphi 对XML的支持---TXMLDocument类 Delphi7 支持对XML文档的操作,可以通过TXMLDocument类来实现对XML文档的读写.可以利用TXMLDocum ...
- XML学习笔记
XML学习笔记 第一部分:XML简介 我们经常可以听到XML.HTML.XHTML这些语言,后两者比较清楚,一直不是很明白XML是什么,这里做一个总结. XML(eXtensible Markup L ...
- Scrapy:学习笔记(1)——XPath
Scrapy:学习笔记(1)——XPath 1.快速开始 XPath是一种可以快速在HTML文档中选择并抽取元素.属性和文本的方法. 在Chrome,打开开发者工具,可以使用$x工具函数来使用XPat ...
- 23 DesignPatterns学习笔记:C++语言实现 --- 2.7 Proxy
23 DesignPatterns学习笔记:C++语言实现 --- 2.7 Proxy 2016-07-18 (www.cnblogs.com/icmzn) 模式理解
- 23 DesignPatterns学习笔记:C++语言实现 --- 2.6 Facade
23 DesignPatterns学习笔记:C++语言实现 --- 2.6 Facade 2016-07-22 (www.cnblogs.com/icmzn) 模式理解
- 23 DesignPatterns学习笔记:C++语言实现 --- 2.5 Factory
23 DesignPatterns学习笔记:C++语言实现 --- 2.5 Factory 2016-07-18 (www.cnblogs.com/icmzn) 模式理解 1. Flyweight ...
- 23 DesignPatterns学习笔记:C++语言实现 --- 2.4 Composite
23 DesignPatterns学习笔记:C++语言实现 --- 2.4 Composite 2016-07-22 (www.cnblogs.com/icmzn) 模式理解
- 23 DesignPatterns学习笔记:C++语言实现 --- 2.2 Adapter
23 DesignPatterns学习笔记:C++语言实现 --- 2.2 Adapter 2016-07-22 (www.cnblogs.com/icmzn) 模式理解
- 23 DesignPatterns学习笔记:C++语言实现 --- 2.1 Bridge
23 DesignPatterns学习笔记:C++语言实现 --- 2.1 Bridge 2016-07-22 (www.cnblogs.com/icmzn) 模式理解
随机推荐
- 实验一报告--认识DOS
实验一 DOS命令解释程序的编写 13物联网 黄鸿佳 201306104107 一. 实验目的 (1)认识DOS: (2)掌握命令解释程序的原理: ...
- Restore Oracle database to another server
1. Copy or remotely mount the backupset folder from the source server to the target server 2. On the ...
- swift的运算符
1.什么是运算符?它有什么作用? 运算符是一种特定的符号或者表达式.它用来验证.修改.合并变量. 2.运算符有哪些? 运算符有很多,很多朋友学的很烦.这里我依据它的作用把它分为几块来介绍: a:赋值运 ...
- DedeCMS织梦系统head.htm里无法调用栏目描述
{dede:channel type='top'} [field:description/] {/dede:channel} channel 这个标签没有description属性你需要自己把这个属性 ...
- [转]Maven 划分模块
所有用Maven管理的真实的项目都应该是分模块的,每个模块都对应着一个pom.xml.它们之间通过继承和聚合(也称作多模块,multi-module)相互关联.那么,为什么要这么做呢?我们明明在开发一 ...
- 决策树 -- C4.5算法
C4.5是另一个分类决策树算法,是基于ID3算法的改进,改进点如下: 1.分离信息 解释:数据集通过条件属性A的分离信息,其实和ID3中的熵: 2.信息增益率 解释:Gain(A)为获的A ...
- JAVA 正则表达式4种常用的功能
下面简单的说下它的4种常用功能: 查询: 以下是代码片段: String str="abc efg ABC"; String regEx="a|f" ...
- net与树莓派的情缘(一)
想做个NAS 由于手中经济有限又不想花太多钱,所以决定买个树莓派自己搭建. 板子:树莓派2代 elem14的 内存卡:闪迪 class10 16g 网线一根,电源线一个 系统:树莓派官方 Raspb ...
- Web Essentials之样式表StyleSheets
返回Web Essentials功能目录 本篇目录 智能感知 视觉提示 验证 Web标准 转换器 Web Essentials中大多数的CSS功能也适用于LESS. 智能感知 生成供应商特定的属性 如 ...
- git版本管理策略及相关技巧(A)
公司几乎所有的项目都是使用 git 仓库来管理代码,以前对 git 只有些肤浅的了解,每次提交代码或者上线的时候总是会提心吊胆,生怕出现一些未知的问题.经过三个月的踩坑和填坑, git 操作颇显成熟. ...