python动态网站爬虫实战(requests+xpath+demjson+redis)
前言
之前简单学习过python爬虫基础知识,并且用过scrapy框架爬取数据,都是直接能用xpath定位到目标区域然后爬取。可这次碰到的需求是爬取一个用asp.net编写的教育网站并且将教学ppt一次性爬取下来,由于该网站部分内容渲染采用了js,所以比较难用xpath直接定位,同时发起下载ppt的请求比较难找。
经过琢磨和尝试后爬取成功,记录整个爬取思路供自己和大家学习。文章比较详细,对于一些工具包和相关函数的使用会在源代码或正文中添加注释来介绍简单相关知识点,如果某些地方看不懂可以通过注释及时去查阅简单了解,然后继续阅读。(尾部有源代码,全文仅对一些敏感的个人信息数据进行了省略。)
一、主要思路
1、观察网站
研究从进入网站到成功下载资源需要几次url跳转。
先进入目标网站首页,依次点击教材->选择初中->选择教辅->选择学科->xxx->资源列表->点击下载ppt。
目标网站首页
资源列表
资源详情页
分析url每步跳转以及资源下载是否需要cookie等header信息。
通过一步步跳转进入到最终的资源详情页,最终点击下载资源按钮时网站提示并且跳转到了登陆页面,说明发起下载的请求可能需要携带cookie等头部信息。
2、编写爬虫代码
- 登陆账户,获取到识别用户的cookies
- 请求资源列表页面,定位获得左侧目录每一章的跳转url。
- 请求每个跳转url,定位资源列表页面右侧下载资源按钮的url请求(注意2、3步是图资源列表)
- 发起url请求,进入资源详情页,定位获得下载资源按钮的url请求(第4步是图资源详情页)
- 发起请求,将下载的资源数据写入文件。
这是本次爬虫实战编写代码的大致思路,具体每次步骤碰到的难点以及如何解决在接下来的实战介绍中会进行详细分析。
二、爬虫实战
1、登陆获取cookie
首先网站登陆,获取到cookie和user-agent,作为之后请求的头部。设置全局变量HEADER,方便调用
HEADER = {
'User-Agent':
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko)Chrome/93.0.4577.63 Safari/537.36", 'Cookie':"xxxxxxx",
}
2、请求资源列表页面,定位获得左侧目录每一章的跳转url(难点)
首先使用requests发起资源列表页面的请求(资源列表页面url:http://www.guishiyun.com/res_list.aspx?rid=9&tags=2-24,1-21,3-70,12-96)
资源列表
BASE_URL = "http://www.guishiyun.com" #赋值网站根域名作为全局变量,方便调用 res = requests.get(BASE_URL +
"/res_list.aspx?rid=9&tags=1-21,12-96,2-24,3-70",
headers=HEADER).text #发起请求,获得资源列表页面的html
难点:定位获得左侧目录每一章的跳转url
正常思路:打开浏览器控制台,查看网页源代码,寻找页面左侧课程目录的章节在哪个元素内,用xpath定位。
使用xpath定位,发现无法定位到这个a标签,在确认xpath语法无错误后,尝试打印上个代码段中的res变量(也就是该html页面),发现返回的页面和控制台页面不同。
转换思路:可能该页面使用其他渲染方式渲染了html,导致浏览器控制台看到的html和请求返回的不一样(浏览器会将渲染后的页面呈现),打开控制台,查看页面源代码,搜素九年级上册(左侧目录标题),发现在js的script脚本中,得出该页面应该是通过JS渲染DOM得来的,该js对象中含有跳转的url。
xpath行不通后,我选择采用正则表达式的方式直接筛选出该代码。
import re #导入re 正则表达式包 pattern = r'var zNodes = (\[\s*[\s\S]*\])'
#定义正则表达式,规则:找出以"var zNodes = [ \n"开头,含有"[多个字符或空格]"的字符,并且以"]"结尾的文本 (相关知识不熟悉的可以简单看看菜鸟的正则表达式)
result = re.findall(pattern, res, re.M | re.I)
#python正则表达式,查找res中符合pattern规则的文本。re.M多行匹配,re.I忽略大小写。
将前两个代码块封装一下
def getRootText():
res = requests.get(BASE_URL +
"/res_list.aspx?rid=9&tags=1-21,12-96,2-24,3-70",
headers=HEADER).text #请求
pattern = r'var zNodes = (\[\s*[\s\S]*\])'
result = re.findall(pattern, res, re.M | re.I)
return result[0] #获得筛选结果 [{id: 1322, pI': 1122, name: '九年级上册', open: False, url: ?catId=1322&tags=1-21%2c12-96%2c2-24%2c3-70&rid=9#bottom_content', target: '_self'}, {...},{...}]
将结果转换成dict类型,方便遍历,获得每个章节的url。浏览上面得出的result发现,
{id:1322,pId:xxx...}
并不是标准的json格式(key没有引号),此时使用第三方包demjson,用于将不规则的json字符串变成python的dict对象。import demjson
def textToDict(text):
data = demjson.decode(text)
#获得筛选结果[{'id': 1322, 'pId': 1122, 'name': '九年级上册', 'open': False, 'url': '?catId=1322&tags=1-21%2c12-96%2c2-24%2c3-70&rid=9#bottom_content', 'target': '_self'}, {...},{...}]
return data
遍历转换好的dict数据,获得左侧目录每一章的url。此处需要注意的是,本人目的是下载每一章的ppt课件,所以我只需要请求每一个总章节的url(即请求第 1 章,第 2 章,不需要请求 1.1反比例函数),右边就会显示该章节下的所有ppt课件。所以我在遍历的时候,可以通过正则表达式,筛选出符合名称要求的url,添加进list并且返回。
def getUrls(dictData):
list = []
pattern = r'第[\s\S]*?章' #正则规则:找出以"第"开头,中间包含多个空格和文字,以"章"结尾的文本
for data in dictData: #遍历上文转换得到的dict数组对象
if len(re.findall(pattern, data['name'])) != 0:
list.append(data['url']) #如果符合则将该url添加到列表中
return list
3、请求每个跳转url,定位右侧下载资源按钮,获得url请求
遍历从上面获得的url列表,通过拼接网站域名获得网站url,然后发起请求
def download(urlList): # urlList是上面获得的list
for url in urlList:
res = requests.get(BASE_URL + '/res_list.aspx/' + url, HEADER).text #完整url请求,获得页面html
查看源代码,发现可以用xpath定位(目标是获取到
onclick
里的url)分析:该按钮元素 (
<input type=button>
)在<div class='res_list'><ul><li><div class="button_area">
里。xpath定位代码如下:root = etree.HTML(res) # 构造一个xpath对象
liList = root.xpath('//div[@class="res_list"]//ul//li') #xpath语法,返回多个<li>及子元素对象的列表
遍历
liList
,获得资源名字(为之后下载写入ppt的文件命名)以及跳转到资源详情下载页的urlfor li in liList:
name = li.xpath('.//div[@class="info_area"]//div//h1//text()')
name = name[0] # xpath返回的是包含name的列表,从中提取字符串 print(name): 1.1 反比例函数
btnurl = li.xpath('.//div[@class="button_area"]//@onclick') # 获得onlick内的字符串 "window.open('res_view.aspx....')"
pattern = r'\(\'([\s\S]*?)\'\)'# 只需要window.open内的url,所以采用正则提取出来。
btnurl1 = re.findall(pattern, btnurl[0])
4、跳转到资源详情下载页,获得真正的下载请求(难点)
上文代码段中获取到url之后依旧是拼接域名,然后通过完整url发起请求,获得资源详情下载页面的html数据。
res1 = requests.get(BASE_URL + '/' + btnurl1[0], HEADER).text
跳转后的详情页面
查看源代码后按钮本身只是触发表单提交,而且是
post
请求。点击下载资源按钮,使用浏览器控制台抓包查看post请求需要的参数。使用
ctrl+f
在网页源代码中搜素这几个参数,发现存在于<input>
标签中,只是被css
隐藏了,所以接下来就是简单的用xpath
和正则表达式将post
请求中的url
和这几个参数值获得,然后添加到header
中发起请求就行了。VIEWSTATE = '__VIEWSTATE' # 全局变量,定义属性名称
VIEWSTATEGENERATOR = '__VIEWSTATEGENERATOR'
EVENTVALIDATION = '__EVENTVALIDATION'
BUTTON = 'BUTTON'
BUTTON_value = '下 载 资 源'
root1 = etree.HTML(res1) # res1是之前代码段请求的html文本
form = root1.xpath('//form[@id="form1"]') # xpath定位到form
action = root1.xpath('//form[@id="form1"]/@action')
action = re.findall(r'(/[\S]*?&[\S]*?)&', action[0], re.I) #正则表达式获取form中action函数里的url
VIEWSTATE_value = form[0].xpath(
'.//input[@name="__VIEWSTATE"]//@value') #获取参数值
VIEWSTATEGENERATOR_value = form[0].xpath(
'.//input[@name="__VIEWSTATEGENERATOR"]//@value')#获取参数值
EVENTVALIDATION_value = form[0].xpath(
'.//input[@name="__EVENTVALIDATION"]/@value')#获取参数值
data = { # post提交所需要的data参数
VIEWSTATE: VIEWSTATE_value,
VIEWSTATEGENERATOR: VIEWSTATEGENERATOR_value,
EVENTVALIDATION: EVENTVALIDATION_value,
BUTTON: BUTTON_value
}
res2 = requests.post(BASE_URL + action[0],data=data,headers=HEADER).text #发起请求
此时发起请求之后发现返回的仍然是网页html,如果打开控制台工具,查看点击按钮发起请求后的页面。
同时看到由于是更新页面,还产生了许多其他各种各样的请求,一时间很难找到真正下载文件的请求是哪一个。
此时笔者想到的是一个笨方法,通过抓包工具,对所有请求进行拦截,然后一个个请求陆续通过,最终就可以找到下载请求。这里笔者用到的是
BurpSuite
工具,陆续放行请求,观察页面是否有下载界面出现,找到了url:/code/down_res.ashx?id=xxx
,同时在浏览器控制台查找这一串字符串,最终在post
请求返回的页面中找到了这个字符串的位置不用多说,直接正则获取
downUrl = re.search(r'\<script\>[\s]*?location\.href\s=\s\'([\S]*?)\'',res2,re.I) #正则筛选出url
downUrl_text = downUrl.group(1)
发起请求,并且将数据读写进指定的目录中。
downPPT = requests.get(BASE_URL+downUrl_text,headers=HEADER)
with open(f'./test/{name}.ppt','wb') as f: #将下载的数据以二进制的形式写入到当前项目下test文件夹中,并且做好命名。name参数在上文中已经获得。
f.write(downPPT.content)
结果
5、添加额外功能,实现增量爬虫
爬取到一半发现程序终止了,原来该网站对每个账号每天下载数有限额,而我们的程序每次运行都会从头开始检索,如何对已经爬取过的url进行存储,同时下次程序运行时对已爬取过的url进行识别?这里笔者使用的是通过
redis
进行存储,原理是对每次下载的url进行存储,在每次发起下载请求时先判断是否已经存储,如果已经存储则跳过本次循环。if(r.sadd(BASE_URL + action[0],'1')==0): # sadd是redis添加键值的方法,如果==0说明已经存在,添加失败。
continue
6、总源代码
import re
import requests
from lxml import etree
import demjson
import redis
pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
r = redis.Redis('localhost',6379,decode_responses=True)
BASE_URL = "http://www.guishiyun.com"
HEADER = {
'User-Agent':
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36",
'Cookie':
"xxx",
}
VIEWSTATE = '__VIEWSTATE'
VIEWSTATEGENERATOR = '__VIEWSTATEGENERATOR'
EVENTVALIDATION = '__EVENTVALIDATION'
BUTTON = 'BUTTON'
BUTTON_value = '下 载 资 源'
def getRootText():
res = requests.get(BASE_URL +
"/res_list.aspx?rid=9&tags=1-21,12-96,2-24,3-70",
headers=HEADER).text
pattern = r'var zNodes = (\[\s*[\s\S]*\])'
result = re.findall(pattern, res, re.M | re.I)
return result[0]
def textToDict(text):
data = demjson.decode(text)
print(data)
return data
def getUrls(dictData):
list = []
pattern = r'第[\s\S]*?章'
for data in dictData:
if len(re.findall(pattern, data['name'])) != 0:
list.append(data['url'])
return list
def download(urlList):
global r
for url in urlList:
res = requests.get(BASE_URL + '/res_list.aspx/' + url, HEADER).text
root = etree.HTML(res)
liList = root.xpath('//div[@class="res_list"]//ul//li')
for li in liList:
name = li.xpath('.//div[@class="info_area"]//div//h1//text()')
name = name[0]
btnurl = li.xpath('.//div[@class="button_area"]//@onclick')
pattern = r'\(\'([\s\S]*?)\'\)'
btnurl1 = re.findall(pattern, btnurl[0])
res1 = requests.get(BASE_URL + '/' + btnurl1[0], HEADER).text
root1 = etree.HTML(res1)
form = root1.xpath('//form[@id="form1"]')
action = root1.xpath('//form[@id="form1"]/@action')
action = re.findall(r'(/[\S]*?&[\S]*?)&', action[0], re.I)
VIEWSTATE_value = form[0].xpath(
'.//input[@name="__VIEWSTATE"]//@value')
VIEWSTATEGENERATOR_value = form[0].xpath(
'.//input[@name="__VIEWSTATEGENERATOR"]//@value')
EVENTVALIDATION_value = form[0].xpath(
'.//input[@name="__EVENTVALIDATION"]/@value')
data = {
VIEWSTATE: VIEWSTATE_value,
VIEWSTATEGENERATOR: VIEWSTATEGENERATOR_value,
EVENTVALIDATION: EVENTVALIDATION_value,
BUTTON: BUTTON_value
}
if(r.sadd(BASE_URL + action[0],'1')==0):
continue
res2 = requests.post(BASE_URL + action[0],data=data,headers=HEADER).text
downUrl = re.search(r'\<script\>[\s]*?location\.href\s=\s\'([\S]*?)\'',res2,re.I)
downUrl_text = downUrl.group(1)
if(r.sadd(BASE_URL+downUrl_text,BASE_URL+downUrl_text,downUrl_text)==0):
continue
downPPT = requests.get(BASE_URL+downUrl_text,headers=HEADER)
with open(f'./test/{name}.ppt','wb') as f:
f.write(downPPT.content)
def main():
text = getRootText()
dictData = textToDict(text)
list = getUrls(dictData)
# download(list)
if __name__ == '__main__':
main()
三、总结
之前只是学习过最简单最基础的requests
请求+xpath
定位的爬虫方式,这次碰巧遇到了较为麻烦的爬虫实战,所以写下爬虫思路和实战笔记,加深自己印象的同时也希望能对大家有所帮助。当然这次爬虫总的来说还是比较简单,还没有考虑代理+多线程等情况,同时还可以使用selenium
等浏览器渲染工具,就可以不用正则定位了,当然笔者是为了顺便学习一下正则。
如果有所帮助,欢迎大家点赞收藏并且进行友好的评论交流。同时欢迎访问我的个人博客空间进行各种技术学习 欢迎来到菜鸟小白的空间
python动态网站爬虫实战(requests+xpath+demjson+redis)的更多相关文章
- Python简单网络爬虫实战—下载论文名称,作者信息(下)
在Python简单网络爬虫实战—下载论文名称,作者信息(上)中,学会了get到网页内容以及在谷歌浏览器找到了需要提取的内容的数据结构,接下来记录我是如何找到所有author和title的 1.从sou ...
- Python动态网页爬虫-----动态网页真实地址破解原理
参考链接:Python动态网页爬虫-----动态网页真实地址破解原理
- python应用之爬虫实战2 请求库与解析库
知识内容: 1.requests库 2.selenium库 3.BeautifulSoup4库 4.re正则解析库 5.lxml库 参考: http://www.cnblogs.com/wupeiqi ...
- python应用之爬虫实战1 爬虫基本原理
知识内容: 1.爬虫是什么 2.爬虫的基本流程 3.request和response 4.python爬虫工具 参考:http://www.cnblogs.com/linhaifeng/article ...
- Python动态网站的抓取
网页下载器 # coding:utf-8import requestsimport urllib2import systype = sys.getfilesystemencoding()class H ...
- 爬虫系列2:Requests+Xpath 爬取租房网站信息
Requests+Xpath 爬取租房网站信息 [抓取]:参考前文 爬虫系列1:https://www.cnblogs.com/yizhiamumu/p/9451093.html [分页]:参考前文 ...
- Python爬虫实战(4):豆瓣小组话题数据采集—动态网页
1, 引言 注释:上一篇<Python爬虫实战(3):安居客房产经纪人信息采集>,访问的网页是静态网页,有朋友模仿那个实战来采集动态加载豆瓣小组的网页,结果不成功.本篇是针对动态网页的数据 ...
- 爬虫系列4:Requests+Xpath 爬取动态数据
爬虫系列4:Requests+Xpath 爬取动态数据 [抓取]:参考前文 爬虫系列1:https://www.cnblogs.com/yizhiamumu/p/9451093.html [分页]:参 ...
- python爬虫实战---爬取大众点评评论
python爬虫实战—爬取大众点评评论(加密字体) 1.首先打开一个店铺找到评论 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经 ...
随机推荐
- Shell-14-常用命令和工具
常用命令 有人说 Shell 脚本是命令堆积的一个文件, 按顺序去执行 还有人说想学好 Shell 脚本,要把 Linux 上各种常见的命令或工具掌握了,这些说法都没错 Shell 语言本身在语法结构 ...
- Java的安装过程和开发环境
首先需要安装jdk(Java Development Kit开发工具包) 下载地址:https://www.oracle.com/java/technologies/javase-downloads. ...
- STM32—位带操作
STM32中的位带操作: 名字为位带操作,实际上是对位的操作,位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见. 51 单片机中通过关键字 sbit 来实现位定义, STM32 ...
- Oracle11 创建表空间、创建角色及导入
针对日常工作中经常使用命令创建表空间,导入数据,特此记录下(windows环境下),记录中的testSpaceName是表空间名称,testUserName是用户名,userPwd 是用户密码. 1. ...
- 微信小程序中wx.login和wx.getUserProfile的使用
在使用微信登录时,通常会在调用wx.login获取code后再通过wx.getUserProfile获取iv和encryptedData(加密数据)一起发到后端进行登录验证 在实际使用中如果在wx.l ...
- ☕【Java技术指南】「OpenJDK专题」想不想编译属于你自己的JDK呢?(Windows10环境)
Win10下编译OpenJDK8 编译环境 Windows10专业版64位: 编译前准备 Tip: 以下软件的安装和解压目录尽量不要包含中文或空格,不然可能会出现问题 安装 Visual Studio ...
- windows上python3安装
下载python 下载地址 https://www.python.org/downloads/windows/ 安装python 1.添加python到环境变量 2.自定义安装 3.下一步 4.选择安 ...
- git忽略文件夹提交以及gitignore修改后不生效的解决办法
1.在 .gitgnore 文件加入需要忽略的问价夹正则表达式: 在配置完以后提交代码,你可能会发现git忽略配置不生效! 解决办法,将缓存的文件重新添加一下即可 2.打开命令行,将下面三个命令复制粘 ...
- (二)MQTT客户端模拟连接阿里云并上传数据
本文主要讲述使用MQTT.fx接入物联网平台 一.下载MQTT.fx客户端 官网链接 二.设置相关参数 打开MQTT单片机编程工具,将三元组复制进去,生成所需要的信息 单片机工具下载地址 三元组还记得 ...
- Spring 钩子之BeanFactoryPostProcessor和BeanPostProcessor的源码学习,FactoryBean
BeanFactoryPostProcessor 是用于增强BeanFactory的(例如可以增强beanDefination), BeanPostProcessor是用于增强bean的,而Facto ...