TeX 排版中文字体嵌入问题,兼谈不拘小节的中文字体设计

Garfileo post @ 2008年07月11日 08:31PM in 文档 & 资源 with tags: tex 中文字体
 

本文转载自 yulewang 的博客,原文地址:http://yulewang.spaces.live.com/blog/cns!5C815C994ABB661E!262.entry。本文基本上没有改动,只是将文中出现的一个错字进行了修改。


低于 18 周岁的小朋友们请不要看此文,会被吓到的。如果你和中国的字体公司有联系,请把我所描述的问题转告给他们。

考完作文后,在上海小歇,除了吃喝玩乐加上逛街还有被导师虐着写文章,自己看两眼G词以外,就是在尝试解决中文 TeX 用户几年来一直抱怨的关于 TeX 产生的 pdf 文档中文字体看上去太细的问题。

这个问题的来龙去脉是这样的。很久以前,大家都需要用 dvips 或者 dvipdfm 来转换 TeX 产生的 dvi 文件为 pdf 文件,这个路径很麻烦,需要把一个中文 trueype 字体转为大约一百多个 Type1 字体,然后再把这些字体的 Subfont 给嵌入到文档中。南韩的一个数学家 Cho,因为不满意 TeX 的这种状况,因此写了一个 dvipdfm 程序的扩展,名为 dvipdfmx。dvipdfmx 可以直接嵌入 CID 的 TrueType 和 OpenType 字体,不需要把字体分割成许多个 subfont,产生的 pdf 文件小巧,而且比原先的方法在阅览器下渲染速度快,质量比较高。因此得到了中日韩用户的喜爱。但是许久以来,中文用户一直抱怨一个问题,就是貌似由 dvipdfmx 产生的文档,在 Adobe 公司官方的 Acrobat Reader 下,显得发虚,严重地影响阅读,而相同的字体,如果不用 dvipdfmx,比如使用 cairo、quartz 或者 word2007 进行转化,产生的 pdf 就没有这个问题。如果文章中都是中文,那只不过是难以阅读而已。而如果是中文和英文混排的,而英文部分又比较正常,比如使用了 Times 字体或者 Palatino 字体,则整个 type block 就显得斑驳,一眼看到的都是突出的英文,所以一直以来,我们都建议用户使用本来就细得要命的默认的 Computer Modern 字体,以使得文档不至于失调。

几年来,从来没有人认真地研究过这个问题,南韩的用户都觉得 dvipdfmx 产生的 pdf 质量不错,因为他们使用的随 KTUG 定制的发行版发布的字体本来就比较粗,而中国的用户如果使用 Windows 下的中易公司的宋体,即 simsun,则相当不能忍,方正公司的字体,比如书宋,则稍微好一点,但怎么也好不过 Adobe 公司的 OpenType 字体 Adobe Songti Std Light。若干年前,jjgod 同学写过一篇文章比较了几十个中文字体,他是使用了 dvipdfmx 输出的结果的视觉效果来评价字体的质量,结果一个相当好的字体,方正博雅宋,由于在阅览器中显示过细,被他认为不如方正书宋。而几年以后,这个问题被泛 化,当今,TeX 正在走向国际化,如今的 TeX 已经支持 Unicode 和各种高级的字体格式,也全面从 dvi 时代过渡到 pdf 时代,目前两大炒作得很热的 TeX 引擎,都和 dvipdfmx 扯上了关系。 XeTeX 直接使用了 dvipdfmx 的一个变种,xdvipdfmx 来产生 pdf,而 LuaTeX,则用 dvipdfmx 的代码替换掉部分产原先 pdfTeX 的代码,来产生 pdf。这就导致,目前所有的先进的 TeX 系统,在 CID 字体嵌入方面的代码,是近亲关系,所有产生的中文 pdf,都虚得离谱,LuaTeX 尤甚。

