第5章
CHAPTER 5


解 析 网 页





在前面的章节已经能够使用requests库从网页把整个源代码爬取下来了,接着需要从每个网页中提取一些数据。会用到类库,常用的类库有3种,分别为lxm、BeautifulSoup及 re(正则)。
5.1获取豆瓣电影
下面以获取豆瓣电影正在热映的电影名为例,网址为: url='https://movie.douban.com/cinema/nowplaying/beijing/',利用这3种方法实现解析网页,然后再分别对这3种类库进行介绍。
1. 网页分析
部分网页源码为: 




<ul class="lists">

<li

id="3878007"

class="list-item"

data-title="海王"

data-score="8.2"

data-star="40"

data-release="2018"

data-duration="143分钟"

data-region="美国澳大利亚"

data-director="温子仁"

data-actors="杰森·莫玛 / 艾梅柏·希尔德 / 威廉·达福"

data-category="nowplaying"

data-enough="True"

data-showed="True"

data-votecount="105013"

data-subject="3878007"

>



由分析可知,我们要的电影名称信息在li标签的datatitle属性中。
2. 编写代码
爬虫完整的源码展示如下: 







import requests

from lxml import etree#导入库

from bs4 import BeautifulSoup

import re

import time

#定义爬虫类

class Spider():

def init(self):

self.url = 'https://movie.douban.com/cinema/nowplaying/beijing/'

self.headers = {

'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'

}

r = requests.get(self.url,headers=self.headers)

r.encoding = r.apparent_encoding

self.html = r.text        

def lxml_find(self):

'''用lxml解析'''

start = time.time()                     #3种方式速度对比

selector = etree.HTML(self.html)        #转换为lxml解析的对象

titles = selector.xpath('//li[@class="list-item"]/@data-title') #这里返回的
#是一个列表

for each in titles:

title = each.strip()        #去掉字符左右的空格

print(title)

end = time.time()

print('lxml耗时', end-start)



def BeautifulSoup_find(self):

'''用BeautifulSoup解析'''

start = time.time()

soup = BeautifulSoup(self.html, 'lxml')   #转换为BeautifulSoup的解析对象,其中'lxml'为
#解析方式

titles = soup.find_all('li', class_='list-item')

for each in titles:

title = each['data-title']

print(title)

end = time.time()

print('BeautifulSoup耗时', end-start)

def re_find(self):

'''用re解析'''

start = time.time()

titles = re.findall('data-title="(.+)"',self.html)

for each in titles:

print(each)

end = time.time()

print('re耗时', end-start)

if name == 'main':

spider = Spider()

spider.lxml_find()

spider.BeautifulSoup_find()

spider.re_find()



3. 爬取结果

爬取的结果输出如下: 




哥斯拉2: 怪兽之王

哆啦A梦: 大雄的月球探险记

阿拉丁

潜艇总动员: 外星宝贝计划

托马斯大电影之世界探险记

巧虎大飞船历险记

大侦探皮卡丘

…

妈阁是座城

lxml耗时 0.019963502883911133

哥斯拉2: 怪兽之王

哆啦A梦: 大雄的月球探险记

阿拉丁

潜艇总动员: 外星宝贝计划

托马斯大电影之世界探险记

巧虎大飞船历险记

大侦探皮卡丘

…

妈阁是座城

BeautifulSoup耗时 0.12004613876342773

哥斯拉2: 怪兽之王

哆啦A梦: 大雄的月球探险记

阿拉丁

潜艇总动员: 外星宝贝计划

托马斯大电影之世界探险记

巧虎大飞船历险记

大侦探皮卡丘

…

妈阁是座城

re耗时 0.039952754974365234



下面分别对这3个类库进行分析。
5.2正则表达式解析网页
正则表达式并不是Python的一部分。正则表达式是用于处理字符串的强大工具,拥有自己独特的语法以及一个独立的处理引擎,效率上可能不如str自带的方法,但功能十分强大。得益于这一点,在提供了正则表达式的语言中,正则表达式的语法都是一样的,区别只在于不同的编程语言实现支持的语法数量不同; 但不用担心,不被支持的语法通常是不常用的部分。
图51展示了使用正则表达式进行匹配的流程。


图51正则表达式进行匹配


正则表达式的大致匹配过程是: 依次拿出表达式和文本中的字符比较,如果每一个字符都能匹配,则匹配成功; 一旦有匹配不成功的字符则匹配失败。如果表达式中有量词或边界,那么这个过程会稍微有一些不同。
在提取网页中的数据时,可以先把源代码变成字符串,然后用正则表达式匹配想要的数据。使用正则表达式可以迅速地用极简单的方式实现字符串的复杂控制。
表51是常见的正则字符和含义。


表51常见的正则字符和含义



模式描述模式描述

.匹配任意字符,除了换行符\s匹配空白字符
*匹配前一个字符0次或多次\S匹配任何非空白字符
+匹配前一个字符1次或多次\d匹配数字,等价于[09]
?匹配前一个字符0次或1次\D匹配任何非数字,等价于[^09]
^匹配字符串开头\w匹配字母数字,等价于[AZAZ09]
$匹配字符串末尾\W匹配非字母数字,等价于[^AZAZ09]
()匹配括号内的表达式,也表示一个组[]用来表示一组字符

下面介绍Python正则表达式的3种方法,分别是match、search和findall。
5.2.1字符串匹配
本节利用Python中的re.match实现字符串匹配并找到匹配的位置。而re.match的意思是从字符串起始位置匹配一个模式,如果从起始位置匹配不了,match()就返回none。
re.match的语法格式为: 




re.match(string[, pos[, endpos]]) | re.match(pattern, string[, flags])




match只找到一次可匹配的结果即返回。
这个方法将从string的pos下标处开始尝试匹配pattern; 如果pattern结束时仍可匹配,则返回一个match对象; 如果匹配过程中pattern无法匹配,或者匹配未结束就已到达endpos,则返回none。
pos和endpos的默认值分别为0和len(string); re.match()无法指定这两个参数,参数flags用于编译pattern时指定匹配模式。
注意: 这个方法并不是完全匹配。当pattern结束时,若string还有剩余字符,则仍然视为成功。想要完全匹配,可以在表达式末尾加上边界匹配符'$'。
【例51】使用两个字符串匹配并找到匹配的位置。




# encoding: UTF-8 

import re 

m=re.match('www','www.taobao.com')






print('匹配的结果: ',m)

print('匹配的起始与终点: ',m.span())

print('匹配的起始位置: ',m.start())

print('匹配的终点位置: ',m.end())



运行程序,输出如下: 




匹配的结果: <_sre.SRE_Match object; span=(0, 3), match='www'>

匹配的起始与终点: (0, 3)

匹配的起始位置: 0

匹配的终点位置: 3



上面例子中的pattern只是一个字符串,也可以把pattern改成正则表达式,从而匹配具有一定模式的字符串,例如: 




# encoding: UTF-8 

import re 

line='Fat apples are smarter than bananas,is it right?'

m= re.match(r'(\w+) (\w+)(?P<sign>.*)',line)

print('匹配的整句话',m.group(0))

print('匹配的第一个结果',m.group(1))

