一、前言
网络爬虫,又称网页蜘蛛和网络机器人,是一种按照一定规则,自动地抓取万维网上的信息的程序或脚本。所谓爬取数据,就是通过编写程序,模拟浏览器上网,然后让其去浏览器上抓取数据的过程。爬虫在使用场景中的分类:
- 通用爬虫:抓取系统重要组成部分。抓取的是一整张页面数据。
- 聚焦爬虫:建立在通用爬虫基础之上。抓取的是页面中特定的局部内容。
- 增量式爬虫:检测网站中数据更新的情况。只会抓取网站中最新更新出来的数据。
门户网站的某些数据可能涉及到隐私问题或者为了防止一些爬虫程序对网站造成破坏,会建立相应的反爬机制,通过指定相应的策略或者技术手段,防止爬虫程序进行网站数据的爬取。在几乎和反爬机制诞生的同时,robots.txt反爬协议诞生。robots.txt协议是一个君子协议,明确规定了网站中那些数据可以被爬取哪些不可以被爬取。查看网站的robots反爬协议方式:域名/robots.txt,比如查看百度的robots协议:https://www.baidu.com/robots.txt。有了反爬机制,反反爬也随之诞生。通过制定相关的策略或者技术手段,爬虫程序破解门户网站中具备的反爬机制,从而获取门户网站的数据。
既然是爬取万维网上的信息,不免要先了解一下HTTP与HTTPS协议。HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。简单地理解就是服务器和客户端进行数据交互的一种形式。HTTPS协议是Secure Hyper Text Transfer Protocol(安全超本文传输协议)的缩写,在HTTP上建立SSL加密层并对数据进行加密,较HTTP更安全。两者的关系为:HTTPS=HTTP+TLS/SSL。HTTP存在信息窃听、篡改、劫持等风险,而HTTPS会进行信息加密、完整性校验以及身份验证等工作降低了风险。由HTTPS的工作机制以及和HTTP的关系,可以得出它们存在一下不同点:
- 费用:HTTPS协议需要到ca申请证书,一般免费的证书较少,需要一定费用;
- 传输:HTTP是明文传输方式,而HTTPS是安全超文本传输协议,是具有安全性的SSL加密传输协议;
- 端口:HTTP和HTTPS使用的是完全不同的连接方式,使用的端口也不同,前者使用的是80端口,而后者使用的是443端口;
- 安全:HTTP连接很简单,是无状态的,而HTTPS协议是由SSL+HTTP协议构成的可进行加密和身份认证的网络协议,具有更高的安全性。
在建立连接过程中,数据头部会携带很多重要信息。常用请求头信息:User-Agent:请求载体的身份标识;Connection:浏览器通过这个头告诉服务器,请求完后是断开连接还是保持连接。常用响应头信息:Content-Type:服务器通过这个头,告诉浏览器回送数据的类型。
二、数据爬取
在涉及到网络请求的模块中,有urllib模块和requests模块,后者用法更为简洁,而且高效,现在后者更为常用。对于数据的爬取,这里也主要用到requests模块。requests模块是python中原生的一款基于网络请求的模块,功能非常强大,使用也简单便捷,处理爬虫操作也比较高效。其作用是模拟浏览器发送请求,和平时打开浏览器请求网页一样,首先是在地址栏输入请求网址,然后回车发送请求,发送成功后浏览器显示响应的数据信息,由此可得requests发送网页请求的编码思路如下:
- 指定url
- 发送请求
- 获取响应数据
- 持久化存储
下面由浅入深式介绍数据的爬取。首先以请求百度首页(www.baidu.com)为例,使用requests库请求百度首页并将返回结果进行打印,将响应数据保存到本地中:
# -*- coding:utf-8 -*-
import requests
# 需求:爬取百度网页的数据
if __name__ == '__main__':
# step1: 指定url
url = 'https://www.baidu.com/'
# step2: 发送求情
# get方法会返回一个响应数据
response = requests.get(url=url)
# step3: 获取响应数据
# .text返回字符串式的响应数据
page_text = response.text
print(page_text)
# step4: 持久化存储
with open('./baidu.html','w',encoding='utf-8') as fp:
fp.write(page_text)
print('爬取数据结束!')
通过上面一段代码,便可以获取响应的百度首页的页面数据。当然对于大多数人来说,浏览器的重要功能就是搜索信息,这里以搜索关键字“你好”为例,请求网页:
# -*- coding:utf-8 -*-
import requests
if __name__ == '__main__':
# UA伪装:将对应的User-Agent封装到一个字典中
headers = {
'User-Agent':'你的标识'
}
# step1: 指定url
url = 'https://www.sogou.com/web'
params = '你好'
# step2: 发送请求
response = requests.get(url=url,params=params,headers=headers)
# step3: 获取响应数据
page_text = response.text
# step4: 持久化存储
fileName = query+'.html'
with open(fileName,'w',encoding='utf-8') as fp:
fp.write(page_text)
print(fileName,'保存成功!')
上面代码实现了打开浏览器,输入关键字进行搜索,并返回搜索结果页面数据信息。与上一段代码不同的是,一是设置了url中附带的参数,二是设置头部信息进行UA伪装。所谓UA,即User-Agent,是请求载体的身份标识。一些门户网站会使用UA检测作为反爬机制,其服务器会检测请求载体的身份标识,如果检测到请求载体的身份标识为某一款浏览器,则认为这是一个正常的请求;反之,如果检测到该请求不是基于某一款浏览器,那么认为这不是正常的请求,比如爬虫程序,那么服务器可能拒绝访问,爬虫程序爬取数据失败。因此,在这里设置了User-Agent,进行UA伪装,让请求的服务器认为这是一个正常的请求。
上面都是把响应数据存储为html文件,也可以通过json库把响应数据存储为json格式的文件。以百度翻译为例,当输入翻译内容后,页面的局部内容会进行刷新,这里用到的是ajax技术,可以通过检查-->network-->XHR查看响应数据信息,这里的XHR对应的是ajax的数据请求,在接收到响应数据后将其保存为json格式的文件。编码如下:
# -*- coding:utf-8 -*-
import requests
import json
if __name__ == '__main__':
# 1、指定url
url = 'https://fanyi.baidu.com/sug'
# UA伪装,设置请求载体标识
headers = {
'User-Agent':'你的标识'
}
# 参数处理
kw = input('请输入要翻译的文字:')
data = {
'kw':kw
}
# 2、发送请求
response = requests.post(url=url,data=data,headers=headers)
# 3、获取响应数据。json()方法返回一个json对象。
res_obj = response.json()
print(res_obj)
# 4、持久化存储
fileName = kw+'.json'
fp = open(fileName,'w',encoding='utf-8')
json.dump(res_obj,fp=fp,ensure_ascii=False)
print(kw+'翻译结果已存储!')
另外,与上面两段代码不同的是,在进行持久化存储时,是直接赋给fp,而之前都用的是with关键字。with是从python2.5引入的一个新的语法,是一种上下文管理协议,目的是从流程图中把try、except和finaly关键字和资源的分配及释放相关的代码去掉,简化了try、except、finally的处理流程。其基本语法格式为:with exp [as target]: with body。此外,在这一例中可以发现,一个页面的某些数据很有可能是动态加载出来的,比如上面代码获取到的数据都是动态的,并不是通过网页地址栏的地址得到的。上面的例子都是直接根据网址来抓网页的数据,如果说,想要抓取一个网站数据的详细数据,可能涉及到响应数据的处理和网页的跳转,例如下面一个例子:
# -*- coding:utf-8 -*-
import requests
import json
if __name__ == '__main__':
# get id
# designate home url
home_url = 'http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsList'
# dset headers
headers = {
'你的标识'
}
# store company id and personal name
id_list = []
person_list = []
# set paramenter
for page in range(1,6):
page = str(page)
params = {
"on": "true",
"page": page,
"pageSize": 15,
"productName":"",
"conditionType": 1,
"applyname":""
}
# initiate request
res_json = requests.post(url=home_url,data=params,headers=headers).json()
# print(res_json)
for dic in res_json['list']:
id_list.append(dic['ID'])
# print(id_list)
# start to access personal name according to id
# designate detail url
detail_url = 'http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsById'
# set detail paraments
for id in id_list:
access_detail_data = {
"id":id
}
#initiate request,and get detail response
detail_res = requests.post(url=detail_url,data=access_detail_data,headers=headers).json()
# test
person_list.append(detail_res['businessPerson'])
print(person_list)
# persistently store
fileName = 'personalname.txt'
fp = open(fileName,'w',encoding='utf-8')
json.dump(person_list,fp=fp,ensure_ascii=False)
print(fileName,'已完成存储!')
上面编码以通过药监局获取化妆品企业法定代表人为例,通过一个循环抓取网页的多页数据,对爬取的数据进行提取想需的信息,再根据这些信息拼接成地址,爬取所需的详细信息,最后进行数据切割并保存于本地。
三、数据解析
很多时候,我们并不是需要的不是整张网页的数据,而是网页中的部分有用的信息,因此对于获取的数据需要进行数据处理,解析或提取出需要的信息。数据解析的原理是,所需的网页局部文本内容都会存放在标签内或作为标签的属性,通过定位相应的标签,再从标签内或标签的属性中解析出所需要的文本内容,这里总结三种形式:正则解析、bs4解析、xpath解析。一般,数据的解析工作放在持久化存储之前,获取响应数据之后。由上文的提到的数据爬取流程可以引申为以下:
- 指定url
- 发起请求
- 获取响应数据
- 数据解析
- 持久化存储
以下将通过三个例子分别使用正则表达式、bs4以及xpath进行解析来进行介绍数据解析。
1、正则解析
从以下爬取贴吧背景图片例子可以看出相比于之前的数据爬取流程多了一个数据解析的过程,通过对指定页面发起请求,获取响应数据,再通过正则表达式对响应数据进行解析过滤,获取所需的图片地址数据,最后根据获取的图片地址数据发起请求获取图片,并保存至本地文件夹。python正则解析的原理是,通过导入re模块,设置对应的正则表达式,调用findall方法对请求的响应数据进行相应的匹配,过滤出符合要求的数据。自python1.5版本增加了提供perl风格正则表达式的re模块,该模块使python语言拥有全部的正则表达式功能。在下面的编码中,定义了一个ex变量保存相应的正则表达式,其中的“.”表示除换行以外的所有字符,“*”表示任意多次,“?”表示可有可无;findll方法对本文内容和正则表达式进行匹配,这里使用的是修饰符re.M进行多行匹配,除此之外还有修饰符re.I表示忽略大小写,re.S表示单行匹配,在python数据解析中,一般都是使用修饰符re.M,具体根据需要,且可以同时使用多个修饰符来控制匹配的模式,通过按位OP(|)实现,如想要不区分大小写地匹配多行数据则同时设置I标识和M标识(re.I | re.M)。除此之外,还有其他许多可选模式和修饰符,可参考python正则表达式。
# -*- coding:utf-8 -*-
import requests
import re
import os
if __name__ == '__main__':
# 指定url
url = 'https://tieba.baidu.com/p/6469380781'
# 伪装UA
headers = {
'User-Agent': '你的标识'
}
# 发起请求
res_text = requests.get(url=url,headers=headers).text
# print(res_text)
# 使用聚焦爬虫对网页的数据进行解析或者提取
ex = '<img class="BDE_Image" src="(.*?)" .*?><br>'
img_list = re.findall(ex,res_text,re.S)
# print(img_list)
# 创建文件夹,保存数据
if not os.path.exists('./touxiang'):
os.mkdir('./touxiang')
for img in img_list:
# 获取图片数据
img_data = requests.get(url=img,headers=headers).content
# 设置图片名称
img_name = img[-10:]
# 将图片保存至本地
img_path = './touxiang/'+ img_name
with open(img_path,'wb') as fp:
fp.write(img_data)
print(img_name,'已成功下载!')
2、bs4解析
上面正则解析的方式不仅可以运用在python语言中,还可以运用在其他语言中,是一种比较通用的数据解析方式。而bs4是python语言独有的数据解析方式,只能运用在python语言中。该解析方式需要lxml解析器以及实例化BeautifulSoup对象,因此如要使用bs4进行数据解析,需要进行环境安装bs4模块以及lxml模块:pip install bs4与pip install lxml。其解析原理是,实例化BeautifulSoup对象,将要解析的数据加载到该对象中,再调用BeautifulSoup相关的属性或方法对数据进行标签定位或数据提取。加载到BeautifulSoup对象中的带解析数据可以来自本地文件,也是是网络爬取的文件。若是前者,则可以通过open函数打开本地文件,加载到BeautifulSoup对象中:soup = BeautifulSoup(open('待解析本地文件'), 'lxml');若是后者,则将爬取的网络数据通过.text方法转化为字符串对象的数据,再加载到BeautifulSoup对象中:soup = BeautifulSoup('待解析网页数据', 'lxml')。加载对象后,根据需要进行标签定位或者数据提取:
- 定位标签:soup.tagName。例如soup.a。
- 获取属性:soup.tagName.attrs将返回一个字典,包含该标签的所有属性。如要获取图片名称:soup.img.attrs['alt']或者soup.img['alt']。
- 获取内容:soup.tagName.text , soup.tagName.string , soup.tagName.get_text。string方法只可以获取该标签下的直系文本内容,其他两个可以获取某个标签中的全部文本内容。
- 查找一个符合要求的标签:soup.find('tagName'[,title=""/alt=""/class_=""/id=""])。
- 查找所有符合要求的标签:soup.find_all('tagName')返回所有tagName标签、soup.find_all('tagName1’,'tagName1')返回所有的tagName1和tagName2标签、soup.find_all('tagName', limit=2)返回前两个标签。
- 根据选择器选择指定的内容:soup.select('tag_name/.class_name/#id_name/层级选择器')将返回一个列表。对于层级选择器,可以使用">"和" ",前者表示一个层级,后者表示多个层级。
# -*- coding:utf-8 -*-
import requests
from bs4 import BeautifulSoup
if __name__ == '__main__':
# step1: 爬取章节页面的数据
# 伪装UA
headers = {
'User-Agent': '你的标识'
}
# 指定url
chapter_url = 'http://www.shicimingju.com/book/hongloumeng.html'
# 发起请求
chapter_text = requests.get(url=chapter_url,headers=headers).text
# step2: 对章节页面的数据进行解析,获得章节标题和详情页的url,并将爬取的数据保存至本地
# 实例化beautifulsoup对象,把章节页面的源码数据加载到该对象中
chapter_soup = BeautifulSoup(chapter_text,'lxml')
# print(chapter_soup)
chapter_list = chapter_soup.select('.book-mulu > ul > li')
fp = open('./红楼梦.txt','w',encoding='utf-8')
for li in chapter_list:
# 获得详情页的url、章节标题
detail_url = 'http://www.shicimingju.com'+li.a['href']
chapter_title = li.a.string
# 请求详情页,获得章节详细内容
detail_text = requests.get(url=detail_url,headers=headers).text
# 对详情页的数据进行解析,获得每个章节的具体内容
detail_soup = BeautifulSoup(detail_text,'lxml')
detail_content = detail_soup.find('div',class_='chapter_content').text
fp.write(chapter_title+':'+detail_content)
print(chapter_title,'下载完成!')
3、xpath解析
xpath解析是最常用且最便捷高效的解析方式,通用性强。该解析方式同上面bs4具有很多相似之处,首先一样需要进行环境的安装,因为这种解析需要lxml解析器,使用到etree模块实例化etree 对象:pip install lxml。该解析方式的原理是,实例化一个etree对象,将待解析的源码数据加载到etree 对象中,调用etree中的xpath方法以及xpath表达式实现标签定位和指定数据提取。加载到etree的对象同样可以大致分为两类,一个是本地文件数据,一个是爬取的网络数据。若是本地文件,则调用parse方法实例化etree对象:tree = etree.parse(文件名);如是通用爬虫爬取的网络源码,使用.text将网页源码数据转化为字符串内容,再调用HTML方法加载到etree对象中:tree = etree.HTML(网页内容字符串)。实例化etree对象后,通过 xpath表达式、调用xpath方法进行标签定位和数据提取,常见的xpath表达式如下:
- / 从根节点开始定位,表示一个层级;// 表示多个层级,可以从任意位置开始定位。
属性定位://div[@class="class_name"]。找到class属性值为class_name的div标签。
- 层级和索引定位://
div[@class="class_name"]/tag1/tag2[1]/tag3。找到class属性值为class_name的div直系子标签tag1下的第一个自标签tag2下的直系自标签tag3。
- 取属性://
div[@class="class_name"]//tag1[3]/a/@href。获取class属性值为class_name的div的第三个直系自标签下的a标签的href属性值。
- 取文本://
div[@class="class_name"]/text(),获取class属性值为class_name的div的直系文本内容;
//div[@class="class_name"]//text(),获取class属性值为class_name的div标签中的所有文本内容。
- 模糊匹配:
//div[contains(@class, "ng")]
,找到class属性值中包含ng的div标签;//div[starts-with(@class, "ta")],找到class属性值以ta开头的div标签
- 逻辑运算://a[@href="" and @class="du"],找到href值为空和class值为du的a标签。
# -*- coding:utf-8 -*-
import requests
from lxml import etree
import os
if __name__ == '__main__':
# step1: 获取图片所在网页的源码数据
# 伪装UA
headers = {
'User-Agent': '你的标识'
}
# 指定url
url = 'http://pic.netbian.com/4kfengjing/'
# 发起请求
home_text = requests.get(url=url,headers=headers).text
# print(home_text)
# step2: 对获取的源码数据进行解析
home_etree = etree.HTML(home_text)
li_list = home_etree.xpath('//div[@class="slist"]/ul/li')
# step3: 创建文件夹,保存下载的图片文件
if not os.path.exists('./风景图'):
os.mkdir('./风景图')
# print(li_list)
for li in li_list:
# 获取图片的地址和名称
img_url = 'http://pic.netbian.com'+li.xpath('./a/img/@src')[0]
img_name = li.xpath('./a/img/@alt')[0].encode('iso-8859-1').decode('gbk')+'.jpg'
# print(img_name)
# 访问图片地址,获取图片数据
img_content = requests.get(url=img_url,headers=headers).content
# 对获取的图片进行持久化存储
save_path = './风景图/'+img_name
with open(save_path,'wb') as fp:
fp.write(img_content)
print(img_name,'下载成功!')
参考资料:
1、菜鸟教程
2、小猿圈
3、爬虫开发入门