我在去年和今年,一直使用 LuaTeX 引擎和 ConTeXt 格式,写各种包括论文以内的文档,在使用的过程中,发现了不少问题,就开始和开发者交流。几个月来,报告了不少 bug,其中一些,还给出了补丁。此外,我和 ConTeXt 开发者 Hans 等 TeX 专家讨论,试图让最新的 ConTeXt 和 LuaTeX 来支持中文排版。其他事情,还包括为ConTeXt 的用户们提供 FreeBSD 操作系统的 TeX 二进制文件。因此,我和 LuaTeX 与 ConTeXt 的开发者(其实是同一拨人),有很频繁的联系,也彼此保持着不错的关系。开发者们也很勤快地修复着我汇报的各种 bug,这也使我能够很好地使用这些软件进行各种文档(学术论文、技术文档)的排版。今年考完作文后,我暂时可以小歇一下,同时由于 LuaTeX 的开发者们刚刚搞定了一项新功能,mplib,也有空余的时间。因此,我们就有时间来讨论和解决这个由来已久的问题。

我一开始写信给 LuaTeX 的开发者,Taco Hoekwater,之所以不写给 dvipdfmx 开发者 Cho 而发给他,因为我和他熟,而且 Taco 这两年来,勤勤恳恳地写代码,我比较相信他解决问题的效率。有时候一个 bug 提交给他他不到几十分钟就已经 fix 了。结果 Taco 看了好几天,一无所获,于是,Taco 帮助我把信转给了当今世界上几个重要的大牛,准备来专家会诊。这个会诊的医师阵容庞大,技术高超,随便举几个人:dvipdfmx 开发者 Cho,LuaTeX 开发者 Taco,外加 XeTeX 开发者 Jonathan Kew。不久以后,话说解铃还需系铃人,Cho 找到了一个可能的问题。他把 pdf 文件解开,仔细观察 cairo 输出了 LuaTeX 输出中字体嵌入部分的参数,发现某个数值 StemV,相差悬殊。使用编辑器修改解开的 pdf,将 StemV 调整到相同,两个 pdf 文件顿时就差不多了(虽然还有些许差别,但是这个就是最重要的因素之一)。

问题顿时有了突破性的进展,比较 LuaTeX 产生的 pdf 和 dvipdfmx 产生的 pdf,发现 LuaTeX 的 StemV 数值大得多,不久 Taco 就发现,这是 LuaTeX 的某个 bug 导致的。该 bug 当天就得到了修正,这样产生的 pdf 就和 dvipdfmx 一样了。但是修正以后事实上得到的 pdf 依然很细。cairo 产生的 pdf 文件,一律取成了相同的默认数值,所以看上去宋体表现还不错。而 dvipdfmx 的 TrueType 字体的 StemV 数值到底是怎么产生的呢?它经验性地依赖于一个拟合公式:

stemv = (os2->usWeightClass/65)*(os2->usWeightClass/65)+50

其中,os2->usWeightClass 是字体中 pfmtable 中的信息,是一个数值。这个数值在字体设计的时候就被定了下来,一般和字体的 weight 有关:比如 Light 就是 300,500 表示 Medium,而 800 则表示 Extra Bold。该数值决定了 StemV 的数值,也就是说,如果这个字体越粗,那么 StemV 数值就越大,在阅览器中渲染,就会越虚,合情合理。但是当我们打开中易公司的中文字体,方正公司的字体,还有华文字体,我们失望地发现,他们都取了同样的 数值:400。

于是这个问题,如果扯开拟合公式本来结果就偏大不说,其他的就应该怪罪到中文字体设计上来了。像 Simsun 字体,并不比 AdobeSongStd-Light 粗多少,甚至更细,取一个 400 的值本来就不合理。其次,中易字体不管黑体还是宋体,都取相同的数值,怎么都说不过去。相同的也发生在方正字体上,方正宋黑,方正书宋,小标宋,也都取相 同的数值。这个基本上是不可能让软件来自动判断的问题,本该是字体公司仔细勘酌的,现在却被信手赋值。按照现在的状况,软件不可能自动判断这个值,使得黑 体就是比宋体取值大。