print('匹配的第二个结果',m.group(2))

print('匹配的结果列表',m.group())



运行程序,输出如下: 




匹配的整句话 Fat apples are smarter than bananas,is it right?

匹配的第一个结果 Fat

匹配的第二个结果 apples

匹配的结果列表 Fat apples are smarter than bananas,is it right?



为什么要在match的模式前加上r呢?
r'(\w+) (\w+)(?P<sign>.*)'前面的r的意思是raw string,代表纯粹的字符串,使用它就不会对引号中的反斜杠'\'进行特殊处理。因为在正则表达式中有一些类似'\d'(匹配任何数字)的模式,所以模式中的单个反斜杠'\'符号都要进行转义。
假如需要匹配文本中的字符"\",使用编程语言表示的正则表达式里就需要4个反斜杠“\\\\”,前两个反斜杠“\\”和后两个反斜杠“\\”各自在编程语言中转义成一个反斜杠“\”,所以4个反斜杠“\\\\”就转义成了两个反斜杠“\\”,这两个反斜杠“\\”最终在正则表达式中转义成一个反斜杠“\”。
5.2.2起始位置匹配字符串
re.match只能从字符串的起始位置进行匹配,而re.search扫描整个字符串并返回。re.search()方法扫描整个字符串,并返回第一个成功的匹配,如果匹配失败,则返回None。
与re.match()方法不同,re.match()方法要求必须从字符串的开头进行匹配,如果字符串的开头不匹配,那么整个匹配就失败了; re.search()并不要求必须从字符串的开头进行匹配,也就是说,正则表达式可以是字符串的一部分。
re.search()的语法格式为: 




re.search(pattern, string, flags=0)




其中,pattern: 正则中的模式字符串。string: 要被查找替换的原始字符串。flags: 标志位,用于控制正则表达式的匹配方式,如: 是否区分大小写、多行匹配等。
【例52】从起始位置匹配字符串演示实例。




import re  

content = 'Hello 123456789 Word_This is just a test 666 Test'

result = re.search('(\d+).*?(\d+).*', content)   

print(result)

print(result.group())# print(result.group(0)) 同样效果的字符串

print(result.groups())

print(result.group(1))

print(result.group(2))



运行程序,输出如下: 




<_sre.SRE_Match object; span=(6, 49), match='123456789 Word_This is just a test 666 Test'>

123456789 Word_This is just a test 666 Test

('123456789', '666')

123456789

666



适当调用以下代码,可实现数字匹配。例如: 




import re  

content = 'Hello 123456789 Word_This is just a test 666 Test'

result = re.search('(\d+)', content)

print(result)

print(result.group())# print(result.group(0)) 同样效果的字符串

print(result.groups())

print(result.group(1))



运行程序,输出如下: 




<_sre.SRE_Match object; span=(6, 15), match='123456789'>

123456789

('123456789',)

123456789



5.2.3所有子串匹配
re.findall()在字符串中找到正则表达式所匹配的所有子串,并返回一个列表; 如果没有找到匹配的,则返回空列表。返回结果是列表类型,需要遍历一下才能依次获取每组内容。
re.findall()的语法格式为: 




findall(patern, string, flags=0)




其中,pattern: 正则中的模式字符串。string: 要被查找替换的原始字符串。flags: 标志位,用于控制正则表达式的匹配方式,如: 是否区分大小写、多行匹配等。
【例53】匹配所有子串演示。




import re  

content = 'Hello 123456789 Word_This is just a test 666 Test'

results = re.findall('\d+', content)    

print(results)

for result in results:

print(result)



运行程序,输出如下: 




['123456789', '666']






123456789

666



findall与match、search不同的是,findall能够找到所有匹配的结果,并且以列表的形式返回。
5.2.4Requests爬取猫眼电影排行
本节利用Requests库和正则表达式来爬取猫眼电影TOP100的相关内容。Requests比urllib使用更加方便,在此选用正则表达式来作为解析工具。
【例54】利用Requests和正则表达式爬取猫眼电影排行信息。




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

import re

import os

import json

import requests

from multiprocessing import Pool

from requests.exceptions import RequestException

def get_one_page(url):

'''

获取网页html内容并返回

'''

try:

# 获取网页html内容

response = requests.get(url)

# 通过状态码判断是否获取成功

if response.status_code == 200:

return response.text

return None

except RequestException:

return None

def parse_one_page(html):

'''

解析HTML代码,提取有用信息并返回

'''

# 用正则表达式进行解析

