爬虫进阶

爬虫一定要找最新的教程,因为网站更新速度很快,老版教程很可能会过时,一些案例基本无法复现

爬虫概述

image-20240515215806526

image-20240515220426489

image-20240515220603409

静态页面抓取:

  1. 获取构造需要抓取的所有url列表
  2. 遍历这个列表,使用request等抓取页面数据
  3. 使用xpath+Scrapy Selector或者Xpath+lxml的方式对抓取的页面内容进行解析
    1. xpath的编写可以借助浏览器来完成,选中对应元素右键点击copy as xpath

request构造请求:

  • get请求:
    • 方式一:url+参数拼接方式
    • 方式二:url+params形参的方式
  • post请求
    • 许多登陆页面,需要通过构造post请求,提交信息,破除限制
    • 可以通过form-data或者json的形式提交数据

模拟登陆场景:

许多网站必须登陆之后才能访问里面其他的页面

  1. 通过抓包,找到登陆接口
  2. 编写代码请求登陆接口,传入正确的账号、密码等信息

即使登陆之后,仍然可以访问其他页面吗?不一定,这时我们要关注后台的鉴权方式

后台的鉴权方式一般有两种:

  • 基于cookie+session的鉴权机制
  • 基于Token的鉴权机制
    • image-20240516004458958

如何初步判断是采用的哪种鉴权?

  • form-data大概率是cookie+session
  • 网站首页地址与登陆请求地址不一致,大概率是token鉴权

image-20240516005245589

模拟登录中华网

模拟登录、访问需要登录之后才能够访问的页面

  • 1发送登录请求
  • 保存cookie信息
  • 下次请求需要登录页面直接携带cookie信息
1
2
3
中华网账号 https://passport.china.com/
username:17775990925
password:a546245426

通过抓包找到登录接口地址,注意这里用错误的账号或者密码,如果是正确的则会自动跳转,无法找出登录接口

image-20240516102343020

image-20240516102405228

image-20240516102418641

利用python构造请求模拟登录实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import requests
#url
url = "https://passport.china.com/logon"
#请求头
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",
"Referer": "https://passport.china.com/logon",
}
#请求参数
params={
"userName": "17775990925",
"password": "a546245426"
}
#模拟登录请求
# login_response = requests.post(url,data=params,headers=headers)
# print(login_response.content.decode("utf8"))

# print(login_response.cookies)
"""
如何提取请求的cookie信息:
response.cookies
如何解决利用cookie进行校验的访问请求:
方式一:
request.get(cookies=cookies),利用参数让请求携带cookie信息,cookie字典格式
方式二:
请求头中携带,cookie以字符串格式
headers={
"Cookie":cookie值
}
方式三:requests.session
利用:
requests.session()对象去发送请求,其会自动记住状态信息


"""

#访问其他页面,不携带cookie信息的话会自动跳转到登录页面

#方式一:以字典的形式传递
# url = "https://passport.china.com"
# response = requests.get(url,headers=headers,cookies=login_response.cookies)
# print(response.content.decode())

#方式二:以字符串的形式传递
#这种cookie来源:直接从浏览器复制

# headers["Cookie"]='area_name=%E6%B9%96%E5%8C%97%E7%9C%81%E6%AD%A6%E6%B1%89%E5%B8%82; SESSION_COOKIE=46; Hm_lvt_cbec92dec763e6774898d6d85460f707=1715822413,1715828314; Hm_lpvt_cbec92dec763e6774898d6d85460f707=1715828314; nickname=china_2823hxfg16791058; lastlogindate=2024-05-16; lastlogintime="10:58:48"; lastloginip=211.161.157.200; bindMobile="1@177*****925"; CHINACOMID=218ab446-115d-4fbe-93fe-a5fd744593825; CP_USER=FKBo6w-aaDELXK1EnoT3DPk1faoTCuWOzIpsuaQNIsJWqiRz6o9drrZQMJZRRbngi7eJikd0sv41eZDrzksZGmumfJyC7TEP5dMN41%2F1QIHag0K39t%2FVBxzGqQTN85yGmV%2FbilYB7ulfPMYVXbF6aMqwUoU1rfChWCxyzGnnrK%2FgdpLc0L1F5tS7hOqvGEkrBP23Gu2Sd0E76gm7%2FHCqgRxwGcN7FXOpmlLJ6WjemIAiC44IuLq2ow%3D%3D; CP_USERINFO=4Gkk4uas%2FGU6V4cAn8Kr14YtZHaRsQ3bb0iKxhYvuaLYLT-rPEFbvbaQzjvqSKm2v8Fd1lQ14weg0PM1aAxGqjzFStaNWwdXEhS3Zzs0jusNqPIZSkWIUHBpa7NyrsBUv2O8QVvh3O4yqW9wAjnfpw%3D%3D; china_variable=jpEe7N32pYz8SAjCjL8fnh2eLZiI1D/EC6dYmS6/lLUOPrHJGj-IxLIHbACvhNcaC9z3Z8pi2hy0JtYoQGGXmsutg32di8lhAZaSKKJ8BFBt-lJZl7B3R-LY1hWhKpza; lastlogindate=2024-05-16; lastlogintime="10:58:48"; lastloginip=211.161.157.200'
# response = requests.get(url,headers=headers)
# print(response.content.decode())

#方式三: requests.session
login_url = "https://passport.china.com/logon"
main_url = "https://passport.china.com"
session = requests.session()
#登录
session.post(login_url,data=params,headers=headers)
#访问其他页面,整个过程处于同一会话
response = session.get(main_url,headers=headers)
print(response.content.decode())

Ajax异步数据

背景:大对数网站采用前后端分离方式构建应用,数据都是通过Ajax请求异步加载获取的,直接请求目标网站的话,返回的内容中压根就没有任何数据

比如对于东方财富:https://quote.eastmoney.com/center

浏览器抓包分析,发现页面数据根本不在请求返回的html文件中,他的数据都是通过异步请求得到的

image-20240516111515843

对于这类型的网站,抓取数据的地址根本不是页面上的地址,因为html中压根就没有数据,我们要去分析其发送的AJAX请求,获取对应的URL,然后去抓取对应的数据

异步数据抓取案例

我们选取腾讯招聘网站作为爬取目标,我们可以发现它的网站职位数据就是异步加载的

image-20240516144559312

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
"""
url:https://careers.tencent.com/tencentcareer/api/post/Query
请求参数:
timestamp: 1715841296996
countryId:
cityId:
bgIds:
productId:
categoryId:
parentCategoryId:
attrId:
keyword:
pageIndex: 1
pageSize: 10
language: zh-cn
area: cn

"""
import requests
import time
base_url = "https://careers.tencent.com/tencentcareer/api/post/Query"
params={
"timestamp": 1715841296996,
"countryId":"",
"cityId":"",
"bgIds":"",
"productId":"",
"categoryId":"",
"parentCategoryId":"",
"attrId":"",
"keyword":"",
"pageIndex": 1,
"pageSize": 10,
"language": "zh-cn",
"area": "cn"
}
#分页抓取:
"""
可变的参数:
pageIndex以及timestamp
"""
for pageIndex in range(1,1000):
params["timestamp"] = int(time.time()*1000)
params["pageIndex"] = pageIndex
print("="*10,"current pageInde:{}".format(pageIndex),"="*10)
response = requests.get(url=base_url,params=params)
#转换为json格式
result_data = response.json()
job_count = result_data["Data"]["Count"]
if job_count==0:
break
job_list = result_data["Data"]["Posts"]
for job in job_list:
print(job["RecruitPostName"])
#数据后续可以根据具体业务需求,保存在本地文件或者数据库中

数据格式转换问题

有些网站,我们抓取时,响应返回的数据形式比较复杂,我们如何从中快速提取想要的数据

对于非结构化数据:HTML

  • 用正则表达式、xpath等手段处理