解决这个问题,也只能让用户自己设定了,不久以后 LuaTeX 用户可以通过修改 Fonttable 来实现,dvipdfmx 开发者称,今后会在 map 文件中,让用户指定数值。 XeTeX 开发者估计可能会像先前指定伪粗,伪斜一样的语法来定义这个数值,不过目前没有收到任何他的计划。这个估计就是我们能采用的唯一不是办法的办法,不过终归 而言,这个问题被解决了,今后只要仔细调整参数,就能得到渲染效果得当的 pdf 文件。

类似的中文字体乱设参数的例子还有很多,此前 yindian 同学,提到了XeTeX 的一个 bug,导致没有办法产生正确的 pdf,后来发现这个根本不是 bug,完全也是由于字体设计公司乱设字体参数导致的。后来 jjgod 同学 hack 了一下 xdvipdfmx 总算差不多解决该问题。该问题的详细信息,请参考 XeTeX 的邮件列表,该主题内容在http://tug.org/pipermail/xetex/2007-October/007536.html,和后续的 讨论。

中文字体设计不拘小节也让我也想到了另一个问题,用先前,中文用户使用 XeTeX,需要频繁地切换中英文字体,后来 XeTeX 开发者不得不提供了一个机制来让字体切换变得不那么折腾。而我和 ConTeXt 开发者交流中文排版问题,还要煞费苦心地讲怎么切换,需要编程实现复杂的虚拟字体机制来实现。这个都归罪于中文字体普遍地缺乏高质量的英文部分,仔细看看 simsun 或者 simhei 的英文部分,就可以看出有多么夸张了。

如果说这个问题的原因是中国的字体公司,向来没有很好的英文字体设计基础,同时对这个问题也不加以重视,那么中文标点的设计,就没有丝毫的可以开罪 的地方了,这个问题直接导致用户和开发者都非常为难。我们知道,高质量的中文排版,标点并不是占据一个中文字符的位置,而要比中文字符略小。同时,标点之 间需要存在压缩,比如逗号后紧紧跟随的关门引号,需要使用类似 kerning 的特性把两个 glyph 的距离减小。另外,类似破折号和省略号,其实应该放在一个 glyph 中而不应该分开。而现在所有的中文字体的糟糕程度,竟然到所有的标点符号都占用一个中文字符距离的程度。本来这个问题如果中文字体设计得当,使用默认的排 版算法,就基本上能够解决一般的中文的排版问题,而现在糟糕的设计就使得排版软件的设计难上加难。首先我们需要重新定义一系列的新算法和新规则,然后需要 手工赋值去确定标点的大小和两个标点连在一起时候的压缩程度。更麻烦的是,不同字体中的相同的 glyph,比如逗号或者句号,往往会在这个 box 的不同的位置,大小也会千差万别。调好了中易宋体的冒号和开门引号,把相同的数值使用到中易的隶书中,顿时两个符号就会挤在一起,这就使得如果不针对每一 个字体仔细调整,高质量的中文排版就几乎不可能。我寒假和 ConTeXt 的开发者交流中文排版问题时,这个麻烦搞得头都大了,而这个问题本来就是该在字体公司设计字体时就解决的。

排版软件的开发,永远不是一个软件的事情,它牵扯到政府规范,字体设计,文档标准和字体标准的制定。往往如果排版软件不能做出令人满意的结果,很可 能是由于其他非排版软件的因素造成的。Adobe 或者 LinoType 大公司出品的英文字体,往往都会有较高的水准,正是因为设计者已经仔细调整好字体中的各项参数,使得用户使用排版软件默认的方案,就能够做出很好的作品, 偶尔遇到需要的 glyph 找不到,或者某个 kerning 长度不理想,打开 fontforge 之类的字体软件,也能方便快速地调校从而满足自己的需要。中文字体的设计,离开这个标准还很远很远,有很长一段路要走。

文章的最后,感谢参加专家会诊的几位 TeX 开发者,同时,也要感谢为我这次汇报问题提供大量帮助的同学,他们是 lyanry 同学,水木上的 yakun 同学,这两位同学帮助我完成了大部分的 bug 搜集和汇报工作,尤其是 lyanry 同学,帮助我生成了许多测试用的 pdf 文件,他们的技术工作,使得留给我做的仅仅是把这些资料写成英文然后和开发者交流,整个过程就像写 GRE 作文那样简单。

