current position:Home>[Python crawler] 5. Regular expression re module for data extraction

[Python crawler] 5. Regular expression re module for data extraction

2022-11-08 09:25:30Deng Dashuai


Summary of past content:


一、非结构化数据与结构化数据

一般来讲对我们而言,需要抓取的是某个网站或者某个应用的内容,提取有用的价值.内容一般分为两部分,非结构化的数据 和 结构化的数据.

  • 非结构化数据:先有数据,再有结构.
  • 结构化数据:先有结构、再有数据.
  • 不同类型的数据,我们需要采用不同的方式来处理.
处理方式非结构化数据结构化数据
正则表达式文本、电话号码、邮箱地址、HTML 文件XML 文件
XPathHTML 文件XML 文件
CSS选择器HTML 文件XML 文件
JSON PathJSON 文件
转化成Python类型JSON 文件(json类)、XML 文件(xmltodict)

二、了解正则表达式

There are four main steps in the crawler:

  1. 明确目标 (要知道你准备在哪个范围或者网站去搜索)
  2. 爬 (将所有的网站的内容全部爬下来)
  3. 取 (去掉对我们没用处的数据)
  4. 处理数据(按照我们想要的方式存储和使用)

在前面的学习中,我们掌握了“爬”数据方法,此时我们downThe data below are all web pages,这些数据很庞大并且很混乱,大部分的东西使我们不关心的,因此我们需要将之按我们的需要过滤和匹配出来,This involves the third step of the crawler:提取.

当前,For text filtering or rule matching,最强大的就是正则表达式,It is suitable for both structured and unstructured data extraction,是Python爬虫世界里必不可少的神兵利器.

正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本.

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来表达对字符串的一种过滤逻辑.

给定一个正则表达式和另一个字符串,我们可以达到如下的目的:

  • 给定的字符串是否符合正则表达式的过滤逻辑("匹配");
  • 通过正则表达式,从文本字符串中获取我们想要的特定部分("过滤").

在这里插入图片描述


三、正则表达式匹配规则

在这里插入图片描述

? = {0,1} ; + = {1,+∞} ; * = {0,+∞}
定位: re.findall("(?<=xxxxx).*?(?=xxxxx)") ;其中xxxIt is the content limited before and after,.*?is the acquired content


四:Python 的 re 模块

在 Python 中,我们可以使用内置的 re 模块来使用正则表达式.

有一点需要特别注意的是,正则表达式使用 对特殊字符进行转义,所以如果我们要使用原始字符串,只需加一个 r 前缀,示例:

r'chuanzhiboke\t\.\tpython'

re 模块的一般使用步骤如下:

  1. 使用 compile() 函数将正则表达式的字符串形式编译为一个 Pattern 对象

  2. 通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果,一个 Match 对象.

  3. 最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作

import re

# 将正则表达式编译成 Pattern 对象
pattern = re.compile(r'\d+')

接下来,我们就可以利用 pattern 的一系列方法对文本进行匹配查找了.

Pattern 对象的一些常用方法主要有:

  • match 方法:从起始位置开始查找,一次匹配
  • search 方法:从任何位置开始查找,一次匹配
  • findall 方法:全部匹配,返回列表
  • finditer 方法:全部匹配,返回迭代器
  • split 方法:分割字符串,返回列表
  • sub 方法:替换

(1)match 方法

match 方法用于查找字符串的头部(也可以指定起始位置),它是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果.它的一般使用形式如下:

match(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度).因此,当你不指定 pos 和 endpos 时,match 方法默认匹配字符串的头部.

当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None.

>>> import re
>>> pattern = re.compile(r'\d+')                      # 用于匹配至少一个数字

>>> m = pattern.match('one12twothree34four')          # 查找头部,没有匹配
>>> print (m)
None

>>> m = pattern.match('one12twothree34four', 2, 10)   # 从'e'的位置开始匹配,没有匹配
>>> print (m)
None

>>> m = pattern.match('one12twothree34four', 3, 10)   # 从'1'的位置开始匹配,正好匹配
>>> print (m)                                         # 返回一个 Match 对象
<_sre.SRE_Match object at 0x10a42aac0>

>>> m.group(0)   # 可省略 0
'12'
>>> m.start(0)   # 可省略 0
3
>>> m.end(0)     # 可省略 0
5
>>> m.span(0)    # 可省略 0
(3, 5)

在上面,当匹配成功时返回一个 Match 对象,其中:

  • group([group1, ...]) 方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group() 或 group(0);

  • start([group]) 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;

  • end([group]) 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;

  • span([group]) 方法返回 (start(group), end(group)).

再看看一个例子:

>>> import re
>>> pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I)  # re.I 表示忽略大小写
>>> m = pattern.match('Hello World Wide Web')

>>> print (m)   # 匹配成功,返回一个 Match 对象
<_sre.SRE_Match object at 0x10bea83e8>

>>> m.group(0)  # 返回匹配成功的整个子串
'Hello World'

>>> m.span(0)   # 返回匹配成功的整个子串的索引
(0, 11)

>>> m.group(1)  # 返回第一个分组匹配成功的子串
'Hello'

>>> m.span(1)   # 返回第一个分组匹配成功的子串的索引
(0, 5)

>>> m.group(2)  # 返回第二个分组匹配成功的子串
'World'

>>> m.span(2)   # 返回第二个分组匹配成功的子串
(6, 11)

>>> m.groups()  # 等价于 (m.group(1), m.group(2), ...)
('Hello', 'World')

>>> m.group(3)  # 不存在第三个分组
  Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
  IndexError: no such group

(2)search 方法

search 方法用于查找字符串的任何位置,它也是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果,它的一般使用形式如下:

search(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度).

当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None.

让我们看看例子:

>>> import re
>>> pattern = re.compile('\d+')
>>> m = pattern.search('one12twothree34four')  # 这里如果使用 match 方法则不匹配
>>> m
<_sre.SRE_Match object at 0x10cc03ac0>
>>> m.group()
'12'
>>> m = pattern.search('one12twothree34four', 10, 30)  # 指定字符串区间
>>> m
<_sre.SRE_Match object at 0x10cc03b28>
>>> m.group()
'34'
>>> m.span()
(13, 15)

再来看一个例子:

# -*- coding: utf-8 -*-

import re
# 将正则表达式编译成 Pattern 对象
pattern = re.compile(r'\d+')
# 使用 search() 查找匹配的子串,不存在匹配的子串时将返回 None
# 这里使用 match() 无法成功匹配
m = pattern.search('hello 123456 789')
if m:
    # 使用 Match 获得分组信息
    print ('matching string:',m.group())
    # 起始位置和结束位置
    print ('position:',m.span())

执行结果:

matching string: 123456
position: (6, 12)

(3)findall 方法

上面的 match 和 search 方法都是一次匹配,只要找到了一个匹配的结果就返回.然而,在大多数时候,我们需要搜索整个字符串,获得所有匹配的结果.

findall 方法的使用形式如下:

findall(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度).

findall 以列表形式返回全部能匹配的子串,如果没有匹配,则返回一个空列表.

看看例子:

import re
pattern = re.compile(r'\d+')   # 查找数字

result1 = pattern.findall('hello 123456 789')
result2 = pattern.findall('one1two2three3four4', 0, 10)

print (result1)
print (result2)

执行结果:

['123456', '789']
['1', '2']

再举一个例子:

# re_test.py

import re

#re模块提供一个方法叫compile模块,提供我们输入一个匹配的规则
#然后返回一个pattern实例,我们根据这个规则去匹配字符串
pattern = re.compile(r'\d+\.\d*')

#通过partten.findall()方法就能够全部匹配到我们得到的字符串
result = pattern.findall("123.141593, 'bigcat', 232312, 3.15")

#findall 以 列表形式 返回全部能匹配的子串给result
for item in result:
    print (item)

运行结果:

123.141593
3.15

(4)finditer 方法

finditer 方法的行为跟 findall 的行为类似,也是搜索整个字符串,获得所有匹配的结果.但它返回一个顺序访问每一个匹配结果(Match 对象)的迭代器.

看看例子:

# -*- coding: utf-8 -*-

import re
pattern = re.compile(r'\d+')

result_iter1 = pattern.finditer('hello 123456 789')
result_iter2 = pattern.finditer('one1two2three3four4', 0, 10)

print (type(result_iter1))
print (type(result_iter2))

print 'result1...'
for m1 in result_iter1:   # m1 是 Match 对象
    print ('matching string: {}, position: {}'.format(m1.group(), m1.span()))

print 'result2...'
for m2 in result_iter2:
    print ('matching string: {}, position: {}'.format(m2.group(), m2.span()))

执行结果:

<type 'callable-iterator'>
<type 'callable-iterator'>
result1...
matching string: 123456, position: (6, 12)
matching string: 789, position: (13, 16)
result2...
matching string: 1, position: (3, 4)
matching string: 2, position: (7, 8)

(5)split 方法

split 方法按照能够匹配的子串将字符串分割后返回列表,它的使用形式如下:

split(string[, maxsplit])

其中,maxsplit 用于指定最大分割次数,不指定将全部分割.

看看例子:

import re
p = re.compile(r'[\s\,\;]+')
print (p.split('a,b;; c d'))

执行结果:

['a', 'b', 'c', 'd']

(6)sub 方法

sub 方法用于替换.它的使用形式如下:

sub(repl, string[, count])

其中,repl 可以是字符串也可以是一个函数:

  • 如果 repl 是字符串,则会使用 repl 去替换字符串每一个匹配的子串,并返回替换后的字符串,另外,repl 还可以使用 id 的形式来引用分组,但不能使用编号 0;

  • 如果 repl 是函数,这个方法应当只接受一个参数(Match 对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组).

  • count 用于指定最多替换次数,不指定时全部替换.

看看例子:

import re
p = re.compile(r'(\w+) (\w+)') # \w = [A-Za-z0-9]
s = 'hello 123, hello 456'

print (p.sub(r'hello world', s))  # 使用 'hello world' 替换 'hello 123' 和 'hello 456'
print (p.sub(r'\2 \1', s))        # 引用分组

def func(m):
    print(m)
    return 'hi' + ' ' + m.group(2) #group(0) 表示本身,group(1)表示hello,group(2) 表示后面的数字

print (p.sub(func, s))  #多次sub,每次sub的结果传递给func
print (p.sub(func, s, 1))         # 最多替换一次

执行结果:

hello world, hello world
123 hello, 456 hello
hi 123, hi 456
hi 123, hello 456

(7)匹配中文

在某些情况下,我们想匹配文本中的汉字,有一点需要注意的是,中文的 unicode 编码范围 主要在 [u4e00-u9fa5],这里说主要是因为这个范围并不完整,比如没有包括全角(中文)标点,不过,在大部分情况下,应该是够用的.

假设现在想把字符串 title = u'你好,hello,世界' 中的中文提取出来,可以这么做:

import re

title = '你好,hello,世界'
pattern = re.compile(r'[\u4e00-\u9fa5]+')
result = pattern.findall(title)

print (result)

注意到,We prefixed the regular expression with two prefixes ur,其中 r 表示使用原始字符串,u 表示是 unicode 字符串.

执行结果:

['你好', '世界']

五、贪婪模式与非贪婪模式

  1. 贪婪模式:在整个表达式匹配成功的前提下,尽可能多的匹配 ( * );
  2. 非贪婪模式:在整个表达式匹配成功的前提下,尽可能少的匹配 ( ? );
  3. Python里数量词默认是贪婪的.

示例一 : 源字符串:abbbc

  • 使用贪婪的数量词的正则表达式 ab* ,匹配结果: abbb.

    * 决定了尽可能多匹配 b,所以a后面所有的 b 都出现了.

  • 使用非贪婪的数量词的正则表达式ab*?,匹配结果: a.

    即使前面有 *,但是 ? 决定了尽可能少匹配 b,所以没有 b.

示例二 : 源字符串:aa<div>test1</div>bb<div>test2</div>cc

  • 使用贪婪的数量词的正则表达式:<div>.*</div>

  • 匹配结果:<div>test1</div>bb<div>test2</div>

这里采用的是贪婪模式.在匹配到第一个"</div>"时已经可以使整个表达式匹配成功,但是由于采用的是贪婪模式,所以仍然要向右尝试匹配,查看是否还有更长的可以成功匹配的子串.匹配到第二个"</div>"后,向右再没有可以成功匹配的子串,匹配结束,匹配结果为"<div>test1</div>bb<div>test2</div>"

  • 使用非贪婪的数量词的正则表达式:<div>.*?</div>

  • 匹配结果:<div>test1</div>

正则表达式二采用的是非贪婪模式,在匹配到第一个"</div>"时使整个表达式匹配成功,由于采用的是非贪婪模式,所以结束匹配,不再向右尝试,匹配结果为"<div>test1</div>".

正则表达式测试网址


六、Regular expression crawler combat

现在拥有了正则表达式这把神兵利器,我们就可以进行对爬取到的全部网页源代码进行筛选了.

下面我们一起尝试一下爬取内涵段子网站: http://www.neihan8.com/article/list_5_1.html

打开之后,不难看到里面一个一个灰常有内涵的段子,当你进行翻页的时候,注意url地址的变化:

  • 第一页url: http: //www.neihan8.com/article/list_5_1 .html

  • 第二页url: http: //www.neihan8.com/article/list_5_2 .html

  • 第三页url: http: //www.neihan8.com/article/list_5_3 .html

  • 第四页url: http: //www.neihan8.com/article/list_5_4 .html

这样我们的url规律找到了,要想爬取所有的段子,只需要修改一个参数即可. 下面我们就开始一步一步将所有的段子爬取下来吧.