对于结构化数据:json、xml等

  • 直接转换为python类型

json模块

json.loads

  • 把json格式字符串解码转换成python对象,json数组对应列表、json对象对应字典
  • python中的None,在json中用null表示

json.dumps

  • 实现把python类型转换为json字符串,返回一个str对象,是把python对象编码成json字符串
  • 其序列化默认采用ascii编码
  • 添加参数 ensure_ascii=False禁用ascii编码,按utf-8编码

python json转换参考资料

jsonPath

正如XPath之于XML文档一样,JsonPath为Json文档提供了解析能力,通过使用JsonPath,你可以方便的查找节点、获取想要的数据,JsonPath是Json版的XPath

其用法基本跟xpath一致

参考资料

python jsonpath教程

案例:jsonpath进行数据提取

目标网站小饭桌,我们提取各个咨询的标题、发布作者、发布事件信息

image-20240516153253078

其响应数据格式如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"uid": 10918,
"title": "智愈医疗完成超6000万元Pre-A++轮融资",
"photo": "https://static-image.xfz.cn/1715658195_783.jpg",
"author": {
"photo": "https://static-image.xfz.cn/1552965456_492.jpg",
"authors": [
{
"author_id": 500,
"name": "饭桌君"
}
]
},
"is_original": false,
"article_type": "融资消息",
"intro": "由辰德资本领投,联想之星跟投,老股东顺为资本、礼来亚洲基金、众海投资持续加注。凡卓资本担任本轮融资独家财务顾问。",
"source": "",
"time": "2024-05-14 11:43:15",
"keywords": [
"智愈医疗"
]
}

我们需要从中解析出我们想要的数据

我们首先抓包,分析其数据异步加载的接口以及对应的请求参数,分析不同分页url和请求参数的规律

其请求的数据

image-20240516153515923

image-20240516153529052

python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"""
{
"uid": 10874,
"title": "与微软合作,这家独角兽让GPT对中国企业开放了",
"photo": "https://static-image.xfz.cn/1708917564_964.jpg",
"author": {
"photo": "https://static-image.xfz.cn/1693983197_842.jpg",
"authors": [
{
"author_id": 2295,
"name": "黄泽正"
}
]
},
"is_original": true,
"article_type": "热点",
"intro": "中国企业合法合规使用GPT的时代,终于来了。",
"source": "",
"time": "2024-02-26 11:19:24",
"keywords": [
"GPT",
"易点云",
"IT综合解決方案供应商"
]
}
"""
import requests
import jsonpath
import json

base_url = "https://www.xfz.cn/api/website/articles/"
params={
"p": 2,
"n": 20,
"type":""
}

for p in range(1,100):
params["p"]=p
response = requests.get(url=base_url,params=params)
response_json = response.json()
#提取感兴趣信息
#文章标题
title_list = jsonpath.jsonpath(response_json,"$..title")
#文章作者
author_list = jsonpath.jsonpath(response_json,"$..author..name")
#发布时间
time_list = jsonpath.jsonpath(response_json,"$..time")

for title,author,time in zip(title_list,author_list,time_list):
print(title,"\t",author,"\t",time)

视频抓取

B站视频抓取

资源抓取

B站视频的画面和音频是通过不同的url来异步请求获取,所以我们抓包分析得到画面和音频的请求链接,然后分别抓取画面和音频,最后在本地将两者合成视频文件

抓包分析,最开始的两个请求即为画面和音频资源请求

image-20240516182906785

视频合成

MoviePy是一个用于视频编辑的Python模块,它可以用于一些基本操作,比如剪切、拼接、插入标题、视频合成、视频处理和创建高级特效

官网教程

参考博客

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""
分析:
对于B站而言,其视频和音频内容是分开请求的,最终的视频是由两者一起合成的

"""
import requests

"""
注意:经过验证,如果不携带User-Agent和Referer这两个请求头,会直接返回403 forbidden
"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",
"Referer":"https://www.bilibili.com/video/BV1Mp421m7Ck/?spm_id_from=333.1007.tianma.1-1-1.click&vd_source=91a99cf2cdc8a7bec61c79b14c7a9800"
}
url1="https://cn-hbwh-fx-01-13.bilivideo.com/upgcxcode/03/12/1475871203/1475871203-1-100026.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M=&uipk=5&nbs=1&deadline=1715855097&gen=playurlv2&os=bcache&oi=1939666239&trid=00002988359d2ffb4c87ad295bbe92858e17u&mid=503787920&platform=pc&upsig=9252145cbb94964367b3213d88a75b0f&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,mid,platform&cdnid=3881&bvc=vod&nettype=0&orderid=0,3&buvid=FEF21EE4-4208-C79B-80F6-27AC2A9BBD3459139infoc&build=0&f=u_0_0&agrr=1&bw=173938&np=151388311&logo=80000000"
url2="https://cn-hbwh-fx-01-12.bilivideo.com/upgcxcode/03/12/1475871203/1475871203-1-30280.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M=&uipk=5&nbs=1&deadline=1715855097&gen=playurlv2&os=bcache&oi=1939666239&trid=00002988359d2ffb4c87ad295bbe92858e17u&mid=503787920&platform=pc&upsig=1d8cc071b206465e33d41d8e298308d9&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,mid,platform&cdnid=1237&bvc=vod&nettype=0&orderid=0,3&buvid=FEF21EE4-4208-C79B-80F6-27AC2A9BBD3459139infoc&build=0&f=u_0_0&agrr=1&bw=20005&np=151388311&logo=80000000"

response1 = requests.get(url1,headers=headers)
response2 = requests.get(url2,headers=headers)
with open("b站1.mp4","wb") as f:
f.write(response1.content)
with open("b站2.mp4","wb") as f:
f.write(response2.content)

#合成视频:画面+音频
from moviepy.editor import ffmpeg_tools
ffmpeg_tools.ffmpeg_merge_video_audio("b站1.mp4","b站2.mp4","b站.mp4")

抖音视频抓取

我们抓包分析,抖音各个视频的链接都是杂乱无规则的,所以无法用requests批量抓取,如果要批量抓取,只能模拟人来不断点击获取,这个就必须要用到自动化工具selenium了

image-20240516184053032

对于单个视频的下载比较简单,无任何其他的限制,直接发送请求即可获取

1
2
3
4
5
6
7
8
import requests

url="https://v3-weba.douyinvod.com/02263c98b4f10164f58047cc60b11827/6645f1ed/video/tos/cn/tos-cn-ve-15/osB7nDAWI4MQuoB8ECDoeGezOhEs4BbeV2AqM1/?a=6383&ch=5&cr=3&dr=0&lr=all&cd=0%7C0%7C0%7C3&cv=1&br=1199&bt=1199&cs=0&ds=3&ft=kmz2Nvx2llUpoDtWsRdusEBOG4kbZ0Fl~z7avjZmpmPt188HKw9gHlcApW9w6x&mime_type=video_mp4&qs=0&rc=aDk6Omc7ZjY7OTszN2c7Z0BpM3hnNGQ6Zmd4czMzNGkzM0BiLmBiXjZgX14xX2I0NTQ1YSMvYm1ycjRfMy5gLS1kLTBzcw%3D%3D&btag=c0000e00030000&cquery=101s_100B_100x_100z_100a&dy_q=1715855997&feature_id=f0150a16a324336cda5d6dd0b69ed299&l=20240516183955A37ACE947DDB97002DEE"

response = requests.get(url)

with open("抖音.mp4","wb") as f:
f.write(response.content)

单个视频url无任何规律,所以无法通过代码去批量构造,批量爬取的核心就在于如何批量获取视频的url列表

selenium的使用

selenium是一个Web的自动化测试工具,最初是为了网站自动化测试而开发的,其可以按照指定的指令自动操作,其可以直接运行在浏览器之上,它支持所有主流的浏览器,模拟人去手动操作浏览器

背景