用telnet来发E-Mail

2008年5月17日 18:57

#'以后的部分是我的注解,'>'表示输入,其它的为服务器的回应

rui@ubuntu-home:~$ telnet email.ustc.edu.cn 25
Trying 202.38.64.8...
Connected to email.ustc.edu.cn.
Escape character is '^]'.
220 ESMTP
> ehlo # Say 'hello'
250-AUTH=LOGIN PLAIN
250-AUTH LOGIN PLAIN
250-PIPELINING
250 8BITMIME
> auth login # SMTP服务器需要用户验证
334 VXNlcm5hbWU6 # B64编码的 Username:
> cxVd # B64编码的你的用户名,保密起见,这个是假的
334 UGFzc3dvcmQ6 # B64编码的 Password:
> ebgacd45 # B64编码的你的密码,保密起见,这个是假的
235 go ahead (eyou mta) (
http://eyou.net _80)
> Mail From: <
r...@ustc.edu.cn>
250 OK (eyou mta)
> RCPT To: <
r01u...@gmail.com>
250 OK (eyou mta) (
http://eyou.net _15000000)
> Data
354 go ahead (eyou mta)
> Subject: test from command line
> To:
51p...@gmail.com # 这个好像没什么用的
>
>
> nnd, test from telnet command line
>
>
> .
250 OK:has queued (eyou mta)
> quit
221 close connection (eyou mta)
Connection closed by foreign host.

通达信股票分析软件日线数据格式-股票数据格式大全,股票数据格式分析 http://alantop.5166.info

 
 
股票数据格式大全,股票数据格式分析 http://alantop.5166.info
 
 
我以招商银行为例说明其数据格式:
 
日线数据存放在 C:\Program Files\国泰君安证券\超强版\vipdoc\sh\lday
 
文件名: sh600036.day
 
以下是数据文件图片显示.首页内显示不了图片,请点击标题进入看.
 
 
 
其每32个字节记录的是一日线数据:
 
其数据结构定义如下:
 
typedef struct mystructtag
{
   int date;
   int open;
   int high;
   int low;
   int close;
   float amount;
   int vol;
   int reservation;
} StockData;
 
 
 
 
这个数据是以上图片翻译的两行数据
 
日期 开牌价 最高价 最低价 收盘价 成交量
 
20020409 10.51 10.88 10.51 10.66  414108800
20020410 10.66 10.70 10.39 10.60 67945400



 

用Python学习正则表达式

2008年2月28日 07:05

正则表达式

正则表达式是一种字符串匹配模式。

一般字符

对于绝大多数的字符来说,它们都只是简单地匹配自己。如
>>> p=re.compile('hi')      # 匹配 hi
>>> m=p.search('very,high') #
>>> m.span()                # 找到了位置
(5, 7)
>>>
在正则表达式中,如下的字符是具有特殊含义的
 . ^ $ * + ? { [ ] \ | ( )
它们的含义,我们会慢慢地说。
\
象其它大部分语言一样,'\'表示转义。为了在你的字符串中匹配如上的特殊字符,你就需要用到它。还有就是一些常见的定义,如'\r','\n','\f','\t','\b'等。如
>>> p=re.compile(r'\.e')     #找字符'.'后面紧跟字符'e'
>>> m=p.search('a.exe')
>>> m.span()
(1, 3)
>>>
.
字符.用于匹配任意的非换行字符,在windows下为'\n',在unix下为'\r\n'。在Python中,如果你在构造表达式时指定参数DOTALL的话,字符也可以匹配换行。
>>> p=re.compile(r'.')          #找任意非\n的字符
>>> p.search('abc').span()
(0, 1)
>>> p.search('\ncba').span()
(1, 2)
>>> p=re.compile
(r'.',re.DOTALL) #找任意字符,包括\n在内
>>> p.search('\ncba').span()
(0, 1)
>>>
|
字符|表示选择,匹配它两边的任意一种。
>>> p=re.compile(r'abc|cba') #匹配 abc 或 cba
>>> p.search('cabcd').span() #找到 abc
(1, 4)
>>> p.search('cbcbad').span() # 找到 cba
(2, 5)

>>>

[]表示的字符类

如果你想匹配几个字符中的任意一个,那么就要用到字符类了。一个字符类是由[和]表示的,表示其中的任意一个。如
>>> p=re.compile(r'[aeiou.^]')  #找aeiou,字符.或字符^中的任意一个
>>> m=p.search('b.exe')         #找到了字符.
>>> m.span()
(1, 2)
>>>
在一个字符类中,只有字符^、-、]和\有特殊含义。字符\仍然表示转义,字符-可以定义字符范围,字符^放在前面,表示非,字符]表示字符类的结束。如
>>> p=re.compile(r'[-a-c]')  #匹配字符-和字符a到c之间的字母,即a,b,c和-。
...                          #第1个字符-就表示字符-,第2个表示范围
>>> m=p.search('d-a.exe')    #找到-
>>> m.span
()
(1, 2)
>>> m=p.search('b-a.exe')    #找到b
>>> m.span()
(0, 1)
>>> p=re.compile(r'[^-a-c]') #找非-,a,b,c的字符
>>> m=p.search('b-a.exe')    #找到字符.

>>> m.span()
(3, 4)
>>> p=re.compile(r'[-a-c^]') #^没有在首位,就只表示字符^
>>> m=p.search('def^-')      #匹配字符^
>>> m.span()
(3, 4)
>>> p=re.compile(r'[[-a-c]') #第2个字符[只是表示字符[

>>> m=p.search('a[')         #找到[
>>> m.span()
(0, 1)
>>> p=re.compile(r'[a-c]]')  #第1个]已经表示类的结束,第2个表示字符]
...                          #表示要匹配字符a,b或c后接字符]
>>> m=
p.search(']a')         #匹配失败
>>> m.span()

Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    m.span()
AttributeError: 'NoneType' object has no attribute 'span'

>>> m=p.search('a]')         #成功
>>> m.span()
(0, 2)
>>> p=re.compile(r'[a-c\]]') #要表示字符],需要用转义符\
>>> m=p.search(']a')         #匹配到字符]
>>> 
m.span()
(0, 1)
>>>
有若干常用的字符类的表达
转义 含义 可能的等价表达
\d 任意数字 [0-9]
\w 任意单词字符,包括下划线_,数字和字母 [a-zA-Z0-9_]
\s 空白字符 [ \t\r\f\n\v]
\D 非数字 [^\d]
\W 非单词字符 [^\w]
\S 非空白 [^\s]
注意的就是,对于Unicode的实现,会不一样。如\w会包含汉字字符在内。

{}表示重复

在{和}之间的数,用于指定重复的次数
{n} 重复n次
{n,} 重复n次或n次以上
{n,m} 重复n到m次
>>> p=re.compile(r'ab{2}c');  #匹配abbc
>>> p=re.compile(r'ab{2,}c');  #匹配abbc,abbbbbbc
>>> p=re.compile(r'ab{2,4}c');  #匹配abbc,abbbc,abbbbc

>>>
除了用{}表示重复外,还有几个字符也是这种含义
* 重复0次或更多次,与{0,}等价
? 重复0次或1次,与{0,1}等价
+ 重复1次或更多次,与{1,}等价
>>> p=re.compile(r'windows\d+');  #匹配windows后跟至少1个数字
>>> p=re.compile(r'13\d{9}');     #匹配13后跟9个数字
>>>

懒惰模式

在正则表达式的匹配中,采用的尽可能多的匹配原则,即贪婪模式(Greeding),如
>>> p=re.compile(r'a.*c');
>>> m=p.search(r'abcbca'); #匹配尽可能多的情况,即abcbc,而不是abc
>>> m.span()
(0, 5)
>>>
如果说想要采用尽可能少的匹配的话,可以使用懒惰模式(Lazy)。在表达重复的符号后,加上字符?即可,如+?,*?,{n,}?等。
* ? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复
>>> p=re.compile(r'a.*?c');  #指定Lazy模式
>>> m=p.search(r'abcbca');   #匹配到abc就结束
>>> m.group()
'abc'
>>>

^和$表示行头和行尾

字符^表示在一行的开始位置,字符$则表示行的结尾位置,它们不代表任何字符。在Python中,需指定参数re.M或re.MULTILINE,才会表示行头和行尾;否则则表示字符串的开关和结尾。
>>> p=re.compile(r'^From:',re.M)     #匹配以From:开始的行
>>> p.search('hello,\nFrom: rui').span()  #找到
(7, 12)
>>> p.search('hello, From: rui').span()    #失败


Traceback (most recent call last):
  File "<pyshell#15>", line 1, in <module>
    p.search('hello, From: rui').span()
AttributeError: 'NoneType' object has no attribute 'span'

>>> p=re.compile(r'^From:')           #没加参数,则表示要求字符串以From:开头
>>> p.search('hello,\nFrom: rui').span() #失败

Traceback (most recent call last):
  File "<pyshell#17>", line 1, in <module>

    p.search('hello,\nFrom: rui').span()
AttributeError: 'NoneType' object has no attribute 'span'
>>> p.search('From: rui').span()         #成功
(0, 5)
>>>

除了用^和$表示位置外,还有
\b 匹配一个单词边界,也就是指单词和非单词字符间的位置。
\B [^\b]
\A 匹配字符串的开头
\Z 匹配字符串的结尾
如:
>>> p=re.compile(r'ba\b')   # 找以ba为结尾的单词
>>> m=p.search('cba')       # 找到,后面可以是结束
>>> m.span()                # \b本身不占用字符位
(1, 3)
>>> m=
p.search('cba ')      # 可以是空格
>>> m.span()
(1, 3)
>>> m=p.search('cba-')      # 可以是非单词字符
>>> m.span()
(1, 3)
>>> m=p.search('cbac')      # 失败

>>> m.span()

Traceback (most recent call last):
  File "<pyshell#55>", line 1, in <module>
    m.span()
AttributeError: 'NoneType' object has no attribute 'span'

>>>

()表示分组

前面讲了对单个字符的重复,如果你想重复一个子串的话,就可以采用分组的方法
>>> p=re.compile(r'(ab)+')      #匹配一个以上的ab
>>> p.search('acabababc').span()#成功
(2, 8)
>>> p=re.compile(r'(ab|bc)+')      #匹配一个以上的ab或bc
>>> 
p.search('acabbcababc').span()
(2, 10)
>>>
分组除了当作了表达式外,还有个重要的功能就是记录。(和)中匹配的内容会被记录下来,以备以后引用。在Python中,MatchObject的方法 group(),start(),end(),span()可以传入一个整形参数来得到这些记录。记录以0开始计数,并且0表示整个匹配了的表达式,是这些方法的缺省参数。分组从左到右从1开始计数。
>>> p=re.compile(r'(a(b)c)d')
>>> m=p.search('abcd')
>>> m.group()             #就是缺省参数0
'abcd'
>>> m.group(0)
'abcd'

>>> m.group(1)
'abc'
>>> m.group(2)
'b'
>>> m.group(3)            #超出范围

Traceback (most recent call last):
  File "<pyshell#33>", line 1, in <module>

    m.group(3)
IndexError: no such group
>>> m.groups()            #列出所有的分组
('abc', 'b')
>>>
在正则表达式中,还可以对分组做出向后引用,是用'\数字'来表示。即\1表示第1个分组,\2表示第2个,等。
>>> p = re.compile(r'(\b\w+)\s+\1')   # 匹配两个同样的单词连着出现,
...                        # 即找一个单词,并且在若干个空格后,还是同样的单词
...                        # \1 表示的是前面的分组(\b\w+)匹配的同样的内容
>>> 
p.search('Paris in the the spring').groups()
('the',)
>>> p.search('Paris in the the spring').group()  #找到
'the the'
>>>

非记录分组和命名分组

若一个表达式很复杂,有很多的分组,而每个分组都要记录的话就会很困难。有两种相应的方法来处理这个问题,不过属于正则表达式的扩展。

在Perl中,它定义了一种正则表达式的扩展,就是在括号后紧跟一个问题,即'(?...)'。而紧跟在问号'?'后的字符则表达了扩展的含义,如(?:foo)表示非记录型的分组含foo,而(?=foo)则表示另一种含义(表示前面有foo)。

除了实现Perl的这些扩展外,Python还有自己的扩展。若在'?'后紧跟的是P的话,则表示是Python的扩展。如(?P< name>...)定义了一个命名分组,而(?P=name)则表示的对前面的向后引用(与\1等的功能一样)。若Perl中引入了同样功能的符号,Python会同样引入,同时保留P字符功能。

(?P<name>...)和(?P=name)
(?P<name>...)用来定义一个命名分组,它可以通过MatchObject的方法group('name')得到,同时,在表达式中,也可以用(?P=name)来表示对它的引用。
>>> p = re.compile(r'(?P<word>\b\w+\b).*(?P=word)')   #注意命名分组及对它的引用
>>> m = p.search( '(((( Lots of punctuation aLots)))' )
>>> m.group()
'Lots of punctuation aLots'

>>> m.group('word')                                   #在方法中引用
'Lots'
>>> m.span('word')
(5, 9)
>>>
在imaplib模块中,有个读取INTERNALDATA的例子
InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
	r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'

        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')
显然,用InternalData.group('zonen')比InternalData.group(9)要好得多。
(?:...)
表示非记录分组。即这个分组不会被记录下来。
>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")      #分组不被记录
>>> 
m.groups()
()
(?=...)
称为零宽度正预测先行断言。它断言自身出现的位置的后面能匹配表达式...,但它不包括这些表达式。就像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(断言),因此它们也被称为零宽断言。比如\b\w+(?=ing\b),匹配以ing结尾的单词的前面部分(除了ing以外的部分)。
>>> p=re.compile(r'Rui (?=Zhang)')  #匹配Rui 后面跟Zhang时的情况,不包括Zhang在内
>>> p.search('hi, Rui Zhang').group() #成功,匹配的内容不含Zhang
'Rui '
>>> p.search
('hi, Rui Zhang').groups() #分组中没有被记录的部分
()
>>> p.search('hi, Rui Zhou').group()   #Rui 后面跟的是Zhou,失败

Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>

    p.search('hi, Rui Zhou').group()
AttributeError: 'NoneType' object has no attribute 'group'
>>>
(?<= ...)
与前面类似,也是用于指定位置,不作实际匹配,叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式...。比如(?<= \bre)\w+\b会匹配以re开头的单词的后半部分(除了re以外的部分)
>>> p=re.compile(r'(?<=Rui )Zhang')   #匹配前面是Rui 的字符Zhang
>>> p.search('hi, Rui Zhang').group() #成功
'Zhang'
>>> p.search('hi, Shan Zhang').group() #失败


Traceback (most recent call last):
  File "<pyshell#14>", line 1, in <module>
    p.search('hi, Shan Zhang').group()
AttributeError: 'NoneType' object has no attribute 'group'

>>>
(?!...)
零宽度负预测先行断言。断言此位置的后面不能匹配表达式...。例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。
>>> p=re.compile(r'Rui (?!Zhang)')    #匹配Rui 后面不是Zhang的情况
>>> p.search('hi, Rui Zhang').group() #失败

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>

    p.search('hi, Rui Zhang').group()
AttributeError: 'NoneType' object has no attribute 'group'
>>> p.search('hi, Rui Zhou').group()  #成功
'Rui '
>>>

(?<!...)
零宽度负回顾后发断言。断言此位置的前面不能匹配表达式...。(?<![a-z])\d{7}匹配前面不是小写字母的七位数字。
>>> p=re.compile(r'(?<!Rui )Zhang')   #匹配前面不是Rui 的字符Zhang
>>> p.search('hi, Rui Zhang').group() #失败

Traceback (most recent call last):
  File "<pyshell#16>", line 1, in <module>

    p.search('hi, Rui Zhang').group()
AttributeError: 'NoneType' object has no attribute 'group'
>>> p.search('hi, Shan Zhang').group()  #成功
'Zhang'
>>>

(?#...)
表示注释,其中的内容直接被忽略
>>> p=re.compile(r'Rui (?#Zhang)')  #与'Rui '是等价的
>>> p.search('Rui Zhang').groups()
()
>>> p.search('Rui Zhang').group()
'Rui '

>>>
(?iLmsux)
在"i", "L", "m", "s", "u", "x"中的一个或多个,它不匹配任何字串,而是表示对应(re.I, re.L, re.M, re.S, re.U, re.X)的6种选项。
>>> p=re.compile(r'(?m)^From:')       #与使用re.M选项是一样的
>>> p.search('hi,\nFrom: rui').span()
(4, 9)
>>>
(?(id/name)yes-pattern|no-pattern)
这是Python 2.4后加入的特性。表示若分组id或name存在的话,就匹配yes-pattern,否则就匹配no-pattern。|no-pattern是可选的,若没有的话,就表示忽略它。如(<)?(\w+@\w+(?:\.\w+)+)(?(1)>)就是个简单的E-Mail地址匹配表达式,它可以匹配'<user@host.com>'或'user@host.com'但不能匹配'< user@host.com'。其中最后一个分组表达(?(1)>)就表示若分组(1)存在(这里是指(<),即是否有符号<),则要求匹配表达式>,否则就忽略它。
>>> p=re.compile(r'(<)?(\w+@\w+(?:\.\w+)+)(?(1)>)')
>>> m=p.search('<xxx@gmail.com.cn>')
>>> m.group
()
'<xxx@gmail.com.cn>'
>>> m.groups()
('<', 'xxx@gmail.com.cn')
>>> m=p.search('<
xxx@gmail.com')
>>> m.group()
'xxx@gmail.com'
>>> m.groups()
(None, 'xxx@gmail.com
') >>> m=p.search('xx@gmail.com') >>> m.group() 'xx@gmail.com' >>> m.groups() (None, ' xx@gmail.com') >>>

正则表达式的优先权顺序

在构造正则表达式之后,就可以象数学表达式一样来求值,也就是说,可以从左至右并按照一个优先权顺序来求值。

下表从最高优先级到最低优先级列出各种正则表达式操作符的优先权顺序:

操作符 描述
\ 转义符
(), (?:), (?=), [] 圆括号和方括号
*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, \anymetacharacter 位置和顺序
| "或"操作

小结

"."
(点.)代表任何非回车换行字符。当DOTALL选项指定时候,可以匹配包括回车在内的任意字符。
"^"和"$"
表示字串的头和尾。若指定选项MULTILINE,还表示一行的头和尾
"*"
表示重复0或更多个
"+"
表示重复1个以上
"?"
表示重复0或1个
*?, +?, ??
是"*","+"和"?"的懒惰匹配,即尽可能少的匹配
{m}
重复m个
{m,n}
重复n到m个
{m,n}?
{m,n}的懒惰匹配
"\"
转义字符。
[]
字符集。匹配[]中指定的任何一个。可以用"-"来指定一个范围,也可以用[^...]来表示不是其中的字符
"|"
A|B,其中A和B可以是表达式,表示匹配A或B中的任意一个
(...)
分组
(?...)
正则表达式的扩展标识
(?iLmsux)
(取字符"i", "L", "m", "s", "u", "x"中的1个或多个)不匹配任何字串,仅仅用于设置选项。它们分别对应于(re.I, re.L, re.M, re.S, re.U, re.X),一般放在表达式的第一位
(?:...)
非记录分组
(?P<name>...)
命名分组
(?P=name)
取出命名的分组
(?#...)
注释
(?=...)
先行断言。断言此位置的后面能匹配表达式...。
(?!...)
负先行断言。断言此位置的后面不能匹配表达式...。
(?<=...)
后发断言。断言此位置的前面能匹配表达式...。
(?<!...)
负后发断言。断言此位置的前面不能匹配表达式...。
(?(id/name)yes-pattern|no-pattern)
若分组id若name已经匹配,则使用yes-pattern,否则用no-pattern。|no-pattern可选。

一个好的网址

正则表达式30分钟入门教程
Regular Expression HOWTO

--
6G免费网络U盘: http://www.orbitfiles.com/signup/rleon
Python学习笔记 - 正则表达式 (by Rui Zhang)