Python网络爬虫与信息提取

整理:QT

最近在学python,了解到了爬虫,下了北京理工大学嵩天老师的课程,顺便学了用Typora做笔记,还用第一种技术路线爬了链家的二手房信息,利用百度地图做了热力图,感觉还挺好玩。不过裸辞在家…还是挺艰辛。记笔记勉励自己,也可分享给学友共同进步,个人水平有限,如有错漏还请见谅,如果侵犯到任何人的权益,请告知。大部分内容来源于网络,如需沟通请下方微信或QQ:604615850联系,侵删。

IMG_830611

正文开始

The website is API,掌握定向网络数据爬取和网页解析的基本能力:

  • Requests:自动爬去HTML页面自动网络请求提交;
  • robots.txt:网络爬虫排除标准;
  • Beautiful Soup:解析HTML页面;
  • Re:正则表达式详解提取页面关键信息;
  • Scrapy:网络爬虫原理,专业爬虫框架;

A51E45E5E97DA5F5BC5B3B06BB48C2F6

Request库与HTTP协议

request库主要方法

方法 说明
requests.request() 构造一个请求,支撑以下个方法的基础方法
requests.get() 获取HTML网页的主要方法,对应于HTTP的GET
requests.head() 获取HTML网页头信息的方法,对应于HTTP的HEAD
requests.post() 向HTML网页提交POST请求的方法,对应于HTTP的POST
requests.put() 向HTML网页提交PUT请求的方法,对应于HTTP的PUT
requests.patch() 向HTML网页提交局部修改请求,对应于HTTP的PATCH
requests.delete() 向HTML网页提交删除请求,对应于HTTP的DELETE

为了更好理解上面的方法,我们先了解一下HTTP协议。

HTTP协议

HTTP(Hypertext Transfer Protocol)协议,即,超文本传输协议;

  • 是一种基于”请求与响应“模式的、无状态(前后两次请求之间无关联)的应用层协议;

  • 采用URL作为定位网络资源的标识:URL格式为http://host[:port][path]

    • host:合法的Internet主机域名或IP地址;

    • port:端口号,缺省端口为80;

    • path:请求资源的路径;

      URL是通过HTTP协议存取资源的INTERNET路径,一个URL对应一个数据资源。

  • HTTP协议对资源的操作(与requests对应方法对应);

方法 说明
GET 请求获取URL位置的资源
HEAD 请求获取URL位置资源的响应消息报告,即获得该资源的头部信息
POST 请求获取URL位置的资源后附加新的数据
PUT 请求向URL位置存储一个资源,覆盖原URL位置的资源
PATCH 请求局部更新URL位置的资源,即改变该处资源的部分内容
DELETE 请求删除URL位置存储的资源

response对象的属性

属性 说明
r.starus_code HTTP请求的返回状态,200表示连接成功,404表示失败
r.text HTTP响应内容的字符串形式,即,URL对应的页面内容
r.encoding 从HTTP header中猜测的响应内容编码方式
r.apparent_encoding 从内容中分析出的响应内容编码方式(备选编码方式)
r.content HTTP响应内容的二进制形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> import requests
>>> mysite='http://www.baidu.com'
>>> r = requests.get(mysite)
>>> print(r.status_code)
200
>>> print(type(r))
<class 'requests.models.Response'>
>>> print(r.headers)
{'Server': 'bfe/1.0.8.18', 'Set-Cookie': 'BDORZ=27315; max-age=86400; domain=.baidu.com; path=/', 'Date': 'Mon, 30 Apr 2018 09:45:10 GMT', 'Content-Type': 'text/html', 'Content-Encoding': 'gzip', 'Last-Modified': 'Mon, 23 Jan 2017 13:28:12 GMT', 'Pragma': 'no-cache', 'Keep-Alive': 'timeout=38', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'private, no-cache, no-store, proxy-revalidate, no-transform'}
>>> print(r.encoding)
ISO-8859-1
>>> print(r.apparent_encoding)
utf-8
>>> print(r.content)
b'<!DOCTYPE html>\r\n<!--STATUS OK--><html> 。。。。</body> </html>\r\n'

r.encoding:如果header中不存在charset字段,则认为编码为ISO-8859-1
r.apparent_encoding:根据网页内容分析出的编码方式;

同时包含requests的全部信息:

