python3 cookbook 笔记八

写在前面

终于有空继续cookbook的第二章了,这一章讲的是字符串和文本的处理,看了前面几小节发现用的方法大同小异,于是准备这一章呢就不一节一节的讲了,综合梳理一下再总结一些新学到的知识点和难点。

re模块

有一说一,正则表达式这个东西上手之后真的很方便,在平时的工作里面也有很大用处,比如sublimepycharm的筛选(ctrl+F)可以直接用到。回归正题,re模块我之前用的比较多的应该是searchfindall,在爬虫里面对html过滤拿到需要的部分,match用来匹配字符串,还有一些方法是我不常用但是对字符串能起到奇特效果的,下面介绍一下。

re.split

>>> line = 'asdf fjdk; afed, fjek,asdf, foo'
>>> import re
>>> re.split(r'[;,\s]\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

一般字符串的split方法只能传入一个分隔符,然而这里的re.split技高一筹,这里的分隔符是[]里面任意一个后面紧跟着任意个的空格,只要这个模式被找到,那么匹配的分隔符两边的实体都会被当成是结果中的元素返回。需要特别注意的是正则表达式中是否包含一个括号捕获分组, 如果使用了捕获分组,那么被匹配的文本也将出现在结果列表中。比如:

>>> fields = re.split(r'([;,\s]\s*)', line)
>>> fields
['asdf', ' ', 'fjdk', '; ', 'afed', ', ', 'fjek', ',', 'asdf', ', ', 'foo']

>>> values = fields[::2]
>>> delimiters = fields[1::2] + ['']

re.match

好像没啥好讲的,注意下匹配和分组出来的结果吧:

>>> import re
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>>> m = datepat.match('11/27/2012')
>>> m
<_sre.SRE_Match object at 0x1005d2750>
>>> # Extract the contents of each group
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'
>>> m.group(2)
'27'
>>> m.group(3)
'2012'
>>> m.groups()
('11', '27', '2012')
>>> month, day, year = m.groups()

re.findall

作用类似re.match,不过前者是从字符串开始去匹配,而re.findall会根据模式对整个字符串进行匹配,返回一个包含所有结果的列表:

>>> datepat = re.compile(r'\d+/\d+/\d+')
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
['11/27/2012', '3/13/2013']

>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
>[('11', '27', '2012'), ('3', '13', '2013')]

re.sub

我们都知道字符串替换用str.replace,这种方法只能进行简单的替换,对于复杂的模式可以使用re.sub函数,为了说明这个,假设你想将形式为 11/27/2012 的日期字符串改成 2012-11-27 。示例如下:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> import re
>>> re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'

sub函数的第一个参数自然是被匹配的模式,第二个参数是替换模式。反斜杠数字比如 \3 指向前面模式的捕获组号。
如果你使用了命名分组,那么第二个参数就使用 \g<group_name>

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> re.sub(r'(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)', r'\g<year>-\g<month>-\g<day>', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'

如果你还有更复杂的需求,那么你可以传递一个替换回调函数来代替简单的替换字符串:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> from calendar import month_abbr
>>> def change_date(m):
>>>		mon_name = month_abbr[int(m.group(1))]
>>> 	return '{} {} {}'.format(m.group(2), mon_name, m.group(3))
>>> datepat.sub(change_date, text)
'Today is 27 Nov 2012. PyCon starts 13 Mar 2013.'

字符串开头结尾匹配

这个当然是用str.startswith或者是str.endswith方法啦:

>>> filename = 'spam.txt'
>>> filename.endswith('.txt')
True
>>> filename.startswith('file:')
False

细心的同学会发现startend后面都带了个s,这种命名就是为了提醒了这两个方法可以匹配多种可能,你只需要将所有的匹配项都放到一个元组里面传过去:

>>> import os
>>> filenames = os.listdir('.')
>>> filenames
[ 'Makefile', 'foo.c', 'bar.py', 'spam.c', 'spam.h' ]
>>> [name for name in filenames if name.endswith(('.c', '.h')) ]
['foo.c', 'spam.c', 'spam.h'
>>> any(name.endswith('.py') for name in filenames)
True

切片也可以做,但是不优雅。
正则也可以做,但是不简单。


将Unicode文本标准化

在Unicode中,某些字符能够用多个合法的编码表示。为了说明,考虑下面的这个例子:

>>> s1 = 'Spicy Jalape\u00f1o'
>>> s2 = 'Spicy Jalapen\u0303o'
>>> s1
'Spicy Jalapeño'
>>> s2
'Spicy Jalapeño'
>>> s1 == s2
False
>>> len(s1)
14
>>> len(s2)
15

这里的文本”Spicy Jalapeño”使用了两种形式来表示。 第一种使用整体字符”ñ”(U+00F1),第二种使用拉丁字母”n”后面跟一个”~”的组合字符(U+0303)。

在需要比较字符串的程序中使用字符的多种表示会产生问题。 为了修正这个问题,你可以使用unicodedata模块先将文本标准化:

>>> import unicodedata
>>> t1 = unicodedata.normalize('NFC', s1)
>>> t2 = unicodedata.normalize('NFC', s2)
>>> t1 == t2
True

字符串对齐

对于基本的字符串对齐操作,可以使用字符串的 ljust() , rjust()center() 方法。比如:

>>> text = 'Hello World'
>>> text.ljust(20)
'Hello World         '
>>> text.rjust(20)
'         Hello World'
>>> text.center(20)
'    Hello World     '

>>> text.rjust(20,'=')
'=========Hello World'
>>> text.center(20,'*')
'****Hello World*****'

合并拼接字符串

join()就完事了,千万别写这种代码s+=s1,特别在循环里面。

字符串中插入变量

就是很简单的使用format,但一步一步深入:

>>> s = '{name} has {n} messages.'
>>> s.format(name='Guido', n=37)
'Guido has 37 messages.'
>>> name = 'Guido'
>>> n = 37
>>> s.format_map(vars())
'Guido has 37 messages.'
>>> class Info:
...     def __init__(self, name, n):
...         self.name = name
...         self.n = n
...
>>> a = Info('Guido',37)
>>> s.format_map(vars(a))
'Guido has 37 messages.'

但是formatformat_map的一个缺陷就是它们并不能很好的处理变量缺失的情况,比如:

>>> s.format(name='Guido')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'n'

一种避免这种错误的方法是另外定义一个含有 __missing__() 方法的字典对象,就像下面这样:

class safesub(dict):
	"""防止key找不到"""
	def __missing__(self, key):
    	return '{' + key + '}'

>>> del n # Make sure n is undefined
>>> s.format_map(safesub(vars()))
'Guido has {n} messages.'

如果你发现自己在代码中频繁的执行这些步骤,你可以将变量替换步骤用一个工具函数封装起来。就像下面这样:

import sys

def sub(text):
    return text.format_map(safesub(sys._getframe(1).f_locals))


>>> name = 'Guido'
>>> n = 37
>>> print(sub('Hello {name}'))
Hello Guido
>>> print(sub('You have {n} messages.'))
You have 37 messages.
>>> print(sub('Your favorite color is {color}'))
Your favorite color is {color}

sub() 函数使用 sys._getframe(1) 返回调用者的栈帧。可以从中访问属性 f_locals 来获得局部变量。 毫无疑问绝大部分情况下在代码中去直接操作栈帧应该是不推荐的。 但是,对于像字符串替换工具函数而言它是非常有用的。 另外,值得注意的是 f_locals 是一个复制调用函数的本地变量的字典。 尽管你可以改变 f_locals 的内容,但是这个修改对于后面的变量访问没有任何影响。 所以,虽说访问一个栈帧看上去很邪恶,但是对它的任何操作不会覆盖和改变调用者本地变量的值。


以指定列宽格式化字符串

使用 textwrap 模块来格式化字符串的输出。比如,假如你有下列的长字符串:

s = "Look into my eyes, look into my eyes, the eyes, the eyes, \
the eyes, not around the eyes, don't look around the eyes, \
look into my eyes, you're under."

>>> import textwrap
>>> print(textwrap.fill(s, 70))
Look into my eyes, look into my eyes, the eyes, the eyes, the eyes,
not around the eyes, don't look around the eyes, look into my eyes,
you're under.

>>> print(textwrap.fill(s, 40))
Look into my eyes, look into my eyes,
the eyes, the eyes, the eyes, not around
the eyes, don't look around the eyes,
look into my eyes, you're under.

>>> print(textwrap.fill(s, 40, initial_indent='    '))
    Look into my eyes, look into my
eyes, the eyes, the eyes, the eyes, not
around the eyes, don't look around the
eyes, look into my eyes, you're under.

>>> print(textwrap.fill(s, 40, subsequent_indent='    '))
Look into my eyes, look into my eyes,
    the eyes, the eyes, the eyes, not
    around the eyes, don't look around
    the eyes, look into my eyes, you're
    under.

字符串令牌解析

这一节看起来很厉害的样子,虽然平时可能用不到,还是搬一下吧。
假如你有下面这样一个文本字符串:

text = 'foo = 23 + 42 * 10'

为了令牌化字符串,你不仅需要匹配模式,还得指定模式的类型。 比如,你可能想将字符串像下面这样转换为序列对:

tokens = [('NAME', 'foo'), ('EQ','='), ('NUM', '23'), ('PLUS','+'),
          ('NUM', '42'), ('TIMES', '*'), ('NUM', '10')]

为了执行这样的切分,第一步就是像下面这样利用命名捕获组的正则表达式来定义所有可能的令牌,包括空格:

import re
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'
NUM = r'(?P<NUM>\d+)'
PLUS = r'(?P<PLUS>\+)'
TIMES = r'(?P<TIMES>\*)'
EQ = r'(?P<EQ>=)'
WS = r'(?P<WS>\s+)'

master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))

在上面的模式中, ?P<TOKENNAME> 用于给一个模式命名,供后面使用。

下一步,为了令牌化,使用模式对象很少被人知道的 scanner() 方法。 这个方法会创建一个 scanner 对象, 在这个对象上不断的调用 match() 方法会一步步的扫描目标文本,每步一个匹配。 下面是演示一个 scanner 对象如何工作的交互式例子:

>>> scanner = master_pat.scanner('foo = 42')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('NAME', 'foo')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('WS', ' ')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('EQ', '=')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('WS', ' ')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('NUM', '42')
>>> scanner.match()
>>>

实际使用这种技术的时候,可以很容易的像下面这样将上述代码打包到一个生成器中:

def generate_tokens(pat, text):
    Token = namedtuple('Token', ['type', 'value'])
    scanner = pat.scanner(text)
    for m in iter(scanner.match, None):
        yield Token(m.lastgroup, m.group())

# Example use
for tok in generate_tokens(master_pat, 'foo = 42'):
    print(tok)
# Produces output
# Token(type='NAME', value='foo')
# Token(type='WS', value=' ')
# Token(type='EQ', value='=')
# Token(type='WS', value=' ')
# Token(type='NUM', value='42')

如果你想过滤令牌流,你可以定义更多的生成器函数或者使用一个生成器表达式。 比如,下面演示怎样过滤所有的空白令牌:

tokens = (tok for tok in generate_tokens(master_pat, text)
          if tok.type != 'WS')
for tok in tokens:
    print(tok)

通常来讲令牌化是很多高级文本解析与处理的第一步。 为了使用上面的扫描方法,你需要记住这里一些重要的几点。 第一点就是你必须确认你使用正则表达式指定了所有输入中可能出现的文本序列。 如果有任何不可匹配的文本出现了,扫描就会直接停止。这也是为什么上面例子中必须指定空白字符令牌的原因。

令牌的顺序也是有影响的。 re 模块会按照指定好的顺序去做匹配。 因此,如果一个模式恰好是另一个更长模式的子字符串,那么你需要确定长模式写在前面。比如:

LT = r'(?P<LT><)'
LE = r'(?P<LE><=)'
EQ = r'(?P<EQ>=)'

master_pat = re.compile('|'.join([LE, LT, EQ])) # Correct
# master_pat = re.compile('|'.join([LT, LE, EQ])) # Incorrect

实现一个简单的递归下降分析器

太长就不贴了,重点需要理解这个图:

expr ::= term { (+|-) term }*

term ::= factor { (*|/) factor }*

factor ::= ( expr )
    |   NUM

链接给你自己看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值