pattern = re.compile('<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name">'

+ '<a.*?>(.*?)</a>.*?"star">(.*?)</p>.*?releasetime">(.*?)</p>'

+ '.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S)

# 匹配所有符合条件的内容

items = re.findall(pattern, html)

for item in items:

yield {

'index': item[0],

'image': item[1],

'title': item[2],

'actor': item[3].strip()[3:],

'time': item[4].strip()[5:],

'score': item[5] + item[6]

}

def write_to_file(content):

'''

将文本信息写入文件






'''

with open('result.txt', 'a', encoding='utf-8') as f:

f.write(json.dumps(content, ensure_ascii=False) + '\n')

f.close()

def save_image_file(url, path):

'''

保存电影封面

'''

ir = requests.get(url)

if ir.status_code == 200:

with open(path, 'wb') as f:

f.write(ir.content)

f.close()

def main(offset):

url = 'http://maoyan.com/board/4?offset=' + str(offset)

html = get_one_page(url)

# 封面文件夹不存在则创建

if not os.path.exists('covers'):

os.mkdir('covers')

for item in parse_one_page(html):

print(item)

write_to_file(item)

save_image_file(item['image'], 'covers/' + '%03d'%int(item['index']) + item['title'] + '.jpg')

if name == 'main':

# 使用多进程提高效率

pool = Pool()

pool.map(main, [i*10 for i in range(10)])



运行程序,输出如下: 




{'index': '1', 'image': 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', 'title': '霸王别姬', 'actor': '张国荣,张丰毅,巩俐', 'time': '1993-01-01', 'score': '9.5'}

…

{'index': '100', 'image': 'https://p0.meituan.net/movie/c304c687e287c7c2f9e22cf78257872d277201.jpg@160w_220h_1e_1c', 'title': '龙猫', 'actor': '秦岚,糸井重里,岛本须美', 'time': '2018-12-14', 'score': '9.1'}



5.3BeautifulSoup解析网页
BeautifulSoup是Python的一个HTML解析框架,利用它可以方便地处理HTML和XML文档。BeautifulSoup有3和4两个版本,目前3已经停止开发。所以这里学习最新的BeautifulSoup4。
首先是利用pip安装BeautifulSoup。使用下面的命令。




pip install beautifulsoup4




安装BeautifulSoup后,就可以开始使用它了。
BeautifulSoup只是一个HTML解析库,所以如果想解析网上的内容,第一件事情就是把它下载下来。对于不同的网站,可能对请求进行过滤。糗事百科的网站就会直接拒绝没有UA的请求。所以如果要爬这样的网站,首先需要把请求伪装成浏览器的样子。具体网站具体分析,经过测试,糗事百科只要设置了UA就可以爬取到内容,对于其他网站,你需要测试一下才能确定什么设置可用。
有了Request对象还不够,还需要实际发起请求才行。下面代码的最后一句就使用了Python3的urllib库发起了一个请求。urlopen(req)方法返回的是Reponse对象,调用它的read()函数获取整个结果字符串。最后调用decode('utf8')方法将它解码为最终结果,如果不调用这一步,那么汉字等非ASCII字符就会变成\xXXX这样的转义字符。




import urllib.request as request 

user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36' 

headers = {'User-Agent': user_agent} 

req = request.Request('http://www.qiushibaike.com/', headers=headers) 

page = request.urlopen(req).read().decode('utf-8')



有了文档字符串,就可以开始解析文档了。第一步是建立BeautifulSoup对象,这个对象在bs4模块中。注意,在建立对象的时候可以额外指定一个参数,作为实际的HTML解析器。解析器的值可以指定html.parser,这是内置的HTML解析器。更好的选择是使用下面的lxml解析器,不过它需要额外安装一下,使用pip install lxml就可以安装。




mport bs4

soup = bs4.BeautifulSoup(page, "lxml")



有了BeautifulSoup对象,就可以开始解析了。首先介绍BeautifulSoup的对象种类,常用的有标签(bs4.element.Tag)以及文本(bs4.element.NavigableString)等,其中,注解等对象不常用,在此不展开介绍。在标签对象上,可以调用一些查找方法例如find_all等,还有一些属性返回标签的父节点、兄弟节点、直接子节点、所有子节点等。在文本对象上,可以调用.string属性获取具体文本。
基本所有BeautifulSoup的遍历方法操作都需要通过BeautifulSoup对象来使用。使用方式主要有两种: 一是直接引用属性,例如soup.title,会返回第一个符合条件的节点; 二是通过查找方法,例如find_all,传入查询条件来查找结果。
接下来了解查询条件。查询条件可以是: 字符串,会返回对应名称的节点; 正则表达式,按照正则表达式匹配; 列表,会返回所有匹配列表元素的节点; 真值True,会返回所有标签节点,不会返回字符节点; 方法,可以编写一个方法,按照自己的规则过滤,然后将该方法作为查询条件。
BeautifulSoup支持Python标准库中的HTML解析器,还支持一些第三方的解析器。表52列出了主要的解析器及其优缺点。


表52解析器及其优缺点



解析器使 用 方 法优点缺点

Python标准库BeautifulSoup(markup,"html.parser")Python的内置标准库,执行速度适中,文档容错能力强在Python3.2.2前的版本中文档容错能力差
lxml HTML解析器BeautifulSoup(markup,"lxml")速度快,文档容错能力强需要安装C语言库
lxml XML解析器BeautifulSoup(markup,["lxml","xml"])速度快,唯一支持XML的解析器需要安装C语言库
html5libBeautifulSoup(markup,"html5lib")最好的容错性,以浏览器的方式解析文档,生成HTML5格式的文档速度慢

不依赖外部扩展


使用lxml的解析器将会解析得更快,建议大家使用。
【例55】利用requests 和 BeautifulSoup 爬取猫眼电影排行信息。




import requests

from bs4 import BeautifulSoup

import os 

import time 

start = time.clock()#添加程序运行计时功能

file_path = 'D:\python3.6\scrapy\猫眼' #定义文件夹,方便后续check文件夹是否存在

file_name = 'maoyan.txt'   #自定义命名文件名称

file = file_path+'\\'+file_name     #创建文件全地址,方便后续引用

url = "http://maoyan.com/board/4"  #获取url的开始页

headers = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"} 

def Create_file(file_path,file):   #定义检查和创建目标文件夹和文件的函数	

if os.path.exists(file_path)== False: #check文件夹不存在

os.makedirs(file_path)   #创建新的自定义文件夹

fp = open(file,'w')   #创建新的自定义文件

"""  "w" 以写方式打开,只能写文件,如果文件不存在,创建该文件; 如果文件已存在,先清空,再打开文件 """	

elif os.path.exists(file_path)== True: # check文件夹存在

with open(file, 'w', encoding='utf-8') as f: #打开目标文件夹中的文件

f.seek(0)

"""f.seek(offset[,where])把文件指针移动到相对于where的offset位置。where为0表示文件开始处,这是默认值; 1表示当前位置; 2表示文件结尾"""

f.truncate()

"""清空文件内容,注意: 仅当以 "r+" "rb+" "w" "wb" "wb+"等以可写模式打开的文件才可以执行该功能"""

def get_all_pages(start):

#定义获取所有pages页的目标内容的函数	

pages=[]	

for n in range(0,100,10):

#获取offset的步进值,注意把int的n转换为str

#遍历所有的url,并获取每一页page的目标内容

if n==0:

url=start

else:

url=start+'?offset='+str(n)			

r = requests.get(url, headers=headers) 

soup = BeautifulSoup(r.content, 'lxml')

page= soup.find_all(name='dd')	

#获取该page的所有dd节点的内容		

pages.extend(page)

#将获取的所有page list扩展成pages,方便下面遍历每个dd节点内容	

return pages

#返回所有pages的dd节点的内容,每个dd节点内容都以list方式存储其中	

Create_file(file_path,file) 

text = get_all_pages(url) 

for film in text:

#遍历列表 text中的所有元素,也就是每个dd节点内容

#这个for循环应该优化成自定义函数形式; 

dict ={}

#创建空dict	






#print(type(film))#确认 film 属性为tag,故可以使用tag相关的方法处理film

#print('*'*50) #可以分隔检查输出的内容,方便对照

dict['Index']=film.i.string #选取film的第一个子节点 i 的string属性值	

#获取第三重直接子孙节点,例如下面注释中的<div class="movie-item-info">节点全部元素

comment1 = film.div.div.div	

name= comment1.find_all(name='p')[0].string

star = comment1.find_all(name='p')[1].string

releasetime = comment1.find_all(name='p')[2].string	

dict['name']=name

dict['star']=str.strip(star)

dict['releasetime']=releasetime		

comment2 = comment1.find_next_sibling() 

"""获取第三重直接子孙节点的next节点,例如下面注释中的<div class="movie-item-info">节点全部元素"""

#print(comment2)	 #检查comment2是否为目标文本

sco1=comment2.i.string

sco2=comment2.i.find_next_sibling().string	

#print(type(sco1)) #判断sco1 为tag类型

#print(sco1) #检查sco1是否为目标输出内容

score = (sco1.string+str.strip(sco2))#获取合并后的score字符串

dict['score']=score	

print(dict) #检查dict是否为目标输出内容	

with open(file, 'a', encoding='utf-8') as f: #以打开目标file文件

f.write(str(dict)+'\n')  #注意添加换行符 '\n',实现每个dict自动换行写入txt中

end = time.clock() #添加程序运行计时功能

print('爬取完成','\n','耗时: ',end-start)#添加程序运行计时功能



运行程序,效果如图52所示。


图52爬取猫眼电影排行信息

5.4PyQuery解析库
前面介绍了BeautifulSoup的用法,它是一个非常强大的网页解析库,你是否觉得它的一些方法用起来有点不适用?有没有觉得它的CSS选择器的功能没有那么强大?
下面来介绍一个更适合的解析库——PyQuery。PyQuery库是jQuery的Python实现,能够以jQuery的语法来操作解析HTML文档,易用性和解析速度都很好,使用起来还是可以的,有些地方用起来很方便简洁。
5.4.1使用PyQuery
如果之前没有安装PyQuery,可在命令窗口中直接使用pip install PyQuery进行安装。
1. 初始化
像BeautifulSoup一样,初始化PyQuery的时候,也需要传入HTML文本来初始化一个PyQuery对象,它的初始化方式有多种,比如直接传入字符串、传入URL、传入文件名,等等。下面详细介绍。
1) 字符串初始化
首先,通过一个实例来感受一下: 




html = """

<html lang="en">

<head>

简单好用的

<title>PyQuery</title>

</head>

<body>

<ul id="container">

<li class="object-1">Python</li>

<li class="object-2">爬虫</li>

<li class="object-3">好</li>

</ul>

</body>

</html>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

print(type(doc))

print(doc)



运行程序,输出如下: 




<class 'pyquery.pyquery.PyQuery'>

<html lang="en">

<head>

简单好用的

<title>PyQuery</title>

</head>






<body>

<ul id="container">

<li class="object-1">Python</li>

<li class="object-2">爬虫</li>

<li class="object-3">好</li>

</ul>

</body>

</html>



这里首先引入PyQuery这个对象,取别名为pq,然后声明了一个长HTML字符串,并将其当作参数传递给PyQuery类,这样就成功完成了初始化。接下来,将初始化的对象传入CSS选择器。在这个实例中,传入li节点,这样就可以选择所有的li节点。
2) 对网址响应进行初始化
初始化的参数不仅可以以字符串的形式传递,还可以传入网页的URL,此时只需要指定参数为url即可: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

response = pq(url = 'https://www.baidu.com')

print(type(response))

print(response)



运行程序,输出如下: 




<class 'pyquery.pyquery.PyQuery'>

<html><head><meta http-equiv="content-type" content="text/html;charset=utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=Edge"/><meta content="always" name="refe

…



3) HTML文件初始化
除了传递URL,还可以传递本地的文件名,此时将参数指定为filename即可: 




#filename参数为html文件路径

test_html = pq(filename = 'test.html')

print(type(test_html))

print(test_html)



运行程序,输出如下: 




<class 'pyquery.pyquery.PyQuery'><html lang="en">

<head>

<title>PyQuery学习</title>

</head>

<body>

<ul id="container">

<li class="object-1"/>

<li class="object-2"/>

<li class="object-3"/>

</ul>

</body>

</html>



这里需要有一个本地HTML文件test.html,其内容是待解析的HTML字符串。这样它会首先读取本地的文件内容,然后用文件内容以字符串的形式传递给PyQuery类来初始化。
以上3种初始化方式均可,最常用的初始化方式是以字符串形式传递。
2. CSS选择器
首先,用一个实例来感受PyQuery的CSS选择器的用法: 




html = """

<html lang="en">

<head>

简单好用的

<title>PyQuery</title>

</head>

<body>

<ul id="container">

<li class="object-1">Python</li>

<li class="object-2">爬虫</li>

<li class="object-3">好</li>

</ul>

</body>

</html>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

print(doc('#container'))

print(type(doc('#container')))



运行程序,输出如下: 




<ul id="container">

<li class="object-1">Python</li>

<li class="object-2">爬虫</li>

<li class="object-3">好</li>

</ul>

<class 'pyquery.pyquery.PyQuery'>



这里初始化PyQuery对象之后,传入了一个CSS选择器#container.list li,它的意思为选取id为container的节点,然后再选取其内部的class为list的节点内部的所有li节点。然后,打印输出,可以看到,成功获取了符合条件的节点。最后,将它的类型打印输出,可以看到,它的类型依然是PyQuery类型。
再例如,打印class为object1的标签: 




print(doc('.object-1'))




输出如下: 




<li class="object-1">Python</li>



打印标签名为body的标签: 




print(doc('body'))




输出如下: 




<body>

<ul id="container">

<li class="object-1">Python</li>

<li class="object-2">爬虫</li>






<li class="object-3">好</li>

</ul>

</body>



3. 查找节点
下面介绍一些常用的查询函数。
1) 子节点
查找子节点时,需要用到find()方法,此时传入的参数是CSS选择器。




