页面解析和数据提取

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

  • 结构化数据:先有结构、再有数据
  • 非结构化数据:先有数据,再有结构,
  • 不同类型的数据,我们需要采用不同的方式来处理。

六、数据处理

结构化的数据处理

HTML 文件
  • 正则表达式
  • XPath
  • CSS选择器
JSON 文件
  • JsonPath
  • JSON 模块转化成Python类型进行操作
XML 文件
  • lxml模块 模块转化成Python类型进行操作
  • XPath
  • CSS选择器
  • 正则表达式

非结构化的数据处理

普通文本文件(如提取电话号码、邮箱地址等)
  • 正则表达式
JavaScript 文件、CSS 文件(提取特定值等)
  • 正则表达式
二进制文件(图片、音乐、视频等)
  • 无法提取,直接保存指定格式的磁盘文件

(一)正则

为什么要学正则表达式

实际上爬虫一共就四个主要步骤:

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

我们在昨天的案例里实际上省略了第3步,也就是"取"的步骤。因为我们down下了的数据是全部的网页,这些数据很庞大并且很混乱,大部分的东西使我们不关心的,因此我们需要将之按我们的需要过滤和匹配出来。

那么对于文本的过滤或者规则的匹配,最强大的就是正则表达式,是Python爬虫世界里必不可少的神兵利器。

什么是正则表达式

正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本。 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

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

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

正则表达式匹配规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VgL7Zf3W-1572593860656)(assets/re01.png)]

Python 的 re 模块

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

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

r'zhongguo\t\.\tpython'
re 模块的一般使用步骤如下:
  1. 使用 compile() 函数将正则表达式的字符串形式编译为一个 Pattern 对象
  2. 通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果,一个 Match 对象。
  3. 最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作

compile 函数

compile 函数用于编译正则表达式,生成一个 Pattern 对象,它的一般使用形式如下:

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

在上面,我们已将一个正则表达式编译成 Pattern 对象,接下来,我们就可以利用 pattern 的一系列方法对文本进行匹配查找了。

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

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

——————————————————————————————————————————————————

案例:使用正则表达式的爬虫

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

下面我们一起尝试一下爬取内涵段子网站: 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规律找到了,要想爬取所有的段子,只需要修改一个参数即可。 下面我们就开始一步一步将所有的段子爬取下来吧。

# -*- coding:utf-8 -*-
import requestsclass NeihanSpider(object):def __init__(self):# 请求报头self.headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"}# 固定的url地址self.base_url = "https://www.neihan-8.com/article/list_5_"self.page = 1def send_request(self, url):"""接收url地址,发送请求并返回响应"""response = requests.get(url, headers=self.headers)return responsedef parse_response(self, response):"""接收响应,打印响应内容"""html = response.contentprint(html)def save_file(self, content_list):passdef main(self):full_url = self.base_url + str(self.page) + ".html"print(full_url)try:response = self.send_request(full_url)self.parse_response(response)except Exception as e:print("[ERROR]: 页面抓取失败 {}".format(full_url))print(e)print("谢谢使用!")if __name__ == '__main__':spider = NeihanSpider()spider.main()
  • 程序正常执行的话,我们会在屏幕上打印了内涵段子第一页的全部html代码。 但是我们发现,html中的中文部分显示的可能是乱码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E54AO1fG-1572593860657)(assets/luanma-1559639278886.png)]

那么我们需要简单的将得到的网页源代码处理一下:

# -*- coding:utf-8 -*-
import requests
import reclass NeihanSpider(object):def __init__(self):# 请求报头self.headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"}# 固定的url地址self.base_url = "https://www.neihan-8.com/article/list_5_"self.page = 1def send_request(self, url):"""接收url地址,发送请求并返回响应"""response = requests.get(url, headers=self.headers)return responsedef parse_response(self, response):"""接收响应,并提取数据"""# 将网页字符串编码从 gbk 转为 utf-8html = response.content.decode("gbk").encode("utf-8")print(html)def save_file(self, content_list):passdef main(self):full_url = self.base_url + str(self.page) + ".html"print(full_url)try:response = self.send_request(full_url)self.parse_response(response)except Exception as e:print("[ERROR]: 页面抓取失败 {}".format(full_url))print(e)print("谢谢使用!")if __name__ == '__main__':spider = NeihanSpider()spider.main()

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

  • 这样我们再次执行以下duanzi_spider.py ,会发现之前的中文乱码可以正常显示了。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z4WwYpuA-1572593860657)(assets/jiejueluanma-1559640221003.png)]

第二步:筛选数据

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

  • 首先
import re
  • 然后, 在我们得到的html中进行筛选匹配。

我们需要一个匹配规则:

我们可以打开内涵段子的网页,鼠标点击右键 “ 查看源代码 ” 你会惊奇的发现,我们需要的每个段子的内容都是在一个 <div>标签中,而且每个div都有一个属性class = "f18 mb20"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzAc3W9N-1572593860657)(assets/neihan-1559637074289.png)]

所以,我们只需要匹配到网页中所有<div class="f18 mb20"></div> 的数据就可以了。

根据正则表达式,我们可以推算出一个公式是:

<div.*?class="f18 mb20">(.*?)</div>

  • 这个表达式实际上就是匹配到所有divclass="f18 mb20 里面的内容(具体可以看前面正则介绍)
  • 然后将这个正则应用到代码中,我们会得到以下代码:
import requests
import reclass NeihanSpider(object):def __init__(self):# 请求报头self.headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"}# 固定的url地址self.base_url = "https://www.neihan-8.com/article/list_5_"# 需要自增的页码值self.page = 1# 匹配网页中所有的段子内容,并保存# re.S 作用是启用DOTALL模式,让 . 也可以匹配换行符self.pattern_page = re.compile(r'<div class="f18 mb20">(.*?)</div>', re.S)def send_request(self, url):"""接收url地址,发送请求并返回响应"""response = requests.get(url, headers=self.headers)return responsedef parse_response(self, response):"""接收响应,并提取数据"""# 将网页字符串编码从 gbk 转为 utf-8html = response.content.decode("gbk").encode("utf-8")# findall 返回所有符合匹配结果的列表content_list = self.pattern_page.findall(html)print(content_list)def save_file(self):passdef main(self):full_url = self.base_url + str(self.page) + ".html"print(full_url)try:response = self.send_request(full_url)self.parse_response(response)except Exception as e:print("[ERROR]: 页面抓取失败 {}".format(full_url))print(e)print("谢谢使用!")if __name__ == '__main__':spider = NeihanSpider()spider.main()
  • 这里需要注意一个是re.S是正则表达式中匹配的一个参数。
  • 如果 没有re.S 则是 只匹配一行 有没有符合规则的字符串,如果没有则下一行重新匹配。
  • 如果 加上re.S 则是将 所有的字符串 将一个整体进行匹配,findall 将所有匹配到的结果封装到一个list中。
  • 你会发现段子中有很多 <p> , </p> 很是不舒服,实际上这个是html的一种段落的标签。
  • 在浏览器上看不出来,但是如果按照文本打印会有<p>出现,那么我们只需要把我们不希望的内容去掉即可了。
  • 我们可以如下简单修改一下 .
