[Flash]Flash与3D编程探秘(四)- 摄像机旋转基础知识

mikel阅读(836)

关于三角函数

现在我们已经基本上把最基本的移动摄像机技巧介绍完了,并且我相信上篇的几个例子也应该让你加深了印象。可是你会发现前面我们只是把摄像机沿着x 轴,y轴或者z轴移动我们的摄像机,可是实际中我们可以把摄像机向左,向右,向上或者向下旋转一定的角度,这样我们在观看空间时就有更大的自由度。 不过介绍旋转之前,我们需要知道一些关于三角法的计算(Oh,No!),如果你还对三角函数不熟悉,那么文章看起来可能会有些费解。不过不要担心,中国人 有着聪明的头脑,这些对你来说很容易。提醒一下:如果下面的内容对你来说太容易了,那么你可以跳过这些。不过我想我还是给已经忘记的(Just like me)各位补上一课,毕竟就是这些简单的知识驱使着我们的3D空间旋转着。

 

三角法是数学的一个分支,我们主要用它来分析三角形的边 和角度的关系。为什么要三角形要和角度有关系呢?任意一条在2D空间里有旋转角度的直线,它都会在x轴和y轴有相应的映射(当然在3D空间里, 我们并不光有x轴和y轴)。我们假设有一条线段从原点到点B(4,0),当你把这个线段哦OB沿着原点旋转一定角度,过B(旋转后)向x轴做一条垂线 BC,那么x轴和BC,OB所组成的就是一个直角三角形。当然你可以把OC看作是线段OB在x的投影。那么对这个旋转角度我们用sin和cos就可以计算 得到我们这条线段在x和y的分量。

 


旋转一条直线得到的三角形

 

以原点为中心旋转小P

 

以原点为中心旋转只是个例子,这里提到的旋转点不一定是原点,我们可以把任意一点作为旋转的原点。你会发现在旋转中,当x线段OB旋转一定角度后, 它会与旋转前的线段重合,B在旋转中所过的点的轨迹就是一个圆,那旋转的这条线段就OB就是这个圆的半径。OK,这就是我们在Flash里需要知道基本三 角函数。下面两个是我们根据圆的半径得到x和y分量的公式:

object.x = Math.cos(angle) * radius;
object.y = Math.sin(angle) * radius;

 

 

旋转后的直线在x和y轴的分量

当然,上面的公式在旋转点为原点的情况下成立,如果旋转点不是原点的话,我们使用:

object.x = origin.x + Math.cos(angle) * radius;
object.y = origin.y + Math.sin(angle) * radius;

 

 

弧度

当我们测量物体小P的旋转角度的时候,我们可以使用度数,这也是我们最常用的,它从0到360沿逆时针方面递增。但是我们所用的Flash,并不知 道 360是什么,它所知道的只是弧度(当然你可以自己写sin和cos函数,用0到360作为你的参数)。这里的弧度我们可以这样理解,360度数是 2*PI弧度,那么就是说360度旋转是一个整圆的话,2*PI旋转也是同样效果。把度数转化成弧度的公式是:

randians = degrees * (Math.PI / 180); PI = 3.1415926535897932384626433832795…

 

 

反三角函数

在Flash里,我们可以通过直角三角形的两个边的比率得到角度,用下面的代码即可:

angle = Math.atan2(object.y, object.x);
angle 
= Math.atan(y/x);

 

还有一个要说明的那就是勾股定理:

hypotenuse = Math.sqrt(x*+ y*y);

 

那么你现在已经具备2D旋转的基本知识了,我们再看一下3D,3D旋转中加进了z轴,hum,那么我们就把xy平面,yz平面,zx平面的旋转组合 起来,就得到摄像机的全方位旋转。不过指得注意的是,三角法不能直接运用到我们的摄像机旋转中,我们可以在横向旋转摄像机的时候保持摄像机的旋转角度不 变,取而代之我们旋转x和z轴,要旋转180度的话,我们可是使z旋转到x的位置然后再转到z的位置,同时保持y轴不动。

旋转x和z轴

 

横向旋转摄像机

上面讲述了一些旋转摄像机的原理,我们更关心的是如何使用这些原理来解决问题。联想一下实际,对于摄像机的横向旋转来说,摄像机的所在位置的高度 y,深度z和横向x都是保持不变的,唯一改变的就是摄像机的旋转角度。也就是说当一个空间中的物体在位置不变的情况下,摄像机与它的距离是不变的,于是我 们的问题转化成在角度变化的情况下保证它们的距离不变。我们设旋转后的角度为a,那么物体所在的x和y(以摄像机为原点建立坐标系)就是:

= distance * Math.cos(a);
= distance * Math.sin(a);

 

如果我们摄像机所在的坐标为(x0,y0)的话,我们就可以得到物体所在的坐标(还是上面讲过的公式):

= x0 + distance * Math.cos(a);
= y0 + distance * Math.sin(a);

 

以上所述的仅是对一个物体的操作,同样道理我们对所有舞台上的物体进行操作,那么我们所看到的就是我们摄像机旋转后的景象。

 

      

对比旋转物体和旋转摄像机

 

在上面的动画中,我们俯瞰整个场景,对比一下旋转摄像机和旋转舞台上所有物体的差异。当然对于其他的平面(纵向旋转摄像机)来说,我们可以运用相同 的理论对 物体进行操作。那么,你应该有种感觉你已经具备所有摄像机旋转的理论知识了,不过还有一小点,我们将在下一篇文章进行分析。

作者:Yang Zhou
出处:http://yangzhou1030.cnblogs.com
感谢:Yunqing
本文版权归作者和博客园共有,转载未经作者同意必须保留此段声明。请在文章页面明显位置给出原文连接,作者保留追究法律责任的权利。

