杨记

碎片化学习令人焦虑,系统化学习使人进步

0%

爬虫基础

内容来自黑马程序员

JSON在线解析及格式化验证 - JSON.cn

爬虫基础

爬虫概述

爬虫的概念

模拟浏览器,发送请求,获取响应

网络爬虫(又被称为网页蜘蛛,网络机器人)就是模拟客户端(主要指浏览器)发送网络请求,接收请求响应,一种按照一定的规则,自动地抓取互联网信息的程序。

  • 原则上,只要是客户端(浏览器)能做的事情,爬虫都能够做
  • 爬虫也只能获取客户端(浏览器)所展示出来的数据
爬虫的作用

爬虫在互联网世界中有很多的作用,比如:

  1. 数据采集
    1. 抓取微博评论(机器学习舆情监控)
    2. 抓取招聘网站的招聘信息(数据分析、挖掘)
    3. 新浪滚动新闻
    4. 百度新闻网站
  2. 软件测试
    1. 爬虫之自动化测试
    2. 虫师
  3. 12306抢票
  4. 网站上的投票
    1. 投票网
  5. 网络安全
    1. 短信轰炸
      1. 注册页面1
      2. 注册页面2
      3. 注册页面3
    2. web漏洞扫描
爬虫的分类

根据被爬取网站的数量不同,可以分为:

  • 通用爬虫,如 搜索引擎
  • 聚焦爬虫,如12306抢票,或专门抓取某一个(某一类)网站数据

根据是否以获取数据为目的,可以分为:

  • 功能性爬虫,给你喜欢的明星投票、点赞
  • 数据增量爬虫,比如招聘信息

根据url地址和对应的页面内容是否改变,数据增量爬虫可以分为:

  • 基于url地址变化、内容也随之变化的数据增量爬虫
  • url地址不变、内容变化的数据增量爬虫

爬虫的分类

爬虫的流程

爬虫的工作流程

  1. 获取一个url
  2. 向url发送请求,并获取响应(需要http协议)
  3. 如果从响应中提取url,则继续发送请求获取响应
  4. 如果从响应中提取数据,则将数据进行保存

http协议复习

一提起http协议,大家都会想起它是一个应用层协议,那么http协议跟爬虫有什么关系呢?请看下图:

http作用

http和https的概念和区别

HTTPS比HTTP更安全,但是性能更低

  • HTTP:超文本传输协议,默认端口号是80
    • 超文本:是指超过文本,不仅限于文本;还包括图片、音频、视频等文件
    • 传输协议:是指使用共用约定的固定格式来传递转换成字符串的超文本内容
  • HTTPS:HTTP + SSL(安全套接字层),即带有安全套接字层的超本文传输协,默认端口号:443
    • SSL对传输的内容(超文本,也就是请求体或响应体)进行加密
  • 可以打开浏览器访问一个url,右键检查,点击net work,点选一个url,查看http协议的形式
请求头和响应头

1、特别关注的请求头字段:

http的请求形式

http请求的形式如上图所示,爬虫特别关注以下几个请求头字段

  • Content-Type
  • Host (主机和端口号)
  • Connection (链接类型)
  • Upgrade-Insecure-Requests (升级为HTTPS请求)
  • User-Agent (浏览器名称)
  • Referer (页面跳转处)
  • Cookie (Cookie)
  • Authorization(用于表示HTTP协议中需要认证资源的认证信息,如前边web课程中用于jwt认证)

加粗的请求头为常用请求头,在服务器被用来进行爬虫识别的频率最高,相较于其余的请求头更为重要,但是这里需要注意的是并不意味这其余的不重要,因为有的网站的运维或者开发人员可能剑走偏锋,会使用一些比较不常见的请求头来进行爬虫的甄别

2、特别关注的响应头字段:

http的响应形式

http响应的形式如上图所示,爬虫只关注一个响应头字段

  • Set-Cookie (对方服务器设置cookie到用户浏览器的缓存)
响应状态码
  • 200:成功
  • 302:跳转,新的url在响应的Location头中给出
  • 303:浏览器对于POST的响应进行重定向至新的url
  • 307:浏览器对于GET的响应重定向至新的url
  • 403:资源不可用;服务器理解客户的请求,但拒绝处理它(没有权限)
  • 404:找不到该页面
  • 500:服务器内部错误
  • 503:服务器由于维护或者负载过重未能应答,在响应中可能可能会携带Retry-After响应头;有可能是因为爬虫频繁访问url,使服务器忽视爬虫的请求,最终返回503响应状态码

我们在学习web知识的时候就已经学过了状态码的相关知识,我们知道这是服务器给我的相关反馈,我们在学习的时候就被教育说应该将真实情况反馈给客户端,但是在爬虫中,可能该站点的开发人员或者运维人员为了阻止数据被爬虫轻易获取,可能在状态码上做手脚,也就是说返回的状态码并不一定就是真实情况,比如:服务器已经识别出你是爬虫,但是为了让你疏忽大意,所以照样返回状态码200,但是响应体重并没有数据。

所有的状态码都不可信,一切以是否从抓包得到的响应中获取到数据为准

http请求过程

在回顾完http协议后,我们来了解以下浏览器发送http请求的过程

http发送的过程

  1. 浏览器在拿到域名对应的ip后,先向地址栏中的url发起请求,并获取响应
  2. 在返回的响应内容(html)中,会带有css、js、图片等url地址,以及ajax代码,浏览器按照响应内容中的顺序依次发送其他的请求,并获取相应的响应
  3. 浏览器每获取一个响应就对展示出的结果进行添加(加载),js,css等内容会修改页面的内容,js也可以重新发送请求,获取响应
  4. 从获取第一个响应并在浏览器中展示,直到最终获取全部响应,并在展示的结果中添加内容或修改————这个过程叫做浏览器的渲染
其它参考阅读

https://blog.csdn.net/qq_33301113/article/category/6943422/2
https://www.xuebuyuan.com/3252125.html
https://baike.baidu.com/item/http/243074?fr=aladdin
https://www.jianshu.com/p/cc1fea7810b2
https://blog.csdn.net/qq_30553235/article/details/79282113
https://segmentfault.com/q/1010000002403462

requests模块

requests官方文档:Requests: 让 HTTP 服务人类 — Requests 2.18.1 文档 (python-requests.org)

requests模块是一个第三方模块,需要在你的python(虚拟)环境中额外安装:pip/pip3 install requests

简单get请求

  1. 需求:通过requests向百度首页发送请求,获取该页面的源码
  2. 运行下面的代码,观察打印输出的结果
1
2
3
4
5
6
7
8
import requests 

# 目标url
url = 'https://www.baidu.com'
# 向目标url发送get请求
response = requests.get(url)
# 打印响应内容
print(response.text)

解决乱码

观察上边代码运行结果发现,有好多乱码;这是因为编解码使用的字符集不同早造成的;我们尝试使用下边的办法来解决中文乱码问题

1
2
3
4
5
6
7
8
9
import requests 

# 目标url
url = 'https://www.baidu.com'
# 向目标url发送get请求
response = requests.get(url)
# 打印响应内容
# print(response.text)
print(response.content.decode()) # 注意这里!
  1. response.text是requests模块按照chardet模块推测出的编码字符集进行解码的结果
  2. 网络传输的字符串都是bytes类型的,所以response.text = response.content.decode(‘推测出的编码字符集’)
  3. 我们可以在网页源码中搜索charset,尝试参考该编码字符集,注意存在不准确的情况

response.text 和response.content的区别:

  • response.text
    • 类型:str
    • 解码类型: requests模块自动根据HTTP 头部对响应的编码作出有根据的推测,推测的文本编码
  • response.content
    • 类型:bytes
    • 解码类型: 没有指定

中文乱码常用字符集:

  • response.content.decode() 默认utf-8
  • response.content.decode("GBK")
  • 常见的编码字符集
    • utf-8
    • gbk
    • gb2312
    • ascii
    • iso-8859-1

响应对象