html = """

<div>

<ul>

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

items=doc('.list')

print(type(items))

print(items)

lis=items.find('li')

print(type(lis))

print(lis)



运行程序,输出如下: 




<class 'pyquery.pyquery.PyQuery'>

<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

<class 'pyquery.pyquery.PyQuery'>

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>



首先,选取class为list的节点,然后调用了find()方法,传入CSS选择器,选取其内部的li节点,最后打印输出。可以发现,find()方法会将符合条件的所有节点选择出来,结果的类型是PyQuery类型。
其实find()的查找范围是节点的所有子孙节点,而如果只想查找子节点,那么可以用children()方法: 




lis=items.children()

print(type(lis))

print(lis)



运行程序,输出如下: 




<class 'pyquery.pyquery.PyQuery'>

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>



如果要筛选所有子节点中符合条件的节点,比如想筛选出子节点中class为active的节点,可以向children()方法传入CSS选择器.active: 




lis=items.children('.active')

print(lis)



运行程序,输出如下: 




<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>



可以看到,输出结果已经做了筛选,留下了class为active的节点。
2) 父节点
在Python中,可以用parent()方法来获取某个节点的父节点,例如: 




html = """

<div class="wrap">

<div id="container">

<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>

</div>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

items=doc('.list')

container=items.parent()

print(type(container))

print(container)



运行程序,输出如下: 




<class 'pyquery.pyquery.PyQuery'>






<div id="container">

<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>



代码中,首先用.list选取class为list的节点,然后调用parent()方法得到其父节点,其类型依然是PyQuery。
此处的父节点是该节点的直接父节点,也就是说,它不会再去查找父节点的父节点,即祖先节点。但是如果想获取某个祖先节点,该怎么办呢?可以用parents()方法: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

items=doc('.list')

parents=items.parents()

print(type(parents))

print(parents)



运行程序,输出如下: 




<class 'pyquery.pyquery.PyQuery'>

<div class="wrap">

<div id="container">

<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>

</div><div id="container">

<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>



可以看到,输出结果有两个: 一个是class为wrap的节点; 另一个是id为container的节点。也就是说,parents()方法会返回所有的祖先节点。
要想筛选某个祖先节点,可以向parents()方法传入CSS选择器,这样就会返回祖先节点中符合CSS选择器条件的节点: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

items=doc('.list')

parent=items.parents('.wrap')

print(parent)



运行程序,输出如下: 




<div class="wrap">

<div id="container">

<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>

</div>



由输出结果可以看到,输出结果少了一个节点,只保留了class为wrap的节点。
3) 兄弟节点
除了前面介绍的子节点、父节点外,还有一种节点,那就是兄弟节点。如果要获取兄弟节点,可以使用siblings()方法。例如: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('.list .item-0.active')

print(li.siblings())



运行程序,输出如下: 