1
2
>>> print(r.request.headers)
{'User-Agent': 'python-requests/2.9.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}

requests.request()详解

此方法为最基本的方法,可以构造一个请求,支撑其它各方法的基础方法,格式如下:

requests.request(method,url,**kwargs)

  • method:请求方式,对应get/put/post等7种;
  • url:拟获取页面的url链接;
  • kwargs:控制访问参数,共13个,均为可选项,如下:
    • params:字典或字节序列,作为参数增加到url中;
    • data:字典,字节序列或文件对象,作为Request的内容;
    • json:JSON格式的数据,作为Request的内容;
    • headers:字典,HTTP定制头(模拟浏览器进行访问);
    • cookies:字典或CpplieJar,Request中的cookie;
    • auth:元组,支持HTTP认证功能;
    • files:字典类型,传输文件;
    • timeout:设定超时时间,秒为单位;
    • proxies:字典类型,设定访问代理服务器,可以增加登陆认证;
    • allow_redirects:True//False,默认为True,重定向开关;
    • stream:True/False,默认为True,获取内容立即下载开关;
    • verify:True/False,默认为True,认证SSL证书开关;
    • cert:本地SSL证书路径。
1
2
3
4
5
>>> import requests
>>> kv={'key1':'value','key2':'value2'}
>>> r=requests.request('GET','http://python123.io/ws',params=kv)
>>> print(r.url)
https://python123.io/ws?key1=value&key2=value2

robots协议

Robots Exclusion Standard 网络爬虫排除标准

  • 作用:网站告知网络爬虫哪些页面可以抓取,哪些不行;
  • 形式:在网站根目录下的robots.txt文件;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
案例:京东的Robots协议:https://www.jd.com/robots.txt
User-agent: * #所有的网络爬虫都定义为User-agent,并遵守如下协议:
Disallow: /?* #不允许访问网络以?开头的路径
Disallow: /pop/*.html #不允许访问
Disallow: /pinpai/*.html?* #不允许访问
User-agent: EtaoSpider #这个网络爬虫不允许爬去京东任何资源(以下三个一样)
Disallow: / #根目录
User-agent: HuihuiSpider #
Disallow: / #
User-agent: GwdangSpider #
Disallow: / #
User-agent: WochachaSpider #
Disallow: /#
#Robots基本语法注释: *代表所有,/代表根目录
#   User-agent: *
#   Disallow: /
# 如果没有Robots.txt文件即代表网站对访问没有限制

Robots的遵守方法

  • 协议的使用:

    • 网络爬虫:自动或人工识别robots.txt,再进行内容爬取;
    • 约束性:Robots协议是建议但非约束性网络爬虫可以不遵守,但村子法律风险。
  • 对Robots协议的理解:

爬取网页,玩转网页 爬取网站,爬取系列网站 爬取全网
访问量很小:可以遵守 非商业且偶尔:建议遵守 必须遵守
问量较大:必须遵守 商业利益:必须遵守 -

代码框架及异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
#爬取网页的通用代码框架及异常处理
import requests
def getHTMLText(url):
try:
r = requests.get(url,timeout=30)
r.raise_for_status() #如果r.starus_code不是200,产生一个requests.HTTPError
r.encoding = r.apparent_encoding
return r.text
except:
return "Errors occur"
if __name__ == '__main__':
url = 'http://www.baidu.com'
print(getHTMLText(url))
异常 说明
requests.RequestException There was an ambiguous exception that occurred while handling your request.
requests.ConnectionError 网络连接错误异常,如DNS查询失败,拒绝连接等
requests.HTTPError HTTP错误异常
requests.URLRequired URL缺失异常
requests.TooManyRedirects 超过最大重定向次数,产生重定向错误
requests.ConnectTimeout 连接远程服务器超时异常
requests.Timeout 请求URL超时,产生超时异常
requests.ReadTimeout The server did not send any data in the allotted amount of time.

应用实例

  • 访问亚马逊中国:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import requests
    def getHTMLText(url):
    kv = {'user-agent':'Mozilla/5.0'}#网站过滤爬虫,模拟浏览器访问
    try:
    r = requests.get(url,headers=kv)
    print(r.status_code,r.encoding,r.apparent_encoding)
    r.raise_for_status()#如果r.starus_code不是200,产生一个requests.HTTPError
    # r.encoding = r.apparent_encoding
    print(r.request.headers)
    return r.text
    except:
    return "Errors occur"
    if __name__ == '__main__':
    # url = 'https://item.jd.com/6946631.html'
    url='https://www.amazon.cn/dp/B07C2S9ZYM/ref=sr_1_1?s=wireless&ie=UTF8&qid=1525140296&sr=1-1'
    print(getHTMLText(url)) #503 拒绝某些客户端的连接
    ## 200 UTF-8 ISO-8859-2
    ## {'user-agent': 'Mozilla/5.0', 'Accept': '*/*', 'Connection': 'keep-alive', 'Accept-Encoding': 'gzip, deflate'}
    • 百度提交关键字
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import requests
    try:
    kv={'wd':'Python'}
    url='https://www.baidu.com/s'
    r = requests.get(url,params=kv)
    print(r.request.url)
    r.raise_for_status()
    print(len(r.text))
    except:
    print("Errors occur")
    # https://www.baidu.com/s?wd=Python
    # 227
    • 网络图片的爬取与存储
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import requests
    import os
    url = 'https://news.nationalgeographic.com/content/dam/news/2017/07/13/01-tardigrade.adapt.1190.1.jpg'
    root = './mypic/'
    #注:ubuntu中根目录”/“;当前目录“./”;当前目录的上一级目录(如果有上一级目录的话)“../”
    path = root + url.split('/')[-1].replace('.','_') + ".jpg"
    try:
    if not os.path.exists(root):
    os.mkdir(root)
    if not os.path.exists(path):
    r = requests.get(url)
    r.raise_for_status()
    with open(path,'wb') as f:
    f.write(r.content)
    f.close()
    print("Saved successfully")
    else:
    print("Already exited")
    except:
    print("Errors occur")
    • IP地址归属地的自动查询
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import requests
    #'http://www.ip138.com/ips138.asp?ip=202.204.80.112&action=2'
    url = 'http://www.ip138.com/ips138.asp?'
    kv = {'ip':'202.204.80.112','action':'2'}

    try:
    r = requests.get(url, params=kv) #, headers=headkv
    r.raise_for_status()#如果r.starus_code不是200,产生一个requests.HTTPError
    r.encoding = r.apparent_encoding
    print(r.text[7250:7380])
    print("Visit successfully")
    except:
    print("Errors occur")

网络爬虫引发的问题

  • 引发的问题
    • 爬虫骚扰,增加服务器负担;
    • 爬虫的法律风险;
    • 爬虫泄露隐私,突破简单的访问控制。
爬取网页,玩转网页 爬取网站,爬取系列网站 爬取全网
小规模,数据量小,爬取速度不敏感 中规模,数据规模较大,爬取速度敏感 大规模,搜索引擎爬取速度关键
requests库 scrapy库 定制开发
  • 限制网络爬虫的技术手段:
    • 来源审查:检查来访HTTP协议头的user-agent域,判断user-agent进行限制,只响应浏览器或友好爬虫的访问;
    • 发布公告:通过robots.txt协议,告知所有爬虫,网站允许的爬取策略,要求爬虫遵守。

BeautifulSoup库

BeautifulSoup 库及安装

请求把数据返回来之后就要提取目标数据,不同的网站返回的内容通常有多种不同的格式,

  • JSON:有类型的键值对key:nalue,适合程序处理;移动应用云端和节点的信息通信,无注释
  • XML:最早的通用信息标记语言,标签占用过多;Internet上的信息交互与传递;
  • YAML:无类型的键值对,文本信息比例最高,可读性好;各类系统的配置文件,有注释易读;

    HTML文档从属与XML,现在就来讲讲如何从 HTML 中提取出感兴趣的数据,BeautifulSoup 是一个用于解析 HTML 文档的 Python 库,通过 BeautifulSoup,你只需要用很少的代码就可以提取出 HTML 中任何感兴趣的内容,此外,它还有一定的 HTML 容错能力,对于一个格式不完整的HTML 文档,它也可以正确处理。Beautiful Soup是python的一个库,最主要的功能是从网页抓取数据。

安装:pip install beautifulsoup4

文档:Beautiful Soup 4.2.0 文档

HTML 标签初识

学习 BeautifulSoup4 前有必要先对 HTML 文档有一个基本认识,如下代码,HTML 是一个树形组织结构。

1
2
3
4
5
6
7
8
9
<html> 
<head>
<title>hello, world</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<p>如何使用BeautifulSoup</p>
<body>
</html>
  • 通过预定义的<>...</>标签形式组织不同类型的信息;
  • 它由很多标签(Tag)组成,比如 html、head、title等等都是标签;
  • 一个标签对构成一个节点,比如 … 是一个根节点;
  • 节点之间存在某种关系,比如 h1 和 p 互为邻居,他们是相邻的兄弟(sibling)节点;
  • h1 是 body 的直接子(children)节点,还是 html 的子孙(descendants)节点;
  • body 是 p 的父(parent)节点,html 是 p 的祖辈(parents)节点;
  • 嵌套在标签之间的字符串是该节点下的一个特殊子节点,比如 “hello, world” 也是一个节点,只不过没名字;

使用 BeautifulSoup

构建一个 BeautifulSoup 对象需要两个参数,第一个参数是将要解析的 HTML 文本字符串,第二个参数告诉 BeautifulSoup使用哪个解析器来解析 HTML。解析器负责把 HTML 解析成相关的对象,而 BeautifulSoup 负责操作数据(增删改查)。”html.parser”是Python内置的解析器,”lxml”则是一个基于c语言开发的解析器,它的执行速度更快,不过它需要额外安装。

通过BeautifulSoup 对象就可以定位到 HTML 中的任何一个标签节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> from bs4 import BeautifulSoup 
>>> text = """
<html>
<head>
<title >hello, world</title>
</head>
<body>
<h1>BeautifulSoup</h1>
<p class="bold">如何使用BeautifulSoup</p>
<p class="big" id="key1"> 第二个p标签</p>
<a href="http://www.fool.com" >python</a>
</body>
</html>
"""
>>> soup = BeautifulSoup(text, "html.parser")
>>> soup.title
<title>hello, world</title>
>>> soup.p
<p class="bold">如何使用BeautifulSoup</p>
>>> soup.p.string
'如何使用BeautifulSoup'

BeatifulSoup 将 HTML 抽象成为 4 类基本元素,分别是Tag , AttributesNavigableString, Comment 。每个标签节点就是一个Tag对象,NavigableString 对象一般是包裹在Tag对象中的字符串,BeautifulSoup 对象代表整个 HTML 文档。例如:

1
2
3
4
5
6
>>> type(soup)
<class 'bs4.BeautifulSoup'>
>>> type(soup.h1)
<class 'bs4.element.Tag'>
>>> type(soup.p.string)
<class 'bs4.element.NavigableString'>

Tag

每个 Tag 都有一个名字,它对应 HTML 的标签名称.

1
2
3
4
>>> soup.h1.name
'h1'
>>> soup.p.name
'p'

标签还可以有属性,属性的访问方式和字典是类似的,它返回一个列表对象。

1
2
>>> soup.p['class']
['bold']

获取标签中的内容,直接使用 .stirng 即可获取,它是一个 NavigableString 对象,你可以显式地将它转换为 unicode字符串。

1
2
3
4
>>> soup.p.string
'如何使用BeautifulSoup'
>>> type(soup.p.string)
<class 'bs4.element.NavigableString'>

数据提取

如何从 HTML 中找到我们关心的数据?BeautifulSoup 提供了两种方式,一种是遍历,另一种是搜索,通常两者结合来完成查找任务。

  • 遍历文档树

    顾名思义,就是是从根节点 html 标签开始遍历,直到找到目标元素为止,遍历的一个缺陷是,如果你要找的内容在文档的末尾,那么它要遍历整个文档才能找到它,速度上就慢了,主要的遍历方向有三种:

    • 下行遍历
属性 说明
.contents 子节点的列表,将<tag>所有儿子节点存入列表
.children 子节点的迭代类型,与.conents类似,用于循环遍历儿子节点
.descendants 子孙节点的迭代类型,包含所有子孙节点,用于循环遍历
  • 上行遍历
属性 说明
.parent 节点的父亲标签
.parents 节点先辈标签的迭代类型用于循环遍历先辈节点
  • 平行遍历(同一个父亲节点下)
属性 说明
.next_sibling 返回按照HTML文本顺序的下一个平行节点标签
.previous_sibling 返回按照HTML文本顺序的上一个平行节点标签
.next_siblings 迭代类型,返回按照HTML文本顺序的后续所有平行节点标签
.previous_siblings 迭代类型,返回按照HTML文本顺序的前序所有平行节点标签

通过遍历文档树的方式获取标签节点可以直接通过.标签名的方式获取,内容也是一个节点,这里就可以用 .string的方式得到。遍历文档树的另一个缺点是只能获取到与之匹配的第一个子节点,例如,如果有两个相邻的 p 标签时,第二个标签就没法通过.p 的方式获取,这是需要借用 next_sibling 属性获取相邻且在后面的节点。此外,比如:.contents 获取所有子节点,.parent 获取父节点,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#获取body标签:
>>> soup.body
<body>
<h1>BeautifulSoup</h1>
<p class="bold">如何使用BeautifulSoup</p>
<p class="big" id="key1"> 第二个p标签</p>
<a href="http://foofish.net" rel="external nofollow">python</a>
</body>
#获取p标签
>>> soup.body.p
<p class="bold">如何使用BeautifulSoup</p>
#获取p标签的内容
>>> soup.body.p.string
'如何使用BeautifulSoup'
  • 搜索文档

    搜索文档树是通过指定标签名来搜索元素,另外还可以通过指定标签的属性值来精确定位某个节点元素,最常用的两个方法就是 findfind_all。这两个方法在 BeatifulSoupTag 对象上都可以被调用。

    • find_all( name , attrs , recursive , string , **kwargs )

      • 返回一个列表类型,存储查找的结果;
      • name:对标签名称的检索字符串;
      • attrs:对标签名称属性值的检索字符串,可标注属性检索;
      • recursive:是否对子孙全部检索,默认为True;
      • string<>...</>中字符串区域的检索字符串;
      • 简写形式<tag>(..)等价于<tag>.find_all(..)
    • find_all 的返回值是一个 Tag 组成的列表,方法调用非常灵活,所有的参数都是可选的:

      • 第一个参数 name 是标签节点的名字;

        1
        2
        >>> soup.find_all("title")
        [<title>hello, world</title>]
      • 第二个参数是标签的class属性值;

        1
        2
        3
        >>> soup.find_all("p","big")
        #class_="big",因为class是Python关键字,所以这里指定为 class_ 。
        [<p class="big" id="key1"> 第二个p标签</p>]
      • kwargs 是标签的属性名值对,例如:查找有href属性值为 “http://www.fool.com" 的标签;

        1
        2
        >>> soup.find_all(href="http://www.fool.com")
        [<a href="http://www.fool.com">python</a>]
      • 正则表达式;

        1
        2
        3
        >>> import re
        >>> soup.find_all(href=re.compile("^http"))
        [<a href="http://foofish.net">python</a>]
      • 布尔值(True/Flase),表示有属性或者没有该属性;

        1
        2
        3
        4
        >>> soup.find_all(id="key1")
        [<p class="big" id="key1"> 第二个p标签</p>]
        >>> soup.find_all(id=True)
        [<p class="big" id="key1"> 第二个p标签</p>]
      • 遍历和搜索相结合查找,先定位到 body 标签,缩小搜索范围,再从 body 中找 a 标签。

        1
        2
        3
        >>> body_tag = soup.body
        >>> body_tag.find_all('a')
        [<a href="http://www.fool.com">python</a>]
    • find( name , attrs , recursive , string , **kwargs )

      find 方法跟 find_all 类似,唯一不同的地方是,它返回的单个 Tag 对象而非列表,如果没找到匹配的节点则返回 None。如果匹配多个 Tag,只返回第0个。

      1
      2
      3
      4
      5
      6
      >>> body_tag.find("a")
      <a href="http://foofish.net">python</a>
      >>> body_tag.find("p")
      <p class="bold">如何使用BeautifulSoup</p>
      >>> body_tag.find_all("p")
      [<p class="bold">如何使用BeautifulSoup</p>, <p class="big" id="key1"> 第二个p标签</p>]
    • get_text()

      获取标签里面内容,除了可以使用.string 之外,还可以使用 get_text 方法,不同的地方在于前者返回的一个 NavigableString 对象,后者返回的是 unicode类型的字符串。但这个方法获取到tag中包含的所有文版内容包括子孙tag中的内容。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      >>> p1=body_tag.find('p').get_text()
      >>> type(p1)
      <class 'str'>
      >>> p1
      '如何使用BeautifulSoup'
      >>> p2=body_tag.find('p').string
      >>> type(p2)
      <class 'bs4.element.NavigableString'>
      >>> p2
      '如何使用BeautifulSoup'
      #返回包含所有文版内容包括子酸tag中的内容
      >>> soup.find('body').get_text()
      '\nBeautifulSoup\n如何使用BeautifulSoup\n 第二个p标签\npython\n'
      #通过参数指定tag的文本内容的分隔符
      >>> soup.find('body').get_text("|")
      '\n|BeautifulSoup|\n|如何使用BeautifulSoup|\n| 第二个p标签|\n|python|\n'
      #去除获得文本内容的前后空白
      >>> soup.find('body').get_text("|",strip=True)
      'BeautifulSoup|如何使用BeautifulSoup|第二个p标签|python'

      实际场景中我们一般使用 get_text 方法获取标签中的内容。

  • 扩展方法:

方法(参数同find_all() 说明
<>.find() 搜索只返回一个结果,字符串类型
<>.find_parents() 在先辈节点中搜索,返回列表类型
<>.find_parent() 在先辈节点中返回一个结果,字符串类型
<>.find_next_siblings() 在后续平行节点中搜索,返回列表类型
<>.find_next_sibling() 在后续平行节点中返回一个结果,字符串类型
<>.find_previoust_siblings() 在前序平行节点中搜索,返回列表类型
<>.find_previoust_sibling() 在前序平行节点中返回一个结果,字符串类型

小结

BeatifulSoup 是一个用于操作 HTML 文档的 Python 库,初始化 BeatifulSoup 时,需要指定 HTML 文档字符串和具体的解析器。BeatifulSoup 有3类常用的数据类型,分别是 TagNavigableStringBeautifulSoup。查找 HTML元素有两种方式,分别是遍历文档树和搜索文档树,通常快速获取数据需要二者结合。

正则表达式库Re

正则表达式:regular expression,是用来简洁表达一组字符串的表达式。

  • 通用的字符串表达框架;
  • 简洁表达一组字符串的表达式;
  • 针对字符串表达的“简洁”和“特征”思想的工具;
  • 判断某字符串的特征属性。

正则表达式在文本处理中十分有用:

  • 表达文本类型的特征(病毒、入侵等);
  • 同时查找或替换一组字符串;
  • 匹配字符串额全部或部分。

正则表达式的使用,需先编译,即将符合正则表达式语法的字符串转换成正则表达式特征。

正则表达式的语法

正则表达式由字符和操作符构成。

  • 常用操作符:
操作符 说明 实例
. 表示任何单个字符 a..b为所有以a头且以b尾的四字符串
[] 字符集,对单个字符给出取值范围 [a-z]表示a到z单个字符
[^] 非字符集,对单个字符给出排除范围 [^abc]表示非a或b或c的单个字符
* 前一个字符0此或无限次扩展 abc*表示ab,abc,abcc,abccc等
+ 前一个字符1此或无限次扩展 abc+表示abc,abcc,abccc等
? 前一个字符0次或1次拓展 abc?表示ab,abc
| 左右表达式任意一个 abc|def表示abc或def
{m} 扩展前一个字符m次 ab{2}c表示abbc
{m,n} 扩展前一个字符m至n次(含n) ab{1,2}c表示abc,abbc
^ 匹配字符串开头 ^abc表示abc且在一个字符串的开头
$ 匹配字符串结尾 abc$表示abc且在一个字符串的结尾
() 分组标记,内部只能使用|操作符 (abc|def)表示abc或def
\d(\D) 数字 等价于[0-9]([^0-9])
\w(\W) 单词字符 等价于[A-Za-z0-9]([^A-Za-z0-9])
\s(\S) 与所有空白字符匹配 等价于[ \t\v\n\f\r]([^ \t\v\n\f\r])
\b(\B) 单词边界,在单词边界位置匹配空串 \\b123\\b或r\b123\b

注释:正则表达式里的空格也作为常规字符,因此能与自己匹配。

  • 经典正则表达式实例

    • ^[A-Za-z]+$:由26个字母组成的字符串
    • ^[A-Za-z0-9]+$:由26个字母和数字组成的字符串
    • ^[-+]?\d+$:整数形式的字符串
    • [1-9]\d{5}:中国境内邮政编码,6位
    • [\u4e00-\u9fa5]:匹配中文字符

re库的基本使用

re库是python的标准库,主要用于字符串匹配。

1
2
#直接导入库
import re

正则表达式的表示类型

  • raw string类型(原生字符串类型):不包含转义符的字符串;
  • string类型,有转义字符;

常用功能函数

函数 说明
re.search() 在一个字符串中搜索匹配正则表达式的第一个位置,返回match对象
re.match() 从一个字符串的开始位置起(前缀)匹配正则表达式,返回match对象
re.findall() 搜索字符串,以列表类型返回全部能匹配的子串
re.split() 将一个字符串按照正则表达式匹配结果进行分割,返回列表类型
re.finditer() 搜索字符串,返回一个匹配结果的迭代类型,每个迭代元素是match对象
re.sub() 在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串
  • match对象

    主要属性:

属性 说明
.string 待匹配的文本
.re 匹配时使用的pattern对象(正则表达式)
.pos 正则表达式搜索文本的开始的位置
.endpos 正则表达式搜索文本的结束位置
.group() 获得匹配后的字符串
.start() 匹配字符串再原始字符串的开始位置
.end() 匹配字符串再原始字符串的结束位置
.span() 返回(.start(),.end())
  • re.search(pattern,string,flags=0)

    • pattern:正则表达式的字符串或原生字符串表示;
    • string:待匹配字符串;
    • flags:正则表达式使用时的控制标记;
常用标记 说明
re.I re.IGNORECASE 忽略正则表达式的大小写,[A-Z]能够匹配小写字符
re.M re.MULTILINE ^操作符能够将给定字符串的每行当做匹配开始
re.S re.DOTALL .操作符能够匹配所有字符,默认匹配处换行外的所有字符
1
2
3
4
import re
match=re.search('\\b[1-9]\d{5}\\b','BIT 100081')
if match:
print(match.group(0))
  • re.match(pattern,string,flags=0)

    1
    2
    3
    4
    5
    6
    7
    8
    import re
    match=re.match('[1-9]\d{5}','BIT 100081')
    if not match:
    print('Return is None')
    else:
    print(match.groups())
    # match需要从头开始匹配,所以这个例子返回None,
    # 需要把字符调整'100081 BIT'
  • re.findall(pattern,string,flags=0)

    1
    2
    3
    import re
    ls = re.findall('[1-9]\d{5}','BIT100081 TSU100084')
    print(ls)
  • re.split(pattern,string,maxsplit=0,flags=0)

    • maxsplit:最大分割数,剩余部分作为最后一个元素输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import re
    ls=re.split('[1-9]\d{5}','BIT100081 TSU100084')
    print(ls)
    #output
    ['BIT', ' TSU', '']
    ##
    ls=re.split('[1-9]\d{5}','BIT100081 TSU100084',maxsplit=1)
    print(ls)
    #output
    ['BIT', ' TSU100084']
  • re.finditer(pattern,string,flags=0)

    1
    2
    3
    4
    5
    6
    7
    #search只能找到第一个匹配结果,如果需要多个匹配用finditer
    import re
    for m in re.finditer('[1-9]\d{5}','BIT100081 TSU100084'):
    print(m.group(0))
    #output
    100081
    100084
  • re.sub(pattern,repl,string,count=0,flags=0)

    • repl:替换匹配字符串的字符串
    • count:匹配的最大替换次数
    1
    2
    3
    4
    5
    import re
    s=re.sub('[1-9]\d{5}',':zipcode','BIT100081 TSU100084')
    print(s)
    #output
    BIT:zipcode TSU:zipcode
  • match.group()模式里的组

    在正则表达式中一个重要的概念是组,使用圆括号括起来的模式段(...),在考虑匹配时,它与被括起来的子模式匹配的串匹配,同时,圆括号还确定了一个被匹配的组。

    在一次成功匹配中,模式串里的各个组也都成功匹配,与它们匹配成功的那一组字符串将从1开始编号,而后可以通过调用match.group(n)获取,match.groups()将得到这个从1开始的各个组匹配的串。作为特殊情况,组0就是与整个模式匹配的字符串,也可以通过match.group()来获取。

    • 模式里各队圆括号确定的组按开括号的顺序编号,例如:
    1
    2
    3
    4
    5
    6
    7
    import re
    mat = re.search('.((.)e)f','abcdef')
    print(mat.group())
    print(mat.groups())
    #output
    cdef
    ('de', 'd')
    • 组的另一个重要用途,再匹配中应用前面的成功匹配,建立前后的部分匹配之间的约束关系,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import re
    mat = re.search('(.{2}) \\1','bbb cc aa aab')
    print(mat)
    print(mat.group())
    print(mat.groups())
    #output
    <_sre.SRE_Match object; span=(7, 12), match='aa aa'>
    aa aa
    ('aa',)

re库的另一种等价用法

  • 函数式用法:一次性操作

    1
    rst=re.search('[1-9]\d{5}','BIT 100081');
  • 面向对象法:编译后的多次操作

    1
    2
    pat=re.compile('[1-9]\d{5}')
    rst=pat.search('BIT 100081')
  • re.compile(pattern,flags=0),将正则表达式的字符串形式编译成正则表达式对象

    • pattern:正则表达式的字符串或原生字符串表示;
    • flags:正则表达式使用时的控制标记;
    1
    regex = re.compile('[1-9]\d{5}')

    等价的方法调用:

函数 说明
regex.search() 在一个字符串中搜索匹配正则表达式的第一个位置,返回match对象
regex.match() 从一个字符串的开始位置起(前缀)匹配正则表达式,返回match对象
regex.findall() 搜索字符串,以列表类型返回全部能匹配的子串
regex.split() 将一个字符串按照正则表达式匹配结果进行分割,返回列表类型
regex.finditer() 搜索字符串,返回一个匹配结果的迭代类型,每个迭代元素是match对象
regex.sub() 在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串

贪婪匹配和最小匹配

1
2
3
4
5
match = re.search(r'PY.*N','PYANBNCNDN')
#PYAN,PYANBN,PYANBNCN,PYANBNCNDN
print(match.group())
#output
PYANBNCNDN
  • python默认为贪婪匹配,即输出匹配长度最长的子串;

  • 如何输出最小匹配?

    1
    2
    3
    4
    5
    6
    #在*后加?
    match = re.search(r'PY.*?N','PYANBNCNDN')
    #PYAN,PYANBN,PYANBNCN,PYANBNCNDN
    print(match.group())
    #output
    PYAN

    最小匹配操作符:

操作符 说明
*? 前一个字符0次或无限次扩展,最小匹配
+? 前一个字符1次或无限次扩展,最小匹配
?? 前一个字符0次或1次扩展,最小匹配
{m,n}? 扩展前一个字符m至n次(含n),最小匹配

应用实例(空)

XPath的介绍与配置

XPath是什么

  • 是一门语言;
  • 可以在XML文档中查找信息;
  • 支持HTML;
  • 通过元素和属性进行导航;

作用:

  • 用来提取信息
  • 比正则表达式厉害
  • 比正则表达式简单

如何使用XPath

  • 安装lxml库
  • from lxml import etree
  • Selector = etree.HTML(网页源代码)
  • Selector.xpath(表达式)

XPath与HTML结构

  • 树状结构
  • 逐层展开
  • 逐层定位
  • 寻找独立节点

获取网页元素的XPath

  • 手动分析法
  • Chrome生成法

应对XPath提取内容

  • //定位根节点
  • /往下层寻找
  • /text()提取文本内容
  • /@xxx提取属性内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
text = """
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="COOKING">
<title lang="en"> Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="WEB">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
<book category="WEB">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
"""
from lxml import etree
html = etree.HTML(text)
>>> html.xpath("//book[@category='COOKING']/title/text()")
[' Everyday Italian']
>>> html.xpath("//book[@category='COOKING']/title/@lang")
['en']
>>> html.xpath("//book[@category='WEB']/title/text()")
['XQuery Kick Start', 'Learning XML']
>>> html.xpath("//book[@category='WEB']/title/@lang")
['en', 'en']

XPath特殊用法:

  • 包含字符串
    • contains(@属性名称,属性字符)
  • 以相同的字符开头
    • start-with(@属性名称,属性字符相同部分)
  • 标签套标签
    • string(.)

Scrapy库

Scrapy是一个三方库,需用pip install scrapy --user安装。但其并不是一个简单的函数功能库,而是一个爬虫框架。

  • 爬虫框架是实现爬虫功能的一个软件结构和功能组合集合;
  • 爬虫框架是一个半成品,能够帮助用户实现专业网络爬虫。

框架结构

“5个模块+2个中间件”结构:

20170821202114253

上图的数字代表数据的流向,解释如下

  1. 引擎从Spider 获取初始Request对象;
  2. 引擎将获取的Request对象交给调度器Scheduler,并向Spider要下一个Request对象;
  3. 调度器将下一个Request对象交给引擎;
  4. 引擎将Request对象交给下载器Downloader,途径下载器中间件;
  5. 网页下载完成,下载器Downloader生成一个Response对象, 并经过下载中间件交给引擎;
  6. 引擎收到Response对象, 并交给Spider处理, 途径 Spider Middleware;
  7. Spider 处理Response 对象, 并将提取的结构化数据构成Item,同时生成新的Request对象,一并交给引擎, 途径 Spider Middleware;
  8. 引擎将Item 交给Item Pipeline 处理, 将Request对象交给调度器Scheduler,并继续向Spider要Request对象,直到没有Request对象可处理;

从上面的结构图可看出, Scrapy 框架以Engine 为核心来运转,当调度器中没有Request需要爬取时,爬取任务结束。图中EngineDownloaderScheduler是已有实现,用户需要配置SpiderItem Piplines。其中Spider用来想整个框架提供URL链接,同时要解析从网络页面获得的内容;Item Piplines负责对提取的信息进行后处理。

用户需要或可能编写的模块如下:

Downloader Middleware中间件:

  • 实施ENGINEDOWNLOADERSCHEDULER之间进行用户可配置的控制;
  • 功能:修改、丢弃、新增请求或响应(用户可以编写配置代码);

Spider

  • 解析Download返回的响应(Response);
  • 产生爬取项;
  • 产生额外的爬取请求(Request)

Item Piplines

  • 以流水线方式处理Spider产生的爬取项;
  • 由一组操作顺序组成,类似流水线,每个操作是一个Item Pipline类型;
  • 可能操作包括:清理、检验和查重爬取项中的HTML数据、将数据存储到数据库;

Spider Middleware

  • 目的:对请求和爬取项的再处理;
  • 功能:修改、丢弃、新增请求和爬取项;

使用步骤及数据类型

Scrapy爬虫的使用步骤:

  1. 创建一个工程和Spider模板;
  2. 编写Spider
  3. 编写Item Pipeline
  4. 优化配置策略;

Scrapy爬虫的数据类型:

  • Request

    class scrapy.http.Request()

    • Request对象表示一个HTTP请求;
    • Spider生成,由Downloader执行;

    常用的属性和方法:

属性或方法 说明
.url Request 对应的请求URL地址
.method 对应的请求方法,GET、POST等
.headers 字典类型风格的请求头
.body 请求内容主体,字符串类型
.meta 用户添加的扩展信息,在Scrapy内部模块间传递信息使用
.copy() 复制该请求
  • Response

    class scrapy.http.Reponse()

    • Reponse对象表示一个HTTP响应;
    • Downloader生成,由Spider处理;

    常用的属性和方法:

属性或方法 说明
.url Response 对应的请求URL地址
.status HTTP状态码,默认是200
.headers Response对应的头部信息
.body Response对应的内容信息,字符串类型
.flags 一组标记
.request 产生Response类型对应的Request对象
.copy() 复制该请求
  • Item

    class scrapy.http.Item()

    • Item对象表示一个从HTTP页面中提取的信息内容;
    • Spider生成,由Item Pipeline处理;
    • Item类似字典类型,可以按照字典类型操作;

信息提取方法

Srapy爬虫支持多种HTML信息提取方法:

  • Beautiful Soup
  • lxml
  • re
  • XPath Selector
  • CSS Selector

其它的方法前面都有介绍,这里我们简单说下CSS Selector的基本使用:

1
2
<HTML>.css('a::attr(href)').extract()
#标签名称a,标签属性href

Scrapy命令行

Scrapy是为了持续运行设计的专业爬虫框架,提供操作的命令行。命令行格式如下:

1
>>> Scrapy <command> [optioins][args]

Scrapy常用命令

命令 说明 格式
startproject 创建一个工程 scrapy startproject <name> [dir]
genspider 创建一个爬虫 scrapy genspider [options] <name> <domian>
settings 获得爬虫配置信息 scrapy settings [options]
crwal 运行一个爬虫 scrapy crawl <spider>
list 列出工程中所有的爬虫 scrapy list
shell 启动URL调试命令行 scrapy shell [url]

为什么Scrapy采用命令行创建和运行爬虫?

  • 命令行(不是图像界面)更容易自动化,适合脚步控制;
  • 本质上,Scrapy是给程序员用的,功能(不是图形界面)更重要。

ScrapyRequest比较

相同点:

  • 两者都可以进行页面请求和爬取,python爬虫的两个重要技术路线;
  • 两者可用性都好,文档丰富,入门简单;
  • 两者都没有处理js,提交表单,应对验证码等功能(可扩展);

不同点:

request scrapy
页面及爬虫 网站级爬虫
功能库 框架
并发性考虑不足,性能较差 并发性好,性能较高
重点在于页面下载 重点在于爬虫结构
定制灵活 一般定制灵活,深度定制困难
上手十分简单 入门稍难

选用哪个技术路线开发爬虫

  • 非常小的需求,request库;
  • 不太小的需求,Scrapy库;
  • 定制程度很高的需求(不考虑规模),自搭框架,request库 > Scrapy库。

yield关键字

yield生成器:

  • 生成器是一个不断产生值的函数;
  • 包含yield语句的函数是一个生成器;
  • 生成器每次产生一个值(yield语句),函数被冻结,被唤醒后再产生一个值。

为何要用生成器:

  • 更节省存储空间;
  • 响应更迅速;
  • 使用更加灵活。

实例1: 一个简单的demo

演示HTML地址:http://python123.io/ws/demo.html

1). 生成工程文件scrapy startproject demo1

  • demo1/ :外层目录
  • scrapy.cfg :部署Scrapy爬虫的配置文件
  • demo1 Scrapy:框架的用户自定义python代码
    • __init__.py:初始化脚本
    • items.pyItems代码模板(继承类)
    • middlewares.pyMiddlewares代码模板(继承类)
    • pipelines.pyPipilines代码模板(继承类)
    • settings.pyScrapy爬虫的配置文件
    • spiders/Spiders代码模板目录(继承类)
      • __init__.py:初始化文件,无需修改
      • __pycache__/:缓存目录,无需修改

2). 在工程中产生一个Scrapy爬虫