我们以领导留言板这个网站为例,抓取里面的内容,通过抓包获取url,请求参数以及请求等信息,但还是会被拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

url = "https://liuyan.people.com.cn/v1/threads/list/df"

params={
"appCode": "PC42ce3bfa4980a9",
"token": "",
"signature": "315b4bdafbf17b3dbb6a48a244b67dff",
"param": "{\"fid\":\"571\",\"showUnAnswer\":1,\"typeId\":5,\"lastItem\":\"\",\"position\":\"0\",\"rows\":10,\"orderType\":2}"
}

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",
"Referer": """https://liuyan.people.com.cn/threads/list?checkStatus=0&fid=571&formName=%E6%B9%96%E5%8C%97%E7%9C%81%E5%A7%94%E4%B9%A6%E8%AE%B0%E7%8E%8B%E8%92%99%E5%BE%BD&position=0&province=28&city=&saveLocation=28&pForumNames=%E6%B9%96%E5%8C%97%E7%9C%81"""
,"Cookie":"__jsluid_s=4b575e53f2419a15d98413b3c2ade788; Hm_lvt_40ee6cb2aa47857d8ece9594220140f1=1715856547; language=zh-CN; deviceId=52e3892d-030a-4de8-8d77-c62c7c0a1a87; Hm_lpvt_40ee6cb2aa47857d8ece9594220140f1=1715856654"
}


response = requests.get(url=url,headers=headers,data=params)
print(response.content.decode())

响应内容:

其网站内部设置了防护和反扒机制,单纯靠requests是无法处理的,所以这是我们就需要借助于Selenium,模拟人类登录网站的行为,来进行数据的抓取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
C:\environment\anaconda3\envs\scrap\python.exe "C:\bang\MyOwn\Java重要知识点学习\BaiduSyncdisk\07 python网络爬虫\my_code\ch04-selenium\领导留言板.py" 
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body{ background:#eff1f0; font-family: microsoft yahei; color:#969696; font-size:12px;}
.online-desc-con { text-align:center; }
.r-tip01 { color: #969696; font-size: 16px; display: block; text-align: center; width: 600px; padding: 0 10px; overflow: hidden; text-overflow: ellipsis; margin: 0 auto 15px; }
.r-tip02 { color: #b1b0b0; font-size: 12px; display: block; margin-top: 20px; margin-bottom: 20px; }
.r-tip02 a:visited { text-decoration: underline; color: #0088CC; }
.r-tip02 a:link { text-decoration: underline; color: #0088CC; }
img { border: 0; }
</style>
</head>
<body>
<div class="online-desc-con" style="width:650px;padding-top:15px;margin:34px auto;">
<a id="official_site" href="http://www.365cyd.com" target="_blank">
<img id="wafblock" alt="" style="margin: 0 auto 17px auto;" />
</a>
<span class="r-tip01" id="error_403"></span>
<span class="r-tip01" id="error_403_en"></span>
<span class="r-tip02">如果您是网站管理员<a href="http://help.365cyd.com/cyd-error-help.html?code=403" target="_blank">点击这里</a>查看详情</span>
<hr/>
<center>client: <span id="client_ip"></span>, server: e85ed68, time: <span id="time_error"></span><span id="rule_id"></span></center>
<img alt="" src="/cdn-cgi/image/logo.png" />
</div>
<script>
void(function fuckie6(){if(location.hash && /MSIE 6/.test(navigator.userAgent) && !/jsl_sec/.test(location.href)){location.href = location.href.split('#')[0] + '&jsl_sec' + location.hash}})();
var data = {"error_403_type":"cli_ip","error_403":"您的IP( 218.106.117.241 )最近有可疑的攻击行为,请稍后重试.","client_ip":"218.106.117.241","time_error":"16\/May\/2024:19:01:37 +0800","error_403_en":""};
var rule_id = parseInt(data['rule_id']) || '';
if (rule_id != '') {
rule_id = '[' + rule_id + ']';
}
document.getElementById("wafblock").src = '/cdn-cgi/image/' + (data['error_403_type'] || 'hacker') + '.png';
document.getElementById('error_403').innerText = data['error_403'] || '当前访问疑似黑客攻击,已被创宇盾拦截。';
document.getElementById('error_403_en').innerText = data['error_403_en'];
document.getElementById('client_ip').innerText = data['client_ip'];
document.getElementById('time_error').innerText = data['time_error'];
document.getElementById('rule_id').innerText = rule_id;
</script>
</body>
</html>


Process finished with exit code 0

使用Selenium的一般流程

  1. selenium打开浏览器
  2. 访问要爬取的页面,如果有需要可以设置对页面进行一些操作
  3. 获取想要爬取的页面数据

selenium环境安装

selenium安装

1
pip install selenium

浏览器对应的驱动chromedriver的下载

chromeDriver下载参考资料

chromeDriver参考资料

ChromeDriver下载地址列表

安装之后可以在代码中指定charomeDriver的地址或者我们可以直接将chromeDriver放在python的安装文件夹中,更推荐后者

测试是否安装成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from selenium import webdriver
import requests
import time

url="https://www.baidu.com"
driver = webdriver.Chrome()
#请求
driver.get(url)
#短暂延时
time.sleep(1)
#获取页面内容
html = driver.page_source
print(html)
#关闭
driver.quit()

会自动弹出百度首页

selenium基本用法

Python selenium库官方文档

启动浏览器

1
2
from selenium import webdriver
driver = webdriver.Chrome()

访问页面

1
driver.get(url)

窗口最大化

1
driver.maximize_window()

刷新页面

1
driver.refresh()

获取页面源码

1
html = driver.page_source

页面截图

1
driver.save_screenshot("xxx.png")

关闭浏览器

1
driver.quit()

注意:

使用Selenium时,如果关闭的时候没有使用driver.quit退出,会导致系统中会启动多个chromeDriver程序,可以在命令行中通过如下命令批量删除chromedriver进程

1
taskkill /F /im chromedriver.exe

Selenium driver对象的相关属性

  • current_url:当前url地址
  • title:页面标题
  • page_source:页面html源码,注意这个页面源码与requests获取的源码是不一样的,其是所有js执行之后的完整的源码
  • window_handler:获取浏览器上所有窗口句柄,窗口句柄即各个窗口id
  • curren_window_handler:获取当前窗口的句柄

selenium元素定位和等待

元素定位的方法:

selenium查找元素官网教程

一般我们知道如何通过xpath来定位元素就够了

案例:打开百度首页,在输入框中输入值进行搜索

首先打开百度首页,定位带搜索输入框元素,向输入框中输入值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
#打开浏览器
driver = webdriver.Chrome()
#url
url = "https://www.baidu.com"
#发送请求
driver.get(url)
driver.maximize_window()
#定位到搜索框
element = driver.find_element(By.XPATH,'//*[@id="kw"]')
#输入内容,发送请求
element.send_keys("三国演义")
#关闭浏览器
time.sleep(3)
driver.quit()

image-20240516202419933

元素的属性和操作

元素属性
  • tagname:获取标签名
  • text:获取标签文本
  • parent:获取父级标签
  • get_attribute():获取属性
  • s_displayed():判断元素是否可见
元素的操作
  • click(),点击元素
  • send_keys():输入内容
  • clear():清空表单

selenium模拟登录案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"""
需求分析:
selenium模拟登录中华网:https://passport.china.com/logon
"""
from selenium import webdriver
from selenium.webdriver.common.by import By
import time

params={
"userName": "17775990925",
"password": "a546245426"
}
#打开浏览器
driver = webdriver.Chrome()
#发送请求
url = "https://passport.china.com/logon"
driver.get(url)
driver.maximize_window()

#定位登录的表单元素
#用户名元素
userNameElement = driver.find_element(By.XPATH,'//*[@id="userInput"]/input[@name="userName"]')
#密码元素
passWordElement = driver.find_element(By.XPATH,'//*[@id="55"]')
#登录按钮
loginButtonElement = driver.find_element(By.XPATH,'//*[@id="index_logonid"]')
#填入信息
userNameElement.send_keys(params["userName"])
passWordElement.send_keys(params["password"])
#点击登录按钮
loginButtonElement.click()

#关闭浏览器
time.sleep(5)
driver.quit()

selenium爬取数据案例

需求分析

利用selenium爬取领导留言板数据

主要抓取各个留言的:

  • 标题
  • 正文内容
  • 时间
  • 留言ID

image-20240516205322272

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
"""
selenium访问指定页面,并对页面解析,获取对应数据
"""
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
from scrapy import selector

url='https://liuyan.people.com.cn/threads/list?checkStatus=0&fid=571&formName=%E6%B9%96%E5%8C%97%E7%9C%81%E5%A7%94%E4%B9%A6%E8%AE%B0%E7%8E%8B%E8%92%99%E5%BE%BD&position=0&province=28&city=&saveLocation=28&pForumNames=%E6%B9%96%E5%8C%97%E7%9C%81'

#打开浏览器
driver = webdriver.Chrome()
#访问页面
driver.get(url)
driver.maximize_window()
#等待
time.sleep(5)
#利用selenium定位元素
titleElementList = driver.find_elements(By.XPATH,'//h1')
textElementList = driver.find_elements(By.XPATH,'//p/span')
timeElementList = driver.find_elements(By.XPATH,'//div[@class="headMainS fl"]/p')
IdElementList = driver.find_elements(By.XPATH,'//span[contains(text(),"留言ID")]')
for titleElement,timeElement,IdElement,textElemen in zip(titleElementList,timeElementList,IdElementList,textElementList):
print(timeElement.text,"\t",titleElement.text,"\t",IdElement.text)
print(textElemen.text)
print("="*20)
#获取页面加载后的源码
# html = driver.page_source
# print(html)

"""
页面元素分析,如何利用xpath进行解析:
标题: //h1/text()
正文: //p/span/text()
时间: //div[@class="headMainS fl"]/p/text()
留言ID://span[contains(text(),"留言ID")]/text()
"""

#关闭浏览器
time.sleep(3)
driver.quit()

#解析
# title_list = selector.Selector(text=html).xpath("//h1/text()").extract()
# time_list = selector.Selector(text=html).xpath('//div[@class="headMainS fl"]/p/text()').extract()
# ID_list = selector.Selector(text=html).xpath('//span[contains(text(),"留言ID")]/text()').extract()
# for title,time,ID in zip(title_list,time_list,ID_list):
# print(title+"\t"+time+"\t"+ID)

目前存在的问题

  • 正文内容抓取不全,如下图所示,每一条帖子需要点击查看全文才能显示完全

    • image-20240516212857275
  • 内容抓取不全,目前只能抓取第一页数据,需要手动点击查看更多才能获取下一页记录

    • image-20240516212942046

selenium进阶使用

等待机制

官网 等待页面加载完成文档

背景

现在大多数网页都是动态加载的,如果页面内容发生变更,就需要时间来进行渲染,代码是自动执行的,有可能在执行的时候页面新的元素还没有加载出来,就查不到,报 no such element的错误,如果报这个错误,则有可能是定位表达式不正确,或者也可能是页面元素已经发生了变化

我们爬虫的过程中,如果打开新的页面、页面跳转等情况都要添加合适的等待,否则有的情况下会导致程序错误,无法进行下去

等待的三种方式

  • 强制等待
    • time.sleep(n)
  • 隐式等待
    • driver.implicitly_wait(n)
  • 显示等待(需要等待条件满足)
    • 隐式等待某些情况找不到,就要用显示等待

强制等待

time.sleep()

不管网页有没有加载完成,都会等待指定时间

隐式等待

driver.implicitly_wait(n)

如果某些元素不是立即可用的,隐式等待是告诉WebDriver去等待一定的时间后去查找元素。 默认等待时间是0秒

在n秒内如果还是找不到,则会抛出异常,在指定时间内,每隔一段时间就去看一下,如果成功找到,则程序会继续往下执行,如果没找到则继续等,直到最大等待时间

显示等待

可以设置等待条件,比如等待到某个元素可见、某个元素可点击

这种情况比较复杂,我们重点关注这种情况下如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

#第一步:创建等待对象
wait = WebDriverWait(driver, 10) #WebDriverWait 三个参数,第一个为driver对象,第二个为最大时间,第三个为间隔时间,比如:WebDriverWait(driver,30,0.2),即在30s内每个0.2s去找一次
#第二步:定义元素查找对象
located = (By.XPATH,"//input[@id='u'")
#第三步:定位的等待条件,比如指定元素可见为等待条件
conditions = EC.visibility_of_element_located(located)
#第四步:通过等待计时器对象去查找
wait.until(conditions)

#一行代码表示
WebDriverWait(driver, 10)
.until(EC.visibility_of_element_located((By.XPATH,"//input[@id='u'")))
常用显示等待条件
  • visibility_of_element_located:元素可见
  • element_to_be_clickable:元素可点击
等待条件
  • presence_of_element_located:元素存在
  • visibility_of_element_located:元素可见
  • element_to_be_clickable:元素可点击
  • title_contains:标题包含某内容
  • new_window_is_opened:等待新窗口出现
  • frame_to_be_available_and_switch_to_it:加载并切换到iframe中
  • alert_is_present:出现Alert弹框

iframe切换

背景

对于QQ邮箱登录页面,其内部是一个网页嵌套另一个网页构成的,如果直接查找或者点击里面的某些元素是找不到的,必须要进行iframe切换

image-20240517091621440

selenium控制iframe切换

1
2
3
4
5
6
7
8
9
10
11
12
13
#方式一:切换iframe,通过iframe的名字(name属性)进行切换
driver.switch_to.frame('frame_name')
#方式二:通过element元素去切换
iframeElement = driver.find_element_by_xpath('//iframe[@id="login_frame"]')
driver.switch_to.frame(iframeElement)
#方式三:通过索引切换
driver.switch_to.frame(1) #根据第几个iframe切换

#切换回默认的html页面
driver.switch_to.default_content()

#切换到父级的iframe中
driver.switch_to.parent_frame()

案例

模拟QQ邮箱登录页面点击密码登录,然后输入账号密码模拟登陆

image-20240517092226636

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from selenium import webdriver
from selenium.webdriver.common.by import By
import time

driver = webdriver.Chrome()
#url
url = "https://wx.mail.qq.com/"
driver.get(url=url)
driver.maximize_window()

#切换进入第一个iframe
driver.switch_to.frame(1)
#切换进入第二个iframe
iframeElement = driver.find_element(By.XPATH,'//iframe[@id="ptlogin_iframe"]')
driver.switch_to.frame(iframeElement)

#查找密码登录按钮,并点击
passwordLoginButton = driver.find_element(By.XPATH,'//*[@id="switcher_plogin"]')
passwordLoginButton.click()

#找到账号和密码输入框,模拟登录
driver.find_element(By.XPATH,'//*[@id="u"]').send_keys("wujjjj")
driver.find_element(By.XPATH,'//*[@id="p"]').send_keys("99899")
time.sleep(1)
#登录按钮
driver.find_element(By.XPATH,'//*[@id="login_button"]').click()
#关闭浏览器
time.sleep(10)
driver.quit()

执行结果

最终还会弹出

image-20240517094617205

窗口滚动

背景

有的时候,元素虽然被加载出来了,但是如果看不到,是无法进行操作的,这时就需要滚动页面才会显示新的内容

比如对于我们之前爬取的领导留言板网站,就需要手动滚动页面到最底部,然后点击查看更多,才能分页查看下一页数据

所以要想抓取数据完全,需要不断查找查看更多按钮,然后点击进行翻页

image-20240517111858981

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import time
import random
from selenium import webdriver
from selenium.webdriver.common.by import By

url = "https://liuyan.people.com.cn/threads/list?checkStatus=0&fid=1179&formName=%E5%AE%9C%E6%98%8C%E5%B8%82%E5%A7%94%E4%B9%A6%E8%AE%B0%E7%86%8A%E5%BE%81%E5%AE%87&position=0&province=28&city=230&saveLocation=28&pForumNames=%E6%B9%96%E5%8C%97%E7%9C%81&pForumNames=%E5%AE%9C%E6%98%8C%E5%B8%82"
driver = webdriver.Chrome()
#隐式等待
driver.implicitly_wait(10)
driver.get(url)
driver.maximize_window()
while True: #循环迭代 找到查看更多,并翻页,无法确定有多少也,只能死循环,找不到即触发异常结束
try:
# 找到 查看更多按钮
element = driver.find_element(By.XPATH, '//*[@class="mordList"]')
print(element)
time.sleep(random.randint(1,3))
element.location_once_scrolled_into_view # 页面滚动到 查看更多按钮可见的位置
# 点击查看更多按钮
time.sleep(random.randint(1,3))
element.click()
except:
break
#整体页面解析
#利用selenium定位元素
titleElementList = driver.find_elements(By.XPATH,'//h1')
textElementList = driver.find_elements(By.XPATH,'//p/span')
timeElementList = driver.find_elements(By.XPATH,'//div[@class="headMainS fl"]/p')
IdElementList = driver.find_elements(By.XPATH,'//span[contains(text(),"留言ID")]')

for titleElement,timeElement,IdElement,textElemen in zip(titleElementList,timeElementList,IdElementList,textElementList):
print(timeElement.text,"\t",titleElement.text,"\t",IdElement.text)
print(textElemen.text)
print("="*20)
print("总条数:{}".format(len(titleElementList)))
time.sleep(5)
driver.quit()

JS模拟滚动页面

js滚动窗口到指定坐标位置

  • scrollTo():可把内容滚动到指定的坐标

    • ```js
      scrollTo(x,y)

      参数说明

      x :必须,要在窗口文档显示区左上角显示的文档的x坐标
      y : 必须,要在窗口文档显示区左上角显示的文档的y坐标
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17

      **selenium如何执行js代码**

      ```python
      #移动Y坐标到500的位置
      js = "window.scrollTo(0,500)"
      driver.execute_script(js)

      #滚动到窗口底板
      js = "window.scrollTo(0,document.body.scrollHeight)"
      driver.execute_script(js)

      #相对滚动
      #scrollBy(x1,x2)表示相对当前位置(x,y),页面下一步滚动到 x+x1,y+y1
      #有的时候,突然滚动到底部,可能会被反爬机制识别,这是用相对滑动,模拟人的操作
      js = "window.scrollBy(0,100)"
      driver.execute_script(js)

鼠标操作

比如:有的页面需要模拟鼠标操作,对页面元素进行拖拽操作,比如滑块验证码之类的

1
from selenium.webdriver import ActionChains

ActionChains:鼠标操作类

  • click:鼠标点击
  • double_click:鼠标双击
  • context_click:鼠标右击
  • move_to_element:鼠标移动到某个节点
  • click_and_hold:鼠标左键按下鼠标
  • move_by_offse:鼠标相对当前位置进行移动
  • drag_and_drop():在一个位置按下鼠标,到另一个位置进行释放
  • release:释放鼠标
  • perform:执行动作

鼠标操作练习网站

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from selenium.webdriver import ActionChains
from selenium import webdriver
from selenium.webdriver.common.by import By
import time

"""
ActionChains:鼠标操作类

- click:鼠标点击
- double_click:鼠标双击
- context_click:鼠标右击
- move_to_element:鼠标移动到某个节点
- click_and_hold:鼠标左键按下鼠标
- move_by_offse:鼠标相对当前位置进行移动
- drag_and_drop():在一个位置按下鼠标,到另一个位置进行释放
- release:释放鼠标
- perform:执行动作

"""
driver = webdriver.Chrome()
driver.implicitly_wait(10)
driver.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')
driver.maximize_window()

#切换到子iframe
driver.switch_to.frame(driver.find_element(By.XPATH,'//*[@id="iframeResult"]'))

#找到拖拽块元素
dragElement = driver.find_element(By.XPATH,'//*[@id="draggable"]')

#实例化 ActionChains对象
ac = ActionChains(driver)
#将鼠标移动到该元素上
ac.move_to_element(dragElement)
#按住鼠标不放
ac.click_and_hold()
#将鼠标移动到另一个元素
ac.move_to_element(driver.find_element(By.XPATH,'//*[@id="droppable"]'))
#执行鼠标动作
ac.perform()

#关闭浏览器
time.sleep(5)
driver.quit()

执行结果

image-20240517184401515

selenium携带cookie登录

很多网站需要登录才能访问其他页面,这种情况我们需要首先需要利用selenium模拟登录获取网站的cookie信息,然后在访问其他网站的时候模拟

参考资料

selenium特征检测

参考资料 js文件下载地址

selenium启动的浏览器可能具有以下特殊的特征

  • User-Agent字符串
    • Selenium启动的浏览器通常具有特定的User-Agent字符串,可以通过检查User-Agent来判断是否为Selenium启动的浏览器
  • 自动化工具标识
    • Selenium启动的浏览器可能会在请求头中包含一些自动化工具的标识,例如Requested-WithDNT
  • WebDriver相关属性
    • Selenium启动的浏览器可能会在全局window对象中注入一些特殊的属性,例如webdrivernavigator.webdriver
  • 页面加载行为
    • Selenium启动的浏览器通常会以自动化的方式加载页面,可能会出现一些快速点击、输入文本等行为
  • 元素检测
    • Selenium启动浏览器的时候会在DOM中插入一些特定的元素或者属性,用于控制浏览器行为,可以通过检测这些特定的元素或属性来判断是否为Selenium启动的浏览器

如果想使用selenium正常访问,则需要在创建webdriver时隐藏浏览器相关的特征

  • --disable-infobars:禁止显示Chrome浏览器正在受到自动测试软件控制的通知栏
  • excludeSwitchesenable-automation:排除启动自动化扩展程序的开关,可以防止被网站检测到使用了自动化测试工具,减少被反爬虫封锁的可能
  • useAutomationExtension:False:禁用自动化扩展程序,同样为了避免被网站检测到使用了自动化工具
  • 通过Page.addScriptToEvaluateOnNewDocument方法,可以在每次页面加载时执行指定的JavaScript代码,我们每次打开新页面之前,执行hide.js来隐藏selenium启动浏览器生成的属性,从而防止被检测出来时爬虫

绕过特征检测案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from selenium import webdriver
import time

"""
目标网站: https://www.aqistudy.cn/ 对selenium做了反爬
如果用selenium打开,是不会有数据显示的
"""

#设置浏览器配置,隐藏selenium一些特性
options = webdriver.ChromeOptions()
#添加防检测的参数
options.add_argument("--disable-infobars")
options.add_experimental_option("excludeSwitches",["enable-automation"])
options.add_experimental_option("useAutomationExtension",False)
#添加浏览器头
options.add_argument('user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36')

driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)

#每次打开新的页面之前,执行该脚本,去除selenium打开浏览器生成的相关属性特征
with open("../stealth.min.js") as f:
js = f.read()
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument",{
"source": js
})
url = 'https://www.aqistudy.cn/'
driver.get(url)
driver.maximize_window()

#关闭
time.sleep(20)
driver.quit()

JS逆向分析

js参数加密案例

许多网站的请求参数都是加密传递的,要模拟发送请求,就得直到参数是怎么加密的,然后用代码实现加密的过程

比如对于一些网站的登录页面,其账号、密码传输时是通过某种加密算法加密后再发送请求的

需求分析

对于 网站的登录界面,

image-20240517194933379

我们输入账号、密码模拟登录,抓包分析,发现其密码是加密后的数据

image-20240517195033515

找到调用的js文件

image-20240517202540845

我们班通过打断点分析,定位带其js代码中加密的函数,实际上是一个 盐值拼接,然后采用md5加密得到

image-20240517202750673

image-20240517202817522

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
import hashlib
#md5加密
def hashPwd(password):
#盐值
saltValue = "Hq44cyp4mT9Fh5eNrZ67bjifidFhW%fb0ICjx#6gE59@P@Hr8%!WuYBa1yvytq$qh1FEM18qA8Hp9m3VLux9luIYpeYzA2l2W3Z"
#加密
strVal = password+saltValue

hashObj = hashlib.md5()
hashObj.update(strVal.encode("utf-8"))
md5StrVal = hashObj.hexdigest()
return md5StrVal

当然,如果我们采用selenium模拟登录,无需直到其加密算法,直接在输入框中输入明文,即可模拟登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

url = "https://www.94mxd.com.cn/signin"
params={
"email": "1985439923@qq.com",
"password": "wu123456"
}
#模拟登录
driver = webdriver.Chrome()
driver.get(url)
driver.maximize_window()

#定位元素输入值
driver.find_element(By.XPATH,'//*[@id="email"]').send_keys(params["email"])
driver.find_element(By.XPATH,'//*[@id="password"]').send_keys(params["password"])
time.sleep(1)
#点击登录按钮
driver.find_element(By.XPATH,'//*[@id="container"]/div/div/div[2]/div[2]//button').click()
#关闭浏览器
time.sleep(5)
driver.quit()

调用js加密代码

在逆向分析时,有时候会发现某个js加密算法会比较繁琐,用python还原同样的算法会比较费劲,此时,我们可以不必使用python去还原,而是利用python去直接调用JavaScript中定义的功能,通过调式分析加密算法所在的位置和源码,然后直接在代码中调用该js代码

python调用js,需要借助于第三方库pyexcejs,这个库底层调用node.js,所以还需要安装node.js

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"""
python调用js用第三方库:
需要安装python第三方库:pyexecjs
需要安装: node.js
"""
import execjs
jsCode="""
function add(a,b){
return a+b
}

function say(){
return "say,hello!!!"
}
"""
#第一步:编译代码
JS = execjs.compile(jsCode)
#第二部调用
result = JS.call("add",100,200)
message = JS.call("say")

print(result)
print(message)

验证码破解

图像验证码破解

许多网站,在登陆的时候,除了输入账号、密码之外,还会显示一个图像验证码,那么这类网站应该如何破解?

对于 超级鹰:网站登陆页面,如下图所示:其会显示一个图像验证码,要想破解这类网站,就需要能够识别图像验证码中的字符

image-20240518200110879

抓包分析,一打开登陆界面,其会自动向后台发送一个请求,获取一个图像验证码,且该同一链接地址多次请求,返回的结果均是不同的

image-20240518200524218

对于其点击登陆按钮发送的登陆请求,抓包如下图所示:

image-20240518200840384

综上所述,该问题的难点就在于验证码的识别问题

破解步骤:

  • 发送请求获取验证码

    • 这个问题比较棘手,因为发现,同一个图片地址,每次请求时,其返回的图片都是不一样的
    • 那么他是如何将验证码与登陆关联起来的呢?通过同一会话session关联起来的
  • 识别验证码内容

    • ```python
      “””
      图像验证码: 需要获取验证码图片,然后识别图片里面的文本内容,这个就需要用到ocr技术

      python中ocr识别库: ddddocr
      该库是免费的,但是精度不是很高,有的时候会识别错误

      如果精度要求比较高,可以找一些第三方收费服务:百度直接搜:打码平台 一般会支持各种类型的验证码识别
      “””
      import ddddocr

      创建ocr对象

      ocr = ddddocr.DdddOcr()

      加载图片

      with open(“1.png”,”rb”) as f:

      image = f.read()
      

      识别图片内容

      code = ocr.classification(image)
      print(“识别结果:”,code)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45

      - 发送登陆请求

      **破解超级鹰网站登陆页面代码示例**

      ```python
      import requests
      import ddddocr

      """
      流程:
      ①请求网站,获取图像验证码图片
      ②调用第三方库识别图像验证码文本
      ③构造请求,发送登陆请求

      注意:这整个过程需在同一会话session中进行
      """
      session = requests.session()

      #获取图形验证码
      codeUrl = "https://www.chaojiying.com/include/code/code.php?u=1"
      codeImage = session.get(url=codeUrl).content
      with open("code.png","wb") as f:
      f.write(codeImage)
      #调用第三方库识别图像验证码
      ocr = ddddocr.DdddOcr()
      code = ocr.classification(codeImage)
      print("验证码:",code)
      #构造登陆请求
      loginUrl = "https://www.chaojiying.com/user/login/"
      params={
      "user": "HustRich",
      "pass": "wu123456",
      "imgtxt": code,
      "act": 1
      }
      headers={
      "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
      # ,"Cookie":"PHPSESSID=c2s08og1j8244us3qc4ub9d217; __51cke__=; __tins__16851773=%7B%22sid%22%3A%201716036582790%2C%20%22vd%22%3A%203%2C%20%22expires%22%3A%201716039030500%7D; __51laig__=4"
      }
      response = session.post(url=loginUrl,data=params,headers=headers)
      print(response.content)

      with open("res.html","wb") as f:
      f.write(response.content)

js逆向:滑动验证码的轨迹破解

案例网站:去哪儿

image-20240518100404382

需求分析

我们输入手机号,然后点击获取验证码,会弹出一个滑动验证码窗口,经过验证之后才会收到验证码,然后点击登陆注册,此过程向后端的请求包括:

  • 滑动验证码的请求
  • 获取手机验证码的请求
  • 登陆接口的请求

滑动验证码请求

只有将滑动验证码拖动到最右端才能成功发送该请求

image-20240518100928636

image-20240518100848451

观察滑动验证码请求参数,发现其中有一个未知的参数 data

手机验证码请求

image-20240518100959013

image-20240518101033801

登陆接口请求

验证码破解相关知识等后续有需求再了解

异步爬虫

当我们有大量数据需要抓取时,总的抓取时间会比较长,这个时候,如果需要提高数据的抓取效率,需要用到多线程异步并发爬取

该方法缺陷,并发会导致短时间对目标网站服务器大量请求,使得目标网站压力过大,如果被检测,可能触发一系列的反爬机制

该方法一般需要配合代理IP去使用,每次请求从不同ip发出,这样即使访问很频繁,由于请求来自于不同ip,被检测概率会比较低

抓取需求

目标网站:https://www.myfreemp3.com.cn/

image-20240518104553106

image-20240518105026649

image-20240518105052266

我们的目标是抓取该网站上周杰伦所有歌曲

  • 要求:下载每一首歌曲的封面图、mp3文件,歌词,然后保存在本地,要求保存的格式如下:

    • image-20240518105221295
  • 子目录名为各个歌曲名,里面包含mp3文件、封面图png,歌词文本txt文件

思路分析

实现思路

  • 先抓取搜索歌手所有的歌曲,提取歌曲名、歌手、封面图、歌词信息
  • 然后再去下载歌曲、封面、歌词保存为文件

获取歌曲列表接口

抓包分析:

image-20240518110007948

image-20240518110055152

image-20240518110125909

image-20240518110235118

歌曲列表接口:

url地址:

请求方法:

  • POST

参数:

  • input: 歌手名
  • filter: name
  • page: 页码
  • type: netease

返回响应:

image-20240518110437045

单线程下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
"""
分析:
抓取网站:https://www.myfreemp3.com.cn/ 中指定歌手的歌曲:
下载数据包括: 歌曲封面图片、歌曲歌词文本文件,歌曲mp3资源文件
文件存储格式:
歌手名:
歌曲名:
歌曲名_作者.png
歌曲名_作者.txt
歌曲名_作者.mp3
"""
import requests
import os

class CrawMusic:
def __init__(self,baseDir):
self.url = "https://www.myfreemp3.com.cn"
self.headers={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
,"X-Requested-With":"XMLHttpRequest"
}
self.params={
"input": "周杰伦",
"filter": "name",
"page": 1,
"type": "netease"
}
self.baseDir = baseDir

def sendRequest(self,name,pageIndex):
"""

:param name: 歌手姓名
:param pageIndex: 当前页码
:return:
"""
self.params["name"]=name
self.params["page"]=pageIndex
response = requests.post(url=self.url, data=self.params, headers=self.headers)

data = response.json()["data"]["list"]
curSaveDir = os.path.join(self.baseDir,name)
if not os.path.exists(curSaveDir):
os.makedirs(curSaveDir)
self.parseData(curSaveDir,data)

def parseData(self,curSaveDir,data):
"""

:param curSaveDir: 该歌手数据存储根目录
:param data: 需要被解析的数据
:return:
"""
for item in data:
# 歌词标题
title = item["title"]
# 作者
author = item["author"]
# 封面图片链接
pic = item["pic"]
# 歌词文本
tex = item["lrc"]
# 歌词下载链接
mp3Link = item["url"]

#当前歌曲数据存储路径
curMusicSaveDir = os.path.join(curSaveDir, title)
if not os.path.exists(curMusicSaveDir):
os.mkdir(curMusicSaveDir)
print("benign dowmload {}".format(title))
# 图片
curPicSavePath = os.path.join(curMusicSaveDir, title + ".png")
self.downloadFile(pic, curPicSavePath)
# 歌词
curTexSavePath = os.path.join(curMusicSaveDir, title + ".txt")
with open(curTexSavePath, "w") as f:
f.write(tex)
# MP3
curMp3SavePath = os.path.join(curMusicSaveDir, title + ".mp3")
self.downloadFile(mp3Link, curMp3SavePath)
else:
print("歌曲{}已被下载,无需重复下载".format(title))

def downloadFile(self,url, path):
response = requests.get(url)
if response.status_code == 200:
with open(path, "wb") as f:
f.write(response.content)
else:
print(url+" 下载失败")

if __name__=="__main__":
crawDownload = CrawMusic(r"E:\tempData\musics")
crawDownload.sendRequest("周杰伦",1)

异步下载

上面代码是单线程下载,而且只是下载单页数据,当需要下载大量数据时,就必须采用多线程异步下载

整个爬虫其实主要分为以下三个模块:

  • 发送请求获取响应
  • 数据解析
  • 数据存储
  • 如下图所示,对于这三个模块我们可以进行异步处理,模块一线程请求后的数据进入一个管道,模块二线程从管道中拿取数据进行解析,然后解析后的结果放入另一个管道,模块三线程从管道二中拿取解析后的数据进行存储,这个过程,三个模块相互独立,如果哪个部分耗时较多,可以采用多线程进行处理

image-20240518182355861

多线程代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""
分析:
抓取网站:https://www.myfreemp3.com.cn/ 中指定歌手的歌曲:
下载数据包括: 歌曲封面图片、歌曲歌词文本文件,歌曲mp3资源文件
文件存储格式:
歌手名:
歌曲名:
歌曲名_作者.png
歌曲名_作者.txt
歌曲名_作者.mp3
并发下载:
请求、数据解析、数据存储三个部分用多个不同线程处理
各个线程之间用 queue.Queue来进行数据传递,该对象是线程安全的
queue的用法参考:https://blog.csdn.net/songpeiying/article/details/131761990
queue = queue.Queue()
queue.put() 添加数据 使用put()方法将元素放入队列。如果队列已满,put()方法会阻塞直到有空余空间
queue.get() 获取数据 使用get()方法从队列中取出元素。如果队列为空,get()方法会阻塞直到有元素可用

queue.get(timeout=xx) 等待xx秒如果还未获取数据则会报错,如果不添加timeout参数则会一直阻塞
"""
import queue
from concurrent.futures.thread import ThreadPoolExecutor
import requests
import os

class CrawMusic:
def __init__(self,baseDir):
self.url = "https://www.myfreemp3.com.cn"
self.headers={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
,"X-Requested-With":"XMLHttpRequest"
}
self.params={
"input": "周杰伦",
"filter": "name",
"page": 1,
"type": "netease"
}
self.baseDir = baseDir
#响应数据类管道
self.responseDataQueue = queue.Queue()
#解析数据管道
self.parseDataQueue = queue.Queue()

def sendRequest(self,name,pageIndex):
"""

:param name: 歌手姓名
:param pageIndex: 当前页码
:return:
"""
self.params["input"]=name
self.params["page"]=pageIndex
response = requests.post(url=self.url, data=self.params, headers=self.headers)

data = response.json()["data"]["list"]
curSaveDir = os.path.join(self.baseDir,name)
if not os.path.exists(curSaveDir):
os.makedirs(curSaveDir)
self.responseDataQueue.put((curSaveDir,data))

def parseData(self):
"""

:param curSaveDir: 该歌手数据存储根目录
:param data: 需要被解析的数据
:return:
"""
while True:
try:
curSaveDir, data = self.responseDataQueue.get(timeout=2)
except queue.Empty:
break
for item in data:
# 歌词标题
title = item["title"]
# 作者
author = item["author"]
# 封面图片链接
pic = item["pic"]
# 歌词文本
tex = item["lrc"]
# 歌词下载链接
mp3Link = item["url"]

#当前歌曲数据存储路径
curMusicSaveDir = os.path.join(curSaveDir, title)
self.parseDataQueue.put((curMusicSaveDir,title,author,pic,tex,mp3Link))

def saveData(self):
while True:
try:
curMusicSaveDir, title, author,pic, tex, mp3Link = self.parseDataQueue.get(timeout=2)
except queue.Empty:
break
if not os.path.exists(curMusicSaveDir):
os.mkdir(curMusicSaveDir)
print("benign dowmload {}".format(title))
# 图片
curPicSavePath = os.path.join(curMusicSaveDir, title +"_"+author+ ".png")
self.downloadFile(pic, curPicSavePath)
# 歌词
curTexSavePath = os.path.join(curMusicSaveDir, title +"_"+author+ ".txt")
with open(curTexSavePath, "w") as f:
f.write(tex)
# MP3
curMp3SavePath = os.path.join(curMusicSaveDir, title +"_"+author+ ".mp3")
self.downloadFile(mp3Link, curMp3SavePath)
else:
print("歌曲{}已被下载,无需重复下载".format(title))
def downloadFile(self,url, path):
response = requests.get(url)
if response.status_code == 200:
with open(path, "wb") as f:
f.write(response.content)
else:
print(url+" 下载失败")

if __name__=="__main__":
crawDownload = CrawMusic(r"E:\tempData\musics")
"""
多线程,我们采用线程池来进行处理
"""
with ThreadPoolExecutor(max_workers=5) as pool:
pool.submit(crawDownload.sendRequest,"蔡依林",1)
pool.submit(crawDownload.parseData)
#save data比较耗时,采用三个线程
pool.submit(crawDownload.saveData)
pool.submit(crawDownload.saveData)
pool.submit(crawDownload.saveData)

Cookie池和IP代理

多线程并发存在问题:同一时间向目标服务器发送请求过多,且请求来自于同一客户端,容易被对方察觉,触发反爬机制导致失败

代理IP的使用

代理IP的基本概念

背景:为什么要使用代理
  • 一段时间内,检测IP访问频率,访问太频繁会被识别成爬虫被对方屏蔽
  • 使用代理IP可以让服务器以为不是同一客户端在请求
  • 防止我们真实的IP地址被泄露,被追究
使用代理的请求过程

image-20240518190455761

代理IP的获取

目前有许多平台会提供代理IP的服务,且一般注册之后都会赠送一些免费使用的额度

代理IP平台

在python代码中如何配置代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "https://www.baidu.com"
headers={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}
ip ="122.239.153.110"
port =40020
proxies={
"http":f"htpp://{ip}.{port}"
}
#通过参数 proxies配置代理
response = requests.get(url=url,headers=headers,proxies=proxies)
print(proxies)
print(response.content)

动态ip池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests
import random
"""
天启平台有教程,更具生成的api链接发送get请求可以获取代理Ip列表
每次在爬虫时,随机从列表中选择一个代理ip
"""

class SpiderCraw:

def __init__(self):
self.proxyGetUrl = "http://api.tianqiip.com/getip?secret=ka9cjsm0egycakir&num=10&type=json&port=1&time=5&mr=1&sign=d50e433630f39d20c428a0d5c6c033e7"
self.ipList = requests.get(self.proxyGetUrl).json()["data"]
def getProxyIp(self):
return random.choice(self.ipList)
def getSendRequest(self):
#每次发送请求,从ip池中随机选择一个代理ip
proxyIp = self.getProxyIp()
proxies = {
"http":"http://{}:{}".format(proxyIp["ip"],proxyIp["port"])
}
url = "https://www.baidu.com"
requests.get(url=url,proxies=proxies)
url =
ipPool = requests.get(url)
print(ipPool.json()["data"])

Cookie池的搭建

爬虫中使用cookie的利弊

带上cookie好处
  • 能够访问登陆后的页面
  • 能够实现部分反爬

带上cookie的坏处

  • 一套cookie往往对应一个用户的信息,请求太频繁容易被对方识别为爬虫
  • 上面问题如何解决?使用多个账号,每次请求的cookie不一样即可

cookie池的搭建

思路分析

  • 主备多个账号,越多越好
  • 爬取数据之前对这些账号进行批量登录,并将cookie保存在本地cookie池中
    • 需要登陆账号的可以这么做
    • 不需要登陆账号的,可以启动多个浏览器,手动登陆,然后将cookie值复制存储在本地
  • 爬虫执行之后,每次从cookie池中随机选择一个cookie

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import random
class CookieManager:
def __init__(self):
self.cookieList=[]
#实现准备一组账号密码
self.userInfoList=[
{"userName": "123321","password": "wwxxx1"},
{"userName": "123322", "password": "wwxxx2"},
{"userName": "123323", "password": "wwxxx3"},
{"userName": "123324", "password": "wwxxx4"},
{"userName": "123325", "password": "wwxxx5"}
]
for userInfo in self.userInfoList:
self.loginGetCookie(userInfo)

for item in self.cookieList:
print(item)
def loginGetCookie(self,userInfo):
"""
登陆函数,并从响应中获取cookie,然后保存在cookie池中
:return:
"""
url="https://passport.china.com/logon"
response = requests.post(url,data=userInfo)
self.cookieList.append(response.cookies)
def getCookie(self):
"""
供外部使用,随机从cookie池中选择一个cookie
外部使用时:直接在requests中以cookies参数进行传递
:return:
"""
return random.choice(self.cookieList)
if __name__=="__main__":
cookieManager = CookieManager()
print(cookieManager.getCookie())

Cookie池+IP池

同一个IP使用不同的Cookie,还是会出现同一个IP频繁请求,如果每个Cookie都绑定一组代理IP,这个时候就很难会被检测出来

基本思路:用不同ip去登陆不同账号,得到不同cookie,这样每一个代理Ipd都会对应多个cookie,在爬虫启动时,每次从ip_cookei池中选择一个ip,cookie对

综合项目实战案例

需求分析

某原创音乐网站数据抓取

需求:抓取某原创音乐平台整站数据

地址:https://5sing.kugou.com/

分页抓取该网站所有的原创音乐数据,一共50页

image-20240518222840513

image-20240518222855995

抓包分析

通过抓包分析,我们发现该网站数据不是Ajax请求,直接抓取请求页面即可获取对应数据

image-20240518223255287

image-20240518223819051

image-20240518224023061

image-20240518230616148

思路分析

  1. 获取歌曲列表、提取歌曲的歌名、歌曲首页地址
    1. 发送请求,获取页面的HTML
    2. 解析HTML,提取歌名和歌曲ID、首页地址
  2. 获取每首歌的下载地址
    1. 发送请求获取歌曲的下载地址
  3. 下载歌曲,保存在本地
    1. 下载歌曲,保存本地

数据量分析:一共50页,每页20条数据,要完整的下载一首歌的话,需要通过两次请求,第一次请求页面,获取歌曲id,第二次针对该歌曲地址,将其下载到本地

示例代码

爬取所有歌曲的名称和id数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests
from lxml import etree

url = "https://5sing.kugou.com/yc/list"
headers={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}
#参数,经过分析,前三个参数应该是固定的

for page in range(1,5):
params={
"t":2,
"l":"",
"s":"",
"p":page #页码
}

response = requests.get(url=url,headers=headers,params=params).content.decode("utf8")

#解析htm,获取歌曲名称、链接地址
html = etree.HTML(response)
dlList = html.xpath('//div[@class="lists"]/dl')
for dlItem in dlList:
title = dlItem.xpath('.//h3/a/text()')[0] #名称
url = 'https://5sing.kugou.com{}'.format(dlItem.xpath('.//h3/a/@href')[0]) #首页地址
id = dlItem.xpath('./dd[@class="l_action"]/a[@class="m_date_shou"]/@argid')[0]#id
print(title,url,id)

获取各个歌曲源文件的下载地址

>
>

抓包分析

image-20240519000844468

image-20240519001011699

两个不同歌曲请求的参数对比:

我们发现只有songid和signature这两个参数不一样,songid即为我们前面抓取的歌曲id,而难点就在于signature不知从何而来

image-20240519001105427

难点:参数signature不知从何而来,这个可能可能设计到js加密

如何分析网站生成signature的js代码?

在所有源代码中搜索signature

image-20240519002901327

定位到该位置

image-20240519003003459

我们在控制台执行,发现n的值为

image-20240519003149366

1
2
3
4
5
6
n的结构:
字符串: o = "5uytoxQewcvIc1gn1PlNF0T2jbbOzRl"
与请求参数的子字符串事项首位拼接得到拼接得到

即 n = o + params +o
我们经过多次实验发现字符串o为定值,不同请求、不同浏览器该值都是一致的

这个js代码比较复杂,我们无法用python代码等价复现,只能将js代码拷贝至本地,由python调用执行

其他补充知识

免费邮箱资源

针对账户限制,注册账号需要大量邮箱进行注册新的账号

需要很多邮箱怎么办?分享几个不错的办法可无限安全生成多个邮箱

免费代理IP资源

设置自己本地的梯子IP地址:

打开clash软件,查看代理端口为7890,在代码首部添加如下代码,即可使用本地代理转发

1
2
3
#配置代理
os.environ["http_proxy"] = "http://localhost:7890"
os.environ["https_proxy"] = "http://localhost:7890"

image-20240604093135783

参考资料一

网站指纹反爬

背景

我们使用requests库访问网站https://www.digikey.cn时,无论怎么设置头部信息,均是返回403页面,但是用浏览器或者PostMan访问时,均可得到正常的页面结果,这种情况大概率就是遇到了**原生模拟浏览器 TLS/JA3 指纹的验证**,览器和postman都有自带指纹验证,而唯独requests库没有。

解决办法

使用第三方库curl_cffi访问,即可解决这个问题,其可以模仿对浏览器指纹

1
2
3
4
5
from curl_cffi import requests
from lxml import etree
r = requests.get("https://www.digikey.cn", impersonate="chrome110")

print(r.content.decode("utf8"))

参考资料

某网站指纹反爬处理

JS逆向_浏览器JA3指纹信息

curl-cffif官方文档