• 写爬虫,怎么可以不会正则呢?


    很多人觉得正则很难,在我看来,这些人一定是没有用心。其实正则很简单,根据二八原则,我们只需要懂 20% 的内容就可以解决 80% 的问题了。我曾经有几年几乎每天都跟正则打交道,刚接手项目的时候我对正则也是一无所知,花半小时百度了一下,然后写了几个 demo,就开始正式接手了。三年多时间,我用到的正则鲜有超出我最初半小时百度到的知识的。

    1、正则基础

    1.1、基础语法

    (1)常用元字符

    语法描述
     匹配单词的开始或结束
    d 匹配数字
    s 匹配任意不可见字符(空格、换行符、制表符等),等价于[ f v]。
    w 匹配任意 Unicode 字符集,包括字母、数字、下划线、汉字等
    . 匹配除换行符( )以外的任意字符
    ^ 或 A 匹配字符串或行的起始位置
    $ 或  匹配字符串或行的结束位置

    (2)限定词(又叫量词)

    语法描述
    * 重复零次或更多次
    + 重复一次或更多次
    ? 重复零次或一次
    {n} 重复 n 次
    {n,} 重复 n 次或更多次
    {n,m} 重复 n 到 m 次

    (3)常用反义词

    语法描述
    B 匹配非单词的开始或结束
    D 匹配非数字
    S 匹配任意可见字符, [^ f v]
    W 匹配任意非 Unicode 字符集
    [^abc] 除 a、b、c 以外的任意字符

    (4)字符族

    语法描述
    [abc] a、b 或 c
    [^abc] 除 a、b、c 以外的任意字符
    [a-zA-Z] a 到 z 或 A 到 Z
    [a-d[m-p]] a 到 d 或 m 到 p,即 [a-dm-p](并集)
    [a-z&&[def]] d、e 或 f(交集)
    [a-z&&[^bc]] a 到 z,除了 b 和 c:[ad-z](减去)
    [a-z&&[^m-p]] a 到 z,减去 m 到 p:[a-lq-z](减去)

    以上便是正则的基础内容,下面来写两个例子看下:

    s = '123abc你好'
    re.search('d+', s).group()
    re.search('w+', s).group()

    结果:

    123
    123abc你好

    是不是很简单?

    1.2、修饰符

    修饰符在各语言中也是有差异的。

    Python 中的修饰符:

    修饰符描述
    re.A 匹配 ASCII字符类,影响 w, W, , B, d, D
    re.I 忽略大小写
    re.L 做本地化识别匹配(这个极少极少使用)
    re.M 多行匹配,影响 ^ 和 $
    re.S 使 . 匹配包括换行符( )在内的所有字符
    re.U 匹配 Unicode 字符集。与 re.A 相对,这是默认设置
    re.X 忽略空格和 # 后面的注释以获得看起来更易懂的正则。

    (1)re.A

    修饰符 A 使 w 只匹配 ASCII 字符,W 匹配非 ASCII 字符。

    s = '123abc你好'
    re.search('w+', s, re.A).group()
    re.search('W+', s, re.A).group()

    结果:

    123abc
    你好

    但是描述中还有 d 和 D,数字不都是 ASCII 字符吗?这是什么意思?别忘了,还有 全角和半角

    s = '0123456789'    # 全角数字
    re.search('d+', s, re.U).group()

    结果:

    0123456789

    (2)re.M
    多行匹配的模式其实也不常用,很少有一行行规整的数据。

    s = 'aaa
    bbb
    ccc'
    
    re.findall('^[sw]*?$', s)
    re.findall('^[sw]*?$', s, re.M)

    结果:

    ['aaa
    bbb
    ccc']        # 单行模式
    ['aaa
    ', 'bbb
    ', 'ccc']    # 多行模式

    (3)re.S
    这个简单,直接看个例子。

    s = 'aaa
    bbb
    ccc'
    
    re.findall('^.*', s)
    re.findall('^.*', s, re.S)

    结果:

    ['aaa
    ']
    ['aaa
    bbb
    ccc']
     

    (4)re.X
    用法如下:

    rc = re.compile(r"""
    d+ # 匹配数字
    # 和字母
    [a-zA-Z]+
    """, re.X)
    rc.search('123abc').group()
     

    结果:

    123abc

    注意,用了 X 修饰符后,正则中的所有空格会被忽略,包括正则里面的原本有用的空格。如果正则中有需要使用空格,只能用 s 代替。

    (5)(?aiLmsux)
    修饰符不仅可以代码中指定,也可以在正则中指定。(?aiLmsux) 表示了以上所有的修饰符,具体用的时候需要哪个就在 ? 后面加上对应的字母,示例如下,(?a) 和 re.A 效果是一样的:

    s = '123abc你好'
    re.search('(?a)w+', s).group()
    re.search('w+', s, re.A).group()

    结果是一样的:

    123abc
    123abc

    1.3、贪婪与懒惰

    当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。

    s = 'aabab'
    re.search('a.*b', s).group()    # 这就是贪婪
    re.search('a.*?b', s).group()   # 这就是懒惰
     

    结果:

    aabab
    aab

    简单来说:

    • 所谓贪婪,就是尽可能  的匹配;

    • 所谓懒惰,就是尽可能  的匹配。

    • *+{n,} 这些表达式属于贪婪;

    • *?+?{n,}? 这些表达式就是懒惰(在贪婪的基础上加上 ?)。

    2、正则进阶

    2.1、捕获分组

    语法描述
    (exp) 匹配exp,并捕获文本到自动命名的组里
    (?Pexp) 匹配exp,并捕获文本到名称为 name 的组里
    (?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号
    (?P=name) 匹配之前由名为 name 的组匹配的文本

    注意:在其他语言或者网上的一些正则工具中,分组命名的语法是 (?<name>exp) 或 (?'name'exp) ,但在 Python 里,这样写会报错:This named group syntax is not supported in this regex dialect。Python 中正确的写法是:(?P<name>exp)

    示例一:

    分组可以让我们用一条正则提取出多个信息,例如:

    s = '姓名:张三;性别:男;电话:138123456789'
    m = re.search('姓名[::](w+).*?电话[::](d{11})', s)
    if m:
        name = m.group(1)
        phone = m.group(2)
        print(f'name:{name}, phone:{phone}')

    结果:

    name:张三, phone:13812345678

    示例二:

    (?P<name>exp) 有时还是会用到的, (?P=name) 则很少情况下会用到。我想了一个 (?P=name) 的使用示例,给大家看下效果:

    s = '''
    <name>张三</name>
    <age>30</age>
    <phone>138123456789</phone>
    '''
    
    pattern = r'<(?P<name>.*?)>(.*?)</(?P=name)>'
    It = re.findall(pattern, s)
     

    结果:

    [('name', '张三'), ('age', '30'), ('phone', '138123456789')]
     

    2.2、零宽断言

    语法描述
    (?=exp) 匹配exp前面的位置
    (?<=exp) 匹配exp后面的位置
    (?!exp) 匹配后面跟的不是exp的位置
    (?<!exp) 匹配前面不是exp的位置

    注意:正则中常用的前项界定 (?<=exp) 和前项否定界定 (?<!exp) 在 Python 中可能会报错:look-behind requires fixed-width pattern,原因是 python 中 前项界定的表达式必须是定长的,看如下示例:

    (?<=aaa)        # 正确
    (?<=aaa|bbb)    # 正确
    (?<=aaa|bb)     # 错误
    (?<=d+)        # 错误
    (?<=d{3})      # 正确
     

    2.3、条件匹配

    这大概是最复杂的正则表达式了。语法如下:

    语法描述
    (?(id/name)yes|no) 如果指定分组存在,则匹配 yes 模式,否则匹配 no 模式

    此语法极少用到,印象中只用过一次。

    以下示例的要求是:如果以 _ 开头,则以字母结尾,否则以数字结尾。

    s1 = '_abcd'
    s2 = 'abc1'
    
    pattern = '(_)?[a-zA-Z]+(?(1)[a-zA-Z]|d)'
    
    re.search(pattern, s1).group()
    re.search(pattern, s2).group()

    结果:

    _abcd
    abc1

    2.4、findall

    Python 中的 re.findall 是个比较特别的方法(之所以说它特别,是跟我常用的 C# 做比较,在没看注释之前我想当然的掉坑里去了)。我们看这个方法的官方注释:

    Return a list of all non-overlapping matches in the string.

    If one or more capturing groups are present in the pattern, return 
    a list of groups; this will be a list of tuples if the pattern 
    has more than one group.

    Empty matches are included in the result.

    简单来说,就是

    • 如果没有分组,则返回整条正则匹配结果的列表;

    • 如果有 1 个分组,则返回分组匹配到的结果的列表;

    • 如果有多个分组,则返回分组匹配到的结果的元组的列表。

    看下面的例子:

    s = 'aaa123bbb456ccc'
    
    re.findall('[a-z]+d+', s)          # 不包含分组
    re.findall('[a-z]+(d+)', s)        # 包含一个分组
    re.findall('([a-z]+(d+))', s)      # 包含多个分组
    re.findall('(?:[a-z]+(d+))', s)    # ?: 不捕获分组匹配结果
     

    结果:

    ['aaa123', 'bbb456']
    ['123', '456']
    [('aaa123', '123'), ('bbb456', '456')]
    ['123', '456']

    零宽断言中讲到 Python 中前项界定必须是定长的,这很不方便,但是配合 findall 有分组时只取分组结果的特性,就可以模拟出非定长前项界定的效果了。

    结语

    其实正则就像是一个数学公式,会背公式不一定会做题。但其实这公式一点也不难,至少比学校里学的数学简单多了,多练习几次也就会了。

    摘自:微信公众号: 码农升级

  • 相关阅读:
    校园导游图的课程设计(三)
    校园导游图的课程设计(二)
    vim中Mapping already in use: "<LocalLeader>is", mode "n"错误解决的方法解释
    fedora 的截图快捷键
    校园导游图的课程设计(一)
    theos(一)
    脱壳
    初识Mach-O
    Cycript(二)
    Cycript(一)
  • 原文地址:https://www.cnblogs.com/tjp40922/p/14058123.html
Copyright © 2020-2023  润新知