1
2
3
4
------------------------------------------------------------
~/myapp/scrapy_spider » cd demo1
------------------------------------------------------------
~/myapp/scrapy_spider/demo1 » scrapy genspider demo python123.io

spiders/目录下生成了 demo.py

1
2
3
4
5
6
7
8
9
# -*- coding: utf-8 -*-
import scrapy
class DemoSpider(scrapy.Spider):
name = 'demo'
allowed_domains = ['python123.io']
start_urls = ['http://python123.io/']

def parse(self, response):
pass

其中,parse()用于处理响应,解析内容形成字典,发现新的URL爬取请求。

3). 配置产生的spider爬虫

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-
import scrapy
class DemoSpider(scrapy.Spider):
name = 'demo'
#allowed_domains = ['python123.io']
start_urls = ['http://python123.io/ws/demo.html']

def parse(self, response):
fname = response.url.split('/')[-1]
with open(fname,'wb') as f:
f.write(response.body)
self.log('Saved file %s.' % name)

4). 运行爬虫获取网页

1
2
>>> scrapy crawl demo
#页面保存与demo.html

实例2:股票信息爬取

功能描述:

  • 技术路线:scrapy
  • 目标:获取上交所和深交所所有股票的名称和交易信息;
  • 输出:保存到文件中。