[C#]BlogEngine.Net架构与源代码分析系列part2:业务对象——共同的父类Busin

mikel阅读(795)

上篇文章朋友的评论给了我很大的动力写这个系列的文章,看来大家都比较关注这个系列。为了后续文章做一个铺垫,我在这篇文章的前半部分讲解一下BlogEngine.Net的整体架构,后半部分主要是对于BusinessBase类的分析。

     下载源代码以后打开解决方案,我们发现从项目的组织结构上BlogEngine.Net分成两个项目:一个是 BlogEngine.Core,顾名思意,它就是BlogEngine.Net的核心逻辑层。所有的业务逻辑和一些功能都在这个项目中体现,实际上这个 核心业务层中也有数据访问的一部分,那就是Provider模式。在BlogEngine.Net中,关系数据库或XML等的作用只有一个,那就是存储数 据,BlogEngine.Net的业务对象的ID生成是由核心层控制的,而不是用数据存储部分生成的,因为这样可以支持更多的数据源。它不同于很多其他 业务系统,数据库里面可能有很多存储过程,触发器,函数等来完成一定的业务运算和数据处理。在BlogEngine.Net中,我们甚至可以使用一 个.txt文件来自己开发一个Provider给BlogEngine.Net使用,方法很简单,只要实现 BlogProvider(BlogEngine.Net提供),MembershipProvider和RoleProvider就可以了,实际上 BlogEngine.Net也在很大程度上利用了.Net本身的经典模型。另外的一个项目是一个站点,主要就是具体的Web实现,但是具体的功能都是调 用核心层来完成的。

     实际上刚开始看BlogEngine.Net的源代码时我也很难入手,不知道从哪里看起,找不到入口的地方。其实也难怪,官方提供的资料 大都是关于使用和开发扩展的,社区里找到的东西也不是自己最想要的。研究了一段时间以后我发现整个BlogEngine.Net都在围绕这 BusinessBase这个基类展开,其它的类都是为它提供服务或接收它的消息,例如Provider,Extension等。 BusinessBase是所有业务类的基类,里面封装了很多业务类共有的特征。它的子类有:

AuthorProfile:用户的Profile的封装。

Page:这个类实际上是对应着BlogEngine.Net中的一篇静态文章,page和post具体区别不是很重要,感兴趣的朋友可以参照一下官方提供的说明。

Post:在BlogEngine.Net应用最多的一个类,代表作者提交的一篇文章。

Category:文章分类,一篇文章可以属于多个分类,分类之上还可以有父分类。

     下图是他们的继承关系:

     

 图中的IPublishable接口我会在以后的文章中做详细的讲解。

 从BusinessBase的原型

BusinessBase原型

我们可以看出:

1.BusinessBase是一个泛型类。这个泛型设计的很好,Type用来标识具体的子类类型,Key主要是子类对象的唯一ID,这个ID在AuthorProfile是String类型,而在Page等其它类中是Guid类型,所以定义基类时才会采取这种泛型设置。

2.BusinessBase实现的接口也是微软推荐的业务对象应该实现的接口。

IDataErrorInfo用来标识对象内部的错误信息,这个接口的实现主要用来定义某个属性的验证规则。
INotifyPropertyChanged用户通知客户端数据发生了改变。主要是对象内部数据发生改变时,对于一些绑定控件数据的同步更新。
IChangeTracking如果对象内部数据发生改变,它用来完成接收了数据的改变,包括更新数据存储等。
IDisposable这个就不用解释了吧,做.Net都知道。

     从以上我们可以看出凡是BusinessBase的子类对象都具有当修改数据时,通过属性的改变对外发出属性改变的通知,并实现 INotifyPropertyChanged来通知绑定控件,实现IChangeTracking来更新数据的存储。我们再看一下源代码发现 BlogEngine.Net将所有业务对象的数据验证交给了Validation模型来处理,这一点运用的很巧妙,统一了验证模型,我很推荐。

Validation

     那么,BusinessBase的子类都需要做什么呢?它们需要重写数 据存储的操作方法(通过Provider的调用完成,主要是DataSelect等),这也是面向接口编程所提倡的。对于内部数据的处理 BusinessBase使用了IsNew,IsChanged和IsDeleted统一了编程模型,并定义了一个SaveAction枚举来实现统一的 处理与通知消息的封装。

     BusinessBase提供了两个事件在内部数据进行存储前后触发——保存前事件和保存后事件,用来给外部提供访问点,有点类似于ASP.NET的管道 事件的东西,不过这些事件都是类的事件,并非属于某个对象。这样外部可以对于保存前的事件进行处理,也可以对于保存后的事件处理,这种模型很有利于扩展。 例如我们可以很轻松的纪录业务日志,而且纪录的很统一。此外,BusinessBase重写了相等的方法或操作符,用于两个对象的排序和比较,这都是业务 对象所共有的特性。

     那么,对于派生类从这个基类继承之后我们要实现什么呢,无非就是自己的数据存储方法,数据检验规则,自己的ID类型,还需要大家注意的就是每个派生类都提 供了一个静态属性用来提取整个对象列表,还可以根据具体的查询信息获得对象列表,不过他们都属于类的方法,也就是静态方法。对于结构和关系比较复杂的派生 类,例如Post包含了Comment列表及其相应的方法用来表示和操作对象之间的关系。此外基类的MarkOld方法用于标识这个对象已经经过了处理, 不需要在处理了,子类可以重写这个方法用于提供自己的实现。其余的一些方法或属性都是具体类中所需要的,这个很好分析。

     例如,对于客户端程序我们只要这样就可以完成数据操作:

添加文章:

Code

修改文章:

Code

删除文章:

Code

      很明显,BlogEngine.Net采用了面向对象的设计方法,事件和继承得到了很广泛的应用,此外它更好的运用了.Net平台自身 提供的模型解决问题,例如Provider模型,一些接口规范等。此外我们可以看到,BlogEngine.Net在内存中有很多对象,以空间换取时间的 处理方法在这样的系统中还是比较可靠的。

     做一些总结是很有必要的。

     上一篇:BlogEngine.Net架构与源代码分析系列part1:开篇介绍

版权声明
作者:Thriving.country
出处:http://thriving-country.cnblogs.com/
本文版权归作者和博客园共同所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接.

[C#]BlogEngine.Net架构与源代码分析系列part1:开篇介绍

mikel阅读(1029)

  最近我要开始这个系列了,这是我的第一个系列。关于BlogEngine.Net我想说的是,它设计的真的很棒,代码简洁但是功能很多,真是麻雀虽小,五 脏俱全啊,而且具有了很多Web2.0的特征,甚至它的每一行代码都值得我们去研究一下,它的开发团队很棒。实际上很多国外的个人Blog都是采用 BlogEngine.Net加上自定义皮肤实现的,如果您是一个Blog的开发者,这更是您的必备参考!

  很多兄弟都推荐BlogEngine.Net看一看,甚至www.ASP.NET也 把它放在了很重要的位置。前一阶段我仔细的把它的源代码阅读了一遍,看完以后兴奋的很冲动,心想居然还有这么好的玩意,这个开源项目设计的真的不错。实际 上前不久的一个Podcast项目我就是采用了类似BlogEngine.Net这种架构开发的,感觉还是很不错的。最近利用空闲的时间做了一下总结,准 备写一个关于BlogEngine.Net系列文章,其实我早就想写一个系列文章,但是一直没有好的想法,对于我比较熟悉的方面发现园子里的兄弟都给写完 了,但是关于BlogEngine.Net的文章似乎很少,所以我决定写这个系列,还希望园子中的兄弟们多支持一下啊。

  开篇声明

  本系列文章介绍的是BlogEngine.Net 1.4.5版本,这是官方前不久发布的一个版本。大家可以在http://www.codeplex.com/blogengine 下载最新的源代码和相应的说明文档。此外,您还可以在它的官方站点http://www.dotnetblogengine.net/上了解更多的安装和一些扩展开发等问题,还可以下载一些开发者已经做好的皮肤。讲解代码使用C#,基于.Net Framework2.0。

  BlogEngine.Net简介

  BlogEngine.NET是一个开源的.NET博客项目。整个项目采用C#开发,它的结构比较简单,但是扩展起来很容易,它的复杂程度较低,易于定制。扩展特性主要体现在以下三个方面:
    1.Widget小工具
    2.Extension扩展功能
    3.自定义个性化Theme

  最初它是一个单人博客,很容易将它实现成多人博客。codeplex上有一个案例就是基于BlogEngine.Net的多人博客。BlogEngine.Net的主要特性:
    1.很容易被安装,只要把文件上传到Web服务器就可以运行。因为它默认采用XML存储数据。
    2.具有很多Blog的新特性并提供了开放接口。例如Ajax评论,支持TrackBack等。
    3.具有很多Web2.0特性,例如OpenSearch, XFN tags, tag cloud等。
    4.自定义主题,您可以自己开发很多主题,类似博客园的主题。
    5.可以配置自己的数据源,例如XML,SQL Server,SQLite等。

  这个系列文章我将从BlogEngine.Net的架构入手,对于每个相对独立的部分进行一下代码分析并作出相应的总结,对于一些部分我会给出我个人的评价,对于一些比较好的细节部分我也会深入探讨。如果大家有一些反馈我还会及时调整。

  暂定目录

  下面是我初步定出的一个目录结构,也反映了系列文章的主要路线,请大家参考,这个目录可能在写的过程中会随时进行调整并加入已经完成文章的链接:

  1.BlogEngine.Net架构与源代码分析系列part1:开篇介绍

  2.BlogEngine.Net架构与源代码分析系列part2:业务对象–共同的父类BusinessBase

  3.BlogEngine.Net架构与源代码分析系列part3:数据存储–基于Provider模式的实现

  4.BlogEngine.Net架构与源代码分析系列part4:Blog全局设置–BlogSettings

  5.BlogEngine.Net架构与源代码分析系列part5:对象搜索–IPublishable与Search

  6.BlogEngine.Net架构与源代码分析系列part6:开放API–MetaWeblog与BlogImporter

  7.BlogEngine.Net架构与源代码分析系列part7:Web2.0特性–Pingback&Trackback

  8.BlogEngine.Net架构与源代码分析系列part8:开发扩展(上)–Extension与管理上的实现

  9.BlogEngine.Net架构与源代码分析系列part9:开发扩展(中)–Widget小工具

  10.BlogEngine.Net架构与源代码分析系列part10:开发扩展(下)–自定义Theme

  11.BlogEngine.Net架构与源代码分析系列part11:页面共同的基类–BlogBasePage

  12.BlogEngine.Net架构与源代码分析系列part12:实现分析(上)–HttpHandlers与HttpModules

  13.BlogEngine.Net架构与源代码分析系列part13:实现分析(下)–网站页面上值得参考的部分

  14.BlogEngine.Net架构与源代码分析系列part14:总结篇

  我写这个系列文章的目的有三个。首先将好的东西分享给大家,其次让我更加深入的研究BlogEngine.Net提高自己,最后作为一个备忘录存储在博客园上。

   分享是一种美。

版权声明
作者:Thriving.country
出处:http://thriving-country.cnblogs.com/
本文版权归作者和博客园共同所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接.

[DBMS]Managing Hierarchical Data in MySQL

mikel阅读(799)

Managing Hierarchical Data in MySQL

Introduction

Most users at one time or another have dealt with hierarchical data in a SQL database and no doubt learned that the management of hierarchical data is not what a relational database is intended for. The tables of a relational database are not hierarchical (like XML), but are simply a flat list. Hierarchical data has a parent-child relationship that is not naturally represented in a relational database table.

For our purposes, hierarchical data is a collection of data where each item has a single parent and zero or more children (with the exception of the root item, which has no parent). Hierarchical data can be found in a variety of database applications, including forum and mailing list threads, business organization charts, content management categories, and product categories. For our purposes we will use the following product category hierarchy from an fictional electronics store:

These categories form a hierarchy in much the same way as the other examples cited above. In this article we will examine two models for dealing with hierarchical data in MySQL, starting with the traditional adjacency list model.

The Adjacency List Model

Typically the example categories shown above will be stored in a table like the following (I'm including full Create and Insert statements so you can follow along):

Create TABLE category(
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL);
Insert INTO category
VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
(4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),
(7,'MP3 PLAYERS',6),(8,'FLASH',7),
(9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);
Select * FROM category orDER BY category_id;
+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS          |      1 |
|           3 | TUBE                 |      2 |
|           4 | LCD                  |      2 |
|           5 | PLASMA               |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+
10 rows in set (0.00 sec)

In the adjacency list model, each item in the table contains a pointer to its parent. The topmost element, in this case electronics, has a NULL value for its parent. The adjacency list model has the advantage of being quite simple, it is easy to see that FLASH is a child of mp3 players, which is a child of portable electronics, which is a child of electronics. While the adjacency list model can be dealt with fairly easily in client-side code, working with the model can be more problematic in pure SQL.

Retrieving a Full Tree

The first common task when dealing with hierarchical data is the display of the entire tree, usually with some form of indentation. The most common way of doing this is in pure SQL is through the use of a self-join:

Select t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
Where t1.name = 'ELECTRONICS';
+-------------+----------------------+--------------+-------+
| lev1        | lev2                 | lev3         | lev4  |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS          | TUBE         | NULL  |
| ELECTRONICS | TELEVISIONS          | LCD          | NULL  |
| ELECTRONICS | TELEVISIONS          | PLASMA       | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS  | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS   | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL  |
+-------------+----------------------+--------------+-------+
6 rows in set (0.00 sec)

Finding all the Leaf Nodes

We can find all the leaf nodes in our tree (those with no children) by using a LEFT JOIN query:

Select t1.name FROM
category AS t1 LEFT JOIN category as t2
ON t1.category_id = t2.parent
Where t2.category_id IS NULL;
+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+

Retrieving a Single Path

The self-join also allows us to see the full path through our hierarchies:

Select t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
Where t1.name = 'ELECTRONICS' AND t4.name = 'FLASH';
+-------------+----------------------+-------------+-------+
| lev1        | lev2                 | lev3        | lev4  |
+-------------+----------------------+-------------+-------+
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
+-------------+----------------------+-------------+-------+
1 row in set (0.01 sec)

The main limitation of such an approach is that you need one self-join for every level in the hierarchy, and performance will naturally degrade with each level added as the joining grows in complexity.

Limitations of the Adjacency List Model

Working with the adjacency list model in pure SQL can be difficult at best. Before being able to see the full path of a category we have to know the level at which it resides. In addition, special care must be taken when deleting nodes because of the potential for orphaning an entire sub-tree in the process (delete the portable electronics category and all of its children are orphaned). Some of these limitations can be addressed through the use of client-side code or stored procedures. With a procedural language we can start at the bottom of the tree and iterate upwards to return the full tree or a single path. We can also use procedural programming to delete nodes without orphaning entire sub-trees by promoting one child element and re-ordering the remaining children to point to the new parent.

The Nested Set Model

What I would like to focus on in this article is a different approach, commonly referred to as the Nested Set Model. In the Nested Set Model, we can look at our hierarchy in a new way, not as nodes and lines, but as nested containers. Try picturing our electronics categories this way:

Notice how our hierarchy is still maintained, as parent categories envelop their children.We represent this form of hierarchy in a table through the use of left and right values to represent the nesting of our nodes:

Create TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
Insert INTO nested_category
VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
(4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),
(7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
(9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);
Select * FROM nested_category orDER BY category_id;
+-------------+----------------------+-----+-----+
| category_id | name                 | lft | rgt |
+-------------+----------------------+-----+-----+
|           1 | ELECTRONICS          |   1 |  20 |
|           2 | TELEVISIONS          |   2 |   9 |
|           3 | TUBE                 |   3 |   4 |
|           4 | LCD                  |   5 |   6 |
|           5 | PLASMA               |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |  10 |  19 |
|           7 | MP3 PLAYERS          |  11 |  14 |
|           8 | FLASH                |  12 |  13 |
|           9 | CD PLAYERS           |  15 |  16 |
|          10 | 2 WAY RADIOS         |  17 |  18 |
+-------------+----------------------+-----+-----+

We use lft and rgt because left and right are reserved words in MySQL, see http://dev.mysql.com/doc/mysql/en/reserved-words.html for the full list of reserved words.

So how do we determine left and right values? We start numbering at the leftmost side of the outer node and continue to the right:

This design can be applied to a typical tree as well:

When working with a tree, we work from left to right, one layer at a time, descending to each node's children before assigning a right-hand number and moving on to the right. This approach is called the modified preorder tree traversal algorithm.

Retrieving a Full Tree

We can retrieve the full tree through the use of a self-join that links parents with nodes on the basis that a node's lft value will always appear between its parent's lft and rgt values:

Select node.name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND parent.name = 'ELECTRONICS'
ORDER BY node.lft;
+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| TELEVISIONS          |
| TUBE                 |
| LCD                  |
| PLASMA               |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
| CD PLAYERS           |
| 2 WAY RADIOS         |
+----------------------+

Unlike our previous examples with the adjacency list model, this query will work regardless of the depth of the tree. We do not concern ourselves with the rgt value of the node in our BETWEEN clause because the rgt value will always fall within the same parent as the lft values.

Finding all the Leaf Nodes

Finding all leaf nodes in the nested set model even simpler than the LEFT JOIN method used in the adjacency list model. If you look at the nested_category table, you may notice that the lft and rgt values for leaf nodes are consecutive numbers. To find the leaf nodes, we look for nodes where rgt = lft + 1:

Select name
FROM nested_category
Where rgt = lft + 1;
+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+

Retrieving a Single Path

With the nested set model, we can retrieve a single path without having multiple self-joins:

Select parent.name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'FLASH'
ORDER BY parent.lft;
+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
+----------------------+

Finding the Depth of the Nodes

We have already looked at how to show the entire tree, but what if we want to also show the depth of each node in the tree, to better identify how each node fits in the hierarchy? This can be done by adding a COUNT function and a GROUP BY clause to our existing query for showing the entire tree:

Select node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| ELECTRONICS          |     0 |
| TELEVISIONS          |     1 |
| TUBE                 |     2 |
| LCD                  |     2 |
| PLASMA               |     2 |
| PORTABLE ELECTRONICS |     1 |
| MP3 PLAYERS          |     2 |
| FLASH                |     3 |
| CD PLAYERS           |     2 |
| 2 WAY RADIOS         |     2 |
+----------------------+-------+

We can use the depth value to indent our category names with the CONCAT and REPEAT string functions:

Select CONCAT( REPEAT(' ', COUNT(parent.name) - 1), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+

Of course, in a client-side application you will be more likely to use the depth value directly to display your hierarchy. Web developers could loop through the tree, adding <li></li> and <ul></ul> tags as the depth number increases and decreases.

Depth of a Sub-Tree

When we need depth information for a sub-tree, we cannot limit either the node or parent tables in our self-join because it will corrupt our results. Instead, we add a third self-join, along with a sub-query to determine the depth that will be the new starting point for our sub-tree:

Select node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
Select node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
orDER BY node.lft
)AS sub_tree
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| FLASH                |     2 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+

This function can be used with any node name, including the root node. The depth values are always relative to the named node.

Find the Immediate Subordinates of a Node

Imagine you are showing a category of electronics products on a retailer web site. When a user clicks on a category, you would want to show the products of that category, as well as list its immediate sub-categories, but not the entire tree of categories beneath it. For this, we need to show the node and its immediate sub-nodes, but no further down the tree. For example, when showing the PORTABLE ELECTRONICS category, we will want to show MP3 PLAYERS, CD PLAYERS, and 2 WAY RADIOS, but not FLASH.

This can be easily accomplished by adding a HAVING clause to our previous query:

Select node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
Select node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
orDER BY node.lft
)AS sub_tree
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+

If you do not wish to show the parent node, change the HAVING depth <= 1 line to HAVING depth = 1.

Aggregate Functions in a Nested Set

Let's add a table of products that we can use to demonstrate aggregate functions with:

Create TABLE product(
product_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(40),
category_id INT NOT NULL
);
Insert INTO product(name, category_id) VALUES('20" TV',3),('36" TV',3),
('Super-LCD 42"',4),('Ultra-Plasma 62"',5),('Value Plasma 38"',5),
('Power-MP3 5gb',7),('Super-Player 1gb',8),('Porta CD',9),('CD To go!',9),
('Family Talk 360',10);
Select * FROM product;
+------------+-------------------+-------------+
| product_id | name              | category_id |
+------------+-------------------+-------------+
|          1 | 20" TV            |           3 |
|          2 | 36" TV            |           3 |
|          3 | Super-LCD 42"     |           4 |
|          4 | Ultra-Plasma 62"  |           5 |
|          5 | Value Plasma 38"  |           5 |
|          6 | Power-MP3 128mb   |           7 |
|          7 | Super-Shuffle 1gb |           8 |
|          8 | Porta CD          |           9 |
|          9 | CD To go!         |           9 |
|         10 | Family Talk 360   |          10 |
+------------+-------------------+-------------+

Now let's produce a query that can retrieve our category tree, along with a product count for each category:

Select parent.name, COUNT(product.name)
FROM nested_category AS node ,
nested_category AS parent,
product
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.category_id = product.category_id
GROUP BY parent.name
ORDER BY node.lft;
+----------------------+---------------------+
| name                 | COUNT(product.name) |
+----------------------+---------------------+
| ELECTRONICS          |                  10 |
| TELEVISIONS          |                   5 |
| TUBE                 |                   2 |
| LCD                  |                   1 |
| PLASMA               |                   2 |
| PORTABLE ELECTRONICS |                   5 |
| MP3 PLAYERS          |                   2 |
| FLASH                |                   1 |
| CD PLAYERS           |                   2 |
| 2 WAY RADIOS         |                   1 |
+----------------------+---------------------+

This is our typical whole tree query with a COUNT and GROUP BY added, along with a reference to the product table and a join between the node and product table in the Where clause. As you can see, there is a count for each category and the count of subcategories is reflected in the parent categories.

Adding New Nodes

Now that we have learned how to query our tree, we should take a look at how to update our tree by adding a new node. Let's look at our nested set diagram again:

If we wanted to add a new node between the TELEVISIONS and PORTABLE ELECTRONICS nodes, the new node would have lft and rgt values of 10 and 11, and all nodes to its right would have their lft and rgt values increased by two. We would then add the new node with the appropriate lft and rgt values. While this can be done with a stored procedure in MySQL 5, I will assume for the moment that most readers are using 4.1, as it is the latest stable version, and I will isolate my queries with a LOCK TABLES statement instead:

LOCK TABLE nested_category WRITE;
Select @myRight := rgt FROM nested_category
Where name = 'TELEVISIONS';
Update nested_category SET rgt = rgt + 2 Where rgt > @myRight;
Update nested_category SET lft = lft + 2 Where lft > @myRight;
Insert INTO nested_category(name, lft, rgt) VALUES('GAME CONSOLES', @myRight + 1, @myRight + 2);
UNLOCK TABLES;
We can then check our nesting with our indented tree query:
Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+

If we instead want to add a node as a child of a node that has no existing children, we need to modify our procedure slightly. Let's add a new FRS node below the 2 WAY RADIOS node:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft FROM nested_category
Where name = '2 WAY RADIOS';
Update nested_category SET rgt = rgt + 2 Where rgt > @myLeft;
Update nested_category SET lft = lft + 2 Where lft > @myLeft;
Insert INTO nested_category(name, lft, rgt) VALUES('FRS', @myLeft + 1, @myLeft + 2);
UNLOCK TABLES;

In this example we expand everything to the right of the left-hand number of our proud new parent node, then place the node to the right of the left-hand value. As you can see, our new node is now properly nested:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

Deleting Nodes

The last basic task involved in working with nested sets is the removal of nodes. The course of action you take when deleting a node depends on the node's position in the hierarchy; deleting leaf nodes is easier than deleting nodes with children because we have to handle the orphaned nodes.

When deleting a leaf node, the process if just the opposite of adding a new node, we delete the node and its width from every node to its right:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
Where name = 'GAME CONSOLES';
Delete FROM nested_category Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt - @myWidth Where rgt > @myRight;
Update nested_category SET lft = lft - @myWidth Where lft > @myRight;
UNLOCK TABLES;

And once again, we execute our indented tree query to confirm that our node has been deleted without corrupting the hierarchy:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

This approach works equally well to delete a node and all its children:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
Where name = 'MP3 PLAYERS';
Delete FROM nested_category Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt - @myWidth Where rgt > @myRight;
Update nested_category SET lft = lft - @myWidth Where lft > @myRight;
UNLOCK TABLES;

And once again, we query to see that we have successfully deleted an entire sub-tree:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

The other scenario we have to deal with is the deletion of a parent node but not the children. In some cases you may wish to just change the name to a placeholder until a replacement is presented, such as when a supervisor is fired. In other cases, the child nodes should all be moved up to the level of the deleted parent:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
Where name = 'PORTABLE ELECTRONICS';
Delete FROM nested_category Where lft = @myLeft;
Update nested_category SET rgt = rgt - 1, lft = lft - 1 Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt - 2 Where rgt > @myRight;
Update nested_category SET lft = lft - 2 Where lft > @myRight;
UNLOCK TABLES;

In this case we subtract two from all elements to the right of the node (since without children it would have a width of two), and one from the nodes that are its children (to close the gap created by the loss of the parent's left value). Once again, we can confirm our elements have been promoted:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+---------------+
| name          |
+---------------+
| ELECTRONICS   |
|  TELEVISIONS  |
|   TUBE        |
|   LCD         |
|   PLASMA      |
|  CD PLAYERS   |
|  2 WAY RADIOS |
|   FRS         |
+---------------+

Other scenarios when deleting nodes would include promoting one of the children to the parent position and moving the child nodes under a sibling of the parent node, but for the sake of space these scenarios will not be covered in this article.

Final Thoughts

While I hope the information within this article will be of use to you, the concept of nested sets in SQL has been around for over a decade, and there is a lot of additional information available in books and on the Internet. In my opinion the most comprehensive source of information on managing hierarchical information is a book called Joe Celko's Trees and Hierarchies in SQL for Smarties, written by a very respected author in the field of advanced SQL, Joe Celko. Joe Celko is often credited with the nested sets model and is by far the most prolific author on the subject. I have found Celko's book to be an invaluable resource in my own studies and highly recommend it. The book covers advanced topics which I have not covered in this article, and provides additional methods for managing hierarchical data in addition to the Adjacency List and Nested Set models.

In the References / Resources section that follows I have listed some web resources that may be of use in your research of managing hierarchal data, including a pair of PHP related resources that include pre-built PHP libraries for handling nested sets in MySQL. Those of you who currently use the adjacency list model and would like to experiment with the nested set model will find sample code for converting between the two in the Storing Hierarchical Data in a Database resource listed below.

[DBMS]一种理想的在关系数据库中存储树型结构数据的方法

mikel阅读(1032)

在各种基于关系数据库的应用系统开发中,我们往往需要存储树型结构的数据,目前有很多流行的方法,如邻接列表模型(The Adjacency List Model),在此基础上也有很多人针对不同的需求做了相应的改进,但总是在某些方面存在的各种各样的缺陷。
    那么理想中的树型结构应具备哪些特点呢?数据存储冗余小、直观性强;方便返回整个树型结构数据;可以很轻松的返回某一子树(方便分层加载);快整获以某节 点的祖谱路径;插入、删除、移动节点效率高等等。带着这些需求我查找了很多资料,发现了一种理想的树型结构数据存储及操作算法,改进的前序遍历树模型 (The Nested Set Model)。

一、数据

    在本文中,举一个在线食品店树形图的例子。这个食品店通过类别、颜色和品种来组织食品。树形图如下:

二、邻接列表模型(The Adjacency List Model)

在这种模型下,上述数据在关系数据库的表结构数据通常如下图所示:

由于该模型比较简单,在此不再详细介绍其算法,下面列出它的一些不足:

    在大多数编程语言中,他运行很慢,效率很差。这主要是“递归”造成的。我们每次查询节点都要访问数据库。每次数据库查询都要花费一些时间,这让函数处理庞 大的树时会十分慢。造成这个函数不是太快的第二个原因可能是你使用的语言。不像Lisp这类语言,大多数语言不是针对递归函数设计的。对于每个节点造成这 个函数不是太快的第二个原因可能是你使用的语言。不像Lisp这类语言,大多数语言不是针对递归函数设计的。对于每个节点,函数都要调用他自己,产生新的 实例。这样,对于一个4层的树,你可能同时要运行4个函数副本。对于每个函数都要占用一块内存并且需要一定的时间初始化,这样处理大树时递归就很慢了。

三、改进的前序遍历树模型(The Nested Set Model)

原理:

    我们先把树按照水平方式摆开。从根节点开始(“Food”),然后他的左边写上1。然后按照树的顺序(从上到下)给“Fruit”的左边写上2。这样,你 沿着树的边界走啊走(这就是“遍历”),然后同时在每个节点的左边和右边写上数字。最后,我们回到了根节点“Food”在右边写上18。下面是标上了数字 的树,同时把遍历的顺序用箭头标出来了。

 

    我们称这些数字为左值和右值(如,“Food”的左值是1,右值是18)。正如你所见,这些数字按时了每个节点之间的关系。因为“Red”有3和6两个 值,所以,它是有拥有1-18值的“Food”节点的后续。同样的,我们可以推断所有左值大于2并且右值小于11的节点,都是有2-11的“Fruit” 节点的后续。这样,树的结构就通过左值和右值储存下来了。这种数遍整棵树算节点的方法叫做“改进前序遍历树”算法。

表结构设计:

常用的操作:

下面列出一些常用操作的SQL语句

返回完整的树(Retrieving a Full Tree)

Select node.name
  
FROM nested_category node, nested_category parent
 
Where node.lft BETWEEN parent.lft AND parent.rgt
   
AND parent.name = 'electronics'
 
ORDER BY node.lft

返回某结点的子树(Find the Immediate Subordinates of a Node)

Select V.*
  
FROM (Select node.name,
               (
COUNT(parent.name)  (AVG(sub_tree.depth) + 1)) depth
          
FROM nested_category node,
               nested_category parent,
               nested_category sub_parent,
               (
Select V.*
                  
FROM (Select node.name, (COUNT(parent.name)  1) depth
                          
FROM nested_category node, nested_category parent
                         
Where node.lft BETWEEN parent.lft AND parent.rgt
                           
AND node.name = 'portable electronics'
                         
GROUP BY node.name) V,
                       nested_category T
                 
Where V.name = T.name
                 
ORDER BY T.lft) sub_tree
         
Where node.lft BETWEEN parent.lft AND parent.rgt
           
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
           
AND sub_parent.name = sub_tree.name
         
GROUP BY node.name) V,
       nested_category T
 
Where V.name = T.name
   
and V.depth <= 1
   
and V.depth > 0
 
ORDER BY T.Lft

返回某结点的祖谱路径(Retrieving a Single Path)

Select parent.name
  
FROM nested_category node, nested_category parent
 
Where node.lft BETWEEN parent.lft AND parent.rgt
   
AND node.name = 'flash'
 
ORDER BY node.lft

返回所有节点的深度(Finding the Depth of the Nodes)

Select V.*
  
FROM (Select node.name, (COUNT(parent.name)  1) depth
          
FROM nested_category node, nested_category parent
         
Where node.lft BETWEEN parent.lft AND parent.rgt
         
GROUP BY node.name) V,
       nested_category T
 
Where V.name = T.name
 
ORDER BY T.Lft

返回子树的深度(Depth of a Sub-Tree)

Select V.*
  
FROM (Select node.name,
               (
COUNT(parent.name)  (AVG(sub_tree.depth) + 1)) depth
          
FROM nested_category node,
               nested_category parent,
               nested_category sub_parent,
               (
Select V.*
                  
FROM (Select node.name, (COUNT(parent.name)  1) depth
                          
FROM nested_category node, nested_category parent
                         
Where node.lft BETWEEN parent.lft AND parent.rgt
                           
AND node.name = 'portable electronics'
                         
GROUP BY node.name) V,
                       nested_category T
                 
Where V.name = T.name
                 
ORDER BY T.lft) sub_tree
         
Where node.lft BETWEEN parent.lft AND parent.rgt
           
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
           
AND sub_parent.name = sub_tree.name
         
GROUP BY node.name) V,
       nested_category T
 
Where V.name = T.name
 
ORDER BY T.Lft

返回所有的叶子节点(Finding all the Leaf Nodes)

Select name FROM nested_category Where rgt = lft + 1

插入节点(Adding New Nodes)

LOCK TABLE nested_category WRITE;
Select @myRight := rgt FROM nested_category Where name = 'TELEVISIONS';
Update nested_category SET rgt = rgt + 2 Where rgt > @myRight;
Update nested_category SET lft = lft + 2 Where lft > @myRight;
Insert INTO nested_category
  (name, lft, rgt)
VALUES
  (
'GAME CONSOLES'@myRight + 1@myRight + 2);
UNLOCK TABLES;

删除节点(Deleting Nodes)

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt  lft + 1
  
FROM nested_category
 
Where name = 'GAME CONSOLES';
Delete FROM nested_category Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt  @myWidth Where rgt > @myRight;
Update nested_category SET lft = lft  @myWidth Where lft > @myRight;
UNLOCK TABLES;

 


[参考资料]

http://dev.mysql.com/tech-resources/articles/hierarchical-data.html

http://www.nirvanastudio.org/php/hierarchical-data-database.html

[正则表达式]非常经典的正则表达式

mikel阅读(682)

前言:
       半年前我对正则表达式产生了兴趣,在网上查找过不少资料,看过不少的教程,最后在使用一个正则表达式工具RegexBuddy时发现他的教程写的非常好,可以说是我目前见过最好的正则表达式教程。于是一直想把他翻译过来。这个愿望直到这个五一长假才得以实现,结果就有了这篇文章。关于本文的名字,使用深入浅出似乎已经太俗。但是通读原文以后,觉得只有用深入浅出才能准确的表达出该教程给我的感受,所以也就不能免俗了。
       本文是Jan GoyvaertsRegexBuddy写的教程的译文,版权归原作者所有,欢迎转载。但是为了尊重原作者和译者的劳动,请注明出处!谢谢!

 

1.      什么是正则表达式

基本说来,正则表达式是一种用来描述一定数量文本的模式。Regex代表Regular Express。本文将用<<regex>>来表示一段具体的正则表达式。

一段文本就是最基本的模式,简单的匹配相同的文本。

 

2.      不同的正则表达式引擎

正则表达式引擎是一种可以处理正则表达式的软件。通常,引擎是更大的应用程序的一部分。在软件世界,不同的正则表达式并不互相兼容。本教程会集中讨论Perl 5 类型的引擎,因为这种引擎是应用最广泛的引擎。同时我们也会提到一些和其他引擎的区别。许多近代的引擎都很类似,但不完全一样。例如.NET正则库,JDK正则包。

 

3.      文字符号

最基本的正则表达式由单个文字符号组成。如<<a>>,它将匹配字符串中第一次出现的字符“a”。如对字符串“Jack is a boy”。“J”后的“a”将被匹配。而第二个“a”将不会被匹配。

正则表达式也可以匹配第二个“a”,这必须是你告诉正则表达式引擎从第一次匹配的地方开始搜索。在文本编辑器中,你可以使用“查找下一个”。在编程语言中,会有一个函数可以使你从前一次匹配的位置开始继续向后搜索。

类似的,<<cat>>会匹配“About cats and dogs”中的“cat”。这等于是告诉正则表达式引擎,找到一个<<c>>,紧跟一个<<a>>,再跟一个<<t>>

要注意,正则表达式引擎缺省是大小写敏感的。除非你告诉引擎忽略大小写,否则<<cat>>不会匹配“Cat”。

 

·        特殊字符

对于文字字符,有11个字符被保留作特殊用途。他们是:

[ ] " ^ $ . | ? * + ( )

这些特殊字符也被称作元字符。

如果你想在正则表达式中将这些字符用作文本字符,你需要用反斜杠“"”对其进行换码 (escape)。例如你想匹配“1+1=2”,正确的表达式为<<1"+1=2>>.

需要注意的是,<<1+1=2>>也是有效的正则表达式。但它不会匹配“1+1=2”,而会匹配“123+111=234”中的“111=2”。因为“+”在这里表示特殊含义(重复1次到多次)。

在编程语言中,要注意,一些特殊的字符会先被编译器处理,然后再传递给正则引擎。因此正则表达式<<1"+2=2>>C++中要写成“1""+1=2”。为了匹配“C:"temp”,你要用正则表达式<<C:""temp>>。而在C++中,正则表达式则变成了“C:""""temp”。

 

·        不可显示字符

可以使用特殊字符序列来代表某些不可显示字符:

<<"t>>代表Tab(0x09)

<<"r>>代表回车符(0x0D)

<<"n>>代表换行符(0x0A)

要注意的是Windows中文本文件使用“"r"n”来结束一行而Unix使用“"n”。

 

4.      正则表达式引擎的内部工作机制

知道正则表达式引擎是如何工作的有助于你很快理解为何某个正则表达式不像你期望的那样工作。

有两种类型的引擎:文本导向(text-directed)的引擎和正则导向(regex-directed)的引擎。Jeffrey Friedl把他们称作DFANFA引擎。本文谈到的是正则导向的引擎。这是因为一些非常有用的特性,如“惰性”量词(lazy quantifiers)和反向引用(backreferences),只能在正则导向的引擎中实现。所以毫不意外这种引擎是目前最流行的引擎。

你可以轻易分辨出所使用的引擎是文本导向还是正则导向。如果反向引用或“惰性”量词被实现,则可以肯定你使用的引擎是正则导向的。你可以作如下测试:将正则表达式<<regex|regex not>>应用到字符串“regex not”。如果匹配的结果是regex,则引擎是正则导向的。如果结果是regex not,则是文本导向的。因为正则导向的引擎是“猴急”的,它会很急切的进行表功,报告它找到的第一个匹配

 

·        正则导向的引擎总是返回最左边的匹配

这是需要你理解的很重要的一点:即使以后有可能发现一个“更好”的匹配,正则导向的引擎也总是返回最左边的匹配。

当把<<cat>>应用到“He captured a catfish for his cat”,引擎先比较<<c>>和“H”,结果失败了。于是引擎再比较<<c>>和“e”,也失败了。直到第四个字符,<<c>>匹配了“c”。<<a>>匹配了第五个字符。到第六个字符<<t>>没能匹配“p”,也失败了。引擎再继续从第五个字符重新检查匹配性。直到第十五个字符开始,<<cat>>匹配上了“catfish”中的“cat”,正则表达式引擎急切的返回第一个匹配的结果,而不会再继续查找是否有其他更好的匹配。

 

 

5.      字符集

字符集是由一对方括号“[]”括起来的字符集合。使用字符集,你可以告诉正则表达式引擎仅仅匹配多个字符中的一个。如果你想匹配一个“a”或一个“e”,使用<<[ae]>>。你可以使用<<gr[ae]y>>匹配graygrey。这在你不确定你要搜索的字符是采用美国英语还是英国英语时特别有用。相反,<<gr[ae]y>>将不会匹配graaygraey。字符集中的字符顺序并没有什么关系,结果都是相同的。

你可以使用连字符“”定义一个字符范围作为字符集。<<[0-9]>>匹配09之间的单个数字。你可以使用不止一个范围。<<[0-9a-fA-F] >>匹配单个的十六进制数字,并且大小写不敏感。你也可以结合范围定义与单个字符定义。<<[0-9a-fxA-FX]>>匹配一个十六进制数字或字母X。再次强调一下,字符和范围定义的先后顺序对结果没有影响。

 

·        字符集的一些应用

查找一个可能有拼写错误的单词,比如<<sep[ae]r[ae]te>> <<li[cs]en[cs]e>>

查找程序语言的标识符,<<A-Za-z_][A-Za-z_0-9]*>>(*表示重复0或多次)

查找C风格的十六进制数<<0[xX][A-Fa-f0-9]+>>(+表示重复一次或多次)

 

·        取反字符集

在左方括号“[”后面紧跟一个尖括号“^”,将会对字符集取反。结果是字符集将匹配任何不在方括号中的字符。不像“.”,取反字符集是可以匹配回车换行符的。

需要记住的很重要的一点是,取反字符集必须要匹配一个字符。<<q[^u]>>并不意味着:匹配一个q,后面没有u跟着。它意味着:匹配一个q,后面跟着一个不是u的字符。所以它不会匹配“Iraq”中的q,而会匹配“Iraq is a country”中的q和一个空格符。事实上,空格符是匹配中的一部分,因为它是一个“不是u的字符”。

如果你只想匹配一个q,条件是q后面有一个不是u的字符,我们可以用后面将讲到的向前查看来解决。

 

·        字符集中的元字符

需要注意的是,在字符集中只有4 字符具有特殊含义。它们是:“] " ^ –”。“]”代表字符集定义的结束;“"”代表转义;“^”代表取反;“”代表范围定义。其他常见的元字符在字符集定义内部都是正常字符,不需要转义。例如,要搜索星号*或加号+,你可以用<<[+*]>>。当然,如果你对那些通常的元字符进行转义,你的正则表达式一样会工作得很好,但是这会降低可读性。

在字符集定义中为了将反斜杠“"”作为一个文字字符而非特殊含义的字符,你需要用另一个反斜杠对它进行转义。<<[""x]>>将会匹配一个反斜杠和一个X。“]^-”都可以用反斜杠进行转义,或者将他们放在一个不可能使用到他们特殊含义的位置。我们推荐后者,因为这样可以增加可读性。比如对于字符“^”,将它放在除了左括号“[”后面的位置,使用的都是文字字符含义而非取反含义。如<<[x^]>>会匹配一个x^<<[]x]>>会匹配一个“]”或“x”。<<[-x]>><<[x-]>>都会匹配一个“”或“x”。

 

·        字符集的简写

因为一些字符集非常常用,所以有一些简写方式。

<<"d>>代表<<[0-9]>>;

<<"w>>代表单词字符。这个是随正则表达式实现的不同而有些差异。绝大多数的正则表达式实现的单词字符集都包含了<<A-Za-z0-9_]>>

<<"s>>代表“白字符”。这个也是和不同的实现有关的。在绝大多数的实现中,都包含了空格符和Tab符,以及回车换行符<<"r"n>>

字符集的缩写形式可以用在方括号之内或之外。<<"s"d>>匹配一个白字符后面紧跟一个数字。<<["s"d]>>匹配单个白字符或数字。<<["da-fA-F]>>将匹配一个十六进制数字。

取反字符集的简写

<<["S]>> = <<[^"s]>>

<<["W]>> = <<[^"w]>>

<<["D]>> = <<[^"d]>>

·        字符集的重复

如果你用“?*+”操作符来重复一个字符集,你将会重复整个字符集。而不仅是它匹配的那个字符。正则表达式<<[0-9]+>>会匹配837以及222

如果你仅仅想重复被匹配的那个字符,可以用向后引用达到目的。我们以后将讲到向后引用。

 

 

6.      使用?*+ 进行重复

?:告诉引擎匹配前导字符0次或一次。事实上是表示前导字符是可选的。

+:告诉引擎匹配前导字符1次或多次

*:告诉引擎匹配前导字符0次或多次

<[A-Za-z][A-Za-z0-9]*>匹配没有属性的HTML标签,“<”以及“>”是文字符号。第一个字符集匹配一个字母,第二个字符集匹配一个字母或数字。

我们似乎也可以用<[A-Za-z0-9]+>。但是它会匹配<1>。但是这个正则表达式在你知道你要搜索的字符串不包含类似的无效标签时还是足够有效的。

 

·        限制性重复

许多现代的正则表达式实现,都允许你定义对一个字符重复多少次。词法是:{min,max}minmax都是非负整数。如果逗号有而max被忽略了,则max没有限制。如果逗号和max都被忽略了,则重复min次。

因此{0,}*一样,{1}+ 的作用一样。

你可以用<<"b[1-9][0-9]{3}"b>>匹配1000~9999之间的数字("b”表示单词边界)<<"b[1-9][0-9]{2,4}"b>>匹配一个在100~99999之间的数字。

 

·        注意贪婪性

假设你想用一个正则表达式匹配一个HTML标签。你知道输入将会是一个有效的HTML文件,因此正则表达式不需要排除那些无效的标签。所以如果是在两个尖括号之间的内容,就应该是一个HTML标签。

许多正则表达式的新手会首先想到用正则表达式<< <.+> >>,他们会很惊讶的发现,对于测试字符串,“This is a <EM>first</EM> test”,你可能期望会返回<EM>,然后继续进行匹配的时候,返回</EM>

但事实是不会。正则表达式将会匹配“<EM>first</EM>”。很显然这不是我们想要的结果。原因在于“+”是贪婪的。也就是说,“+”会导致正则表达式引擎试图尽可能的重复前导字符。只有当这种重复会引起整个正则表达式匹配失败的情况下,引擎会进行回溯。也就是说,它会放弃最后一次的“重复”,然后处理正则表达式余下的部分。

和“+”类似,“?*”的重复也是贪婪的。

 

·        深入正则表达式引擎内部

让我们来看看正则引擎如何匹配前面的例子。第一个记号是“<”,这是一个文字符号。第二个符号是“.”,匹配了字符“E”,然后“+”一直可以匹配其余的字符,直到一行的结束。然后到了换行符,匹配失败(.”不匹配换行符)。于是引擎开始对下一个正则表达式符号进行匹配。也即试图匹配“>”。到目前为止,“<.+”已经匹配了“<EM>first</EM> test”。引擎会试图将“>”与换行符进行匹配,结果失败了。于是引擎进行回溯。结果是现在“<.+”匹配“<EM>first</EM> tes”。于是引擎将“>”与“t”进行匹配。显然还是会失败。这个过程继续,直到“<.+”匹配“<EM>first</EM”,“>”与“>”匹配。于是引擎找到了一个匹配“<EM>first</EM>”。记住,正则导向的引擎是“急切的”,所以它会急着报告它找到的第一个匹配。而不是继续回溯,即使可能会有更好的匹配,例如“<EM>”。所以我们可以看到,由于“+”的贪婪性,使得正则表达式引擎返回了一个最左边的最长的匹配。

 

·        用懒惰性取代贪婪性

一个用于修正以上问题的可能方案是用“+”的惰性代替贪婪性。你可以在“+”后面紧跟一个问号“?”来达到这一点。“*”,“{}”和“?”表示的重复也可以用这个方案。因此在上面的例子中我们可以使用“<.+?>”。让我们再来看看正则表达式引擎的处理过程。

再一次,正则表达式记号“<”会匹配字符串的第一个“<”。下一个正则记号是“.”。这次是一个懒惰的“+”来重复上一个字符。这告诉正则引擎,尽可能少的重复上一个字符。因此引擎匹配“.”和字符“E”,然后用“>”匹配“M”,结果失败了。引擎会进行回溯,和上一个例子不同,因为是惰性重复,所以引擎是扩展惰性重复而不是减少,于是“<.+”现在被扩展为“<EM”。引擎继续匹配下一个记号“>”。这次得到了一个成功匹配。引擎于是报告“<EM>”是一个成功的匹配。整个过程大致如此。

 

·        惰性扩展的一个替代方案

我们还有一个更好的替代方案。可以用一个贪婪重复与一个取反字符集:“<[^>]+>”。之所以说这是一个更好的方案在于使用惰性重复时,引擎会在找到一个成功匹配前对每一个字符进行回溯。而使用取反字符集则不需要进行回溯。

最后要记住的是,本教程仅仅谈到的是正则导向的引擎。文本导向的引擎是不回溯的。但是同时他们也不支持惰性重复操作。

 

7.      使用“.”匹配几乎任意字符

在正则表达式中,“.”是最常用的符号之一。不幸的是,它也是最容易被误用的符号之一。

.”匹配一个单个的字符而不用关心被匹配的字符是什么。唯一的例外是新行符。在本教程中谈到的引擎,缺省情况下都是不匹配新行符的。因此在缺省情况下,“.”等于是字符集[^"n"r](Window)[^"n]( Unix)的简写。

这个例外是因为历史的原因。因为早期使用正则表达式的工具是基于行的。它们都是一行一行的读入一个文件,将正则表达式分别应用到每一行上去。在这些工具中,字符串是不包含新行符的。因此“.”也就从不匹配新行符。

现代的工具和语言能够将正则表达式应用到很大的字符串甚至整个文件上去。本教程讨论的所有正则表达式实现都提供一个选项,可以使“.”匹配所有的字符,包括新行符。在RegexBuddy, EditPad ProPowerGREP等工具中,你可以简单的选中“点号匹配新行符”。在Perl中,“.”可以匹配新行符的模式被称作“单行模式”。很不幸,这是一个很容易混淆的名词。因为还有所谓“多行模式”。多行模式只影响行首行尾的锚定(anchor),而单行模式只影响“.”。

其他语言和正则表达式库也采用了Perl的术语定义。当在.NET Framework中使用正则表达式类时,你可以用类似下面的语句来激活单行模式:Regex.Match(“string”,”regex”,RegexOptions.SingleLine)

 

 

·        保守的使用点号“.

点号可以说是最强大的元字符。它允许你偷懒:用一个点号,就能匹配几乎所有的字符。但是问题在于,它也常常会匹配不该匹配的字符。

我会以一个简单的例子来说明。让我们看看如何匹配一个具有“mm/dd/yy”格式的日期,但是我们想允许用户来选择分隔符。很快能想到的一个方案是<<"d"d."d"d."d"d>>。看上去它能匹配日期“02/12/03”。问题在于02512703也会被认为是一个有效的日期。

<<"d"d[-/.]"d"d[-/.]"d"d>>看上去是一个好一点的解决方案。记住点号在一个字符集里不是元字符。这个方案远不够完善,它会匹配“99/99/99”。而<<[0-1]"d[-/.][0-3]"d[-/.]"d"d>>又更进一步。尽管他也会匹配“19/39/99”。你想要你的正则表达式达到如何完美的程度取决于你想达到什么样的目的。如果你想校验用户输入,则需要尽可能的完美。如果你只是想分析一个已知的源,并且我们知道没有错误的数据,用一个比较好的正则表达式来匹配你想要搜寻的字符就已经足够。

 

8.      字符串开始和结束的锚定

锚定和一般的正则表达式符号不同,它不匹配任何字符。相反,他们匹配的是字符之前或之后的位置。“^”匹配一行字符串第一个字符前的位置。<<^a>>将会匹配字符串“abc”中的a<<^b>>将不会匹配“abc”中的任何字符。

类似的,$匹配字符串中最后一个字符的后面的位置。所以<<c$>>匹配“abc”中的c

 

·        锚定的应用

在编程语言中校验用户输入时,使用锚定是非常重要的。如果你想校验用户的输入为整数,用<<^"d+$>>

用户输入中,常常会有多余的前导空格或结束空格。你可以用<<^"s*>><<"s*$>>来匹配前导空格或结束空格。

 

·        使用“^”和“$”作为行的开始和结束锚定

如果你有一个包含了多行的字符串。例如:“first line"n"rsecond line(其中"n"r表示一个新行符)。常常需要对每行分别处理而不是整个字符串。因此,几乎所有的正则表达式引擎都提供一个选项,可以扩展这两种锚定的含义。“^”可以匹配字串的开始位置(f之前),以及每一个新行符的后面位置("n"rs之间)。类似的,$会匹配字串的结束位置(最后一个e之后),以及每个新行符的前面(e"n"r之间)

.NET中,当你使用如下代码时,将会定义锚定匹配每一个新行符的前面和后面位置:Regex.Match("string", "regex", RegexOptions.Multiline)

应用:string str = Regex.Replace(Original, "^", "> ", RegexOptions.Multiline)–将会在每行的行首插入“> ”。

 

·        绝对锚定

<<"A>>只匹配整个字符串的开始位置,<<"Z>>只匹配整个字符串的结束位置。即使你使用了“多行模式”,<<"A>><<"Z>>也从不匹配新行符。

即使"Z$只匹配字符串的结束位置,仍然有一个例外的情况。如果字符串以新行符结束,则"Z$将会匹配新行符前面的位置,而不是整个字符串的最后面。这个“改进”是由Perl引进的,然后被许多的正则表达式实现所遵循,包括Java.NET等。如果应用<<^[a-z]+$>>到“joe"n”,则匹配结果是“joe”而不是“joe"n”。

 

9.      单词边界

 

元字符<<"b>>也是一种对位置进行匹配的“锚”。这种匹配是0长度匹配。

4种位置被认为是“单词边界”:

1)        在字符串的第一个字符前的位置(如果字符串的第一个字符是一个“单词字符”)

2)        在字符串的最后一个字符后的位置(如果字符串的最后一个字符是一个“单词字符”)

3)        在一个“单词字符”和“非单词字符”之间,其中“非单词字符”紧跟在“单词字符”之后

4)        在一个“非单词字符”和“单词字符”之间,其中“单词字符”紧跟在“非单词字符”后面

 “单词字符”是可以用“"w”匹配的字符,“非单词字符”是可以用“"W”匹配的字符。在大多数的正则表达式实现中,“单词字符”通常包括<<[a-zA-Z0-9_]>>

例如:<<"b4"b>>能够匹配单个的4而不是一个更大数的一部分。这个正则表达式不会匹配“44”中的4

换种说法,几乎可以说<<"b>>匹配一个“字母数字序列”的开始和结束的位置。

 

“单词边界”的取反集为<<"B>>,他要匹配的位置是两个“单词字符”之间或者两个“非单词字符”之间的位置。

 

·        深入正则表达式引擎内部

让我们看看把正则表达式<<"bis"b>>应用到字符串“This island is beautiful”。引擎先处理符号<<"b>>。因为"b0长度 ,所以第一个字符T前面的位置会被考察。因为T是一个“单词字符”,而它前面的字符是一个空字符(void),所以"b匹配了单词边界。接着<<i>>和第一个字符“T”匹配失败。匹配过程继续进行,直到第五个空格符,和第四个字符“s”之间又匹配了<<"b>>。然而空格符和<<i>>不匹配。继续向后,到了第六个字符“i”,和第五个空格字符之间匹配了<<"b>>,然后<<is>>和第六、第七个字符都匹配了。然而第八个字符和第二个“单词边界”不匹配,所以匹配又失败了。到了第13个字符i,因为和前面一个空格符形成“单词边界”,同时<<is>>和“is”匹配。引擎接着尝试匹配第二个<<"b>>。因为第15个空格符和“s”形成单词边界,所以匹配成功。引擎“急着”返回成功匹配的结果。

 

10. 选择符

正则表达式中“|”表示选择。你可以用选择符匹配多个可能的正则表达式中的一个。

如果你想搜索文字“cat”或“dog”,你可以用<<cat|dog>>。如果你想有更多的选择,你只要扩展列表<<cat|dog|mouse|fish>>

选择符在正则表达式中具有最低的优先级,也就是说,它告诉引擎要么匹配选择符左边的所有表达式,要么匹配右边的所有表达式。你也可以用圆括号来限制选择符的作用范围。如<<"b(cat|dog)"b>>,这样告诉正则引擎把(cat|dog)当成一个正则表达式单位来处理。

·        注意正则引擎的“急于表功”性

正则引擎是急切的,当它找到一个有效的匹配时,它会停止搜索。因此在一定条件下,选择符两边的表达式的顺序对结果会有影响。假设你想用正则表达式搜索一个编程语言的函数列表:GetGetValueSetSetValue。一个明显的解决方案是<<Get|GetValue|Set|SetValue>>。让我们看看当搜索SetValue时的结果。

因为<<Get>><<GetValue>>都失败了,而<<Set>>匹配成功。因为正则导向的引擎都是“急切”的,所以它会返回第一个成功的匹配,就是“Set”,而不去继续搜索是否有其他更好的匹配。

和我们期望的相反,正则表达式并没有匹配整个字符串。有几种可能的解决办法。一是考虑到正则引擎的“急切”性,改变选项的顺序,例如我们使用<<GetValue|Get|SetValue|Set>>,这样我们就可以优先搜索最长的匹配。我们也可以把四个选项结合起来成两个选项:<<Get(Value)?|Set(Value)?>>。因为问号重复符是贪婪的,所以SetValue总会在Set之前被匹配。

一个更好的方案是使用单词边界:<<"b(Get|GetValue|Set|SetValue)"b>><<"b(Get(Value)?|Set(Value)?"b>>。更进一步,既然所有的选择都有相同的结尾,我们可以把正则表达式优化为<<"b(Get|Set)(Value)?"b>>

 

 

11. 组与向后引用

把正则表达式的一部分放在圆括号内,你可以将它们形成组。然后你可以对整个组使用一些正则操作,例如重复操作符。

要注意的是,只有圆括号“()”才能用于形成组。“[]”用于定义字符集。“{}”用于定义重复操作。

当用“()”定义了一个正则表达式组后,正则引擎则会把被匹配的组按照顺序编号,存入缓存。当对被匹配的组进行向后引用的时候,可以用“"数字”的方式进行引用。<<"1>>引用第一个匹配的后向引用组,<<"2>>引用第二个组,以此类推,<<"n>>引用第n个组。而<<"0>>则引用整个被匹配的正则表达式本身。我们看一个例子。

假设你想匹配一个HTML标签的开始标签和结束标签,以及标签中间的文本。比如<B>This is a test</B>,我们要匹配<B></B>以及中间的文字。我们可以用如下正则表达式:“<([A-Z][A-Z0-9]*)[^>]*>.*?</"1>

首先,“<”将会匹配“<B>”的第一个字符“<”。然后[A-Z]匹配B[A-Z0-9]*将会匹配0到多次字母数字,后面紧接着0到多个非“>”的字符。最后正则表达式的“>”将会匹配“<B>”的“>”。接下来正则引擎将对结束标签之前的字符进行惰性匹配,直到遇到一个“</”符号。然后正则表达式中的“"1”表示对前面匹配的组“([A-Z][A-Z0-9]*)”进行引用,在本例中,被引用的是标签名“B”。所以需要被匹配的结尾标签为“</B>

你可以对相同的后向引用组进行多次引用,<<([a-c])x"1x"1>>将匹配“axaxa”、“bxbxb”以及“cxcxc”。如果用数字形式引用的组没有有效的匹配,则引用到的内容简单的为空。

一个后向引用不能用于它自身。<<([abc]"1)>>是错误的。因此你不能将<<"0>>用于一个正则表达式匹配本身,它只能用于替换操作中。

后向引用不能用于字符集内部。<<(a)["1b]>>中的<<"1>>并不表示后向引用。在字符集内部,<<"1>>可以被解释为八进制形式的转码。

向后引用会降低引擎的速度,因为它需要存储匹配的组。如果你不需要向后引用,你可以告诉引擎对某个组不存储。例如:<<Get(?:Value)>>。其中“(”后面紧跟的“?:”会告诉引擎对于组(Value),不存储匹配的值以供后向引用。

·        重复操作与后向引用

当对组使用重复操作符时,缓存里后向引用内容会被不断刷新,只保留最后匹配的内容。例如:<<([abc]+)="1>>将匹配“cab=cab”,但是<<([abc])+="1>>却不会。因为([abc])第一次匹配“c”时,“"1”代表“c”;然后([abc])会继续匹配“a”和“b”。最后“"1”代表“b”,所以它会匹配“cab=b”。

应用:检查重复单词当编辑文字时,很容易就会输入重复单词,例如“the the”。使用<<"b("w+)"s+"1"b>>可以检测到这些重复单词。要删除第二个单词,只要简单的利用替换功能替换掉“"1”就可以了。

 

 

·        组的命名和引用

PHPPython中,可以用<<(?P<name>group)>>来对组进行命名。在本例中,词法?P<name>就是对组(group)进行了命名。其中name是你对组的起的名字。你可以用(?P=name)进行引用。

.NET的命名组

.NET framework也支持命名组。不幸的是,微软的程序员们决定发明他们自己的语法,而不是沿用PerlPython的规则。目前为止,还没有任何其他的正则表达式实现支持微软发明的语法。

下面是.NET中的例子:

(?<first>group)(?’second’group)

正如你所看到的,.NET提供两种词法来创建命名组:一是用尖括号“<>”,或者用单引号“’’”。尖括号在字符串中使用更方便,单引号在ASP代码中更有用,因为ASP代码中“<>”被用作HTML标签。

要引用一个命名组,使用"k<name>"k’name’.

当进行搜索替换时,你可以用“${name}”来引用一个命名组。

 

12. 正则表达式的匹配模式

本教程所讨论的正则表达式引擎都支持三种匹配模式:

<</i>>使正则表达式对大小写不敏感,

<</s>>开启“单行模式”,即点号“.”匹配新行符

<</m>>开启“多行模式”,即“^”和“$”匹配新行符的前面和后面的位置。

 

·        在正则表达式内部打开或关闭模式

如果你在正则表达式内部插入修饰符(?ism),则该修饰符只对其右边的正则表达式起作用。(?-i)是关闭大小写不敏感。你可以很快的进行测试。<<(?i)te(?-i)st>>应该匹配TEst,但是不能匹配teSTTEST.

 

13. 原子组与防止回溯

在一些特殊情况下,因为回溯会使得引擎的效率极其低下。

让我们看一个例子:要匹配这样的字串,字串中的每个字段间用逗号做分隔符,第12个字段由P开头。

我们容易想到这样的正则表达式<<^(.*?,){11}P>>。这个正则表达式在正常情况下工作的很好。但是在极端情况下,如果第12个字段不是由P开头,则会发生灾难性的回溯。如要搜索的字串为“1,2,3,4,5,6,7,8,9,10,11,12,13”。首先,正则表达式一直成功匹配直到第12个字符。这时,前面的正则表达式消耗的字串为“1,2,3,4,5,6,7,8,9,10,11,”,到了下一个字符,<<P>>并不匹配“12”。所以引擎进行回溯,这时正则表达式消耗的字串为“1,2,3,4,5,6,7,8,9,10,11”。继续下一次匹配过程,下一个正则符号为点号<<.>>,可以匹配下一个逗号“,”。然而<<>>并不匹配字符“12”中的“1”。匹配失败,继续回溯。大家可以想象,这样的回溯组合是个非常大的数量。因此可能会造成引擎崩溃。

用于阻止这样巨大的回溯有几种方案:

一种简单的方案是尽可能的使匹配精确。用取反字符集代替点号。例如我们用如下正则表达式<<^([^,"r"n]*,){11}P>>,这样可以使失败回溯的次数下降到11次。

另一种方案是使用原子组。

原子组的目的是使正则引擎失败的更快一点。因此可以有效的阻止海量回溯。原子组的语法是<<(?>正则表达式)>>。位于(?>)之间的所有正则表达式都会被认为是一个单一的正则符号。一旦匹配失败,引擎将会回溯到原子组前面的正则表达式部分。前面的例子用原子组可以表达成<<^(?>(.*?,){11})P>>。一旦第十二个字段匹配失败,引擎回溯到原子组前面的<<^>>

 

14. 向前查看与向后查看

Perl 5 引 入了两个强大的正则语法:“向前查看”和“向后查看”。他们也被称作“零长度断言”。他们和锚定一样都是零长度的(所谓零长度即指该正则表达式不消耗被匹 配的字符串)。不同之处在于“前后查看”会实际匹配字符,只是他们会抛弃匹配只返回匹配结果:匹配或不匹配。这就是为什么他们被称作“断言”。他们并不实 际消耗字符串中的字符,而只是断言一个匹配是否可能。

几乎本文讨论的所有正则表达式的实现都支持“向前向后查看”。唯一的一个例外是JavaScript只支持向前查看。

·        肯定和否定式的向前查看

如我们前面提过的一个例子:要查找一个q,后面没有紧跟一个u。也就是说,要么q后面没有字符,要么后面的字符不是u。采用否定式向前查看后的一个解决方案为<<q(?!u)>>。否定式向前查看的语法是<<(?!查看的内容)>>

肯定式向前查看和否定式向前查看很类似:<<(?=查看的内容)>>

如果在“查看的内容”部分有组,也会产生一个向后引用。但是向前查看本身并不会产生向后引用,也不会被计入向后引用的编号中。这是因为向前查看本身是会被抛弃掉的,只保留匹配与否的判断结果。如果你想保留匹配的结果作为向后引用,你可以用<<(?=(regex))>>来产生一个向后引用。

·        肯定和否定式的先后查看

向后查看和向前查看有相同的效果,只是方向相反

否定式向后查看的语法是:<<(?<!查看内容)>>

肯定式向后查看的语法是:<<(?<=查看内容)>>

我们可以看到,和向前查看相比,多了一个表示方向的左尖括号。

例:<<(?<!a)b>>将会匹配一个没有“a”作前导字符的“b”。

值得注意的是:向前查看从当前字符串位置开始对“查看”正则表达式进行匹配;向后查看则从当前字符串位置开始先后回溯一个字符,然后再开始对“查看”正则表达式进行匹配。

 

·        深入正则表达式引擎内部

让我们看一个简单例子。

把正则表达式<<q(?!u)>>应用到字符串“Iraq”。正则表达式的第一个符号是<<q>>。正如我们知道的,引擎在匹配<<q>>以前会扫过整个字符串。当第四个字符“q”被匹配后,“q”后面是空字符(void)。而下一个正则符号是向前查看。引擎注意到已经进入了一个向前查看正则表达式部分。下一个正则符号是<<u>>,和空字符不匹配,从而导致向前查看里的正则表达式匹配失败。因为是一个否定式的向前查看,意味着整个向前查看结果是成功的。于是匹配结果“q”被返回了。

我们在把相同的正则表达式应用到“quit”。<<q>>匹配了“q”。下一个正则符号是向前查看部分的<<u>>,它匹配了字符串中的第二个字符“i”。引擎继续走到下个字符“i”。然而引擎这时注意到向前查看部分已经处理完了,并且向前查看已经成功。于是引擎抛弃被匹配的字符串部分,这将导致引擎回退到字符“u”。

因为向前查看是否定式的,意味着查看部分的成功匹配导致了整个向前查看的失败,因此引擎不得不进行回溯。最后因为再没有其他的“q”和<<q>>匹配,所以整个匹配失败了。

为了确保你能清楚地理解向前查看的实现,让我们把<<q(?=u)i>>应用到“quit”。<<q>>首先匹配“q”。然后向前查看成功匹配“u”,匹配的部分被抛弃,只返回可以匹配的判断结果。引擎从字符“i”回退到“u”。由于向前查看成功了,引擎继续处理下一个正则符号<<i>>。结果发现<<i>>和“u”不匹配。因此匹配失败了。由于后面没有其他的“q”,整个正则表达式的匹配失败了。

 

·        更进一步理解正则表达式引擎内部机制

让我们把<<(?<=a)b>>应用到“thingamabob”。引擎开始处理向后查看部分的正则符号和字符串中的第一个字符。在这个例子中,向后查看告诉正则表达式引擎回退一个字符,然后查看是否有一个“a”被匹配。因为在“t”前面没有字符,所以引擎不能回退。因此向后查看失败了。引擎继续走到下一个字符“h”。再一次,引擎暂时回退一个字符并检查是否有个“a”被匹配。结果发现了一个“t”。向后查看又失败了。

向后查看继续失败,直到正则表达式到达了字符串中的“m”,于是肯定式的向后查看被匹配了。因为它是零长度的,字符串的当前位置仍然是“m”。下一个正则符号是<<b>>,和“m”匹配失败。下一个字符是字符串中的第二个“a”。引擎向后暂时回退一个字符,并且发现<<a>>不匹配“m”。

在下一个字符是字符串中的第一个“b”。引擎暂时性的向后退一个字符发现向后查看被满足了,同时<<b>>匹配了“b”。因此整个正则表达式被匹配了。作为结果,正则表达式返回字符串中的第一个“b”。

·        向前向后查看的应用

我们来看这样一个例子:查找一个具有6位字符的,含有“cat”的单词。

首先,我们可以不用向前向后查看来解决问题,例如:

<< cat"w{3}|"wcat"w{2}|"w{2}cat"w|"w{3}cat>>

足够简单吧!但是当需求变成查找一个具有6-12位字符,含有“cat”,“dog”或“mouse”的单词时,这种方法就变得有些笨拙了。

我们来看看使用向前查看的方案。在这个例子中,我们有两个基本需求要满足:一是我们需要一个6位的字符,二是单词含有“cat”。

满足第一个需求的正则表达式为<<"b"w{6}"b>>。满足第二个需求的正则表达式为<<"b"w*cat"w*"b>>

把两者结合起来,我们可以得到如下的正则表达式:

     <<(?="b"w{6}"b)"b"w*cat"w*"b>>

具体的匹配过程留给读者。但是要注意的一点是,向前查看是不消耗字符的,因此当判断单词满足具有6个字符的条件后,引擎会从开始判断前的位置继续对后面的正则表达式进行匹配。

最后作些优化,可以得到下面的正则表达式:

<<"b(?="w{6}"b)"w{0,3}cat"w*>>

 

15. 正则表达式中的条件测试

条件测试的语法为<<(?ifthen|else)>>。“if”部分可以是向前向后查看表达式。如果用向前查看,则语法变为:<<(?(?=regex)then|else)>>,其中else部分是可选的。

如果if部分为true,则正则引擎会试图匹配then部分,否则引擎会试图匹配else部分。

需要记住的是,向前先后查看并不实际消耗任何字符,因此后面的thenelse部分的匹配时从if测试前的部分开始进行尝试。

 

16. 为正则表达式添加注释

在正则表达式中添加注释的语法是:<<(?#comment)>>

例:为用于匹配有效日期的正则表达式添加注释:

 (?#year)(19|20)"d"d[- /.](?#month)(0[1-9]|1[012])[- /.](?#day)(0[1-9]|[12][0-9]|3[01])

[SQL]SQLServer : EXEC和sp_executesql的区别

mikel阅读(1058)

摘要

1,EXEC的使用

2,sp_executeSQL的使用

       MSSQL为我们提供了两种动态执行SQL语句的命令,分别是EXEC和sp_executesql;通常,sp_executesql则更具有优势,它 提供了输入输出接口,而EXEC没有。还有一个最大的好处就是利用sp_executesql,能够重用执行计划,这就大大提供了执行性能(对于这个我在 后面的例子中会详加说明),还可以编写更安全的代码。EXEC在某些情况下会更灵活。除非您有令人信服的理由使用EXEC,否侧尽量使用 sp_executesql.

1,EXEC的使用

EXEC命令有两种用法,一种是执行一个存储过程,另一种是执行一个动态的批处理。以下所讲的都是第二种用法。

下面先使用EXEC演示一个例子,代码1

DECLARE @TableName VARCHAR(50),@Sql NVARCHAR(MAX),@OrderID INT;
SET @TableName = 'Orders';
SET @OrderID = 10251;
SET @sql = 'Select * FROM '+QUOTENAME(@TableName) +'Where orderID = '+CAST(@OrderID AS VARCHAR(10))+' orDER BY orDERID DESC'
EXEC(@sql);

注:这里的EXEC括号中只允许包含一个字符串变量,但是可以串联多个变量,如果我们这样写EXEC:

EXEC('Select TOP('+ CAST(@TopCount AS VARCHAR(10)) +')* FROM '+QUOTENAME(@TableName) +' orDER BY orDERID DESC');
 
SQL编译器就会报错,编译不通过,而如果我们这样:
EXEC(@sql+@sql2+@sql3);
编译器就会通过;
 
所以最佳的做法是把代码构造到一个变量中,然后再把该变量作为EXEC命令的输入参数,这样就不会受限制了;
 
EXEC不提供接口
这里的接口是指,它不能执行一个包含一个带变量符的批处理,这里乍一听好像不明白,不要紧,我在下面有一个实例,您一看就知道什么意思.
DECLARE @TableName VARCHAR(50),@Sql NVARCHAR(MAX),@OrderID INT;
SET @TableName = 'Orders';
SET @OrderID = 10251;
SET @sql = 'Select * FROM '+QUOTENAME(@TableName) +'Where OrderID = @OrderID orDER BY orDERID DESC'
EXEC(@sql);

关键就在SET @sql这一句话中,如果我们运行这个批处理,编译器就会产生一下错误

Msg 137, Level 15, State 2, Line 1
必须声明标量变量 "@OrderID"。

使用EXEC时,如果您想访问变量,必须把变量内容串联到动态构建的代码字符串中,如:SET @sql = 'Select * FROM '+QUOTENAME(@TableName) +'Where orderID = '+CAST(@OrderID AS VARCHAR(10))+' orDER BY orDERID DESC'

串联变量的内容也存在性能方面的弊端。SQL Server为每一个的查询字符串创建新的执行计划,即使查询模式相同也是这样。为演示这一点,先清空缓存中的执行计划

DBCC FREEPROCCACHE (这个不是本文所涉及的内容,您可以查看MS的MSDN)

http://msdn.microsoft.com/zh-cn/library/ms174283.aspx 

将代码1运行3次,分别对@OrderID 赋予下面3个值,10251,10252,10253。然后使用下面的代码查询

Select cacheobjtype,objtype,usecounts,sql FROM sys.syscacheobjects Where sql NOT LIKE '%cach%' AND sql NOT LIKE '%sys.%' 

点击F5运行,就会出现下面如图所示的查询结果:
dynmicsql1

我们可以看到,每执行一次都要产生一次的编译,执行计划没有得到充分重用。

EXEC除了不支持动态批处理中的输入参数外,他也不支持输出参数。默认情况下,EXEC把查询的输出返回给调用者。例如下面代码返回Orders表中所有的记录数

DECLARE @sql NVARCHAR(MAX)
SET @sql = 'Select COUNT(ORDERID) FROM orders';
EXEC(@sql);

然而,如果你要把输出返回给调用批处理中的变量,事情就没有那么简单了。为此,你必须使用Insert EXEC语法把输出插入到一个目标表中,然后从这表中获取值后赋给该变量,就像这样:

DECLARE @sql NVARCHAR(MAX),@RecordCount INT
SET @sql = 'Select COUNT(ORDERID) FROM orders';
 
Create TABLE #T(TID INT);
Insert INTO #T EXEC(@sql);
SET @RecordCount = (Select TID FROM #T)
Select @RecordCount
Drop TABLE #T

2,sp_executesql的使用

sp_executesql命令在SQL Server中引入的比EXEC命令晚一些,它主要为重用执行计划提供更好的支持。

为了和EXEC作一个鲜明的对比,我们看看如果用代码1的代码,把EXEC换成sp_executesql,看看是否得到我们所期望的结果

DECLARE @TableName VARCHAR(50),@sql NVARCHAR(MAX),@OrderID INT ,@sql2 NVARCHAR(MAX);
SET @TableName = 'Orders ';
SET @OrderID = 10251;
SET @sql = 'Select * FROM '+QUOTENAME(@TableName) + ' Where orderID = '+CAST(@OrderID AS VARCHAR(50)) + ' orDER BY orDERID DESC'
EXEC sp_executesql @sql

注意最后一行;

事实证明可以运行;

sp_executesql提供接口

sp_executesql命令比EXEC命令更灵活,因为它提供一个接口,该接口及支持输入参数也支持输出参数。这功能使你可以创建带参数的查询 字符串,这样就可以比EXEC更好的重用执行计划,sp_executesql的构成与存储过程非常相似,不同之处在于你是动态构建代码。它的构成包括: 代码快,参数声明部分,参数赋值部分。说了这么多,还是看看它的语法吧

EXEC sp_executesql

@stmt = <statement>,–类似存储过程主体

@params = <params>, –类似存储过程参数部分

<params assignment> –类似存储过程调用

@stmt参数是输入的动态批处理,它可以引入输入参数或输出参数,和存储过程的主体语句一样,只不过它是动态的,而存储过程是静态的,不过你也可以在存储过程中使用sp_executesql;

@params参数与定义输入/输出参数的存储过程头类似,实际上和存储过程头的语法完全一样;

@<params assignment> 与调用存储过程的EXEC部分类似。

为了说明sp_executesql对执行计划的管理优于EXEC,我将使用前面讨论EXEC时用到的代码。

   1:  DECLARE @TableName VARCHAR(50),@sql NVARCHAR(MAX),@OrderID INT;
   2:  SET @TableName = 'Orders ';
   3:  SET @OrderID = 10251;
   4:  SET @sql = 'Select * FROM '+QUOTENAME(@TableName) + ' Where orderID = @OID orDER BY orDERID DESC'
   5:  EXEC sp_executesql
   6:      @stmt = @sql,
   7:      @params = N'@OID AS INT ',
   8:      @OID = @OrderID

在调用该代码和检查它生成的执行计划前,先清空缓存中的执行计划;

DBCC FREEPROCCACHE

将上面的动态代码执行3次,每次执行都赋予@OrderID 不同的值,然后查询sys.syscacheobjects表,并注意它的输出,优化器只创建了一个备用计划,而且该计划被重用的3次

Select cacheobjtype,objtype,usecounts,sql FROM sys.syscacheobjects Where sql NOT LIKE '%cache%' AND sql NOT LIKE '%sys.%' AND sql NOT LIKE '%sp_executesql%'
点击F5运行,就会出现如下表所示的结果;
dynmicsql2
sq_executesql的另一个与其接口有关的强大功能是,你可以使用输出参数为调用批处理中的 变量返回值。利用该功能可以避免用临时表返回数据,从而得到更高效的代码和更少的重新编译。定义和使用输出参数的语法与存储过程类似。也就是说,你需要在 声明参数时指定OUTPUT子句。例如,下面的静态代码简单的演示了如何从动态批处理中利用输出参数@p把值返回到外部批处理中的变量@i.
DECLARE @sql AS NVARCHAR(12),@i AS INT;
SET @sql = N' SET @p = 10';
EXEC sp_executesql 
    @stmt = @sql,
    @params = N'@p AS INT OUTPUT',
    @p = @i OUTPUT
Select @i
该代码返回输出10
 
          以上就是EXEC和sp_executesql的主要区别,如果各位看官觉得哪不对或者表达不清楚的,还请多多指出^_^
 
          作者:兴百放
          时间:2008-11-2 22:30
          网址:Http://xbf321.cnblogs.com

[MVC]《ASP.NET MVC案例教程》索引贴

mikel阅读(857)

      本系列文章通过一个虚拟的案例——《MVC公告发布系统》的开发过程,全面展示了ASP.NET MVC的基本使用方法,同时在讨论了这个框架的基本原理。
      这个文章系列的目的就是使朋友们更轻松的入门ASP.NET MVC。
      这个系列会包含的内容有:ASP.NET MVC基本应用、基本原理、路由处理、表单处理、与ASP.NET AJAX结合、与JQuery结合、拦截器、结合Silverlight等。
 

      摘要: 本文将简要介绍这个文章系列的目的、形式及大体内容。并且完成开始学习这个系列前所必要的准备工作。

      摘要: 本文首先一步一步完成Demo的第一个页面——首页。然后根据实现过程,说明一下其中用到的与ASP.NET MVC相关的概念与原理。

      摘要: 本文对ASP.NET MVC的全局运行机理进行一个简要的介绍,以使得朋友们更好的理解后续文章。

      摘要: 本文将完成我们“MVC公告发布系统”的公告发布功能,以此展示在ASP.NET MVC中如何传递处理表单的数据。

[SVN]AnkhSVN有1.02版支持Vs2008

mikel阅读(855)

AnkhSvn 是一个Subversion plugin for Visual Studio.这个软件允许你在您的Microsoft Visual Studio IDE内执行共同的版本控制操作.使用AnkhSVN你不再需要离开你的IDE去执行查看你的源代码状态,更新你Subversion copy和提交.你能在浏览你的知识库和你能插件式插入你的喜欢的不同的工具.

地址


Optimized workflow    


(1)不用离开你的IDE就可操作
(2)立即查看你的工程所有文件的源代码控制状态.
(3)查看协助copy信息,如最后提交者,最后提交数据和知识库URL
(4)能从及时从solution explorer中导入solutions
(5)得到所有Subversion transfer protocols


Pluggable diff/merge


(1)插入你的选种的diff/merge tool.
(2)为大部分共同的merge tools使用command line templates.


Working Copy Explorer


很容易与你的非projects/solution的部分的文件协同工作.
Visual Studio solution explorer中使用相同的命令.


Repository Explorer


很容易浏览 Subversion repository
在Visual Studio Properties window中View remote files and directories 的扩展信息. 
图片
The working copy explorer.

The commit dialog.

The internal diff.

Adding a solution to a repository.

The Visual solution explorer.

[网站]Picnik

mikel阅读(832)

www.picnik.com

这 个站点允许我们将照片,图片等上传至网站之上,然后运用pinik及第三方开发的图像滤镜进行图片的各种编辑和处理,里面的功能超酷,而且,我还发现这个 站点可以运用一些Flash Player10的特效功能,比如pixel bender对图形进行实时处理,感兴趣的开发者可以快去尝试一把!这个站点有繁体中文版本支持!