<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0">first item</li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>



从结果可以看到,这正是刚才所说的4个兄弟节点。
如果要筛选某个兄弟节点,依然可以向siblings方法传入CSS选择器,这样就会从所有兄弟节点中挑选出符合条件的节点了: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('.list .item-0.active')

print(li.siblings('.active'))



运行程序,输出如下: 




<li class="item-1 active"><a href="link4.html">fourth item</a></li>



这里筛选了class为active的节点,通过刚才的结果可以观察到,class为active的兄弟节点只有第四个li节点,所以结果应该是一个。
4. 遍历
由刚才可观察到,PyQuery的选择结果可能是多少节点,也可能是单个节点,类型都是PyQuery,并没有返回像BeautifulSoup那样的列表。
对于单个节点来说,可以直接打印输出,也可以直接转成字符串: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('.item-0.active')

print(li)

print(str(li))



运行程序,输出如下: 




<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>



对于多个节点的结果,就需要遍历来获取了。例如,这里把每个li节点进行遍历,需要调用items()方法: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

lis=doc('li').items()

print(type(lis))

for li in lis:

print(li,type(li))



运行程序,输出如下: 




<class 'generator'>

<li class="item-0">first item</li>

<class 'pyquery.pyquery.PyQuery'>

<li class="item-1"><a href="link2.html">second item</a></li>

<class 'pyquery.pyquery.PyQuery'>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<class 'pyquery.pyquery.PyQuery'>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<class 'pyquery.pyquery.PyQuery'>

<li class="item-0"><a href="link5.html">fifth item</a></li>

<class 'pyquery.pyquery.PyQuery'>



由结果可发现,调用items()方法后,会得到一个生成器,遍历一下,就可以逐个得到li节点对象了,它的类型也是PyQuery类型。每个li节点还可以调用前面所说的方法进行选择,比如继续查询子节点,寻找某个祖先节点等,非常灵活。
5. 获取信息
提取到节点之后,最终目的是提取节点所包含的信息。比较重要的信息有两类: 一是获取属性,二是获取文本。下面具体说明。
1) 获取属性
提取到某个PyQuery类型的节点后,就可以调用attr()方法来获取属性: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

a=doc('.item-0.active a')

print(a,type(a))

print(a.attr('href'))



运行程序,输出如下: 




<a href="link3.html"><span class="bold">third item</span></a><class 'pyquery.pyquery.PyQuery'>

link3.html



在代码中,首先选中class为item0和active的li节点内的a节点,它的类型是PyQuery。然后调用attr()方法。在这个方法中传入属性的名称,就可以得到这个属性值了。
此外,也可以通过调用attr属性来获取属性,例如: 




print(a.attr.href)



运行程序,输出如下: 




link3.html



这两种方法的结果完全一样。如果选中的是多个元素,然后调用attr()方法,会出现怎样的结果呢?用实例来测试一下: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

a=doc('a')

print(a,type(a))

print(a.attr('href'))

print(a.attr.href)



运行程序,输出如下: 




<a href="link2.html">second item</a><a href="link3.html"><span class="bold">third item</span></a><a href="link4.html">fourth item</a><a href="link5.html">fifth item</a><class 'pyquery.pyquery.PyQuery'>

link2.html

link2.html



照理来说,选中的a节点有4个,打印结果也应该是4个,但是当调用attr()方法时,返回结果却只是第一个。这是因为,当返回结果包含多个节点时,调用attr()方法,只会得到第一个节点的属性。那么,遇到这种情况,如果想获取所有的a节点的属性,就要用到前面所说的遍历了: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

a=doc('a')

for item in a.items():

print(item.attr('href'))



运行程序,输出如下: 




link2.html






link3.html

link4.html

link5.html



因此,在进行属性获取时,可以观察返回节点是一个还是多个。如果是多个,则需要遍历才能依次获取每个节点的属性。
2) 获取文本
获取节点之后的另一个主要操作就是获取其内部的文本了,此时可以调用text()方法来实现: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

a=doc('.item-0.active a')

print(a)

print(a.text())



运行程序,输出如下: 




<a href="link3.html"><span class="bold">third item</span></a>

third item



此处首先选中一个a节点,然后调用text()方法,就可以获取其内部的文本信息了。此时它会忽略掉节点内部包含的所有HTML,只返回纯文字内容。
但如果想要获取这个节点内部的HTML文本,就要用html()方法了: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('.item-0.active')

print(li)

print(li.html())



运行程序,输出如下: 




<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<a href="link3.html"><span class="bold">third item</span></a>



在程序中选中了第三个li节点,然后调用了html()方法,它返回的结果应该是li节点内的所有HTML文本。
这里同样有一个问题,如果选中的结果是多个节点,text()或html()会返回什么内容?用实例来测试下: 




html = """

<div class="wrap">

<div id="container">

<ul class="list">

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>






</div>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('li')

print(li.html())

print(li.text())

print(type(li.text))



运行程序,输出如下: 




<a href="link2.html">second item</a>

second item third item fourth item fifth item

<class 'method'>



结果可能比较出人意料,html()方法返回的是第一个li节点的内部HTML文本,而text()返回了所有的li节点内部的纯文本,中间用一个空格分隔开,即返回结果是一个字符串。
值得注意的是,如果得到的结果是多个节点,并且想要获取每个节点的内部HTML文本,则需要遍历每个节点; 而使用text()方法不需要遍历就可以获取,它对所有节点取文本之后合并成一个字符串。
6. 节点操作
PyQuery提供了一系列方法来对节点进行动态修改,比如为某个节点添加一个class,移除某个节点等,这些操作有时会为提取信息带来极大的便利。
由于节点操作的方法太多,下面举几个典型的例子来说明它的用法。
1) addClass和removeClass
下面先体会实例演示: 




html = """

<div class="wrap">

<div id="container">

<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>

</div>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('.item-0.active')

print(li)

li.removeClass('active')

print(li)






li.addClass('active')

print(li)



在代码中,首先选中了第三个li节点,然后调用removeClass()方法,将li节点的active这个class移除; 接着又调用addClass()方法,将class添加回来。每执行一次操作,就打印输出当前li节点的内容。
运行程序,输出如下: 




<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-0"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>



从结果可看到,一共输出了3次。第二次输出时,li节点的active这个class被移除了,第三次class又添加回来了。
所以,addClass()和removeClass()这些方法可以动态改变节点的class属性。
2) attr、text和html
当然,除了操作class这个属性外,也可以用attr()方法对属性进行操作。此外,还可以用text()和html()方法来改变节点内部的内容。例如: 