数据网站的确定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#建立工程和Spider模板
>>> scrapy startproject BaiduStocks
>>> cd BaiduStocks
>>> scrapy genspider stocks baidu.com
##spider
# -*- coding: utf-8 -*-
import scrapy
import re
class StocksSpider(scrapy.Spider):
name = 'stocks'
#allowed_domains = ['baidu.com']

start_urls = ['http://quote.eastmoney.com/stocklist.html']

def parse(self, response):
for href in response.css('a::attr(href)').extract():
try:
stock = re.findall(r'[s][hz]\d{6}',href)[0]
url = 'https://gupiao.baidu.com/stock/' + stock + '.html'
yield scrapy.Request(url,callback=self.parse_stock)
except:
continue

def parse_stock(self, response):
infoDict = {}
stockInfo = response.css('.stock-bets')
name = stockInfo.css('.bets-name').extract()[0]
keyList = stockInfo.css('dt').extract()
valueLIst = stockInfo.css('dd').extract()
for i in range(len(keyList)):
key = re.findall(r'>.*</dt>',keyList[i])[0][1:-5]
try:
val = re.findall(r'\d+\.?.*</dd>',valueLIst[i])[0][0:-5]
except:
val = '--'
infoDict[key] = val
infoDict.update({'股票名称':re.findall('\s.*\(',name)[0].split()[0] + re.findall('\>.*\<',name)[0][1:-1]})
yield infoDict
##end spider
##pipeline
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
class BaidustocksPipeline(object):
def process_item(self, item, spider):
return item