import requests
import reclass NeihanSpider(object):def __init__(self):# 请求报头self.headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"}# 固定的url地址self.base_url = "https://www.neihan8.com/article/list_5_"# 需要自增的页码值self.page = 1# 匹配网页中所有的段子内容,并保存# re.S 作用是启用DOTALL模式,让 . 也可以匹配换行符self.pattern_page = re.compile(r'<div class="f18 mb20">(.*?)</div>', re.S)# 匹配每条段子里无用字符部分,并替换为空:# \s 表示空白符,如果空格、换行符等# &.*?; 表示 HTML实体字符,如 &nbsp; 等# <.*?> 表示 标签,如 <p> 、 <\br> 等#   或者 u"\u3000".encode("utf-8") 表示 中文全角空格#self.pattern_content = re.compile(r"\s|&.*?;|<.*?>| ")self.pattern_content = re.compile(r"\s|&.*?;|<.*?>|" + u"\u3000".encode("utf-8"))def send_request(self, url):"""接收url地址,发送请求并返回响应"""response = requests.get(url, headers=self.headers)return responsedef parse_response(self, response):"""接收响应,并提取数据"""# 将网页字符串编码从 gbk 转为 utf-8html = response.content.decode("gbk").encode("utf-8")# findall 返回所有符合匹配结果的列表content_list = self.pattern_page.findall(html)return content_listdef save_file(self):passdef main(self):full_url = self.base_url + str(self.page) + ".html"print(full_url)try:response = self.send_request(full_url)self.parse_response(response)except Exception as e:print("[ERROR]: 页面抓取失败 {}".format(full_url))print(e)print("谢谢使用!")if __name__ == '__main__':spider = NeihanSpider()spider.main()

第三步:保存数据
  • 我们可以将所有的段子存放在文件中。
#coding:utf-8import requests
import reclass NeihanSpider(object):def __init__(self):# 请求报头self.headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"}# 固定的url地址self.base_url = "https://www.neihan8.com/article/list_5_"# 需要自增的页码值self.page = 1# 匹配网页中所有的段子内容,并保存# re.S 作用是启用DOTALL模式,让 . 也可以匹配换行符self.pattern_page = re.compile(r'<div class="f18 mb20">(.*?)</div>', re.S)# 匹配每条段子里无用字符部分,并替换为空:# \s 表示空白符,如果空格、换行符等# &.*?; 表示 HTML实体字符,如 &nbsp; 等# <.*?> 表示 标签,如 <p> 、 <\br> 等#   或者 u"\u3000".encode("utf-8") 表示 中文全角空格#self.pattern_content = re.compile(r"\s|&.*?;|<.*?>| ")self.pattern_content = re.compile(r"\s|&.*?;|<.*?>|" + u"\u3000".encode("utf-8"))def send_request(self, url):"""接收url地址,发送请求并返回响应"""response = requests.get(url, headers=self.headers)return responsedef parse_response(self, response):"""接收响应,并提取数据"""# 将网页字符串编码从 gbk 转为 utf-8html = response.content.decode("gbk").encode("utf-8")# findall 返回所有符合匹配结果的列表content_list = self.pattern_page.findall(html)return content_listdef save_file(self, content_list):"""接收每页的段子,并保存在文件中"""with open("duanzi.txt", "a") as f:f.write("第{}页:\n".format(self.page))for content in content_list:# 对每条段子进行清洗,去除无用字符,并返回清洗后的文本result = self.pattern_content.sub("", content)# 写入文本到文件中f.write(result)f.write("\n")f.write("\n\n")def main(self):full_url = self.base_url + str(self.page) + ".html"print(full_url)try:response = self.send_request(full_url)self.parse_response(response)except Exception as e:print("[ERROR]: 页面抓取失败 {}".format(full_url))print(e)print("谢谢使用!")if __name__ == '__main__':spider = NeihanSpider()spider.main()
第四步:保存所有数据
  • 接下来我们就通过参数的传递对page进行叠加来遍历 内涵段子吧的全部段子内容。
  • 只需要在外层加一些逻辑处理即可。
#coding:utf-8import requests
import reclass NeihanSpider(object):def __init__(self):# 请求报头self.headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"}# 固定的url地址self.base_url = "https://www.neihan8.com/article/list_5_"# 需要自增的页码值self.page = 1# 匹配网页中所有的段子内容,并保存# re.S 作用是启用DOTALL模式,让 . 也可以匹配换行符self.pattern_page = re.compile(r'<div class="f18 mb20">(.*?)</div>', re.S)# 匹配每条段子里无用字符部分,并替换为空:# \s 表示空白符,如果空格、换行符等# &.*?; 表示 HTML实体字符,如 &nbsp; 等# <.*?> 表示 标签,如 <p> 、 <\br> 等#   或者 u"\u3000".encode("utf-8") 表示 中文全角空格#self.pattern_content = re.compile(r"\s|&.*?;|<.*?>| ")self.pattern_content = re.compile(r"\s|&.*?;|<.*?>|" + u"\u3000".encode("utf-8"))def send_request(self, url):"""接收url地址,发送请求并返回响应"""response = requests.get(url, headers=self.headers)return responsedef parse_response(self, response):"""接收响应,并提取数据"""# 将网页字符串编码从 gbk 转为 utf-8html = response.content.decode("gbk").encode("utf-8")# findall 返回所有符合匹配结果的列表content_list = self.pattern_page.findall(html)return content_listdef save_file(self, content_list):"""接收每页的段子,并保存在文件中"""with open("duanzi.txt", "a") as f:f.write("第{}页:\n".format(self.page))for content in content_list:# 对每条段子进行清洗,去除无用字符,并返回清洗后的文本result = self.pattern_content.sub("", content)# 写入文本到文件中f.write(result)f.write("\n")f.write("\n\n")def main(self):while True:if raw_input("输入回车抓取下一页(输入q则退出):") == 'q':breakfull_url = self.base_url + str(self.page) + ".html"try:response = self.send_request(full_url)content_list = self.parse_response(response)self.save_file(content_list)except Exception as e:print("[ERROR]: 页面抓取失败 {}".format(full_url))print(e)self.page += 1print("谢谢使用!")if __name__ == '__main__':spider = NeihanSpider()spider.main()
  • 最后,我们执行我们的代码,完成后查看当前路径下的duanzi.txt文件,里面已经有了我们要的内涵段子。

以上便是一个非常精简使用的小爬虫程序,使用起来很是方便,如果想要爬取其他网站的信息,只需要修改其中某些参数和一些细节就行了。

——————————————————————————————————————————

有同学说,我正则用的不好,处理HTML文档很累,有没有其他的方法?

有!那就是XPath,我们可以先将 HTML文件 转换成 XML文档,然后用 XPath语法 查找 HTML 节点或元素。