第一步:获取数据

按照我们之前的用法,我们需要写一个加载页面的方法.这里我们统一定义一个类,将url请求作为一个成员方法处理.

创建一个文件,叫duanzi_spider.py,然后定义一个Spider类,并且添加一个加载页面的成员方法

class Duanzi_spider():
    def __init__(self):
        self.url = "http://www.neihan8.com/article/list_5_%s.html"
        self.headers = {
    
            "User_Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleW\ ebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
            "Accept-Encoding":None,
            "Accept-Language": "zh-CN,zh;q=0.8"

        }

    def load_page(self,url):
            '''A reusable page request method '''
            response = requests.get(url,timeout=10,headers=self.headers)
            if response.status_code==200:
                    print(response.request.headers)
                    return response.content.decode("gbk")
            else:
                    raise ValueError("status_code is:",response.status_code)
  • 程序正常执行的话,我们会在屏幕上打印了内涵段子第一页的全部html代码. 但是我们发现,html中的中文部分显示的可能是乱码 .

注意 :对于每个网站对中文的编码各自不同,所以html.decode('gbk')的写法并不是通用写法,根据网站的编码而异

第二步:筛选数据

接下来我们已经得到了整个页面的数据. 但是,很多内容我们并不关心,所以下一步我们需要进行筛选. 如何筛选,就用到了正则表达式.

First we need a matching rule,Open the website of Inner Duanzi to view the source code:

<a href="/article/44959.html"><b>Go home for a funeral</b></a></h4>
    <div class="f18 mb20">
        An old lady traveled through mountains and rivers to the army,to visit her grandson,<br />
      the guard asked:“Who is she looking for?”said the old lady:“找xx,”The guard finished the phone and said:<br />
      “xxShe said three days ago that his grandmother passed away,Go home for a funeral,Went to bereavement,去了..”

   </div>

在我们得到的responseUse regular expressions to filter and match:

import re

def get_content(self,html):
        ''' 根据网页内容,Match both title and paragraph content '''
        pattern = re.compile(r'<a\shref="/article/\d+\.html">(.*?)</a>.*?<div\sclass="f18 mb20">(.*?)</div>', re.S)
        t = pattern.findall(html)
        result = []
        for i in t:
            temp = []
            for j in i:
                    j = re.sub(r"[<b>|</b>|<br />|<br>|<p>|</p>|\\u3000|\\r\\n|\s]","",j)
                    j = j.replace("&ldqo;",'"').replace("&helli;","...").replace("&dqo;",'"').strip()
                    # j = re.sub(r"[&ldqo;|&dqo;]","\"",j)?
                    # j = re.sub(r"…","...",j)
                    temp.append(j)
                print(temp)
            result.append(temp)
    return result
  • 这里需要注意一个是re.S是正则表达式中匹配的一个参数.

  • 如果 没有re.S 则是 只匹配一行 有没有符合规则的字符串,如果没有则下一行重新匹配.

  • 如果 加上re.S 则是将 所有的字符串 将一个整体进行匹配,findall 将所有匹配到的结果封装到一个list中.

第三步:保存数据

  • 我们可以将所有的段子存放在文件中.比如,我们可以将得到的每个item不是打印出来,而是存放在一个叫 duanzi.txt 的文件中也可以.
def save_content(self,content):
    myFile = open("./duanzi.txt", 'a')
    for temp in content:
        myFile.write("\n"+temp[0]+"\n"+temp[1]+"\n")
        myFile.write("-----------------------------------------------------")
    myFile.close()
  • Then we implement the save method ,当前页面的所有段子就存在了本地的duanzi.txt文件中.

第四步:Realize circular grabbing

  • 接下来我们就通过参数的传递对page进行叠加来遍历 内涵段子吧的全部段子内容.

  • Also pass thisrunThe method implements the main logic of the entire program

def run(self):
        i = 1
        while True:
                html = self.load_page(self.url%i)
                result = self.get_content(html)
                print ("按回车继续...")
                print ("输入 quit 退出")
                command = input()
                if (command == "quit"):
                        break
                i+=1

最后,我们执行我们的代码,完成后查看当前路径下的duanzi.txt文件,里面已经有了我们要的内涵段子.


Subsequent summaries:


The above is a very streamlined small crawler program,使用起来很是方便,If you want to scrape information from other websites,Just need to modify some of these parameters and some details.如果您有任何疑问或者好的建议,Looking forward to your comments and comments!

copyright notice
author[Deng Dashuai],Please bring the original link to reprint, thank you.
https://en.pythonmana.com/2022/312/202211080910333792.html

Random recommended