class BaidustocksInfoPipeline(object):
def open_spider(self, spider):
self.f = open('BaiduStcokInfo.txt','w')

def close_spider(self,spider):
self.f.close()

def process_item(self,item,spider):
try:
line = str(dict(item)) + '\n'
self.f.write(line)
except:
pass
return item
##end pipeline
##settings
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'BaiduStocks.pipelines.BaidustocksInfoPipeline': 300,
}
##end settings
##运行
>>> scrapy crawl stocks

配置并发连接选项优化爬取速度:

setting.py文件

选项 说明
CONCURRENT_REQUESTS Downloader最大并发请求下载数量,默认32
CONCURRENT_ITEMS Item Pipeline 最大并发ITEM处理数量,默认100
CONCURRENT_REQUESTS_PER_DOMAIN 每个目标域名最大的并发请求数量,默认8
CONCURRENT_REQUESTS_PER_IP 每个目标IP最大的并发请求数量,默认0,非0有效

Scrapy爬虫的地位

  • Python语言最好的爬虫框架;
  • 具备企业级专业爬虫的扩展性(7*24高可靠性);
  • 千万级URL爬取管理与部署。

Scrapy爬虫足以支撑一般商业服务所需的爬虫能力。

  • 普通价值:
    • 基于linux服务器,7*24,稳定爬取输出;
    • 商业级部署和应用(scrapyd-*);
    • 千万级URL爬取、内容分析和存储;
  • 高阶价值:
    • 基于docker,虚拟化部署;
    • 中间件扩展,增加调度和监控;
    • 各种反爬取对抗技术。

写在最后

两种技术路线:

  • requests-bs4-re
  • scrapy(5+2结构)

由于这两种技术路线,还没法处理js的表单提交、爬取周期、入库管理等相关功能,还需要配置pyantomJS库来扩展来解析js

对于https://pypi.python.org网站上以scrapy-开头的文件都是用来完善scrapy库,也可以用来尝试。

“君子曰:学不可以已。积土成山,风雨兴焉。”–荀子《劝学》

生命不息,学习不止。

感谢嵩老师的课程。