response = requests.get(url)中response是发送请求获取的响应对象;response响应对象中除了text、content获取响应内容以外还有其它常用的属性或方法:

  • response.url响应的url;有时候响应的url和请求的url并不一致
  • response.status_code 响应状态码
  • response.request.headers 响应对应的请求头
  • response.headers 响应头
  • response.request._cookies 响应对应请求的cookie;返回cookieJar类型
  • response.cookies 响应的cookie(经过了set-cookie动作;返回cookieJar类型
  • response.json()自动将json字符串类型的响应内容转换为python对象(dict or list)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

# 目标url
url = 'https://www.baidu.com'

# 向目标url发送get请求
response = requests.get(url)

# 打印响应内容
# print(response.text)
# print(response.content.decode()) # 注意这里!
print(response.url) # 打印响应的url
print(response.status_code) # 打印响应的状态码
print(response.request.headers) # 打印响应对象的请求头
print(response.headers) # 打印响应头
print(response.request._cookies) # 打印请求携带的cookies
print(response.cookies) # 打印响应中携带的cookies

发送请求

携带请求头
1
requests.get(url, headers=headers)
  • headers参数接收字典形式的请求头
  • 请求头字段名作为key,字段对应的值作为value
1
2
3
4
5
6
7
8
9
10
11
12
13
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/54.0.2840.99 Safari/537.36"}

# 在请求头中带上User-Agent,模拟浏览器发送请求
response = requests.get(url, headers=headers)

print(response.content)

# 打印请求头信息
print(response.request.headers)
携带参数字典

我们在使用百度搜索的时候经常发现url地址中会有一个 ?,那么该问号后边的就是请求参数,又叫做查询字符串

1.构建请求参数字典

2.向接口发送请求的时候带上参数字典,参数字典设置给params

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

headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"}

# 这是目标url
# url = 'https://www.baidu.com/s?wd=python'

# 最后有没有问号结果都一样
url = 'https://www.baidu.com/s?'

# 请求参数是一个字典 即wd=python
kw = {'wd': 'python'}

# 带上请求参数发起请求,获取响应
response = requests.get(url, headers=headers, params=kw)

print(response.content)
携带cookie

网站经常利用请求头中的Cookie字段来做用户访问状态的保持,那么我们可以在headers参数中添加Cookie,模拟普通用户的请求。我们以github登陆为例:

github登陆抓包分析:

  1. 打开浏览器,右键-检查,点击Net work,勾选Preserve log
  2. 访问github登陆的url地址 https://github.com/login
  3. 输入账号密码点击登陆后,访问一个需要登陆后才能获取正确内容的url,比如点击右上角的Your profile访问https://github.com/USER_NAME
  4. 确定url之后,再确定发送该请求所需要的请求头信息中的User-Agent和Cookie

requests-3-2-1-4

  • 从浏览器中复制User-Agent和Cookie
  • 浏览器中的请求头字段和值与headers参数中必须一致
  • headers请求参数字典中的Cookie键对应的值是字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = 'https://github.com/USER_NAME'

# 构造请求头字典
headers = {
# 从浏览器中复制过来的User-Agent
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
# 从浏览器中复制过来的Cookie
'Cookie': 'xxx这里是复制过来的cookie字符串'
}

# 请求头参数字典中携带cookie字符串
resp = requests.get(url, headers=headers)

print(resp.text)

除了在headers参数中携带cookie,也可以使用专门的cookies参数

  1. cookies参数的形式:字典

    cookies = {"cookie的name":"cookie的value"}

    • 该字典对应请求头中Cookie字符串,以分号、空格分割每一对字典键值对
    • 等号左边的是一个cookie的name,对应cookies字典的key
    • 等号右边对应cookies字典的value
  2. cookies参数的使用方法

    response = requests.get(url, cookies)

  3. 将cookie字符串转换为cookies参数所需的字典:

    cookies_dict = {cookie.split('=')[0]:cookie.split('=')[-1] for cookie in cookies_str.split('; ')}

  4. 注意:cookie一般是有过期时间的,一旦过期需要重新获取

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

url = 'https://github.com/USER_NAME'

# 构造请求头字典
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'
}
# 构造cookies字典
cookies_str = '从浏览器中copy过来的cookies字符串'

cookies_dict = {cookie.split('=')[0]:cookie.split('=')[-1] for cookie in cookies_str.split('; ')}

# 请求头参数字典中携带cookie字符串
resp = requests.get(url, headers=headers, cookies=cookies_dict)

print(resp.text)
cookieJar对象

使用requests获取的resposne对象,具有cookies属性。该属性值是一个cookieJar类型,包含了对方服务器设置在本地的cookie。我们如何将其转换为cookies字典呢?

  1. 转换方法

    cookies_dict = requests.utils.dict_from_cookiejar(response.cookies)

  2. 其中response.cookies返回的就是cookieJar类型的对象

  3. requests.utils.dict_from_cookiejar函数返回cookies字典

timeout超时

在平时网上冲浪的过程中,我们经常会遇到网络波动,这个时候,一个请求等了很久可能任然没有结果。

在爬虫中,一个请求很久没有结果,就会让整个项目的效率变得非常低,这个时候我们就需要对请求进行强制要求,让他必须在特定的时间内返回结果,否则就报错。

  1. 超时参数timeout的使用方法

    response = requests.get(url, timeout=3)

  2. timeout=3表示:发送请求后,3秒钟内返回响应,否则就抛出异常

1
2
3
4
import requests

url = 'https://twitter.com'
response = requests.get(url, timeout=3) # 设置超时时间
proxies代理

proxy代理参数通过指定代理ip,让代理ip对应的正向代理服务器转发我们发送的请求,那么我们首先来了解一下代理ip以及代理服务器

  1. 代理ip是一个ip,指向的是一个代理服务器
  2. 代理服务器能够帮我们向目标服务器转发请求

使用代理的过程

正向代理和反向代理的区别

前边提到proxy参数指定的代理ip指向的是正向的代理服务器,那么相应的就有反向服务器;现在来了解一下正向代理服务器和反向代理服务器的区别

  1. 从发送请求的一方的角度,来区分正向或反向代理
  2. 为浏览器或客户端(发送请求的一方)转发请求的,叫做正向代理
    • 浏览器知道最终处理请求的服务器的真实ip地址,例如VPN
  3. 不为浏览器或客户端(发送请求的一方)转发请求、而是为最终处理请求的服务器转发请求的,叫做反向代理
    • 浏览器不知道服务器的真实地址,例如nginx

代理ip(代理服务器)的分类

  1. 根据代理ip的匿名程度,代理IP可以分为下面三类:

    • 透明代理(Transparent Proxy):透明代理虽然可以直接“隐藏”你的IP地址,但是还是可以查到你是谁。目标服务器接收到的请求头如下:

      1
      2
      3
      REMOTE_ADDR = Proxy IP
      HTTP_VIA = Proxy IP
      HTTP_X_FORWARDED_FOR = Your IP
    • 匿名代理(Anonymous Proxy):使用匿名代理,别人只能知道你用了代理,无法知道你是谁。目标服务器接收到的请求头如下:

      1
      2
      3
      REMOTE_ADDR = proxy IP
      HTTP_VIA = proxy IP
      HTTP_X_FORWARDED_FOR = proxy IP
    • 高匿代理(Elite proxy或High Anonymity Proxy):高匿代理让别人根本无法发现你是在用代理,所以是最好的选择。毫无疑问使用高匿代理效果最好。目标服务器接收到的请求头如下:

      1
      2
      3
      REMOTE_ADDR = Proxy IP
      HTTP_VIA = not determined
      HTTP_X_FORWARDED_FOR = not determined
  2. 根据网站所使用的协议不同,需要使用相应协议的代理服务。从代理服务请求使用的协议可以分为:

    • http代理:目标url为http协议
    • https代理:目标url为https协议
    • socks隧道代理(例如socks5代理)等:
      1. socks 代理只是简单地传递数据包,不关心是何种应用协议(FTP、HTTP和HTTPS等)。
      2. socks 代理比http、https代理耗时少。
      3. socks 代理可以转发http和https的请求

proxies代理参数的使用

为了让服务器以为不是同一个客户端在请求;为了防止频繁向一个域名发送请求被封ip,所以我们需要使用代理ip;那么我们接下来要学习requests模块是如何使用代理ip的

  • 用法:response = requests.get(url, proxies=proxies)

  • proxies的形式:字典

  • 例如:

    1
    2
    3
    4
    proxies = { 
    "http": "http://12.34.56.79:9527",
    "https": "https://12.34.56.79:9527",
    }
  • 注意:如果proxies字典中包含有多个键值对,发送请求时将按照url地址的协议来选择使用相应的代理ip