html = """

<ul class="list">

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

</ul>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('.item-0.active')

print(li)

li.attr('name','link')

print(li)

li.text('changed item')

print(li)

li.html('<span>changed item</span>')

print(li)



运行程序,输出如下: 




<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-0 active" name="link"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-0 active" name="link">changed item</li>

<li class="item-0 active" name="link"><span>changed item</span></li>



这里首先选中li节点,然后调用attr()方法来修改属性,其中该方法的第一个参数为属性名,第二个参数为属性值。接着,调用text()和html()方法来改变节点内部的内容。两次操作后,分别打印输出当前的li节点。
由结果可发现,调用attr()方法后,li节点多了一个原本不存在的属性name,其值为link。接着调用text()方法,传入文本之后,li节点内部的文本全部被改为传入的字符串文本了。最后,调用html()方法传入HTML文本后,li节点内部又变为传入的HTML文本了。
所以,如果attr()方法只传入第一个参数的属性名,则是获取这个属性值; 如果传入第二个参数,则可以用来修改属性值。text()和html()方法如果不传参数,则是获取节点内纯文本和HTML文本; 如果传入参数,则进行赋值。
3) remove()
remove()的方法即为移除,它有时会为信息的提取带来非常大的便利。下面有一段HTML文本: 




html = """

<div class="wrap">

Hello, World

<p> This is a paragraph.</p>

</div>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

wrap=doc('.wrap')

print(wrap.text())



运行程序,输出如下: 




Hello, World

This is a paragraph.



我们想提取的是“Hello, World”这个字符串,而这个结果还包含了内部的p节点的内容,也就是说,text()把所有的纯文本全提取出来了。如果想去掉p节点内部的文本,可以选择再把p节点内的文本提取一遍,然后从整个结果中移除这个子串,但这个做法明显比较烦琐。而remove()方法就可以实现该功能,例如: 




from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

wrap=doc('.wrap')

wrap.find('p').remove()

print(wrap.text())



运行程序,输出如下: 




Hello, World



以上代码的思路是: 首先选中p节点,然后调用remove()方法将其移除,这时wrap内部就只剩下“Hello,World”了,再利用text()方法提取即可。
7. 伪类选择器
CSS选择器之所以强大,有一个重要的原因,那就是它支持多种多样的伪类选择器,例如,选择第一个节点、最后一个节点、奇偶数节点、包含某一文本的节点等等。实例如下: 




html = """

<div class="wrap">

<div id="container">






<ul class="list">

<li class="item-0">first item</li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

</ul>

</div>

</div>

"""

from pyquery import PyQuery as pq

#初始化为PyQuery对象

doc = pq(html)

li=doc('li:first-child')

print(li)

li=doc('li:last-child')

print(li)

li=doc('li:nth-child(2)')

print(li)

li=doc('li:gt(2)')

print(li)

li=doc('li:nth-child(2n)')

print(li)

li=doc('li:contains(second)')

print(li)



运行程序,输出如下: 




<li class="item-0">first item</li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-0"><a href="link5.html">fifth item</a></li>

<li class="item-1"><a href="link2.html">second item</a></li>

<li class="item-1 active"><a href="link4.html">fourth item</a></li>

<li class="item-1"><a href="link2.html">second item</a></li>



在代码中,使用了CSS的伪类选择器,依次选择了第一个li节点、最后一个li节点、第二个li节点、第三个li之后的li节点、偶数位置的li节点、包含second文本的li节点。
5.4.2PyQuery爬取煎蛋网商品图片
图片一般都是以链接的形式出现在HTML文本中,因此只需要找到图片连接即可(一般是在img src中),这时再把图片url打开,利用content保存成具体的文件。这里使用的hashlib是一个编码库,为了使得每一个图片的名字不一样,就用md5这个方法把图片的内容进行了编码。爬取煎蛋网加入了一定的反爬取措施,即并不是直接将图片的url列出来,而是利用一个.js文件,在每一次加载图片的时候都要加载这个.js文件,进而把图片的url解析出来。实现代码为: 




import requests

from pyquery import PyQuery as pq

import hashlib






import base64

from hashlib import md5

def ty(body):

print(type(body))

# 处理md5编码问题

def handle_md5(hd_object):

return hashlib.md5(hd_object.encode('utf-8')).hexdigest()

# 处理base64编码问题

def handle_base64(hd_object):

return str(base64.b64decode(hd_object))[2:-1]

# 解密图片链接

def parse(ig_hs, ct):

count = 4

contains = handle_md5(ct)

ig_hs_copy = ig_hs

p = handle_md5(contains[0:16])

m = ig_hs[0:count]

c = p + handle_md5(p + m)

n = ig_hs[count:]

l = handle_base64(n)

k = []

for h in range(256):

k.append(h)

b = []

for h in range(256):

b.append(ord(c[h % len(c)]))

g = 0

for h in range(256):

g = (g + k[h] + b[h]) % 256

tmp = k[h]

k[h] = k[g]

k[g] = tmp

u = ''

q = 0

z = 0

for h in range(len(l)):

q = (q + 1) % 256

z = (z + k[q]) % 256

tmp = k[q]

k[q] = k[z]

k[z] = tmp

u += chr(ord(l[h]) ^ (k[(k[q] + k[g]) % 256]))

u = u[26:]

u = handle_base64(ig_hs_copy)

return u

for i in range(1,10):

url = 'http://jandan.net/ooxx/page-'+str(i)+'#comments'

response = requests.get(url)

doc = pq(response.text)

links = doc('#wrapper #body #content #comments .commentlist .row .text .img-hash')

print(links)

arg = '5HTs9vFpTZjaGnG2M473PomLAGtI37M8'

for link in links:






l=link.text

print(type(l))

u=parse(l,arg)

#print(u)

u1='http:'+u

print(u1)

r=requests.get(u1)

with open('D:/image/'+md5(r.content).hexdigest()+'.jpg','wb') as f:

f.write(r.content)

f.close()



5.5lxml解析网页
lxml是一个HTML/XML的解析器,主要的功能是如何解析和提取HTML/XML数据。lxml和正则表达式一样,也是用C实现的,是一款高性能的Python HTML/XML解析器,可以利用之前学习的XPath语法,来快速定位特定元素以及节点信息。
安装lxml也非常简单,直接使用pip安装,代码为: 




pip install lxml



5.5.1使用lxml
使用lxml爬取网页源代码数据也有3种方法,即XPath选择器、CSS选择器和Beautiful Soup的find()方法。与利用Beautiful Soup相比,lxml还多了一种XPath选择器方法。
下面利用lxml来解析HTML代码: 




from lxml import etree

html = '''

<html>

<head>

<meta name="content-type" content="text/html; charset=utf-8" />

<title>友情链接查询 - 站长工具</title>

<!-- uRj0Ak8VLEPhjWhg3m9z4EjXJwc -->

<meta name="Keywords" content="友情链接查询" />

<meta name="Description" content="友情链接查询" />

</head>

<body>

<h1 class="heading">Top News</h1>

<p style="font-size: 200%">World News only on this page</p>

Ah, and here's some more text, by the way.

<p>...and this is a parsed fragment ...</p>

<a href="http://www.cydf.org.cn/" rel="nofollow" target="_blank">青少年发展基金会</a>

<a href="http://www.4399.com/flash/32979.htm" target="_blank">洛克王国</a>

<a href="http://www.4399.com/flash/35538.htm" target="_blank">奥拉星</a>

<a href="http://game.3533.com/game/" target="_blank">手机游戏</a>

<a href="http://game.3533.com/tupian/" target="_blank">手机壁纸</a>

<a href="http://www.4399.com/" target="_blank">4399小游戏</a>






<a href="http://www.91wan.com/" target="_blank">91wan游戏</a>

</body>

</html>

'''

