系列之十七:如何使用XSL和正则表达式来验证数据的有效性
XSL现在正在逐渐的成为XML中的类似与SQL在数据库设计中的地位。
虽然Microsoft's XSL仅仅是实现了其中的某一些部分的功能
但是你已经能够实现非常复杂的查询了
虽然现在的XSL仅仅还只是一种基于纯粹文本和字符串方式
的查询语言
在下面介绍的例子中,将大量使用到文本内的字符串方式的搜索,
你会发现在XML中对数据的处理很大一部分都是要使用到文本内的查询的。
这在XSL的编写中是一件非常普及的功能。
正是因为这样,你应该了解一些正则表达式应该如何来使用。
正则表达式的简单介绍
正则表达式大部分都是来自与Unix世界的需要。
在Unix中许多编程语言几乎整个都是围绕着正则表达式进行的(例如Perl, Python, Tcl)
但是令人感到奇怪的是正则表达式好象是最近才使用在Windows系列中的,
特别是大量的使用在脚本语言中,例如javascript和VBScript,
尽管你也可以将它们使用在Visual Basic或则Java中,但是显然它们似乎在
脚本语言中更加有吸引力。也许是这个缘故把,大家平时似乎都很少使用正则表达式把。
使用正则表达式,你可以根据你想查询的内容建立一个匹配模板(英文叫pattern)
一旦你使用正则表达式建立了一个模板,你就可以使用它来测试你的字符串了,
使用它可以完成很多功能:
例如判断一个字符串是否在另外一个字符串中(或则在另外一个字符串中的什么位置)
例如使用一个字符串来替换另外一个字符串
例如返回所有满足模板条件的字符串列表
例如。。。等等等等
上面我介绍了有关正则表达式的基本概念,有关它的详细说明和语法可以查阅MSDN和
javascript中的有关帮助。
在VB中如果你想引用正则表达式的话,你需要在项目中引用"Microsoft VBScript Regular Expressions"。但是如果你要是使用脚本的话就不必要了,因为在脚本里面这已经是一个
内在的对象供你引用了。
当然你需要在你的机器上安装IE4以上罗。
这个对象(在javascript中)叫RegExp
下面还是让代码来说明问题把,现在假设你想查看一个文挡里面是否包含一个特定的
字符串(例如"regular expressions")
代码见下:
代码使用VB写成。
Public Function IsTermInDocument(filePath As String,_
expr As String) As Boolean
Dim fs As FileSystemObject
Dim ts As TextStream
Dim re As RegExp
Dim text As String
Set fs = New FileSystemObject
Set ts = fs.OpenTextFile(filePath, ForReading)
text = ts.ReadAll
Set re = New RegExp
re.Pattern = expr
IsTermInDocument = re.Test(text)
End Function
Debug.print IsTermInDocument("c:binmyPage.htm",_
"regular expression")
上面的那个函数将根据文挡里面是否有满足条件的字符串返回True/False.
注意我加粗的部分:
第一句是建立一个正则表达式的对象
第二句是指定该正则表达式的模板
第三句就是根据模板执行查询了
呵呵,如果正则表达式的功能仅仅是这么简单的话,也许你会说
VB中的instr()不就能够代替了吗?
但是,在进行XML的数据格式化的时候,对字符串的处理远比这个复杂得多。
例如:假设你要确保你要验证的字段中是否包含一个well-formed的zip编码
(well-formed意味着它是一个有效的编码,
也许它对于某个给定的地方或则区域或则国家又是无效的
这种界于well-formed和valid的表达式将是本文里面讨论的重点)
如果你要是使用VB来进行这种判断的话将非常的难看
你需要判断是否表达式有5位或则10位数字,或则是否为字母,
然后第6位字母又必须得是一个破则号
但是如果是使用正则表达式的话,将会是这样的简单:
Set IsZipCode = New RegExp
IsZipCode.Pattern = "^d{5}(-d{4})?$"
if IsZipCode.test("32545-2198") then
下面将简单解释一下其中模板的含义:
^ 说明在这个表达式之前没有任何其它的字符串,
意味着要验证的表达式不是某个字符串中间的一部分,而是它的开头
d 表示下一个字符必须是0-9中的一个数字
d{5} 并且必须是连着的5个数字
-d{4} 4个数字必须出现在字符"-"的后面
(-d{4})? 这4个数字是可选的,即可有也可以没有
$ 这个表达式后面应该不会再有其它的什么东西了
最有意思的是一旦你定义好了这么一个模板,你就可以将它使用在
任何其它的正则表达式对象中,而不需要再重新建立一个正则表达式对象了了。
使用这个办法,你甚至可以把一个近2000行代码的javascript程序
减少到只有几百行,设置当你把一些模板组合在一起的时候,就能够完成
正则表达式本来不可能完成的东东了。
下面我再举一个用来验证数据有效性的例子:
例如你现在想验证一个电话号码数据是否有效
对于通用一个电话号码一般下面这几种写法都是有效的:
(800)555-1212
1(800) 555-1212
1-800-555-1212
1.800.555.1212
等等.
如果你使用脚本来写一段满足上面所有要求的代码将非常的复杂。
但是如果你使用正则表达式的话,将非常的简单,只有下面这两句代码:
Set IsPhoneNumber=new RegExp
IsPhoneNumber.pattern="^[01]?s*[(.-]?(d{3})[).-]?s*(d{3})[.-](d{4})$"
你可以仔细体会上面这个代码的意义。
首先它验证第一个字符是否为0或则是否为1或则根本就没有。
然后再进行下面的验证,大家可以自己琢磨其它部分的意思把,呵呵。
查询和替换数据
当然,验证数据的有效性仅仅是它能够做的一件小事而已,
但是更有用的是:如果你能够把上面那么多种电话号码的表达方式转换
成一种统一的方式显示出来。
例如我要把上面的电话号码格式化成XML中的一个片段如下:
<phoneNumber>
<areacode>123</areacode>
<exchange>456</exchange>
<local>7890</local>
</phoneNumber>
这时的电话号码的模板分成三个部分:
(d{3}), (d{3}), (d{4}), 分别表示area code, exchange, 和local number
. 在正则表达式中,正则解释器会自动
将匹配的字符赋值给变量 $1, $2, $3,等.
这样你使用下面的代码就能够实现
re.Replace("1(352)351-4159", "<phoneNumber><areacode>$1</areacode><exchange>$2</exchange><local>$3</local></phoneNumber>")
如果你觉得这个Replace使用起来很不舒服的话,下面将给出一个
类似与VB中的Replace并且扩展了它的函数
Public Function Replacex(sourceStr as String, oldStr as _
String, newStr as String, optional ignoreCase as _
Boolean = False,optional isGlobal as Boolean = True)
Dim re As New RegExp
re.Pattern = oldStr
re.Global = isGlobal
re.IgnoreCase = ignoreCase
Replacex = re.Replace(sourceStr, newStr)
End Function
下面给出使用它的一些例子:
Debug.Print Replacex("This is a test","is","at")
--> "That at a test"
最精彩的还是使用正则表达式了
Debug.Print Replacex("This is a test","ws","at")
--> "That at a tatt"
甚至还可以这样
Debug.Print Replacex("This is a test","(ws)","at$1")
--> "Thatis atis a tatist"
正则表达式的replace方法还有两个参数。
在默认的情况时正则表达式在找到了一个满足条件的时候就会停下来
但是如果你要是将isGlobal参数设置为True的话,它就会全文替换
在默认的情况下正则表达式是区分大小写的
但是如果你将ignoreCase设置为False的话,它将不区分大小写
获得XML的节点
现在,我们将一起来看看正则表达式在XML中是如何运用的。
首先,在Microsoft's XML 2.0解释器里面有两大难题:
第一, 这个XML解释器在装载XML文挡的时候必须要保证入口
满足定义在DTD的范围之内。这是一个大麻烦,因为就目前的情况来看,
XML更改频繁,不时会多出一些标准,不时又会产生新的标志。
第二,如果使用XSL的话不能够操纵DTD,甚至当你使用脚本语言也是一件很费力的事情。
经常要做的是你需要在XSL使用XSL的结构表达式中设置一些变量,例如
浏览器的类型或则ASP的参数呀
这时你可以使用正则表达式来解决这些问题。
当你想获取一个XML元素的时候,也许这个对象有可能并不是你想要的东东。
例如:假设一个很简单的XML结构,一个图书目录.
XML的代码如下:
<catalog>
<book>
<title>XML for Beginners</title>
<author>Fred Fnord</author>
<description>A book on XML for programming neophytes.</description>
</book>
<book>
<title>Pair-O-Dice Lost</title>
<author>U. Wajer</author>
<description>Techniques for throwing the game.</description>
</book>
<book>
<title>The Fields of Oberon</title>
<author>Alan Landis</author>
<description>The wee folk are back, and they aren't happy.</description>
</book>
<book>
<title>Distributed Computing on a Budget</title>
<author>Fred Fnord</author>
<description>Using XML and related techniques for managing distributed applications.</description>
</book>
</catalog>
现在继续我在上一系列中介绍的例子。我们将实现一个简单的搜索机制,
也许你的兴趣不是在获取书籍的标题而是在要获得整个书节点(note)上。
那么使用下面这个函数就能够满足你的要求了
Public Function GetFilteredElements(activeElement As Variant, RegExpfilter As String, _
Optional queryString As String = "",optional IsGlobal as Boolean=True, _
optional IgnoreCase as Boolean = True) As IXMLDOMNodeList
Dim re As RegExp
Dim filterDoc As DOMDocument
Dim nodelist As IXMLDOMNodeList
Dim filterList As IXMLDOMNodeList
Dim node As IXMLDOMElement
Dim baseNode As IXMLDOMElement
Set re = New RegExp
On Error GoTo ErrorHandler
Select Case TypeName(activeElement)
Case "IXMLDOMElement"
Set baseNode = activeElement
Case "DOMDocument"
Set baseNode = activeElement.documentElement
Case Else
Error 1001
Set GetFilteredElements = Nothing
End Select
re.Pattern = RegExpfilter
re.Global=IsGlobal
re.IgnoreCase=IgnoreCase
If queryString = "" Then
Set filterList = baseNode.selectNodes(".[textnode()]|.//*[textnode()]")
Else
Set filterList = baseNode.selectNodes(queryString)
End If
For Each node In filterList
If re.Test(node.Text) Then
node.setAttribute "filter:filteredElementFound", "true"
End If
Next
Set filterList = baseNode.selectNodes(".[@filter:filteredElementFound]|.//*[@filter:filteredElementFound]")
For Each node In filterList
node.removeAttribute "filter:filteredElementFound"
Next
Set GetFilteredElements = filterList
Exit Function
ErrorHandler:
If Err.Number = 1001 Then
MsgBox "Document must be an XML document, or a document element."
Else
Error Err.Number
End If
End Function
下面是对这个方法的简单描述:
GetFilteredElements主要是用来获取一个XML的文档或则一个文档里面的节点,并且
按照我们的需要转换它(包括所有的该节点的子节点),将转换后的节点放到一个
列表类型IXMLDOMNodeList中.
这个过滤器程序按次序对每个节点进行运用,如果一个节点的文本满足了表达式的话,
那么这个节点就被做上一个临时属性的标记为filter:filteredElementFound
(当然为了不和你自己的XML文档里已经存在的标志发生冲突,你可以把这个属性
改成你需要的东西).
一旦所有的节点都被检查完毕后,一个新的列表对象(仅仅只包含那些满足表达式的
节点)被创建了。这些临时的属性会被删除掉,然后返回那些节点。
如果你没有给这个函数输入查询参数的话,
那么将获得整个文档的所有叶子节点(就是那些没有子元素的节点)
如果你给这个函数输入查询参数的话,
那么这个函数将返回满足条件的节点或则子节点
例如,下面的代码将返回第一本书的标题和描述的节点和第四本书的
描述节点
Dim bookXML=new DOMDocument
bookXML.load("bookCatalog.xml")
Set nodelist=GetfilteredElements(bookXML,"xml")
而下面的代码将返回第一和第四本书的节点,然后你可以根据这些节点获取它们的
子节点的属性。
Set nodelist=GetfilteredElements(bookXML,"xml","//book")
通常,你应该尽可能的定义一个XSL的查询过滤器(filter)
因为它会只返回你需要的节点,会大大减少你需要处理的数据量。
这个函数是XML "数据库"的一个运用实例,因为很多SQL的开发者用熟悉了的
参数(例如LIKE)在XML中是没有等效的方法的,但是只要你在XML中能够熟练使用
正则表达式,你会发现它能够实现很多LIKE语句能够实现的功能。
使用XSL转换验证数据
正则表达式使用在XSL的转换中是一个强有力的验证数据有效的工具。
例如,你想根据XML的数据生成一个显示书籍标题和描述的table
当你使用基于DOM版本(就是使用微软的那个模型XDOM)的时候,XSL已经能够实现非常复杂的转换XML为
HTML的功能了。
在XSL的这个节点中<xsl:script>,允许你使用脚本语言。
你可以在输出流中插入文本(但是目前你就不能够把一个DOM节点输出到输出流中).
XSL中默认的脚本语言是javascript,
在使用的过程中你需要注意的是,由于"/"在javascript中是特殊字符,你需要使用
"//"将其转意。
代码如下:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<xsl:script language="javascript"><![CDATA][
IsValidBookTopic=/xml/
]]></xsl:script>
<xsl:template match="/">
<xsl:apply-templates select="//book" />
</xsl:template>
<xsl:template match="book">
<xsl:if expr="IsValidBookTopic.test(this.text)">
<h1><xsl:value-of select="title"/></h1>
<h2><xsl:value-of select="author"/></h2>
<p><xsl:value-of select="description"/></p>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
当然,你也可以改变你的XSL中的查询参数(例如上面例子中使用的参数是xml)
一个直接的方法是生成一个参数实体"entities"
例如下面的代码,你需要使用"%"字符来说明一个字符是参数。
Function SetXSLParameter(XslDoc as DOMDocument ,ParamName as _
String,ParamValue as Variant) as DOMDocument
Dim XslDoc as DOMDocument
Dim ScriptNode as IXMLDOMElement
Dim re as RegExp
Set re=new RegExp
Re.global=True
Re.IgnoreCase=True
Re.pattern="%"+ParamName
For each ScriptNode in xslDoc.selectNodes("xsl:script")
ScriptNode.text=Re.replace(ScriptNode.text,cstr(ParamValue))
Next
Return XslDoc
End Function
SetXSLParameter用来给XSL文档设置参数。
当然你也可以不使用上面这个函数,直接在XSL的脚本里面修改这个参数,
但是如果你使用了上面这个函数的话,可以让你的代码的可扩展性能更好。
注意的是,XSL的代码将被上面这个函数所修改
所以如果你想对相同的XSL对象使用不同的过滤器(就是不同的查询条件的话)
需要使用XSL对象的clone(就是制作一个XSL的副本)方法。
下面就是一个完整的例子
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<xsl:script language="javascript"><![CDATA][
IsValidBookTopic=/%searchStr/
]]></xsl:script>
<xsl:template match="/">
<xsl:apply-templates select="//book" />
</xsl:template>
<xsl:template match="book">
<xsl:if expr="IsValidBookTopic.test(this.text)">
<h1><xsl:value-of select="title"/></h1>
<h2><xsl:value-of select="author"/></h2>
<p><xsl:value-of select="description"/></p>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
然后你就可以使用下面这段代码来修改这个参数了
呵呵,我是搞asp的,下面这段代码就是写在你的asp的程序中的。
Set xmlDoc=new DomDocument
Set xslDoc=new DOMDocument
XmlDoc.load "catalog.xml"
XslDoc.load "catalog.xsl"
SetXSLParameter xslDoc, "searchStr", "pair-o-dice"
Response.write xmlDoc.transformNode(xslDoc)
同样,使用上面的方法,你就可以设置其他的参数了,例如浏览器的类型呀
ASP的查询参数呀,或则一个基于Web的组件呀。
使用正则表达式给XSL带来了强大的功能。
虽然,使用这种通用性很强的方法在目前也许你不会看到能够给你带来很
大的优势(主要是很少有人写XML的程序的说),但是在未来的编程过程中,
你将会体会到它的强大威力了。