verify参数

在使用浏览器上网的时候,有时能够看到下面的提示(2018年10月之前的12306网站):

12306ssl错误

使用verify=False参数,此时requests模块发送请求将不做CA证书的验证:verify参数能够忽略CA证书的认证

response = requests.get(url,verify=False)

post请求

思考:哪些地方我们会用到POST请求?

  1. 登录注册( 在web工程师看来POST 比 GET 更安全,url地址中不会暴露用户的账号密码等信息)
  2. 需要传输大文本内容的时候( POST 请求对数据长度没有要求)

所以同样的,我们的爬虫也需要在这两个地方回去模拟浏览器发送post请求

requests发送post请求的方法

  • response = requests.post(url, data)
  • data参数接收一个字典
  • requests模块发送post请求函数的其它参数和发送get请求的参数完全一致

下面面我们通过金山翻译的例子看看post请求如何使用:

  1. 地址:http://fy.iciba.com/
思路分析
  1. 抓包确定请求的url地址

    img

  2. 确定请求的参数

    img

  3. 确定返回数据的位置

    img

分析结果:

  1. url地址:http://fy.iciba.com/

  2. 请求方法:POST

  3. 请求所需参数:

    1
    2
    3
    4
    5
    data = {
    'f': 'auto', # 表示被翻译的语言是自动识别
    't': 'auto', # 表示翻译后的语言是自动识别
    'w': '人生苦短' # 要翻译的中文字符串
    }
代码实现
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
import requests
import json


class King(object):
def __init__(self, word):
self.url = "http://fy.iciba.com/ajax.php?a=fy"
self.word = word
self.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
}
self.post_data = {
"f": "auto",
"t": "auto",
"w": self.word
}

def get_data(self):
response = requests.post(self.url, headers=self.headers, data=self.post_data)
# 默认返回bytes类型,除非确定外部调用使用str才进行解码操作
return response.content

def parse_data(self, data):
# 将json数据转换成python字典
dict_data = json.loads(data)
# 从字典中抽取翻译结果
try:
print(dict_data['content']['out'])
except:
print(dict_data['content']['word_mean'][0])

def run(self):
# url
# headers
# post——data
# 发送请求
data = self.get_data()
# 解析
self.parse_data(data)

if __name__ == '__main__':
# king = King("人生苦短,及时行乐")
king = King("China")
king.run()

session

requests模块中的Session类能够自动处理发送请求获取响应过程中产生的cookie,进而达到状态保持的目的。接下来我们就来学习它

requests.session的作用以及应用场景

  • requests.session的作用
    • 自动处理cookie,即 下一次请求会带上前一次的cookie
  • requests.session的应用场景
    • 自动处理连续的多次请求过程中产生的cookie

requests.session使用方法

session实例在请求了一个网站后,对方服务器设置在本地的cookie会保存在session中,下一次再使用session请求对方服务器的时候,会带上前一次的cookie

1
2
3
session = requests.session() # 实例化session对象
response = session.get(url, headers, ...)
response = session.post(url, data, ...)
  • session对象发送get或post请求的参数,与requests模块发送请求的参数完全一致

使用requests.session来完成github登陆,并获取需要登陆后才能访问的页面:

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
import requests
import re


# 构造请求头字典
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
}

# 实例化session对象
session = requests.session()

# 访问登陆页获取登陆请求所需参数
response = session.get('https://github.com/login', headers=headers)
authenticity_token = re.search('name="authenticity_token" value="(.*?)" />', response.text).group(1) # 使用正则获取登陆请求所需参数

# 构造登陆请求参数字典
data = {
'commit': 'Sign in', # 固定值
'utf8': '✓', # 固定值
'authenticity_token': authenticity_token, # 该参数在登陆页的响应内容中
'login': input('输入github账号:'),
'password': input('输入github账号:')
}

# 发送登陆请求(无需关注本次请求的响应)
session.post('https://github.com/session', headers=headers, data=data)

# 打印需要登陆后才能访问的页面
response = session.get('https://github.com/1596930226', headers=headers)
print(response.text)

数据提取

响应内容分类

在发送请求获取响应之后,可能存在多种不同类型的响应内容;而且很多时候,我们只需要响应内容中的一部分数据

1、结构化的响应内容

  • json字符串

    • 可以使用re、json等模块来提取特定数据

    • json字符串的例子如下图

      结构化响应内容json

  • xml字符串

    • 可以使用re、lxml等模块来提取特定数据

    • xml字符串的例子如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <bookstore>
      <book category="COOKING">
      <title lang="en">Everyday Italian</title>
      <author>Giada De Laurentiis</author>
      <year>2005</year>
      <price>30.00</price>
      </book>
      <book category="CHILDREN">
      <title lang="en">Harry Potter</title>
      <author>J K. Rowling</author>
      <year>2005</year>
      <price>29.99</price>
      </book>
      <book category="WEB">
      <title lang="en">Learning XML</title>
      <author>Erik T. Ray</author>
      <year>2003</year>
      <price>39.95</price>
      </book>
      </bookstore>

2、非结构化的响应内容

  • html字符串

    • 可以使用re、lxml等模块来提取特定数据

    • html字符串的例子如下图

      非结构化响应内容html

xml和html

xml是一种可扩展标记语言,样子和html很像,功能更专注于对传输和存储数据

上面的xml内容可以表示为下面的树结构:

xml树结构

xml和html的区别如下图

数据格式 描述 设计目标
XML Extensible Markup Language(可扩展标记语言) 被设计为传输和存储数据,其焦点是数据的内容
HTML HyperText Markup Language(超文本标记语言) 更好的显示数据
  • html:
    • 超文本标记语言
    • 为了更好的显示数据,侧重点是为了显示
  • xml:
    • 可扩展标记语言
    • 为了传输和存储数据,侧重点是在于数据内容本身

数据解析方法

常用数据解析方法

jsonpath模块

如果有一个多层嵌套的复杂字典,想要根据key和下标来批量提取value,这是比较困难的。jsonpath模块就能解决这个痛点,接下来我们就来学习jsonpath模块

jsonpath可以按照key对python字典进行批量数据提取

jsonpath是第三方模块,需要额外安装:pip install jsonpath

提取数据的方法:

1
2
from jsonpath import jsonpath
ret = jsonpath(a, 'jsonpath语法规则字符串')

语法规则

jsonpath的方法