(二)什么是XML

  • XML 指可扩展标记语言(EXtensible Markup Language)
  • XML 是一种标记语言,很类似 HTML
  • XML 的设计宗旨是传输数据,而非显示数据
  • XML 的标签需要我们自行定义。
  • XML 被设计为具有自我描述性。
  • XML 是 W3C 的推荐标准

W3School官方文档:http://www.w3school.com.cn/xml/index.asp

XML 和 HTML 的区别
数据格式 描述 设计目标
XML Extensible Markup Language (可扩展标记语言) 被设计为传输和存储数据,其焦点是数据的内容。
HTML HyperText Markup Language (超文本标记语言) 显示数据以及如何更好显示数据。
HTML DOM Document Object Model for HTML (文档对象模型) 通过 HTML DOM,可以访问所有的 HTML 元素,连同它们所包含的文本和属性。可以对其中的内容进行修改和删除,同时也可以创建新的元素。
XML文档示例
<?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="children"> <title lang="en">Harry Potter</title>  <author>J K. Rowling</author>  <year>2005</year>  <price>29.99</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" cover="paperback"> <title lang="en">Learning XML</title>  <author>Erik T. Ray</author>  <year>2003</year>  <price>39.95</price> </book> </bookstore>
HTML DOM 模型示例

HTML DOM 定义了访问和操作 HTML 文档的标准方法,以树结构方式表达 HTML 文档。


XML的节点关系
1. 父(Parent)

每个元素以及属性都有一个父。

下面是一个简单的XML例子中,book 元素是 title、author、year 以及 price 元素的父:

<?xml version="1.0" encoding="utf-8"?><book><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
  1. 子(Children)

元素节点可有零个、一个或多个子。

在下面的例子中,title、author、year 以及 price 元素都是 book 元素的子:

<?xml version="1.0" encoding="utf-8"?><book><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
3. 同胞(Sibling)

拥有相同的父的节点

在下面的例子中,title、author、year 以及 price 元素都是同胞:

<?xml version="1.0" encoding="utf-8"?><book><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
4. 先辈(Ancestor)

某节点的父、父的父,等等。

在下面的例子中,title 元素的先辈是 book 元素和 bookstore 元素:

<?xml version="1.0" encoding="utf-8"?><bookstore><book><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book></bookstore>
5. 后代(Descendant)

某个节点的子,子的子,等等。

在下面的例子中,bookstore 的后代是 book、title、author、year 以及 price 元素:

<?xml version="1.0" encoding="utf-8"?><bookstore><book><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book></bookstore>

(三)什么是XPath?

XPath (XML Path Language) 是一门在 XML 文档中查找信息的语言,可用来在 XML 文档中对元素和属性进行遍历。

W3School官方文档:http://www.w3school.com.cn/xpath/index.asp

XPath 开发工具

  1. 开源的XPath表达式编辑工具:XMLQuire(XML格式文件可用)
  2. Chrome插件 XPath Helper
  3. Firefox插件 XPath Checker

选取节点

XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。

下面列出了最常用的路径表达式:

表达式 描述
nodename 选取此节点的所有子节点。
/ 从根节点选取。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
选取当前节点的父节点。
@ 选取属性。

在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:

路径表达式 结果
bookstore 选取 bookstore 元素的所有子节点。
/bookstore 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!
bookstore/book 选取属于 bookstore 的子元素的所有 book 元素。
//book 选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。
//@lang 选取名为 lang 的所有属性。

谓语(Predicates)

谓语用来查找某个特定的节点或者包含某个指定的值的节点,被嵌在方括号中。

在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:

路径表达式 结果
/bookstore/book[1] 选取属于 bookstore 子元素的第一个 book 元素。
/bookstore/book[last()] 选取属于 bookstore 子元素的最后一个 book 元素。
/bookstore/book[last()-1] 选取属于 bookstore 子元素的倒数第二个 book 元素。
/bookstore/book[position()❤️] 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
//title[@lang] 选取所有拥有名为 lang 的属性的 title 元素。
//title[@lang=’eng’] 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
/bookstore/book[price>35.00] 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
/bookstore/book[price>35.00]/title 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

选取未知节点

XPath 通配符可用来选取未知的 XML 元素。

通配符 描述
* 匹配任何元素节点。
@* 匹配任何属性节点。
node() 匹配任何类型的节点。

在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