page = etree.HTML(html.lower().encode('utf-8'))

hrefs = page.xpath(u"//a")

for href in hrefs:

print(href.attrib)



运行程序,输出如下: 




{'href': 'http://www.cydf.org.cn/', 'rel': 'nofollow', 'target': '_blank'}

{'href': 'http://www.4399.com/flash/32979.htm', 'target': '_blank'}

{'href': 'http://www.4399.com/flash/35538.htm', 'target': '_blank'}

{'href': 'http://game.3533.com/game/', 'target': '_blank'}

{'href': 'http://game.3533.com/tupian/', 'target': '_blank'}

{'href': 'http://www.4399.com/', 'target': '_blank'}

{'href': 'http://www.91wan.com/', 'target': '_blank'}



提示: lxml可以自动修正HTML代码。
5.5.2文件读取
除了直接读取字符串,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>



读取HTML文件中的代码为: 




from 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>



5.5.3XPath使用
XPath是一门在XML文档中查找信息的语言。XPath使用路径表达式来选取XML文档中的节点或节点数,也可以用在HTML获取数据中。
XPath使用路径表达式可以在网页源代码中选取节点,它是沿着路径来选取的,如表53所示。


表53XPath路径表达式及其描述



表达式描述

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

表54列出了XPath的一些路径表达式及结果。


表54路径表达式及结果



路径表达式结果

bookstore选取bookstore元素的所有子节点
/bookstore选择根元素bookstore

解释: 假如路径起始于正斜杠(/),此路径始终代表到某元素的绝对路径
bokstore/book选取属于bookstore子元素的所有book元素
//book选取所有book子元素,无论它们在文档中什么位置
bookstore//book选择属于bookstore元素后代的所有book元素,无论它们位于bookstore下的什么内容
//@lang选取名为lang的所有属性

下面代码为XPath实例测试: 




#使用 lxml 的 etree 库

from lxml import etree

text = '''

<div>

<ul class = 'page'>

<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)

#获取所有的<li>标签

result = html.xpath('//li')

# 获取<li>标签的所有 class属性

result = html.xpath('//li/@class')

#获取<li>标签下hre 为 link1.html 的<a>标签,/用于获取直接子节点,//用于获取子孙节点

result = html.xpath('//li/a[@href="link1.html"]')

#获取<li>的父节点使用..:获取li标签的父节点,然后获取父节点的class属性

result = html.xpath('//li/../@class')

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

result = html.xpath('//li/a//@class')

#获取最后一个<li>的<a>的属性href

result = html.xpath('//li[last()]/a/@href')

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

result = html.xpath('//li[last()-1]/a/text()')

# 获取 class 值为 item-inactive的标签名

result = html.xpath('//*[@class="item-inactive"]')

print(result)



运行程序,输出如下: 




[<Element li at 0x2d47db61308>]



表55总结了各种HTML解析器的优缺点。


表55HTML解析器的优缺点



HTML解析器运 行 速 度易用性提取数据方式

正则表达式快较难正则表达式
BeautifulSoup快(使用lxml解析)简单Find方法

CSS选择器
lxml快简单XPath

CSS选择器

如果你面对的是复杂的网页代码,那么正则表达式的书写可能花费较长时间,这时选择BeautifulSoup和lxml比较简单。由于BeautifulSoup已经支持lxml解析,因此速度和lxml差不多,使用者可以根据熟悉程度进行选择。因为学习新的方法也需要时间,所以熟悉XPath的读者可以选择lxml。假如是初学者,就需要快速掌握提取网页中的数据,推荐使用BeautifulSoup的find()方法。
5.5.4爬取LOL百度贴吧图片
下面一个实例演示lxml解析网页: 爬取LOL百度贴吧的图片,实现代码为: 




from lxml import etree

from urllib import request,error,parse

class Spider:

def init(self):

self.tiebaName = 'lol'

self.beginPage = int(input('请输入开始页:'))






self.endPage = int(input('请输入结束页:'))

self.url = 'http://tieba.baidu.com/f'

self.header = {"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1 Trident/5.0;"}

self.userName = 1  ## 图片编号

def tiebaSpider(self):

for page in range(self.beginPage, self.endPage + 1):

pn = (page - 1) * 50

data = {'pn':pn, 'kw':self.tiebaName}

myUrl = self.url + '?' + parse.urlencode(data)

# 调用页面处理函数 load_Page,获取页面所有帖子链接

links = self.loadPage(myUrl)  

# 读取页面内容

def loadPage(self,url):

req = request.Request(url, headers=self.header)

resp = request.urlopen(req).read()

# 将resp解析为html文档

html = etree.HTML(resp)

# 抓取当前页面的所有帖子的url的后半部分,也就是帖子编号

url_list = html.xpath('//div[@class="threadlist_lz clearfix"]/div/a/@href')

# url_list 类型为 etreeElementString 列表

# 遍历列表,并且合并成一个帖子地址,调用图片处理函数 loadImage

for row in url_list:

row = "http://tieba.baidu.com" + row

self.loadImages(row)

# 下载图片

def loadImages(self, url):

req = request.Request(url, headers=self.header)

resp = request.urlopen(req).read()

# 将resp解析为html文档

html = etree.HTML(resp)

# 获取这个帖子里所有图片的src路径

image_urls = html.xpath('//img[@class="BDE_Image"]/@src')

# 依次取出图片路径,下载保存

for image in image_urls :

self.writeImages(image)

# 保存页面内容

def writeImages(self, url):

'''

将 images 里的二进制内容存入 userName 文件中