使用示例

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
book_dict = { 
"store": {
"book": [
{ "category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{ "category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{ "category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{ "category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}

from jsonpath import jsonpath

print(jsonpath(book_dict, '$..author')) # 如果取不到将返回False # 返回列表,如果取不到将返回False

jsonpath使用示例

jsonpath练习

我们以拉勾网城市JSON文件 http://www.lagou.com/lbs/getAllCitySearchLabels.json 为例,获取所有城市的名字的列表,并写入文件。

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

# 获取拉勾网城市json字符串
url = 'http://www.lagou.com/lbs/getAllCitySearchLabels.json'
headers = {"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"}
response =requests.get(url, headers=headers)
html_str = response.content.decode()

# 把json格式字符串转换成python对象
jsonobj = json.loads(html_str)

# 从根节点开始,获取所有key为name的值
citylist = jsonpath.jsonpath(jsonobj,'$..name')

# 写入文件
with open('city_name.txt','w') as f:
content = json.dumps(citylist, ensure_ascii=False)
f.write(content)

xpath

对html或xml形式的文本提取特定的内容,就需要我们掌握lxml模块的使用和xpath语法。

  • lxml模块可以利用XPath规则语法,来快速的定位HTML\XML 文档中特定元素以及获取节点信息(文本内容、属性值)
  • XPath (XML Path Language) 是一门在 HTML\XML 文档中查找信息的语言,可用来在 HTML\XML 文档中对元素和属性进行遍历。
  • 提取xml、html中的数据需要lxml模块和xpath语法配合使用

xpath helper

xpath helper是一款浏览器插件,可以帮助我们查看xpath语法对应的网页元素

下载Chrome插件 XPath Helper

访问url之后在页面中点击xpath图标,就可以使用了:

xpath_helper插件安装-3

xpath的节点

学习xpath语法需要先了解xpath中的节点关系

每个html、xml的标签我们都称之为节点,其中最顶层的节点称为根节点。我们以xml为例,html也是一样的

节点

节点的关系

xpath中节点的关系

  • authortitle的第一个兄弟节点

xpath语法

1、xpath语法-基础节点选择语法

  1. XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。
  2. 这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。
  3. 使用chrome插件选择标签时候,选中时,选中的标签会添加属性class=”xh-highlight”

xpath定位节点以及提取属性或文本内容的语法

表达式 描述
nodename 选中该元素
/ 从根节点选取、或者是元素和元素间的过渡
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性
text() 选取文本

语法练习,接下来我们通过itcast的页面来练习上述语法:http://www.itcast.cn/

  • 选择所有的h2下的文本://h2/text()
  • 获取所有的a标签的href://a/@href
  • 获取html下的head下的title的文本:/html/head/title/text()
  • 获取html下的head下的link标签的href:/html/head/link/@href

2、xpath语法-节点修饰语法

可以根据标签的属性值、下标等来获取特定的节点

路径表达式 结果
//title[@lang=”eng”] 选择lang属性值为eng的所有title元素
/bookstore/book[1] 选取属于 bookstore 子元素的第一个 book 元素
/bookstore/book[last()] 选取属于 bookstore 子元素的最后一个 book 元素
/bookstore/book[last()-1] 选取属于 bookstore 子元素的倒数第二个 book 元素
/bookstore/book[position()>1] 选择bookstore下面的book元素,从第二个开始选择
//book/title[text()=’Harry Potter’] 选择所有book下的title元素,仅仅选择文本为Harry Potter的title元素
/bookstore/book[price>35.00]/title 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00
  • 在xpath中,第一个元素的位置是1
  • 最后一个元素的位置是last()
  • 倒数第二个是last()-1

语法练习,从itcast的页面中,选择所有学科的名称、第一个学科的链接、最后一个学科的链接:http://www.itcast.cn/

  • 所有的学科的名称://div[@class="nav_txt"]//a[@class="a_gd"]
  • 第一个学科的链接://div[@class="nav_txt"]/ul/li[1]/a/@href
  • 最后一个学科的链接://div[@class="nav_txt"]/ul/li[last()]/a/@href

3、xpath语法-其他常用节点选择语法

可以通过通配符来选取未知的html、xml的元素

通配符 描述
* 匹配任何元素节点
@* 匹配任何属性节点
node() 匹配任何类型的节点

语法练习,从itcast的页面中 http://www.itcast.cn/ ,选中全部的标签、全部的属性

  • 全部的标签://*
  • 全部的属性://@*

通过子节点的值修饰节点

  • 子节点i的值>2000的span节点://span[i>2000]
  • 第二个span子节点的值>=9.4的div节点://div[span[2]>=9.4]

通过包含修饰

  • id属性包含“qiushi_tag_”的div节点://div[contains(@id, "qiushi_tag_")]
  • 值包含“一页”的span节点://span[contains(text(), "一页")]

复合语法:

  • /h/a|/h/span

lxml模块

安装lxml模块:pip install lxml -i https://pypi.tuna.tsinghua.edu.cn/simple

用法

  1. 导入lxml 的 etree 库:from lxml import etree

  2. 利用etree.HTML,将html字符串(bytes类型或str类型)转化为Element对象,Element对象具有xpath的方法,配合xpath表达式来完成对数据的提取,返回结果的列表

    1
    2
    html = etree.HTML(text) 
    ret_list = html.xpath("xpath语法规则字符串")
  3. xpath方法返回列表的三种情况

    • 返回空列表:根据xpath语法规则字符串,没有定位到任何元素
    • 返回由字符串构成的列表:xpath字符串规则匹配的一定是文本内容或某属性的值
    • 返回由Element对象构成的列表:xpath规则字符串匹配的是标签,列表中的Element对象可以继续进行xpath
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
属性定位:
#找到class属性值为song的div标签
//div[@class="song"]
层级&索引定位:
#找到class属性值为tangdiv的直系子标签ul下的第二个子标签li下的直系子标签a
//div[@class="tang"]/ul/li[2]/a
逻辑运算:
#找到href属性值为空且class属性值为dua标签
//a[@href="" and @class="du"]
模糊匹配:
//div[contains(@class, "ng")]
//div[starts-with(@class, "ta")]
取文本:
# /表示获取某个标签下的文本内容
# //表示获取某个标签下的文本内容和所有子标签下的文本内容
//div[@class="song"]/p[1]/text()
//div[@class="tang"]//text()
取属性:
//div[@class="tang"]//li[2]/a/@href

使用示例

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
from lxml import etree

xml = """
<book>
<id>1</id>
<name>野花遍地香</name>
<price>1.23</price>
<nick>臭豆腐</nick>
<author>
<nick id="10086">周大强</nick>
<nick id="10010">周芷若</nick>
<nick class="joy">周杰伦</nick>
<nick class="jolin">蔡依林</nick>
<div>
<nick>热热热热热1</nick>
</div>
<span>
<nick>热热热热热2</nick>
</span>
</author>

<partner>
<nick id="ppc">胖胖陈</nick>
<nick id="ppbc">胖胖不陈</nick>
</partner>
</book>
"""

tree = etree.HTML(xml)
result = tree.xpath("/book") # /表示层级关系. 第一个/是根节点
print(result)
result = tree.xpath("/book/name")
print(result)
result = tree.xpath("/book/name/text()") # text() 拿文本
print(result)
result = tree.xpath("/book/author//nick/text()") # // 后代
print(result)
result = tree.xpath("/book/author/*/nick/text()") # * 任意的节点. 通配符(会儿)
print(result)
result = tree.xpath("/book//nick/text()")
print(result)

# 输出
# [<Element book at 0x1ef1c91e2c0>]
# [<Element name at 0x1ef1c9c0e00>]
# ['野花遍地香']
# ['周大强', '周芷若', '周杰伦', '蔡依林', '热热热热热1', '热热热热热2']
# ['热热热热热1', '热热热热热2']
# ['臭豆腐', '周大强', '周芷若', '周杰伦', '蔡依林', '热热热热热1', '热热热热热2', '胖胖陈', '胖胖不陈']

注意:xpath路径中不能有tbody

etree.tostring

运行下边的代码,观察对比html的原字符串和打印输出的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
from lxml import etree
html_str = ''' <div> <ul>
<li class="item-1"><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>
</ul> </div> '''

html = etree.HTML(html_str)

handeled_html_str = etree.tostring(html).decode()
print(handeled_html_str)
1
2
3
4
5
6
7
<html><body><div> <ul> 
<li class="item-1"><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>

结论

  • lxml.etree.HTML(html_str)可以自动补全标签
  • lxml.etree.tostring函数可以将转换为Element对象再转换回html字符串
  • 爬虫如果使用lxml来提取数据,应该以lxml.etree.tostring的返回结果作为提取数据的依据

selenium

官方文档:使用说明 | selenium中文网

Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,Selenium 可以直接调用浏览器,它支持所有主流的浏览器(包括PhantomJS这些无界面的浏览器),可以接收指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏等。

安装环境

1、在python虚拟环境中安装selenium模块:pip/pip3 install selenium

2、下载浏览器驱动webdriver:以谷歌浏览器为例 chromedriver.storage.googleapis.com/index.html

  1. 查看浏览器内核版本

    image-20220119114803419

  2. 查看网站对应的版本,如果没有刚好对应的,向前查找最近的版本

    image-20220119115053658

  1. 下载操作系统对应的压缩包,查看notes.txt可以帮助选择版本

    image-20220119115001266

  2. 解压后将 chromedriver.exe 文件放在已添加到环境变量的路径下,比如python环境所在的文件夹

工作原理

利用浏览器原生的API,封装成一套更加面向对象的Selenium WebDriver API,直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭,安装插件,配置证书之类的)

selenium的工作原理

  • webdriver本质是一个web-server,对外提供webapi,其中封装了浏览器的各种功能
  • 不同的浏览器使用各自不同的webdriver

简单使用

在下载好chromedriver以及安装好selenium模块后,执行下列代码并观察运行的过程

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

# 如果driver没有添加到了环境变量,则需要将driver的绝对路径赋值给executable_path参数
# driver = webdriver.Chrome(executable_path='/home/worker/Desktop/driver/chromedriver')
# 如果driver添加了环境变量则不需要设置executable_path
driver = webdriver.Chrome()

# 控制浏览器访问url地址
driver.get("https://www.baidu.com/")

# 在百度搜索框中搜索'python'
driver.find_element_by_id('kw').send_keys('python')
# 点击'百度搜索'
driver.find_element_by_id('su').click()

time.sleep(6) # 等待浏览器反应
# 退出浏览器
driver.quit() # 一定要退出!不退出会有残留进程!
  • webdriver.Chrome(executable_path='./chromedriver')中executable参数指定的是下载好的chromedriver文件的路径
  • driver.find_element_by_id('kw').send_keys('python')定位id属性值是’kw’的标签,并向其中输入字符串’python’
  • driver.find_element_by_id('su').click()定位id属性值是su的标签,并点击
    • click函数作用是:触发标签的js的click事件

注意事项:

  1. 谷歌浏览器装在D盘,程序报错没找到

    1
    2
    3
    4
    5
    6
    7
    from selenium.webdriver import Chrome
    from selenium.webdriver.chrome.options import Options

    option = Options()
    option.binary_location = r'D:\Program Files\Google\Chrome\Application\chrome.exe' # 指定谷歌浏览器的路径
    # 创建一个浏览器对象
    driver = Chrome(options=option)
  2. 浏览器加载需要时间,有时候浏览器没加载完成程序执行到下一步没找到元素会报错,可以加上 sleep()等几秒

  3. 浏览器最大化 bro.maximize_window()

提取数据

driver对象

在使用selenium过程中,实例化driver对象后,driver对象有一些常用的属性和方法

  1. driver.page_source 当前标签页浏览器渲染之后的网页源代码
  2. driver.current_url 当前标签页的url
  3. driver.close() 关闭当前标签页,如果只有一个标签页则关闭整个浏览器
  4. driver.quit() 关闭浏览器
  5. driver.forward() 页面前进
  6. driver.back() 页面后退
  7. driver.screen_shot(img_name) 页面截图
获取标签元素

在selenium中可以通过多种方式来定位标签,返回标签元素对象

1
2
3
4
5
6
7
8
find_element_by_id                         (返回一个元素)
find_element(s)_by_class_name (根据类名获取元素列表)
find_element(s)_by_name (根据标签的name属性值返回包含标签对象元素的列表)
find_element(s)_by_xpath (返回一个包含元素的列表)
find_element(s)_by_link_text (根据连接文本获取元素列表)
find_element(s)_by_partial_link_text (根据链接包含的文本获取元素列表)
find_element(s)_by_tag_name (根据标签名获取元素列表)
find_element(s)_by_css_selector (根据css选择器来获取元素列表)
  • find_element和find_elements的区别:
    • 多了个s就返回列表,没有s就返回匹配到的第一个标签对象
    • find_element匹配不到就抛出异常,find_elements匹配不到就返回空列表
  • by_link_text和by_partial_link_tex的区别:全部文本和包含某个文本
  • 以上函数的使用方法:driver.find_element_by_id('id_str')

标签对象提取文本内容和属性值

find_element仅仅能够获取元素,不能够直接获取其中的数据,如果需要获取数据需要使用以下方法

  • 对元素执行点击操作element.click(),对定位到的标签对象进行点击操作
  • 向输入框输入数据element.send_keys(data),对定位到的标签对象输入数据
  • 获取文本element.text,通过定位获取的标签对象的text属性,获取文本内容
  • 获取属性值element.get_attribute("属性名"),通过定位获取的标签对象的get_attribute函数,传入属性名,来获取属性的值

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
from selenium import webdriver

driver = webdriver.Chrome()
driver.get('http://www.itcast.cn/')

ret = driver.find_elements_by_tag_name('h2')
print(ret[0].text) #

ret = driver.find_elements_by_link_text('黑马程序员')
print(ret[0].get_attribute('href'))

driver.quit()

其他使用方法

切换标签页

当selenium控制浏览器打开多个标签页时,如何控制浏览器在不同的标签页中进行切换呢?需要我们做以下两步:

  • 获取所有标签页的窗口句柄
  • 利用窗口句柄字切换到句柄指向的标签页
1
2
3
4
# 1. 获取当前所有的标签页的句柄构成的列表
current_windows = driver.window_handles
# 2. 根据标签页句柄列表索引下标进行切换
driver.switch_to.window(current_windows[0])

代码示例:

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
from selenium.webdriver import Chrome
from selenium.webdriver.common.keys import Keys
import time

web = Chrome()
web.get("http://lagou.com")

web.find_element_by_xpath('//*[@id="cboxClose"]').click()
time.sleep(1)
web.find_element_by_xpath('//*[@id="search_input"]').send_keys("python", Keys.ENTER)
time.sleep(1)
web.find_element_by_xpath('//*[@id="jobList"]/div[1]/div[1]/div[1]/div[1]/div[1]/a').click()

# 如何进入到进窗口中进行提取
# 注意, 在selenium的眼中. 新窗口默认是不切换过来的.
web.switch_to.window(web.window_handles[-1])

# 在新窗口中提取内容
job_detail = web.find_element_by_xpath('//*[@id="job_detail"]/dd[2]/div').text
print(job_detail)

web.close() # 关掉子窗口
# 变更selenium的窗口视角. 回到原来的窗口中
web.switch_to.window(web.window_handles[0])
time.sleep(1)
print(web.find_element_by_xpath('//*[@id="jobList"]/div[1]/div[1]/div[1]/div[1]/div[1]/a').text)
动作链和切换iframe
iframe是html中常用的一种技术,即一个页面中嵌套了另一个网页,selenium默认是访问不了frame中的内容的,对应的解决思路是driver.switch_to.frame(frame_element)。接下来我们通过qq邮箱模拟登陆来学习这个知识点

参考代码:

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 time import sleep
# 导入动作链对应的类
from selenium.webdriver import ActionChains

bro = webdriver.Chrome()
bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')

# 如果定位的标签是存在于iframe标签之中的则必须通过如下操作在进行标签定位
iframe = bro.find_element_by_id("iframeResult")
bro.switch_to.frame(iframe) # 切换浏览器标签定位的作用域
div = bro.find_element_by_id('draggable') # 定位元素

# 动作链
action = ActionChains(bro)
# 点击长按指定的标签
action.click_and_hold(div)

for i in range(8):
# perform()立即执行动作链操作
# move_by_offset(x,y):x水平方向 y竖直方向
action.move_by_offset(20, 0).perform()
sleep(0.5)

# 释放动作链
action.release()
bro.switch_to.default_content() # 切换回原页面
bro.find_element_by_xpath("/html/body/header/div/div[2]/form/input").send_keys("python")
bro.quit()
处理cookie

selenium携带cookies启动,请求_IT周星驰的博客-CSDN博客_selenium带cookie访问

1、获取cookie

driver.get_cookies()返回列表,其中包含的是完整的cookie信息!不光有name、value,还有domain等cookie其他维度的信息。所以如果想要把获取的cookie信息和requests模块配合使用的话,需要转换为name、value作为键值对的cookie字典

1
2
3
4
# 获取当前标签页的全部cookie信息
print(driver.get_cookies())
# 把cookie转化为字典
cookies_dict = {cookie['name']: cookie['value'] for cookie in driver.get_cookies()}

2、删除cookie

1
2
3
4
5
#删除一条cookie
driver.delete_cookie("CookieName")

# 删除所有的cookie
driver.delete_all_cookies()
执行js代码

执行js的方法:driver.execute_script(js)

1
2
3
4
5
6
7
8
9
10
11
12
import time
from selenium import webdriver

driver = webdriver.Chrome()
driver.get("http://www.itcast.cn/")
time.sleep(1)

js = 'window.scrollTo(0,document.body.scrollHeight)' # js语句
driver.execute_script(js) # 执行js的方法

time.sleep(5)
driver.quit()
页面等待

1、强制等待:time.sleep()

  • 设置的时间太短,元素还没有加载出来;设置的时间太长,则会浪费时间

2、隐式等待

  • 隐式等待针对的是元素定位,隐式等待设置了一个时间,在一段时间内判断元素是否定位成功,如果完成了,就进行下一步

  • 在设置的时间内没有定位成功,则会报超时加载

    1
    2
    3
    4
    5
    6
    from selenium import webdriver

    driver = webdriver.Chrome()
    driver.implicitly_wait(10) # 隐式等待,最长等20秒
    driver.get('https://www.baidu.com')
    driver.find_element_by_xpath()

3、显式等待

  • 每经过多少秒就查看一次等待条件是否达成,如果达成就停止等待,继续执行后续代码,如果没有达成就继续等待直到超过规定的时间后,报超时异常

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

    driver = webdriver.Chrome()
    driver.get('https://www.baidu.com')

    # 显式等待
    WebDriverWait(driver, 20, 0.5).until(EC.presence_of_element_located((By.LINK_TEXT, '好123')))
    # 参数20表示最长等待20秒
    # 参数0.5表示0.5秒检查一次规定的标签是否存在
    # EC.presence_of_element_located((By.LINK_TEXT, '好123')) 表示通过链接文本内容定位标签
    # 每0.5秒一次检查,通过链接文本内容定位标签是否存在,如果存在就向下继续执行;如果不存在,直到20秒上限就抛出异常

    print(driver.find_element_by_link_text('好123').get_attribute('href'))
    driver.quit()
无头浏览器

绝大多数服务器是没有界面的,selenium控制谷歌浏览器也是存在无界面模式的。

开启无界面模式的方法:

  • 实例化配置对象:chromeOpt = webdriver.ChromeOptions()
  • 配置对象添加开启无界面模式的命令:chromeOpt.add_argument("--headless")
  • 配置对象添加禁用gpu的命令:chromeOpt.add_argument("--disable-gpu")
  • 实例化带有配置对象的driver对象:driver = webdriver.Chrome(chrome_options=chromeOpt)
1
2
3
4
5
6
7
8
9
10
11
12
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options
from time import sleep

opt = Options()
opt.add_argument("--headless")
opt.add_argument("--disbale-gpu")

web = Chrome(options=opt) # 把参数配置设置到浏览器中
web.get("https://www.endata.com.cn/BoxOffice/BO/Year/index.html")
sleep(1) # 等待一秒
print(web.page_source) # 拿到页面代码Elements(经过数据加载以及js执行之后的结果的html内容)

无头浏览器与有头浏览器的使用场景:

  • 通常在开发过程中我们需要查看运行过程中的各种情况所以通常使用有头浏览器
  • 在项目完成进行部署的时候,通常平台采用的系统都是服务器版的操作系统,服务器版的操作系统必须使用无头浏览器才能正常运行

PhantomJS 是一个基于Webkit的“无界面”(headless)浏览器,它会把网站加载到内存并执行页面上的 JavaScript。下载地址:http://phantomjs.org/download.html

使用代理和UA

使用代理ip和替换UA的方法

  • 实例化配置对象:chromeOpt = webdriver.ChromeOptions()
  • 配置对象添加使用代理ip的命令chromeOpt.add_argument('--proxy-server=http://202.20.16.82:9527')
  • 配置对象添加替换UA的命令chromeOpt.add_argument('--user-agent=Mozilla/5.0 HAHA')
  • 实例化带有配置对象的driver对象:driver = webdriver.Chrome(chrome_options=chromeOpt)
1
2
3
4
5
6
7
8
9
10
from selenium import webdriver

options = webdriver.ChromeOptions() # 创建一个配置对象
options.add_argument('--proxy-server=http://202.20.16.82:9527') # 使用代理ip
options.add_argument('--user-agent=Mozilla/5.0 HAHA') # 替换User-Agent
driver = webdriver.Chrome(chrome_options=options) # 实例化带有配置的driver对象

driver.get('http://www.itcast.cn')
print(driver.title)
driver.quit()
规避检测

浏览器抓包工具的控制台,输入window.navigator.webdriver,正常浏览器显示false,自动化打开的浏览器显示true

1
2
3
4
5
6
7
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.options import Options

option = Options()
options.add_experimental_option('excludeSwitches', ['enable-automation']) # 使用开发者模式
option.add_argument('--disable-blink-features=AutomationControlled') # 禁用启用Blink运行时的功能
web = Chrome(options=option)

image-20230104003502278

Linux部署

linux系统下如何部署selenium爬虫程序_q56731523的博客-CSDN博客_linux使用selenium

CentOS 7 使用 Yum 软件源安装谷歌 Chrome 浏览器 - Oops!# - 博客园 (cnblogs.com)

Linux(红帽,centos)安装谷歌浏览器(google-chrom)_玻璃酸钠滴眼液的博客-CSDN博客_红帽安装谷歌浏览器

Chrome浏览器的options参数_哈齐先生的博客-CSDN博客_chrome浏览器的options参数

爬虫和反爬

反爬原因

  1. 爬虫占总PV(PV是指页面的访问次数,每打开或刷新一次页面,就算做一个pv)比例较高,这样浪费钱(尤其是三月份爬虫)。

  2. 公司可免费查询的资源被批量抓走,丧失竞争力,这样少赚钱。

    数据可以在非登录状态下直接被查询。如果强制登陆,那么可以通过封杀账号的方式让对方付出代价,这也是很多网站的做法。但是不强制对方登录。那么如果没有反爬虫,对方就可以批量复制的信息,公司竞争力就会大大减少。竞争对手可以抓到数据,时间长了用户就会知道,只需要去竞争对手那里就可以了,没必要来我们网站,这对我们是不利的。

  3. 状告爬虫成功的几率小

服务器常反什么样的爬虫

  1. 十分低级的应届毕业生

  2. 十分低级的创业小公司

  3. 不小心写错了没人去停止的失控小爬虫

  4. 不小心写错了没人去停止的失控小爬虫

    有些网站已经做了相应的反爬,但是爬虫依然孜孜不倦地爬取。什么意思呢?就是说,他们根本爬不到任何数据,除了httpcode是200以外,一切都是不对的,可是爬虫依然不停止这个很可能就是一些托管在某些服务器上的小爬虫,已经无人认领了,依然在辛勤地工作着。

  5. 成型的商业对手

  6. 抽风的搜索引擎

反爬虫领域常见的一些概念

  • 爬虫:使用任何技术手段,批量获取网站信息的一种方式。关键在于批量。
  • 反爬虫:使用任何技术手段,阻止别人批量获取自己网站信息的一种方式。关键也在于批量。
  • 误伤:在反爬虫的过程中,错误的将普通用户识别为爬虫。误伤率高的反爬虫策略,效果再好也不能用。
  • 拦截:成功地阻止爬虫访问。这里会有拦截率的概念。通常来说,拦截率越高的反爬虫策略,误伤的可能性就越高。因此需要做个权衡。
  • 资源:机器成本与人力成本的总和。

常见反爬手段

  • 基于身份识别进行反爬
  • 基于爬虫行为进行反爬
  • 基于数据加密进行反爬
基于身份识别

1 通过headers字段来反爬

headers中有很多字段,这些字段都有可能会被对方服务器拿过来进行判断是否为爬虫

1.1 通过headers中的User-Agent字段来反爬

  • 反爬原理:爬虫默认情况下没有User-Agent,而是使用模块默认设置
  • 解决方法:请求之前添加User-Agent即可;更好的方式是使用User-Agent池来解决(收集一堆User-Agent的方式,或者是随机生成User-Agent)

1.2 通过referer字段或者是其他字段来反爬

  • 反爬原理:爬虫默认情况下不会带上referer字段,服务器端通过判断请求发起的源头,以此判断请求是否合法
  • 解决方法:添加referer字段

1.3 通过cookie来反爬

  • 反爬原因:通过检查cookies来查看发起请求的用户是否具备相应权限,以此来进行反爬
  • 解决方案:进行模拟登陆,成功获取cookies之后在进行数据爬取

2 通过请求参数来反爬

请求参数的获取方法有很多,向服务器发送请求,很多时候需要携带请求参数,通常服务器端可以通过检查请求参数是否正确来判断是否为爬虫

2.1 通过从html静态文件中获取请求数据(github登录数据)

  • 反爬原因:通过增加获取请求参数的难度进行反爬
  • 解决方案:仔细分析抓包得到的每一个包,搞清楚请求之间的联系

2.2 通过发送请求获取请求数据

  • 反爬原因:通过增加获取请求参数的难度进行反爬
  • 解决方案:仔细分析抓包得到的每一个包,搞清楚请求之间的联系,搞清楚请求参数的来源

2.3 通过js生成请求参数

  • 反爬原理:js生成了请求参数
  • 解决方法:分析js,观察加密的实现过程,通过js2py获取js的执行结果,或者使用selenium来实现

2.4 通过验证码来反爬

  • 反爬原理:对方服务器通过弹出验证码强制验证用户浏览行为
  • 解决方法:打码平台或者是机器学习的方法识别验证码,其中打码平台廉价易用,更值得推荐
基于爬虫行为

1 基于请求频率或总请求数量

爬虫的行为与普通用户有着明显的区别,爬虫的请求频率与请求次数要远高于普通用户

1.1 通过请求ip/账号单位时间内总请求数量进行反爬

  • 反爬原理:正常浏览器请求网站,速度不会太快,同一个ip/账号大量请求了对方服务器,有更大的可能性会被识别为爬虫
  • 解决方法:对应的通过购买高质量的ip的方式能够解决问题/购买个多账号

1.2 通过同一ip/账号请求之间的间隔进行反爬

  • 反爬原理:正常人操作浏览器浏览网站,请求之间的时间间隔是随机的,而爬虫前后两个请求之间时间间隔通常比较固定同时时间间隔较短,因此可以用来做反爬
  • 解决方法:请求之间进行随机等待,模拟真实用户操作,在添加时间间隔后,为了能够高速获取数据,尽量使用代理池,如果是账号,则将账号请求之间设置随机休眠

1.3 通过对请求ip/账号每天请求次数设置阈值进行反爬

  • 反爬原理:正常的浏览行为,其一天的请求次数是有限的,通常超过某一个值,服务器就会拒绝响应
  • 解决方法:对应的通过购买高质量的ip的方法/多账号,同时设置请求间随机休眠

2 根据爬取行为进行反爬,通常在爬取步骤上做分析

2.1 通过js实现跳转来反爬

  • 反爬原理:js实现页面跳转,无法在源码中获取下一页url
  • 解决方法: 多次抓包获取条状url,分析规律

2.2 通过蜜罐(陷阱)获取爬虫ip(或者代理ip),进行反爬

  • 反爬原理:在爬虫获取链接进行请求的过程中,爬虫会根据正则,xpath,css等方式进行后续链接的提取,此时服务器端可以设置一个陷阱url,会被提取规则获取,但是正常用户无法获取,这样就能有效的区分爬虫和正常用户
  • 解决方法: 完成爬虫的编写之后,使用代理批量爬取测试/仔细分析响应内容结构,找出页面中存在的陷阱

2.3 通过假数据反爬

  • 反爬原理:向返回的响应中添加假数据污染数据库,通常假数据不会被正常用户看到
  • 解决方法: 长期运行,核对数据库中数据同实际页面中数据对应情况,如果存在问题/仔细分析响应内容

2.4 阻塞任务队列

  • 反爬原理:通过生成大量垃圾url,从而阻塞任务队列,降低爬虫的实际工作效率
  • 解决方法: 观察运行过程中请求响应状态/仔细分析源码获取垃圾url生成规则,对URL进行过滤

2.5 阻塞网络IO

  • 反爬原理:发送请求获取响应的过程实际上就是下载的过程,在任务队列中混入一个大文件的url,当爬虫在进行该请求时将会占用网络io,如果是有多线程则会占用线程
  • 解决方法: 观察爬虫运行状态/多线程对请求线程计时/发送请求钱

2.6 运维平台综合审计

  • 反爬原理:通过运维平台进行综合管理,通常采用复合型反爬虫策略,多种手段同时使用
  • 解决方法: 仔细观察分析,长期运行测试目标网站,检查数据采集速度,多方面处理
基于数据加密

1 对响应中含有的数据进行特殊化处理

通常的特殊化处理主要指的就是css数据偏移/自定义字体/数据加密/数据图片/特殊编码格式等

1.1 通过自定义字体来反爬 下图来自猫眼电影电脑版

img

  • 反爬思路: 使用自有字体文件
  • 解决思路:切换到手机版/解析字体文件进行翻译

1.2 通过css来反爬 下图来自猫眼去哪儿电脑版

img

  • 反爬思路:源码数据不为真正数据,需要通过css位移才能产生真正数据
  • 解决思路:计算css的偏移

1.3 通过js动态生成数据进行反爬

  • 反爬原理:通过js动态生成
  • 解决思路:解析关键js,获得数据生成流程,模拟生成数据

1.4 通过数据图片化反爬

1.5 通过编码格式进行反爬

  • 反爬原理: 不适用默认编码格式,在获取响应之后通常爬虫使用utf-8格式进行解码,此时解码结果将会是乱码或者报错
  • 解决思路:根据源码进行多格式解码,或者真正的解码格式

验证码处理

图片验证码

1.1 什么是图片验证码

  • 验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。

1.2 验证码的作用

  • 防止恶意破解密码、刷票、论坛灌水、刷页。有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登录尝试,实际上使用验证码是现在很多网站通行的方式(比如招商银行的网上个人银行,百度社区),我们利用比较简易的方式实现了这个功能。虽然登录麻烦一点,但是对网友的密码安全来说这个功能还是很有必要,也很重要。

1.3 图片验证码在爬虫中的使用场景

  • 注册
  • 登录
  • 频繁发送请求时,服务器弹出验证码进行验证

1.4 图片验证码的处理方案

  • 手动输入(input) 这种方法仅限于登录一次就可持续使用的情况
  • 图像识别引擎解析 使用光学识别引擎处理图片中的数据,目前常用于图片数据提取,较少用于验证码处理
  • 打码平台 爬虫常用的验证码解决方案
图片识别引擎

OCR(Optical Character Recognition)是指使用扫描仪或数码相机对文本资料进行扫描成图像文件,然后对图像文件进行分析处理,自动识别获取文字信息及版面信息的软件。

2.1 什么是tesseract

2.2 图片识别引擎环境的安装

  1. 引擎的安装

    • mac环境下直接执行命令

      1
      brew install --with-training-tools tesseract
    • windows环境下的安装 可以通过exe安装包安装,下载地址可以从GitHub项目中的wiki找到。安装完成后记得将Tesseract 执行文件的目录加入到PATH中,方便后续调用。

    • linux环境下的安装

      1
      sudo apt-get install tesseract-ocr
  2. Python库的安装

    1
    2
    3
    4
    5
    # PIL用于打开图片文件
    pip/pip3 install pillow

    # pytesseract模块用于从图片中解析数据
    pip/pip3 install pytesseract

2.3 图片识别引擎的使用

  • 通过pytesseract模块的 image_to_string 方法就能将打开的图片文件中的数据提取成字符串数据,具体方法如下
1
2
3
4
5
6
7
8
from PIL import Image
import pytesseract

im = Image.open()

result = pytesseract.image_to_string(im)

print(result)

2.4 图片识别引擎的使用扩展

  • tesseract简单使用与训练

  • 其他ocr平台

    1
    2
    3
    4
    微软Azure 图像识别:https://azure.microsoft.com/zh-cn/services/cognitive-services/computer-vision/
    有道智云文字识别:http://aidemo.youdao.com/ocrdemo
    阿里云图文识别:https://www.aliyun.com/product/cdi/
    腾讯OCR文字识别:https://cloud.tencent.com/product/ocr
打码平台

超级鹰验证码识别-专业的验证码云端识别服务,让验证码识别更快速、更准确、更强大 (chaojiying.com)

常见的验证码的种类:

1 url地址不变,验证码不变

这是验证码里面非常简单的一种类型,对应的只需要获取验证码的地址,然后请求,通过打码平台识别即可

2 url地址不变,验证码变化

这种验证码的类型是更加常见的一种类型,对于这种验证码,大家需要思考:

在登录的过程中,假设我输入的验证码是对的,对方服务器是如何判断当前我输入的验证码是显示在我屏幕上的验证码,而不是其他的验证码呢?

在获取网页的时候,请求验证码,以及提交验证码的时候,对方服务器肯定通过了某种手段验证我之前获取的验证码和最后提交的验证码是同一个验证码,那这个手段是什么手段呢?

很明显,就是通过cookie来实现的,所以对应的,在请求页面,请求验证码,提交验证码的到时候需要保证cookie的一致性,对此可以使用requests.session来解决

chrome浏览器

新建隐身窗口

浏览器中直接打开网站,会自动带上之前网站时保存的cookie,但是在爬虫中首次获取页面是没有携带cookie的,这种情况如何解决呢?

使用隐身窗口,首次打开网站,不会带上cookie,能够观察页面的获取情况,包括对方服务器如何设置cookie在本地

img

network

img

Perserve log

默认情况下,页面发生跳转之后,之前的请求url地址等信息都会消失,勾选perserve log后之前的请求都会被保留

img

filter过滤

在url地址很多的时候,可以在filter中输入部分url地址,对所有的url地址起到一定的过滤效果,具体位置在上面第二幅图中的2的位置

观察特定种类的请求

在上面第二幅图中的3的位置,有很多选项,默认是选择的all,即会观察到所有种类的请求

很多时候处于自己的目的可以选择all右边的其他选项,比如常见的选项:

  • XHR:大部分情况表示ajax请求
  • JS:js请求
  • CSS:css请求

但是很多时候我们并不能保证我们需要的请求是什么类型,特别是我们不清楚一个请求是否为ajax请求的时候,直接选择all,从前往后观察即可,其中js,css,图片等不去观察即可

不要被浏览器中的一堆请求吓到了,这些请求中除了js,css,图片的请求外,其他的请求并没有多少个

寻找登录接口

回顾之前人人网的爬虫我们找到了一个登陆接口,那么这个接口从哪里找到的呢?http://www.renren.com

寻找action对应的url地址

img

可以发现,这个地址就是在登录的form表单中action对应的url地址,回顾前端的知识点,可以发现就是进行表单提交的地址,对应的,提交的数据,仅仅需要:用户名的input标签中,name的值作为键,用户名作为值,密码的input标签中,name的值作为键,密码作为值即可

思考:如果action对应的没有url地址的时候可以怎么做?

通过抓包寻找登录的url地址

img

通过抓包可以发现,在这个url地址和请求体中均有参数,比如uniqueTimestamprkey以及加密之后的password

这个时候我们可以观察手机版的登录接口,是否也是一样的

img

可以发现在手机版中,依然有参数,但是参数的个数少一些,这个时候,我们可以使用手机版作为参考,下一节来学习如何分析js

JS解析

确定js位置

对于前面人人网的案例,我们知道了url地址中有部分参数,但是参数是如何生成的呢?

毫无疑问,参数肯定是js生成的,那么如何获取这些参数的规律呢?通过下面的学习来了解

1、观察按钮的绑定js事件

img

通过点击按钮,然后点击Event Listener,部分网站可以找到绑定的事件,对应的,只需要点击即可跳转到js的位置

2、通过search all file 来搜索

部分网站的按钮可能并没有绑定js事件监听,那么这个时候可以通过搜索请求中的关键字来找到js的位置,比如livecell

img

点击美化输出选项

img

可以继续在其中搜索关键字

img

观察js执行过程

找到js的位置之后,我们可以来通过观察js的位置,找到js具体在如何执行,后续我们可以通过python程序来模拟js的执行,或者是使用类似js2py直接把js代码转化为python程序去执行

观察js的执行过程最简单的方式是添加断点

img

添加断点的方式:在左边行号点击即可添加,对应的右边BreakPoints中会出现现有的所有断点

添加断点之后继续点击登录,每次程序在断点位置都会停止,通过如果该行有变量产生,都会把变量的结果展示在Scoope中

在上图的右上角有1,2,3三个功能,分别表示:

  • 1:继续执行到下一个断点
  • 2:进入调用的函数中
  • 3:从调用的函数中跳出来
js2py的使用

在知道了js如何生成我们想要的数据之后,那么接下来我们就需要使用程序获取js执行之后的结果了

js2py是一个js的翻译工具,也是一个通过纯python实现的js的解释器,github上源码与示例

js的执行方式大致分为两种:

  1. 在了解了js内容和执行顺序之后,通过python来完成js的执行过程,得到结果
  2. 在了解了js内容和执行顺序之后,使用类似js2py的模块来执js代码,得到结果

但是在使用python程序实现js的执行时候,需要观察的js的每一个步骤,非常麻烦,所以更多的时候我们会选择使用类似js2py的模块去执行js,接下来我们来使用js2py实现人人网登录参数的获取

具体的实现

定位进行登录js代码

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
formSubmit: function() {
var e, t = {};
$(".login").addEventListener("click", function() {
t.phoneNum = $(".phonenum").value,
t.password = $(".password").value,
e = loginValidate(t),
t.c1 = c1 || 0,
e.flag ? ajaxFunc("get", "http://activity.renren.com/livecell/rKey", "", function(e) {
var n = JSON.parse(e).data;
if (0 == n.code) {
t.password = t.password.split("").reverse().join(""),
setMaxDigits(130);
var o = new RSAKeyPair(n.e,"",n.n)
, r = encryptedString(o, t.password);
t.password = r,
t.rKey = n.rkey
} else
toast("公钥获取失败"),
t.rKey = "";
ajaxFunc("post", "http://activity.renren.com/livecell/ajax/clog", t, function(e) {
var e = JSON.parse(e).logInfo;
0 == e.code ? location.href = localStorage.getItem("url") || "" : toast(e.msg || "登录出错")
})
}) : toast(e.msg)
})
}

从代码中我们知道:

  1. 我们要登录需要对密码进行加密和获取rkey字段的值
  2. rkey字段的值我们直接发送请求rkey请求就可以获得
  3. 密码是先反转然后使用RSA进行加密, js代码很复杂, 我们希望能通过在python中执行js来实现

实现思路:

  1. 使用session发送rKey获取登录需要信息

  2. 根据获取信息对密码进行加密

    2.1 准备用户名和密码

    2.2 使用js2py生成js的执行环境:context

    2.3 拷贝使用到js文件的内容到本项目中

    2.4 读取js文件的内容,使用context来执行它们

    2.5 向context环境中添加需要数据

    2.6 使用context执行加密密码的js字符串

    2.7 通过context获取加密后密码信息

  3. 使用session发送登录请求

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
import requests
import json
import js2py

# - 实现思路:
# - 使用session发送rKey获取登录需要信息
# - url: http://activity.renren.com/livecell/rKey
# - 方法: get
# 获取session对象
session = requests.session()
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Mobile Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
"Content-Type":"application/x-www-form-urlencoded"
}
# 设置session的请求头信息
session.headers = headers

response = session.get("http://activity.renren.com/livecell/rKey")
# print(response.content.decode())
n = json.loads(response.content)['data']

# - 根据获取信息对密码进行加密
# - 准备用户名和密码
phoneNum = "131..."
password = "****"
# - 使用js2py生成js的执行环境:context
context = js2py.EvalJs()
# - 拷贝使用到js文件的内容到本项目中
# - 读取js文件的内容,使用context来执行它们
with open("BigInt.js", 'r', encoding='utf8') as f:
context.execute(f.read())

with open("RSA.js", 'r', encoding='utf8') as f:
context.execute(f.read())
with open("Barrett.js", 'r', encoding='utf8') as f:
context.execute(f.read())


# - 向context环境中添加需要数据
context.t = {'password': password}
context.n = n
# - 执行加密密码的js字符
js = '''
t.password = t.password.split("").reverse().join(""),
setMaxDigits(130);
var o = new RSAKeyPair(n.e,"",n.n)
, r = encryptedString(o, t.password);
'''
context.execute(js)
# - 通过context获取加密后密码信息
# print(context.r)
password = context.r
# - 使用session发送登录请求
# - URL: http://activity.renren.com/livecell/ajax/clog
# - 请求方法: POST
# - 数据:
# - phoneNum: 15565280933
# - password: (加密后生产的)
# - c1: 0
# - rKey: rkey请求获取的
data = {
'phoneNum': '131....',
'password': password,
'c1':0,
'rKey':n['rkey']
}

# print(session.headers)
response = session.post("http://activity.renren.com/livecell/ajax/clog", data=data)
print(response.content.decode())

# 访问登录的资源
response = session.get("http://activity.renren.com/home#profile")
print(response.content.decode())

小结

第三方模块加载js:js2py pyv8 executejs splash

布隆过滤器:url去重

文本内容去重:编辑距离,simhash

欢迎关注我的其它发布渠道