路径表达式 结果
/bookstore/* 选取 bookstore 元素的所有子元素。
//* 选取文档中的所有元素。
//title[@*] 选取所有带有属性的 title 元素。

选取若干路径

通过在路径表达式中使用“|”运算符,您可以选取若干个路径。

实例

在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

路径表达式 结果
//book/title | //book/price 选取 book 元素的所有 title 和 price 元素。
//title | //price 选取文档中的所有 title 和 price 元素。
/bookstore/book/title | //price 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。

XPath的运算符

下面列出了可用在 XPath 表达式中的运算符:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FwwDHEA-1572593860658)(assets/xpath01.png)]

这些就是XPath的语法内容,在运用到Python抓取时要先转换为xml

lxml库

lxml 是 一个HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 数据。

lxml和正则一样,也是用 C 实现的,是一款高性能的 Python HTML/XML 解析器,我们可以利用之前学习的XPath语法,来快速的定位特定元素以及节点信息。

lxml python 官方文档:http://lxml.de/index.html

需要安装C语言库,可使用 pip 安装:pip install lxml (或通过wheel方式安装)

初步使用

我们利用它来解析 HTML 代码,简单示例:

# lxml_test.py# 使用 lxml 的 etree 库
from lxml import etree text = '''
<div><ul><li class="item-0"><a href="link1.html">first item</a></li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-inactive"><a href="link3.html">third item</a></li><li class="item-1"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a> # 注意,此处缺少一个 </li> 闭合标签</ul></div>
'''#利用etree.HTML,将字符串解析为HTML文档
html = etree.HTML(text) # 按字符串序列化HTML文档
result = etree.tostring(html) print(result)

输出结果:

<html><body>
<div><ul><li class="item-0"><a href="link1.html">first item</a></li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-inactive"><a href="link3.html">third item</a></li><li class="item-1"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li>
</ul></div>
</body></html>

lxml 可以自动修正 html 代码,例子里不仅补全了 li 标签,还添加了 body,html 标签。

文件读取:

除了直接读取字符串,lxml还支持从文件里读取内容。我们新建一个hello.html文件:

<!-- hello.html --><div><ul><li class="item-0"><a href="link1.html">first item</a></li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-inactive"><a href="link3.html"><span class="bold">third item</span></a></li><li class="item-1"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li></ul></div>

再利用 etree.parse() 方法来读取文件。

# lxml_parse.pyfrom lxml import etree# 读取外部文件 hello.html
html = etree.parse('./hello.html')
result = etree.tostring(html, pretty_print=True)print(result)

输出结果与之前相同:

<html><body>
<div><ul><li class="item-0"><a href="link1.html">first item</a></li><li class="item-1"><a href="link2.html">second item</a></li><li class="item-inactive"><a href="link3.html">third item</a></li><li class="item-1"><a href="link4.html">fourth item</a></li><li class="item-0"><a href="link5.html">fifth item</a></li>
</ul></div>
</body></html>
XPath实例测试

1. 获取所有的 <li> 标签

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')
print(type(html)  # 显示etree.parse() 返回类型)result = html.xpath('//li')print(result  # 打印<li>标签的元素集合)
print(len(result))
print(type(result))
print(type(result[0]))

输出结果:

<type 'lxml.etree._ElementTree'>
[<Element li at 0x1014e0e18>, <Element li at 0x1014e0ef0>, <Element li at 0x1014e0f38>, <Element li at 0x1014e0f80>, <Element li at 0x1014e0fc8>]
5
<type 'list'>
<type 'lxml.etree._Element'>

2. 继续获取<li> 标签的所有 class属性

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')
result = html.xpath('//li/@class')print(result)

运行结果

['item-0', 'item-1', 'item-inactive', 'item-1', 'item-0']

3. 继续获取<li>标签下hrelink1.html<a> 标签

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')
result = html.xpath('//li/a[@href="link1.html"]')print(result)

运行结果

[<Element a at 0x10ffaae18>]

4. 获取<li> 标签下的所有 <span> 标签

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')#result = html.xpath('//li/span')
#注意这么写是不对的:
#因为 / 是用来获取子元素的,而 <span> 并不是 <li> 的子元素,所以,要用双斜杠result = html.xpath('//li//span')print(result)

运行结果

[<Element span at 0x10d698e18>]

5. 获取 <li> 标签下的<a>标签里的所有 class

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')
result = html.xpath('//li/a//@class')print(result)

运行结果

['blod']

6. 获取最后一个 <li><a> 的 href

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')result = html.xpath('//li[last()]/a/@href')
# 谓语 [last()] 可以找到最后一个元素print(result)

运行结果

['link5.html']

7. 获取倒数第二个元素的内容

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')
result = html.xpath('//li[last()-1]/a')# text 方法可以获取元素内容
print(result[0].text)

运行结果

fourth item

8. 获取 class 值为 bold 的标签名

# xpath_li.pyfrom lxml import etreehtml = etree.parse('hello.html')result = html.xpath('//*[@class="bold"]')# tag方法可以获取标签名
print(result[0].tag)

运行结果

span

——————————————————————————————————————————————————

案例:使用XPath的爬虫

现在我们用XPath来做一个简单的爬虫,我们尝试爬取某个贴吧里的所有帖子,并且将该这个帖子里每个楼层发布的图片下载到本地。

#coding:utf-8import requests
from lxml import etreeclass TiebaSpider(object):def __init__(self):self.headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"}self.base_url = "http://tieba.baidu.com"self.tieba_name = raw_input("请输入需要抓取的贴吧:")self.begin_page = int(raw_input("请输入需要抓取起始页:"))self.end_page = int(raw_input("请输入需要抓取结束页:"))def send_request(self, url, params={}):"""发送请求,返回响应"""response = requests.get(url, params=params, headers=self.headers)return responsedef parse_page(self, response):"""解析帖子列表页,获取每个帖子的链接"""html_obj = etree.HTML(response.content)# 提取帖子详情页链接link_list = html_obj.xpath("//a[@class='j_th_tit ']/@href")#print(link_list)for link in link_list:url = self.base_url + link# 发送帖子的请求page_response = self.send_request(url)self.parse_image(page_response)def parse_image(self, response):"""解析帖子,获取每个图片的链接"""html_obj = etree.HTML(response.content)# 提取帖子里每个图片的链接link_list = html_obj.xpath("//img[@class='BDE_Image']/@src")for link in link_list:# 发送图片请求返回响应image_response = self.send_request(link)# 在保存图片内容self.save_image(image_response, link[-15:])def save_image(self, response, filename):"""接收图片响应,并保存图片"""print("[INFO]: 正在保存图片 {}..".format(filename))with open(filename, "wb") as f:f.write(response.content)def main(self):for page in range(self.begin_page, self.end_page + 1):pn = (page - 1) * 50query_dict = {"kw" : self.tieba_name, "pn" : pn}full_url = self.base_url + "/f?"response = self.send_request(full_url, query_dict)self.parse_page(response)if __name__ == '__main__':spider = TiebaSpider()spider.main()
练习:
爬取果壳网的标题链接,并抓取里面的简介内容,如果该标题有简介则打印。
参考见:goukr.py

——————————————————————————————————————————————————

(四)CSS 选择器:BeautifulSoup4

和 lxml 一样,Beautiful Soup 也是一个HTML/XML的解析器,主要的功能也是如何解析和提取 HTML/XML 数据。

lxml 只会局部遍历,而Beautiful Soup 是基于HTML DOM的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会大很多,所以性能要低于lxml。

BeautifulSoup 用来解析 HTML 比较简单,API非常人性化,支持CSS选择器、Python标准库中的HTML解析器,也支持 lxml 的 XML解析器。

Beautiful Soup 3 目前已经停止开发,推荐现在的项目使用Beautiful Soup 4。使用 pip 安装即可:pip install beautifulsoup4

官方文档:http://beautifulsoup.readthedocs.io/zh_CN/v4.4.0

抓取工具 速度 使用难度 安装难度
正则 最快 困难 无(内置)
BeautifulSoup 最简单 简单
lxml 简单 一般

示例:

首先必须要导入 bs4 库

# beautifulsoup4_test.pyfrom bs4 import BeautifulSouphtml = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""#创建 Beautiful Soup 对象
soup = BeautifulSoup(html)#打开本地 HTML 文件的方式来创建对象
#soup = BeautifulSoup(open('index.html'))#格式化输出 soup 对象的内容
print(soup.prettify())

运行结果:

<html><head><title>The Dormouse's story</title></head><body><p class="title" name="dromouse"><b>The Dormouse's story</b></p><p class="story">Once upon a time there were three little sisters; and their names were<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>and<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p><p class="story">...</p></body>
</html>
  • 如果我们没有显式地指定解析器,所以默认使用这个系统的最佳可用HTML解析器(“lxml”)。如果你在另一个系统中运行这段代码,或者在不同的虚拟环境中,使用不同的解析器造成行为不同。
  • 但是我们可以通过soup = BeautifulSoup(html,“lxml”)方式指定lxml解析器。

四大对象种类

Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种:

  • Tag
  • NavigableString
  • BeautifulSoup
  • Comment
1. Tag

Tag 通俗点讲就是 HTML 中的一个个标签,例如:

<head><title>The Dormouse's story</title></head>
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>

上面的 title head a p等等 HTML 标签加上里面包括的内容就是 Tag,那么试着使用 Beautiful Soup 来获取 Tags:

from bs4 import BeautifulSouphtml = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""#创建 Beautiful Soup 对象
soup = BeautifulSoup(html)print(soup.title)
# <title>The Dormouse's story</title>print(soup.head)
# <head><title>The Dormouse's story</title></head>print(soup.a)
# <a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>print(soup.p)
# <p class="title" name="dromouse"><b>The Dormouse's story</b></p>print(type(soup.p))
# <class 'bs4.element.Tag'>

我们可以利用 soup 加标签名轻松地获取这些标签的内容,这些对象的类型是bs4.element.Tag。但是注意,它查找的是在所有内容中的第一个符合要求的标签。如果要查询所有的标签,后面会进行介绍。

对于 Tag,它有两个重要的属性,是 name 和 attrs

print(soup.name)
# [document] #soup 对象本身比较特殊,它的 name 即为 [document]print(soup.head.name)
# head #对于其他内部标签,输出的值便为标签本身的名称print(soup.p.attrs)
# {'class': ['title'], 'name': 'dromouse'}
# 在这里,我们把 p 标签的所有属性打印输出了出来,得到的类型是一个字典。print(soup.p['class'] # soup.p.get('class'))
# ['title'] #还可以利用get方法,传入属性的名称,二者是等价的soup.p['class'] = "newClass"
print(soup.p # 可以对这些属性和内容等等进行修改)
# <p class="newClass" name="dromouse"><b>The Dormouse's story</b></p>del soup.p['class'] # 还可以对这个属性进行删除
print(soup.p)
# <p name="dromouse"><b>The Dormouse's story</b></p>
2. NavigableString

既然我们已经得到了标签的内容,那么问题来了,我们要想获取标签内部的文字怎么办呢?很简单,用 .string 即可,例如

print(soup.p.string)
# The Dormouse's storyprint(type(soup.p.string))
# In [13]: <class 'bs4.element.NavigableString'>
3. BeautifulSoup

BeautifulSoup 对象表示的是一个文档的内容。大部分时候,可以把它当作 Tag 对象,是一个特殊的 Tag,我们可以分别获取它的类型,名称,以及属性来感受一下

print(type(soup.name))
# <type 'unicode'>print(soup.name )
# [document]print(soup.attrs # 文档本身的属性为空)
# {}
4. Comment

Comment 对象是一个特殊类型的 NavigableString 对象,其输出的内容不包括注释符号。

print(soup.a)
# <a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>print(soup.a.string)
# Elsie print(type(soup.a.string))
# <class 'bs4.element.Comment'>

a 标签里的内容实际上是注释,但是如果我们利用 .string 来输出它的内容时,注释符号已经去掉了。

遍历文档树

1. 直接子节点 :.contents .children 属性

.content

tag 的 .content 属性可以将tag的子节点以列表的方式输出

print(soup.head.contents )
#[<title>The Dormouse's story</title>]

输出方式为列表,我们可以用列表索引来获取它的某一个元素

print(soup.head.contents[0])
#<title>The Dormouse's story</title>

.children

它返回的不是一个 list,不过我们可以通过遍历获取所有子节点。

我们打印输出 .children 看一下,可以发现它是一个 list 生成器对象

print(soup.head.children)
#<listiterator object at 0x7f71457f5710>for child in  soup.body.children:print(child)

结果:

<p class="title" name="dromouse"><b>The Dormouse's story</b></p><p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p><p class="story">...</p>
2. 所有子孙节点: .descendants 属性

.contents 和 .children 属性仅包含tag的直接子节点,.descendants 属性可以对所有tag的子孙节点进行递归循环,和 children类似,我们也需要遍历获取其中的内容。

for child in soup.descendants:print(child)

运行结果:

<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body></html>
<head><title>The Dormouse's story</title></head>
<title>The Dormouse's story</title>
The Dormouse's story<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body><p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<b>The Dormouse's story</b>
The Dormouse's story<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
Once upon a time there were three little sisters; and their names were<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>Elsie
,<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
Lacieand<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
Tillie
;
and they lived at the bottom of a well.<p class="story">...</p>
3. 节点内容: .string 属性

如果一个标签里面没有标签了,那么 .string 就会返回标签里面的内容。如果标签里面只有唯一的一个标签了,那么 .string 也会返回最里面的内容。例如:

print(soup.head.string)
#The Dormouse's story
print(soup.title.string)
#The Dormouse's story

搜索文档树

1.find_all(name, attrs, recursive, text, **kwargs)
1)name 参数

name 参数可以查找所有名字为 name 的tag,字符串对象会被自动忽略掉

A.传字符串

最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的<b>标签:

soup.find_all('b')
# [<b>The Dormouse's story</b>]print(soup.find_all('a'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

B.传正则表达式

如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容.下面例子中找出所有以b开头的标签,这表示<body><b>标签都应该被找到

import re
for tag in soup.find_all(re.compile("^b")):print(tag.name)
# body
# b

C.传列表

如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有<a>标签和<b>标签:

soup.find_all(["a", "b"])
# [<b>The Dormouse's story</b>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
2)keyword 参数
soup.find_all(class_ = "sister")
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
3)text 参数

通过 text 参数可以搜索文档中的字符串内容,与 name 参数的可选值一样, text 参数接受 字符串 , 正则表达式 , 列表

soup.find_all(text="Elsie")
# [u'Elsie']soup.find_all(text=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']soup.find_all(text=re.compile("Dormouse"))
[u"The Dormouse's story", u"The Dormouse's story"]
2. find

find的用法与find_all一样,区别在于find返回 第一个符合匹配结果find_all则返回 所有匹配结果的列表

3. CSS选择器

这就是另一种与 find_all 方法有异曲同工之妙的查找方法,也是返回所有匹配结果的列表。

  • 写 CSS 时,标签名不加任何修饰,类名前加.,id名前加#
  • 在这里我们也可以利用类似的方法来筛选元素,用到的方法是 soup.select(),返回类型是 list
(1)通过标签名查找
print(soup.select('title') )
#[<title>The Dormouse's story</title>]print(soup.select('a'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]print(soup.select('b'))
#[<b>The Dormouse's story</b>]
(2)通过类名查找
print(soup.select('.sister'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
(3)通过 id 名查找
print(soup.select('#link1'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
(4)组合查找

组合查找即和写 class 文件时,标签名与类名、id名进行的组合原理是一样的,例如查找 p 标签中,id 等于 link1的内容,二者需要用空格分开

print(soup.select('p #link1'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]

直接子标签查找,则使用 > 分隔

print(soup.select("head > title"))
#[<title>The Dormouse's story</title>]
(5)属性查找

查找时还可以加入属性元素,属性需要用中括号括起来,注意属性和标签属于同一节点,所以中间不能加空格,否则会无法匹配到。

print(soup.select('a[class="sister"]'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]print(soup.select('a[href="http://example.com/elsie"]'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]

同样,属性仍然可以与上述查找方式组合,不在同一节点的空格隔开,同一节点的不加空格

print(soup.select('p a[href="http://example.com/elsie"]'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
(6) 获取内容

以上的 select 方法返回的结果都是列表形式,可以遍历形式输出,然后用 get_text() 方法来获取它的内容。

soup = BeautifulSoup(html, 'lxml')
print(type(soup.select('title')))
print(soup.select('title')[0].get_text())for title in soup.select('title'):print(title.get_text())

——————————————————————————————————————————————————

案例:使用BeautifuSoup4的爬虫

我们以网易社招页面来做演示:

使用BeautifuSoup4解析器,将招聘网页上的职位名称、职位类别、招聘人数、工作地点、发布时间,以及每个职位详情的点击链接存储出来。

import jsonimport requests
from bs4 import BeautifulSoupclass WangYiSpider(object):def __init__(self):self.base_url = "https://hr.163.com/position/list.do?positionName=&currentPage="self.page = 1# 初始化一个字典列表,保存职位信息self.item_list = []self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"}def send_request(self, full_url):response = requests.get(full_url, headers=self.headers)return responsedef parse_response(self, response):html = response.content.decode("utf-8")soup = BeautifulSoup(html, "lxml")node_list = soup.find_all("tr", attrs={"id":""})node_list.pop(0)for node in node_list:item = {}# 遍历css选择器列表for name in node.select("td a"):# 从a标签获取职位内容, 保存到字典中item["position_name"] = name.get_text()# 部门item["bumen"] = node.find_all("td")[1].get_text()print(item["bumen"])# 职位item["position"] = node.find_all("td")[2].get_text()# 工作类型item["work_type"] = node.find_all("td")[3].get_text()# 工作地点item["work_place"] = node.find_all("td")[4].get_text()# 招聘人数item["counts"] = node.find_all("td")[5].get_text()# 发布时间item["time"] = node.find_all("td")[6].get_text()# 链接详情item["link"] = node.find_all("td")[0].a.get("href")# 每次循环获取一个职位信息,并保存在同一个列表中self.item_list.append(item)def save_data(self):"""实现数据存储"""# 写入了Unicode字符串的json数据# 将Python的列表 转为 Json字符串json_str = json.dumps(self.item_list)# 写入json字符串数据到文件中with open("wangyi.json", "w") as f:f.write(json_str)# 写入utf-8字符串的json数据(Python2 需要通过sys模块修改解释器编码即可)# json_str = json.dumps(self.item_list, ensure_ascii=False)# # 写入json字符串数据到文件中# with open("wangyi.json", "w") as f:#     f.write(json_str)def main(self):while True:q = input("q退出")if q == "q" or self.page == 125:breakfull_url = self.base_url + str(self.page)response = self.send_request(full_url=full_url)self.parse_response(response)self.save_data()self.page += 1if __name__ == '__main__':spider = WangYiSpider()spider.main()
练习:
根据url:https://www.xinpianchang.com/channel/index/type-/sort-like/duration_type-0/resolution_type-/page-2
抓取新片场列表页的数据:
用户名,作者信息,保存片场名, 视频来源,观看次数,点赞数。并将数据保存在json文件中
# xin_pian_chang_bs4.py

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MNzoTDgk-1572593860660)(assets/1567060774891.png)]

——————————————————————————————————————————

(五)数据提取之JSON与JsonPATH

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,它使得人们很容易的进行阅读和编写。同时也方便了机器进行解析和生成。适用于进行数据交互的场景,比如网站前台与后台之间的数据交互。

JSON和XML的比较可谓不相上下。

Python 2.7中自带了JSON模块,直接import json就可以使用了。

官方文档:http://docs.python.org/library/json.html

Json在线解析网站:http://www.json.cn/#

JSON

json简单说就是javascript中的对象和数组,所以这两种结构就是对象和数组两种结构,通过这两种结构可以表示各种复杂的结构

  1. 对象:对象在js中表示为{ }括起来的内容,数据结构为 { key:value, key:value, ... }的键值对的结构,在面向对象的语言中,key为对象的属性,value为对应的属性值,所以很容易理解,取值方法为 对象.key 获取属性值,这个属性值的类型可以是数字、字符串、数组、对象这几种。
  2. 数组:数组在js中是中括号[ ]括起来的内容,数据结构为 ["Python", "javascript", "C++", ...],取值方式和所有语言中一样,使用索引获取,字段值的类型可以是 数字、字符串、数组、对象几种。

import json

json模块提供了四个功能:dumpsdumploadsload,用于字符串 和 python数据类型间进行转换。

1. json.loads()

把Json格式字符串解码转换成Python对象 从json到python的类型转化对照如下:

# json_loads.pyimport jsonstrList = '[1, 2, 3, 4]'strDict = '{"city": "北京", "name": "小明"}'json.loads(strList)
# [1, 2, 3, 4]json.loads(strDict) # json数据自动按Unicode存储
# {u'city': u'\u5317\u4eac', u'name': u'\u5c0f\u660e'}

2. json.dumps()

实现python类型转化为json字符串,返回一个str对象 把一个Python对象编码转换成Json字符串

从python原始类型向json类型的转化对照如下:

# json_dumps.pyimport json
import chardetlistStr = [1, 2, 3, 4]
tupleStr = (1, 2, 3, 4)
dictStr = {"city": "北京", "name": "小明"}json.dumps(listStr)
# '[1, 2, 3, 4]'
json.dumps(tupleStr)
# '[1, 2, 3, 4]'# 注意:json.dumps() 处理中文时默认使用的ascii编码,会导致中文无法正常显示
print(json.dumps(dictStr) )
# {"city": "\u5317\u4eac", "name": "\u5c0f\u660e"}# 记住:处理中文时,添加参数 ensure_ascii=False 来禁用ascii编码
print(json.dumps(dictStr, ensure_ascii=False) )
# {"city": "北京", "name": "小明"}

3. json.dump()

将Python内置类型序列化为json对象后写入文件

# json_dump.pyimport jsonlistStr = [{"city": "北京"}, {"name": "大刘"}]
json.dump(listStr, open("listStr.json","w"), ensure_ascii=False)dictStr = {"city": "北京", "name": "大刘"}
json.dump(dictStr, open("dictStr.json","w"), ensure_ascii=False)

4. json.load()

读取文件中json形式的字符串元素 转化成python类型

# json_load.pyimport jsonstrList = json.load(open("listStr.json"))
print(strList)# [{"city": "\u5317\u4eac", "name": "\u5c0f\u660e"}]strDict = json.load(open("dictStr.json"))
print(strDict)

JsonPath

JsonPath 是一种信息抽取类库,是从JSON文档中抽取指定信息的工具,提供多种语言实现版本,包括:Javascript, Python, PHP 和 Java。

JsonPath 对于 JSON 来说,相当于 XPath 对于 XML。

安装方法:pip install jsonpath

官方文档:http://goessner.net/articles/JsonPath

JsonPath与XPath语法对比:

Json结构清晰,可读性高,复杂度低,非常容易匹配,下表中对应了XPath的用法。

XPath JSONPath 描述
/ $ 根节点
. @ 现行节点
/ .or[] 取子节点
.. n/a 取父节点,Jsonpath未支持
// .. 就是不管位置,选择所有符合条件的条件
* * 匹配所有元素节点
@ n/a 根据属性访问,Json不支持,因为Json是个Key-value递归结构,不需要属性访问。
[] [] 迭代器标示(可以在里边做简单的迭代操作,如数组下标,根据内容选值等)
| [,] 支持迭代器中做多选。
[] ?() 支持过滤操作.
n/a () 支持表达式计算
() n/a 分组,JsonPath不支持

案例:拉钩网

我们以拉勾网城市JSON文件 http://www.lagou.com/lbs/getAllCitySearchLabels.json 为例,获取所有城市。

# jsonpath_lagou.pyimport urllib2
import jsonpath
import jsonurl = 'http://www.lagou.com/lbs/getAllCitySearchLabels.json'
request =urllib2.Request(url)
response = urllib2.urlopen(request)
html = response.read()# 把json格式字符串转换成python对象
jsonobj = json.loads(html)# 从根节点开始,匹配name节点
citylist = jsonpath.jsonpath(jsonobj,'$..name')print(citylist)
print(type(citylist))
fp = open('city.json','w')content = json.dumps(citylist, ensure_ascii=False)
print(content)fp.write(content.encode('utf-8'))
fp.close()
练习1:
创建一个py文件,抓取某一个百度贴吧名的所有列表链接,并保存到json文件中。练习参考答案见:baidu_save_to_json.py练习2:https://www.lagou.com
1.输入需要抓取的职位及城市,爬取拉钩所有页面,抓取列表页每个职位的的薪水、城市、职位名称、地区、创建时间、公司规模大小、公司全称、公司编号,保存到json文件中,
2.步骤1的基础上,继续抓取详情页的数据,保存到txt文本。
参考练习答案见:lagouspdier.py

注意事项:

json.loads() 是把 Json格式字符串解码转换成Python对象,如果在json.loads的时候出错,要注意被解码的Json字符的编码,如果传入的字符串的编码不是UTF-8的话,需要指定字符编码的参数encoding

如:

dataDict = json.loads(jsonStrGBK);

jsonStrGBK是JSON字符串,假设其编码本身是非UTF-8的话而是GBK 的,那么上述代码会导致出错,改为对应的:

    dataDict = json.loads(jsonStrGBK, encoding="GBK");

附:字符串编码转换

这是中国程序员最苦逼的地方,什么乱码之类的几乎都是由汉字引起的。 其实编码问题很好搞定,只要记住一点:

任何平台的任何编码 都能和 Unicode 互相转换

UTF-8 与 GBK 互相转换,那就先把UTF-8转换成Unicode,再从Unicode转换成GBK,反之同理。

# 这是一个 UTF-8 编码的字符串
utf8Str = "你好地球"# 1. 将 UTF-8 编码的字符串 转换成 Unicode 编码
unicodeStr = utf8Str.decode("UTF-8")# 2. 再将 Unicode 编码格式字符串 转换成 GBK 编码
gbkData = unicodeStr.encode("GBK")# 1. 再将 GBK 编码格式字符串 转化成 Unicode
unicodeStr = gbkData.decode("gbk")# 2. 再将 Unicode 编码格式字符串转换成 UTF-8
utf8Str = unicodeStr.encode("UTF-8")

decode的作用是将其他编码的字符串转换成 Unicode 编码

encode的作用是将 Unicode 编码转换成其他编码的字符串

一句话:UTF-8是对Unicode字符集进行编码的一种编码方式

补充:CSV

1.什么是CSV?

CSV,全称为Comma-Separated Values,中文可以叫做逗号分隔值或字符分隔值,其文件以纯文本形式存储表格数据。该文件是一个字符序列,可以由任意数目的记录组成,记录间以某种换行符分隔。每条记录由字段组成,字段间的分隔符是其他字符或字符串,最常见的是逗号或者制表符。不过所有记录都有完全相同的字段序列,相当于一个结构化表的纯文本形式。它比Excel文件更为简洁,XLS文本是电子表格,它包含了文本、数值、公式和数据等内容,而CSV中不包含这些内容,就是特定字符分隔的纯文本,结构简单清晰。

2.保存成csv文本格式

import csvdef save_csv():# 打开文件file = open("1.csv","w", newline="")# 创建csv_writer对象csv_writer = csv.writer(file)# 写入首行csv_writer.writerow(["s_id", "s_name", "s_age"])# 写入数据csv_writer.writerows([[1, "james", "34"],[2,"kobe","36"]])file.close()def json_save_to_csv():# 打开json文件file_json = open("lagou4.json", "r")# 打开并创建csv文件file_csv = open("lagou.csv", "w")# 读取json文件内容content = file_json.read()# 将json数据进行反序列化data_list = json.loads(content)# 获取表头sheet_data = data_list[0].keys()# 获取数据内容content_data = [data.values() for data in data_list]# 创建csv_writer对象csv_writer = csv.writer(file_csv)# 将表头写入csv_writer.writerow(sheet_data)# 写入数据内容csv_writer.writerows(content_data)if __name__ == '__main__':save_csv()

练习2:https://www.lagou.com
1.输入需要抓取的职位及城市,爬取拉钩所有页面,抓取列表页每个职位的的薪水、城市、职位名称、地区、创建时间、公司规模大小、公司全称、公司编号,保存到json文件中,
2.步骤1的基础上,继续抓取详情页的数据,保存到txt文本。
参考练习答案见:lagouspdier.py

注意事项:json.loads() 是把 Json格式字符串解码转换成Python对象,如果在json.loads的时候出错,要注意被解码的Json字符的编码,如果传入的字符串的编码不是UTF-8的话,需要指定字符编码的参数`encoding`如:```python
dataDict = json.loads(jsonStrGBK);

jsonStrGBK是JSON字符串,假设其编码本身是非UTF-8的话而是GBK 的,那么上述代码会导致出错,改为对应的:

    dataDict = json.loads(jsonStrGBK, encoding="GBK");

附:字符串编码转换

这是中国程序员最苦逼的地方,什么乱码之类的几乎都是由汉字引起的。 其实编码问题很好搞定,只要记住一点:

任何平台的任何编码 都能和 Unicode 互相转换

UTF-8 与 GBK 互相转换,那就先把UTF-8转换成Unicode,再从Unicode转换成GBK,反之同理。

# 这是一个 UTF-8 编码的字符串
utf8Str = "你好地球"# 1. 将 UTF-8 编码的字符串 转换成 Unicode 编码
unicodeStr = utf8Str.decode("UTF-8")# 2. 再将 Unicode 编码格式字符串 转换成 GBK 编码
gbkData = unicodeStr.encode("GBK")# 1. 再将 GBK 编码格式字符串 转化成 Unicode
unicodeStr = gbkData.decode("gbk")# 2. 再将 Unicode 编码格式字符串转换成 UTF-8
utf8Str = unicodeStr.encode("UTF-8")

decode的作用是将其他编码的字符串转换成 Unicode 编码

encode的作用是将 Unicode 编码转换成其他编码的字符串

一句话:UTF-8是对Unicode字符集进行编码的一种编码方式

补充:CSV

1.什么是CSV?

CSV,全称为Comma-Separated Values,中文可以叫做逗号分隔值或字符分隔值,其文件以纯文本形式存储表格数据。该文件是一个字符序列,可以由任意数目的记录组成,记录间以某种换行符分隔。每条记录由字段组成,字段间的分隔符是其他字符或字符串,最常见的是逗号或者制表符。不过所有记录都有完全相同的字段序列,相当于一个结构化表的纯文本形式。它比Excel文件更为简洁,XLS文本是电子表格,它包含了文本、数值、公式和数据等内容,而CSV中不包含这些内容,就是特定字符分隔的纯文本,结构简单清晰。

2.保存成csv文本格式

import csvdef save_csv():# 打开文件file = open("1.csv","w", newline="")# 创建csv_writer对象csv_writer = csv.writer(file)# 写入首行csv_writer.writerow(["s_id", "s_name", "s_age"])# 写入数据csv_writer.writerows([[1, "james", "34"],[2,"kobe","36"]])file.close()def json_save_to_csv():# 打开json文件file_json = open("lagou4.json", "r")# 打开并创建csv文件file_csv = open("lagou.csv", "w")# 读取json文件内容content = file_json.read()# 将json数据进行反序列化data_list = json.loads(content)# 获取表头sheet_data = data_list[0].keys()# 获取数据内容content_data = [data.values() for data in data_list]# 创建csv_writer对象csv_writer = csv.writer(file_csv)# 将表头写入csv_writer.writerow(sheet_data)# 写入数据内容csv_writer.writerows(content_data)if __name__ == '__main__':save_csv()

——————————————————————————————————————————————————

爬虫第四课 数据处理相关推荐

  1. Python爬虫第四课:Network、XHR、json

    在爬虫实践当中,如果我们爬取的页面的编写没有做好板块的区分,或者我们选取的标签不合适,最终我们获得的结果会多提取到出一些奇怪的东西. 当使用用request获取的网页源代码里没有我们想要的数据时,需要 ...

  2. 爬虫第四课:猫眼电影

    首先要导入我们需要使用的库 导入库是因为库里有我们需要用的函数,这些函数能帮我们实现某些功能. 使用 import 导入我们需要用的库,写法如图1所示,由图1可以看出导入了requests库和re库, ...

  3. python dataframe 新列_Python第二十四课:Pandas库(四)

    Python第二十四课:Pandas库(四)点击上方"蓝字",关注我们. 不知不觉,我们已经跨越了千难万险,从零开始,一步步揭开了Python神秘的面纱.学到至今,回过头,才晓得自 ...

  4. Scrapy学习第四课

    python爬虫框架scrapy学习第四课 任务:爬取凤凰网导航下所有一级.二级和具体新闻数据 执行:爬虫实例 结果:爬取结果展示 任务:爬取凤凰网导航下所有一级.二级和具体新闻数据 凤凰网导航 一级 ...

  5. 孙鑫mfc学习笔记第十四课

    第十四课 网络的相关知识,网络程序的编写,Socket是连接应用程序与网络驱动程序的桥梁,Socket在应用程序中创建,通过bind与驱动程序建立关系.此后,应用程序送给Socket的数据,由Sock ...

  6. 第四课.LinuxShell编程

    第四课目录 什么是Shell Shell编程 创建脚本 注释 Shell变量 基本运算 字符串,数组,分支循环,函数 应用实例 猜数字 获取CPU使用情况 探测本地网络 什么是Shell 有人说Lin ...

  7. 第四课:算法效率的度量和存储空间需求

    第四课 本课主题: 算法效率的度量和存储空间需求 教学目的: 掌握算法的渐近时间复杂度和空间复杂度的意义与作用 教学重点: 渐近时间复杂度的意义与作用及计算方法 教学难点: 渐近时间复杂度的意义 授课 ...

  8. NeHe OpenGL第二十四课:扩展

    NeHe OpenGL第二十四课:扩展 扩展,剪裁和TGA图像文件的加载: 在这一课里,你将学会如何读取你显卡支持的OpenGL的扩展,并在你指定的剪裁区域把它显示出来.   这个教程有一些难度,但它 ...

  9. 0.0 目录-深度学习第四课《卷积神经网络》-Stanford吴恩达教授

    文章目录 第五课 第四课 第三课 第二课 第一课 第五课 <序列模型> 笔记列表 Week 1 循环序列模型 Week 1 传送门 -> 1.1 为什么选择序列模型 1.2 数学符号 ...

最新文章

  1. SAP MM 采购申请中的物料组字段改成Optional
  2. 会考flash中文字变形为三角形_关于信息技术会考 Flash操作题实用模版
  3. 深度学习框架TensorFlow、Caffe、MXNet、PyTorch如何抉择?6 位大咖现身说法
  4. 怎么用js调用C#后台方法
  5. 主存地址位数怎么算_两位数乘一位数也能口算?对!你没看错,不是特殊情况也行...
  6. 【Linux网络编程】原始套接字实例:MAC 头部报文分析
  7. 设计模式 -- 亨元模式(FlyWeight Pattern)
  8. java8怎么按照两个字段的乘积排序_django-orm F对象的使用 按照两个字段的和,乘积排序实例...
  9. 【faebdc的模拟赛】T2分组
  10. 谷歌修复另一枚已遭利用的 Chrome 释放后使用0day,细节未公开
  11. 如何从文件名字符串中获取文件扩展名_Linux操作系统:文件系统的功能和命名...
  12. MYSQL问题解决方案:Access denied for user ‘root‘@‘localhost‘ (using password:YES)
  13. 7号团队-团队任务5:项目总结
  14. Ajax技术复习---狂神笔记
  15. 海思烧录工具Hitool使用方法
  16. vscode react 代码格式化
  17. 烧写工具DNW的使用
  18. 关于彻底获得管理员权限的方法。(解决PYTHON PS 等报错问题)
  19. 趁表弟上厕所,我复习一下用CSS设置文字文本样式
  20. 计算机带来好处坏处的英语作文,科技给我们带来的好处和坏处英语作文

热门文章

  1. vue + axios config url 转码 空格转成+,导致请求失败(前端解决)
  2. canvs中的arc方法详解
  3. XMind7主题大变样
  4. Ulua ToLua笔记
  5. [生而为人]《李子树上的男孩》
  6. Origin如何使用基础功能?
  7. 如何选择适合自己的节拍器?
  8. html class函数,wordpress函数sanitize_html_class()用法示例
  9. WebGIS和桌面GIS、移动GIS、三维GIS的区别
  10. Android真机界面的大小与MuMu模拟器运行出来的手机屏幕大小不一致问题的一个解决办法