爬虫基本流程还是很清晰的,首先是GET页面,然后对页面进行处理,提取所需信息。重点大多在GET页面和页面处理中。对于GET页面而言,其本身不应该存在技术难题,但是过于频繁的爬取REQUEST会极大的占用页面PV,影响网站用户体验。因此各大网站都会采取一定的反爬虫措施。所以这一部分的难点就是在于,如何避开反爬虫检测。假设我们爬取下来了页面,接下来要进行的就是如何对页面进行处理。一个页面多大几千条的html代码量,使得我们大部分情况下不可能用手工方式提取信息。因此我们需要对html代码文本进行分析
import requests
import aiohttp
大体来看,页面有两种获取方式,一种是GET方式,一种是POST方式,GET方式多返回标准HTML页面,POST凡是则多返回JSON格式数据。其中requests库整合了python中http请求的多种底层组件,相对来说比较好用,而aiohttp则是python中实现异步爬取的库,或者说python的多线程爬取,毕竟python中多线程也是依靠异步方式实现。值得注意的是,aiohttp的默认方式是一次性发送所有请求,等请求发送完毕后,再逐个对返回结果进行处理,因此当请求量比较大时,需要对数据进行分片操作。
一个标准请求大体有两个关键信息,一个是请求的headers,这个信息告诉了服务器请求源机器的部分信息,例如操作系统,浏览器的版本,可处理的语言编码,接受的数据格式,以及表明机器身份、储存用户登陆信息的cookie等等,例子如下:
header = {
"accept": "application/json, text/javascript",
"accept-encoding": "gzip, deflate, br",
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"content-type": "application/x-www-form-urlencoded",
"cookie": "tt_webid=6672604237333153294; WEATHER_CITY=%E5%8C%97%E4%BA%AC; tt_webid=6672604237333153294; UM_distinctid=169b8ffba327cf-072389de003c69-9333061-1fa400-169b8ffba3335c; csrftoken=563a5c4852134b1d10e2959ac3f8512c; CNZZDATA1259612802=1127217988-1553585285-https%253A%252F%252Fwww.google.com%252F%7C1553682487; s_v_web_id=167c46be309f1736398a8f4439439b69; __tasessionId=p62q61bgc1553687603544",
"referer": "https://www.toutiao.com/search/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"x-requested-with": "XMLHttpRequest"
}
另一个关键信息是向服务器上传的数据,在GET方式中以param传递,在POST方式中以data传递,二者和headers不同,不包含固定的字段,为网站服务自定义的信息。
param = {
"aid": "24",
"app_name": "web_search",
"offset": "0",
"format": "json",
"keyword": "一品红",
"autoload": "true",
"count": "20",
"en_qc": '1',
"cur_tab": '1',
"from": "search_tab",
"pd": "synthesis",
"timestamp": "1553687971703"
}
网站反爬虫的原因主要有二,一是爬虫的高请求严重影响了正常用户的体验,二是批量的数据爬取造成了企业信息的泄露。因为爬虫爬取的信息是展示在页面上的公开或半公开信息,因此网站并非是让人无法获取信息,而是不让人轻易获取这些信息。在这个逻辑下,反爬虫反的就是那些浏览行为不像正常人行为的爬虫。低端爬虫大体上有如下特点:
针对如上思路,避免自己的爬虫被反爬,就是要伪造成正常的用户行为。例如,通过建立账号池,分散请求,使得单个账户的请求数量合理。降低请求频率,随机生成请求的间隔时间等等。当然,现代反爬虫技术已经越变越多,包括生成验证码,需要登录等等,但是其本质思路还是没有变化。这些问题的解决方法这里不展开讲了,有兴趣可以关注我的公众号,里面会不定期进行一些分享。
之前讲到爬虫返回的数据分页面和JSON数据,在python中,JSON格式文本数据可以直接被转换成dict数据进行操作,因此这里不进行讨论,只讨论返回HTML页面的情况。
HTML是一种结构化的文本格式,因此去分析这个文本,并从中提取数据也就有了两大思路方向。一是基于文本pattern的信息提取,另一种则是基于HTML树状结构的梳理。先来说第一种,pattern提取。先来看一个html的例子:
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="canonical" href="https://blog.csdn.net/nightwish2018"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="renderer" content="webkit"/>
<meta name="force-rendering" content="webkit"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="referrer" content="always">
<meta http-equiv="Cache-Control" content="no-siteapp" /><link rel="alternate" media="handheld" href="#" />
<meta name="shenma-site-verification" content="5a59773ab8077d4a62bf469ab966a63b_1497598848">
<meta name="csdn-baidu-search" content='{"autorun":true,"install":true,"keyword":"【Felix的博客】"}'>
<link href="https://csdnimg.cn/public/favicon.ico" rel="SHORTCUT ICON">
<title>【Felix的博客】 - CSDN博客title>
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/list-37775d3cab.min.css">
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/themes/skin3-template/skin3-template-9b39979775.min.css">
<script type="text/javascript">
var username = "nightwish2018";
var userPermission = false;
文本处理的思路就是去寻找数据所在位置的关键信息,例如我们要提取用户名称:nightwish2018这个信息,则可以发现在页面中,用户名前会有var username = "
这一关键信息。因此我们可以通过写regex,寻找所有var username = "
后的信息。这种信息效率很高,只需要遍历一遍文本就可以提取所需信息,但是假设页面中有多个关键字,则可能提取到很多无关信息,而且当pattern更加复杂时,代码的可读性降低严重,举一个血淋淋的例子,在代码review的时候,就已经完全不知道这个过程中到底提取了些什么信息:
pattern = re.compile(r"var config .*")
config = pattern.findall(raw_html)
pattern = re.compile(r"\"id\":.*?\"name\":.*?pnid")
config = pattern.findall(config[0])
pattern = re.compile(r"var option .*")
option = pattern.findall(raw_html)
pattern = re.compile(r"\"name\":.*?,\"valueitems\":")
option = pattern.findall(option[0])
pattern = re.compile(r"{\"id\".*?\"valueitems\":")
raw_index = pattern.findall(raw_html)
因为文本处理的困难,因此提出了通过结构进行分层次查找的方式。这种方式的思路是首先将文本页面解析为树状结构的数据,然后通过寻找节点,找到对应的信息。例如在之前的页面中,如果我们想解析https://blog.csdn.net/nightwish2018
这个url信息,就会发现它实际储存于head节点下的link节点中,因此我们只需要进行一次树查找,访问两个节点就可以获取信息。使用etree解析,示例如下:
原始页面:
<div class="plbox">
<div class="plbox_img the_hover">
<a href="/search/p_008690-c_0-b_110-t_59-a_0-d_0-p_1-l_2-pg_2.html" target="_blank">
a>
div>
<h3 class="plbox_name">
<a href="/search/p_008690-c_0-b_110-t_59-a_0-d_0-p_1-l_2-pg_2.html" target="_blank">需要的数据
a>
h3>
div>
解析:
from lxml import etree
etree_html = etree.HTML(raw_html)
text = etree_html.xpath("//div[@class='plbox']/h3/a/text()")
可以清晰看到,text中储存了在页面有class参数为plbox的div模块中,h3标题下的a模块中的需要的数据,代码更有层次感,可读性强