'''

print(url)

print("正在存储文件 %d ..." % self.userName)

# 1. 打开文件,返回一个文件对象

path = r'D:\example' +  '\\' + str(self.userName) + '.png' #example为自己新建存放获取图
#像的文件夹

file = open(path, 'wb')

# 获取图片里的内容

images = request.urlopen(url).read()

# 调用文件对象write() 方法,将page_html的内容写入文件中

file.write(images)

file.close()

# 计数器自增1

self.userName += 1 






#模拟 main 函数

if name == "main":

# 首先创建爬虫对象

mySpider = Spider()

# 调用爬虫对象的方法,开始工作

mySpider.tiebaSpider()



运行程序,即可将LOL网页中的图片下载到新建的example文件夹中,共下载261张图片,如图53所示,输出如下: 




请输入开始页:1

请输入结束页:1

https://imgsa.baidu.com/forum/w%3D580/sign=06f1b0a37ecf3bc7e800cde4e102babd/cb9eb2fd5266d01668dcd286992bd40734fa3518.jpg

…

正在存储文件 260 ...

https://fc-feed.cdn.bcebos.com/0/pic/b509deeac5ba2fc8b259533dcb718c75.jpg

正在存储文件 261 ...





图53下载的图片


5.6爬取二手房网站数据
本节的实践中仅获取了搜索结果的房源数据。
首先,需要分析一下要爬取郑州的二手房信息的网络结构,如图54所示。


图54郑州二手房数据


由上可以看到网页一条条的房源信息,单击进去后就会发现房源的详细信息,如图55所示。


图55房子详细信息


查看页面的源代码的效果如图56所示。


图56房源的源代码



下面采用Python3中的Requests、Beautiful Soup模块来进行爬取页面,先由Requests模块进行请求: 




# 网页的请求头

header = {

'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'

}

# url链接

url = 'https://zhengzhou.anjuke.com/sale/'

response = requests.get(url, headers=header)

print(response.text)



运行程序,得到这个网站的HTML代码如图57所示。


图57HTML代码


通过分析可以得到每个房源都在class="listitem"的 li 标签中,那么就可以根据BeautifulSoup包进行提取: 




from bs4 import BeautifulSoup

# 通过BeautifulSoup解析出每个房源详细列表并进行打印

soup = BeautifulSoup(response.text, 'html.parser')

result_li = soup.find_all('li', {'class': 'list-item'})

for i in result_li:

print(i)



运行程序,效果如图58所示。


图58每个房源详细列表


进一步减少代码量,继续提取: 




# 通过BeautifulSoup解析出每个房源详细列表并进行打印

soup = BeautifulSoup(response.text, 'html.parser')

result_li = soup.find_all('li', {'class': 'list-item'})

# 进行循环遍历其中的房源详细列表

for i in result_li:

# 由于BeautifulSoup传入的必须为字符串,所以进行转换

page_url = str(i)

soup = BeautifulSoup(page_url, 'html.parser')

# 由于通过class解析的为一个列表,所以只需要第一个参数

result_href = soup.find_all('a', {'class': 'houseListTitle'})[0]

print(result_href.attrs['href'])



运行程序,效果如图59所示。


图59打印所有url

下一步即进入页面开始分析详细页面了,所以,就需要先分析该页面是否有下一页,在页面右击选择“审查元素”命令,效果如图510所示。


图510分析网页


其利用Python代码爬取的方法为: 




# 进行下一页的爬取

result_next_page = soup.find_all('a', {'class': 'aNxt'})

if len(result_next_page) != 0:

print(result_next_page[0].attrs['href'])

else:

print('没有下一页了')



运行程序,效果如图511所示。


图511爬取下一页信息


如果存在下一页的时候,网页中就有一个a标签,如果没有,就会成为i标签了,因此,就能完善一下,将以上这些封装为一个函数: 




import requests

from bs4 import BeautifulSoup

# 网页的请求头

header = {

'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'

}

def get_page(url):

response = requests.get(url, headers=header)

# 通过BeautifulSoup解析出每个房源详细列表并进行打印

soup = BeautifulSoup(response.text, 'html.parser')

result_li = soup.find_all('li', {'class': 'list-item'})

# 进行下一页的爬取

result_next_page = soup.find_all('a', {'class': 'aNxt'})

if len(result_next_page) != 0:

# 函数进行递归

get_page(result_next_page[0].attrs['href'])

else:

print('没有下一页了')

# 进行循环遍历其中的房源详细列表

for i in result_li:

# 由于BeautifulSoup传入的必须为字符串,所以进行转换

page_url = str(i)

soup = BeautifulSoup(page_url, 'html.parser')

# 由于通过class解析的为一个列表,所以只需要第一个参数

result_href = soup.find_all('a', {'class': 'houseListTitle'})[0]

# 先不做分析,等一会进行详细页面函数完成后进行调用

print(result_href.attrs['href'])

if name == 'main':

# url链接

url = 'https://zhengzhou.anjuke.com/sale/'

# 页面爬取函数调用

get_page(url)



具体实现详细页面的爬取的完整代码为: 




import requests

from bs4 import BeautifulSoup

# 网页的请求头

header = {

'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'

}

def get_page(url):

response = requests.get(url, headers=header)

# 通过BeautifulSoup解析出每个房源详细列表并进行打印

soup_idex = BeautifulSoup(response.text, 'html.parser')

result_li = soup_idex.find_all('li', {'class': 'list-item'})

# 进行循环遍历其中的房源详细列表

for i in result_li:

# 由于BeautifulSoup传入的必须为字符串,所以进行转换

page_url = str(i)

soup = BeautifulSoup(page_url, 'html.parser')

# 由于通过class解析的为一个列表,所以只需要第一个参数






result_href = soup.find_all('a', {'class': 'houseListTitle'})[0]

# 详细页面的函数调用

get_page_detail(result_href.attrs['href'])

# 进行下一页的爬取

result_next_page = soup_idex.find_all('a', {'class': 'aNxt'})

if len(result_next_page) != 0:

# 函数进行递归

get_page(result_next_page[0].attrs['href'])

else:

print('没有下一页了')

# 进行字符串中空格,换行,Tab键的替换及字符串两边的空格删除

def my_strip(s):

return str(s).replace(" ", "").replace("\n", "").replace("\t", "").strip()

# 由于频繁进行BeautifulSoup的使用,因此要封装一下

def my_Beautifulsoup(response):

return BeautifulSoup(str(response), 'html.parser')

# 详细页面的爬取

def get_page_detail(url):

response = requests.get(url, headers=header)

if response.status_code == 200:

soup = BeautifulSoup(response.text, 'html.parser')

# 标题

result_title = soup.find_all('h3', {'class': 'long-title'})[0]

result_price = soup.find_all('span', {'class': 'light info-tag'})[0]

result_house_1 = soup.find_all('div', {'class': 'first-col detail-col'})

result_house_2 = soup.find_all('div', {'class': 'second-col detail-col'})

result_house_3 = soup.find_all('div', {'class': 'third-col detail-col'})

soup_1 = my_Beautifulsoup(result_house_1)

soup_2 = my_Beautifulsoup(result_house_2)

soup_3 = my_Beautifulsoup(result_house_3)

result_house_tar_1 = soup_1.find_all('dd')

result_house_tar_2 = soup_2.find_all('dd')

result_house_tar_3 = soup_3.find_all('dd')

'''

文博公寓,省实验中学,首付只需70万,大三房,诚心卖,价可谈 270万

宇泰文博公寓金水-花园路-文博东路4号 2010年普通住宅

3室2厅2卫 140平方米南北中层(共32层)

精装修 19285元/m2 81.00万

'''

print(my_strip(result_title.text), my_strip(result_price.text))

print(my_strip(result_house_tar_1[0].text),

my_strip(my_Beautifulsoup(result_house_tar_1[1]).find_all('p')[0].text),

my_strip(result_house_tar_1[2].text), my_strip(result_house_tar_1[3].text))

print(my_strip(result_house_tar_2[0].text), my_strip(result_house_tar_2[1].text),

my_strip(result_house_tar_2[2].text), my_strip(result_house_tar_2[3].text))

print(my_strip(result_house_tar_3[0].text), my_strip(result_house_tar_3[1].text),

my_strip(result_house_tar_3[2].text))

if name == 'main':

# url链接

url = 'https://zhengzhou.anjuke.com/sale/'

# 页面爬取函数调用

get_page(url)



5.7习题
1. lxml是一个HTML/XML的解析器,主要的功能是和数据。
2. 简述正则表达式的定义。
3. 正则表达式的大致匹配过程是怎样的?
4. Beautiful Soup的优势有哪些?
5. 利用PyQuery爬取头条部分指定网页内容。