爬虫

1. 相关概念介绍

  • 解释1:通过一个程序,根据 Url 进行爬取网页,获取有用的信息
  • 解释2:使用程序模拟浏览器,去向服务器发送请求,获取响应信息

1.1 爬虫核心?

  • 爬取网页
  • 解析数据(重点)
  • 难点:爬虫与反爬虫之间的博弈

1.2 爬虫的用途

  • 数据分析/人工数据集
  • 社交软件冷启动
  • 舆情监控
  • 竞争对手监控

1.3 爬虫分类

  • 通用爬虫(不学)
    • 功能:访问网页->抓取数据->数据存储->数据除了->提供检索服务
    • 实例:百度、google等搜索引擎
    • 缺点:
      • 抓取的数据大多是无用的
      • 不能根据用户的需求来精准获取数据
  • 聚焦爬虫
    • 功能:根据需求,实现爬虫程序,抓取需要的数据
    • 设计思路:
      • 确定要爬取的url
      • 模拟浏览器通过 http 协议访问 url,获取服务器返回的 html 代码
      • 解析 html 字符串
  • 增量式爬虫
    • 检测网站中的数据更新情况,只会抓取网站中最新更新出来的数据

1.4 反爬手段

  • User-Agent:
    • 简称 UA,是一个特殊的字符串头,能识别客户使用的操作系统及版本、CPU 类型、浏览器语言、插件等等
  • 代理 IP
  • 验证码访问
  • 动态加载网页
    • 网站返回的是 js 数据,并不是真实的网页数据
  • 数据加密

2. urllib 库使用

2.1 基本使用

python 本身自带,不需要安装

  • urllib.request.rulopen() 模拟浏览器向服务器发送请求
    • response 服务器返回的数据
    • response 的数据类型是 HttpResponse
  • 字节–>字符串
    • 解码decode
  • 字符串–>字节
    • 编码encode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用 urllib 获取百度首页的源码
import urllib.request

# 1. 定义一个 url: 就是你要访问的地址
url = 'https://www.baidu.com'

# 2. 模拟浏览器向服务器发送请求
response = urllib.request.urlopen(url) # 打开网址并获取响应

# 3. 获取响应中的页面源码
# read 方法返回的是字节形式的二进制数据
# 要将二进制的数据转换为字符串
# 解码: 二进制-->字符串 编码的格式?
content = response.read().decode('utf-8')

# 4. 打印数据
print(content)

2.2 一个类型,六个方法

  • response 是 HTTPResponse 类型
  • 方法
    • read()
      • 字节形式读取二进制 扩展:rede(5)返回前几个字节
    • readline() 读取一行
    • readlines() 一行一行读取 直至结束
    • getcode() 获取状态吗
    • geturl() 获取url
    • getheaders() 获取headers
  • 代码:
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
import urllib.request

# 1. 定义一个 url: 就是你要访问的地址
url = 'https://www.baidu.com'

# 2. 模拟浏览器向服务器发送请求
response = urllib.request.urlopen(url) # 打开网址并获取响应

# 3. 一个类型和六个方法
# response 是 HTTPResponse 类型
print(type(response)) # <class 'http.client.HTTPResponse'>

# 3.1 按照一个字节一个字节去读
# content = response.read()
# content = response.read(5) # 返回 5 个字节
# print(content)

# 3.2 读取一行
# content = response.readline()
# print(content)

# 3.3 一行一行读, 直到读完, 但都还是字节
# content = response.readlines()
# print(content)

# 3.4 返回状态吗 200为成功
print(response.getcode())

# 3.5 返回你所访问的 url 地址
print(response.geturl())

# 3.6 返回状态信息
print(response.getheaders())

2.3 下载

  • 方法:urlretrieve
  • 使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import urllib.request

# 1. 下载网页
url_page = 'http://www.baidu.com'

# urlretrieve 的参数, url 代表下载的路径, filename 代表下载保存的文件的名字
urllib.request.urlretrieve(url_page, 'baidu.html')

# 2. 下载图片
url_img = 'https://tse1-mm.cn.bing.net/th/id/OIP-C.Z7jHpg5FBaGIu8DETlUVrgAAAA?w=282&h=159&c=7&r=0&o=5&dpr=1.3&pid=1.7'
urllib.request.urlretrieve(url=url_img, filename='1999.png')

# 3. 下载视频 b站的貌似不行, 换个别的吧
url_video = 'https://www.bilibili.com/97a65886-83e4-4445-a64b-e089da359f0e"'
urllib.request.urlretrieve(url_video, '1999.mp4')

2.4 请求对象的定制

  • 爬虫是模拟浏览器向服务器发送请求的过程,定制对象(UA)是一种反爬虫的手段,需要使用headers定制操作系统
    • 请求对象的定制是为了解决反爬的第一种手段
  • 找到 baidu 的 UA:
    • User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69

1695954812553

  • 语法request = urllib.request.Request()
  • 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import urllib.request

url = 'https://www.baidu.com' # 注意这里变成 https 了, 之后会遇到反爬UA, 返回数据不完整

# url 的组成
# 协议(http/https) 主机(域名/ip地址) 端口号(80/443) 路径 参数 锚点(#)

# 把 UA 写作一个字典形式
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69'
}

# 因为 urlopen 方法中不能存储字典, 所有 headers 不能传递进去
# 所以进行请求对象的定制
# 注意:因为参数顺序的问题, 不能直接写 url 和 headers, 中间还有一个 data, 所以我们需要关键字传参
request = urllib.request.Request(url=url, headers=headers)
# 传入请求对象
response = urllib.request.urlopen(request) # 成功
content = response.read().decode('utf-8')
print(content)

2.5 编解码

大一统编码:Unicode 编码

所以粘贴过来的url会变成这样https://www.baidu.com/s?wd=%E5%91%A8%E6%9D%B0%E4%BC%A6

1695955894782

2.5.1 get 请求的 quote 方法

把中文转换成unicode编码,不常用

  • 用法name = urllib.parse.quote('周杰伦')
  • 代码
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
# https://www.baidu.com/s?wd=%E5%91%A8%E6%9D%B0%E4%BC%A6
# 需求: 获取 https://www.baidu.com/s?wd=周杰伦 的网页源码
import urllib.request
import urllib.parse

url = 'https://www.baidu.com/s?wd='

# 请求对象的定制是为了解决反爬的第一种手段
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69'
}

# 将周杰伦三个字变成 unicode 编码的格式
# 需要依赖于 urllib.parse
name = urllib.parse.quote('周杰伦')
url = url + name
print(url)

request = urllib.request.Request(url=url, headers=headers)

# 模拟浏览器向服务器发送请求
response = urllib.request.urlopen(request)

# 获取响应的内容
content = response.read().decode('utf-8')

# 打印数据
print(content) # 出现了安全验证, 应该是又被反爬了

2.5.2 get 请求的 urlencode 方法

适用于多个参数的情况之下,直接定义为一个字典形式

1
2
3
4
5
6
7
8
9
base_url = 'https://www.baidu.com/s?'
data = {
'wd': '周杰伦',
'sex': '男'
}
new_data = urllib.parse.urlencode(data)
print(new_data) # wd=%E5%91%A8%E6%9D%B0%E4%BC%A6&sex=%E7%94%B7
url = base_url + new_data # 请求资源路径
print(url)

2.5.3 post 请求方式

注意:post请求的参数必须进行编码,编码之后必须调用 encode 方法 data = urllib.parse.urlencode(data).encode('utf-8')

POST的请求参数是不会拼接在url后面的,而是需要放在请求对象定制的参数中

  • 难点:找谁到底是你要的那个接口

    举例:百度翻译 https://fanyi.baidu.com/sug

    1695958780399

  • 代码实例

    • 案例1:百度翻译
    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
    # post 请求百度翻译
    import urllib.request
    import urllib.parse

    # 1. 请求地址
    url = 'https://fanyi.baidu.com/sug'

    # 2. 请求头
    headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69'
    }

    # 3. 请求参数
    data = {
    'kw': 'spider'
    }
    # post 请求的参数, 必须要进行编码
    data = urllib.parse.urlencode(data).encode('utf-8')
    print(data) # kw=spider

    # 4. 定制请求对象 post 的请求参数是不会拼接在 url 后面的, 而是需要放在请求对象定制的参数中
    request = urllib.request.Request(url=url, data=data, headers=headers)

    # 5. 模拟浏览器向服务器发送请求
    response = urllib.request.urlopen(request)

    # 6. 获取响应的数据
    content = response.read().decode('utf-8')
    print(type(content)) # <class 'str'>
    print(content) # "data":[{"k":"spider","v":"n. \u8718\u86db; \u661f\u5f62\u8f6e\uff0c\u5341\u5b57\u53c9;

    # 7. 字符串变成 json 对象, 这回就显示中文了
    import json
    obj = json.loads(content)
    print(obj) # 'data': [{'k': 'spider', 'v': 'n. 蜘蛛; 星形轮,十字叉; 带柄三脚平底锅; 三脚架'},
    • 案例2:百度详细翻译

    利用 pytharm 快速加引号,但是 url 格式注意下

    1
    2
    (.*?):(.*)
    '$1':'$2',

    1695964161361

    1695964878773

    遇到反爬,起决定性因素的是请求头中的 cookie,成功:

    1695964918990

    1695960152654

    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
    # 百度翻译之详细翻译 注意反爬
    import urllib.request
    import urllib.parse

    # 1. 请求地址
    url = 'https://fanyi.baidu.com/v2transapi?from=en&to=zh'

    # 2. 请求头
    headers = {
    # 'Accept': '*/*',
    # # 'Accept-Encoding': ' gzip, deflate, br', # 注释掉这句!
    # 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    # 'Acs-Token': '1695964467309_1695964468063_peDcqkvq6A8T3njv+K77v+KamobkMqSWfpCUJMKQYP/ChyN7W5OvAUIqMV/L2SbPjVfqcMkmR/Ns0/GUOysOeuMeG8t7YLflz8mpoHl1TFwSBWl3iAEg3+VU319melR/J9jHmt0EDdvCJn8EwNHDjWrd1Yis56PZXw2vUdC63L+f16WlARKCXxbTJrvQw5f1qssf+Z/itK3AReRKR+dAOnGWtvdbeU0DKt/HyfFwUKsnenFKcO0c0oMyRFi9/fnCXLy+HEaech+ZzZfB1oyInuQj9G9JJmbq2Qxx2WoOQYSQ4xiNlGNHgzJ8uGFwNLYNbJ6bxTGszkogMnHgYR1luX2o4CBhr+HddUEayDiT3CRsdNoXV4wFIQ13A8+JN1qHeSkpOz3+vGmEuSYnTObE+8CfSkkMUoMAvL/133QQDLpXDPsI1T0eEWNMBue+0EX6yahJB4MSd2iTKVXtlZtdkKHudQ0BfETC7EjMZh+MfIMSaHDV4vWeexbcc0rOI4PFWmwyoZIanp4rOr4LWg0y2d160OC2YNsG+WqsQa7YeHY=',
    # 'Cache-Control': 'no-cache',
    # 'Connection': 'keep-alive',
    # 'Content-Length': '133',
    # 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Cookie': 'BDUSS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BDUSS_BFESS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BIDUPSID=B26B49048A0C9099B1456DF64F0279FF; PSTM=1684546051; BAIDUID=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; PSINO=5; BAIDUID_BFESS=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BA_HECTOR=ag01ag2g8k2hak81242k81001ihcet51p; ZFY=Y15oe:ADDl8fuenODkzShBnht8X9gotT2rS2ZtSq05IQ:C; H_PS_PSSID=39323_39353_39399_39396_39407_39097_39412_39436_39358_39308_39375_39233_39406_26350_39219_22158_39427; APPGUIDE_10_6_5=1; REALTIME_TRANS_SWITCH=1; FANYI_WORD_SWITCH=1; HISTORY_SWITCH=1; SOUND_SPD_SWITCH=1; SOUND_PREFER_SWITCH=1; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1695958547; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1695964467; ab_sr=1.0.1_YTgxNjM4NjQ5Y2FjMmFjMjNmMjZhNDE1NjEwN2YxNmUyNTc0OGY1OGI3ZjMxOGFhMTJlNjBjMTM0NTc0YTI4OWVlNDIyZTUwZmIyNGYzZDdmYWIyZjQ3Y2Y2ZDc0YjVjMzcxM2MzMGVhNmE5OTZlYjA2ZjgyZTg4NTJhZThlYTJjZGE0MzBmYjRhZmUwMDBmYzU3NWU0YjY5YmFjYWQ1YmE1OGRkNzhiNGQ3Y2MwOWE4NTFlOTcxOTMxMjNlYzFl',
    # 'Host': 'fanyi.baidu.com',
    # 'Origin': 'https://fanyi.baidu.com',
    # 'Pragma': 'no-cache',
    # 'Referer': 'https://fanyi.baidu.com/?aldtype=16047',
    # 'Sec-Ch-Ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Microsoft Edge";v="116"',
    # 'Sec-Ch-Ua-Mobile': '?0',
    # 'Sec-Ch-Ua-Platform': '"Windows"',
    # 'Sec-Fetch-Dest': 'empty',
    # 'Sec-Fetch-Mode': 'cors',
    # 'Sec-Fetch-Site': 'same-origin',
    # 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69',
    # 'X-Requested-With': 'XMLHttpRequest'
    }

    # 3. 请求参数
    data = {
    'from': 'en',
    'to': 'zh',
    'query': 'math',
    'transtype': 'realtime',
    'simple_means_flag': '3',
    'sign': '965097.678616',
    'token': 'e98f8f014d0905b8705c2da2366e9207',
    'domain': 'common',
    'ts': '1695960046487'
    }
    # 编码
    data = urllib.parse.urlencode(data).encode('utf-8')

    # 4. 请求对象定制
    request = urllib.request.Request(url, data, headers)

    # 5. 模拟浏览器向服务器发送请求
    response = urllib.request.urlopen(request)

    # 6. 获取响应的数据
    content = response.read().decode('utf-8')
    print(content)

    import json

    obj = json.loads(content)
    print(obj)

2.6 Ajax 请求

2.6.1 get 请求

案例:豆瓣电影:豆瓣电影分类排行榜 - 动作片 (douban.com)

  • 先抓接口,到底谁才是第一页的数据

第一页共 20 个电影,这个接口刚好返回了 0-19 的每个电影的详细信息

1695965455172

  • 查看标头,发现是get请求

1695965528954

  • 代码编写

    • 获取豆瓣电影第一页的数据
    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
    # get 请求
    # 获取豆瓣电影第一页的数据, 并且保存起来

    import urllib.request

    url = 'https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=0&limit=20'

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

    # 1. 请求对象的定制
    request = urllib.request.Request(url=url, headers=headers)

    # 2. 获取响应数据
    response = urllib.request.urlopen(request)
    content = response.read().decode('utf-8')
    print(content)

    # 3. 下载数据到本地 文件相关知识
    # open 方法默认使用 gbk 编码, 要想保存中文就需要指定编码为 utf-8
    fp = open('douban.json', 'w', encoding='utf-8')
    fp.write(content)

    # 上面两句也可这么写
    # with open('douban1.json', 'w', encoding='utf-8') as fp:
    # fp.write(content)
    • 获取豆瓣电影前十页的数据

    难点:接口的寻找:

    https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=0&limit=20 ==> 第一页

    https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=20&limit=20 ==> 第二页

    找规律,可得:start (page-1)*20

    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
    # https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=0&limit=20
    # https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=20&limit=20
    # https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&start=40&limit=20

    # page 1 2 3 4
    # start 0 20 40 60
    # 规律:start (page-1)*20

    # 下载豆瓣电影前10页的数据
    # 1.请求对象的定制
    # 2.获取响应的数据
    # 3.下载数据到本地

    import urllib.request
    import urllib.parse

    # 封装函数实现
    # 1. 请求对象的定制
    def creat_request(page):
    # 每页的 url 不同
    base_url = "https://movie.douban.com/j/chart/top_list?type=5&interval_id=100%3A90&action=&"

    data = {
    'start': (page - 1) * 20,
    'limit': 20
    }
    data = urllib.parse.urlencode(data) # get 请求后面就不用加 encode() 了

    url = base_url + data
    print(url)

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

    request = urllib.request.Request(url=url, headers=headers)
    return request


    # 2.获取响应的数据
    def get_content(request):
    response = urllib.request.urlopen(request)
    content = response.read().decode('utf-8')
    return content


    # 3.下载数据到本地
    def down_load(page, content):
    with open('douban_' + str(page) + '.json', 'w', encoding='utf-8') as fp:
    fp.write(content)


    # 程序的入口
    if __name__ == '__main__':
    start_page = int(input('请输入起始的页码: '))
    end_page = int(input('请输入结束的页码: '))

    # 遍历
    for page in range(start_page, end_page + 1):
    # 每一页都有自己的请求对象的定制
    request = creat_request(page)

    # 获取响应数据
    content = get_content(request)

    # 下载数据
    down_load(page, content)

2.6.2 post 请求

案例:KFC 官网:肯德基餐厅信息查询 (kfc.com.cn)

  • 接口
    • 请求地址: http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname

1695967907333

  • 请求参数

1695969364799

  • 代码实现
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
# 第一页
# 请求地址: http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname
# cname: 北京
# pid:
# pageIndex: 1
# pageSize: 10

# 第二页
# 请求地址: http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname
# cname: 北京
# pid:
# pageIndex: 2
# pageSize: 10

# 实现前十页数据的获取
import urllib.request
import urllib.parse

# 1. 请求对象的定制
def creat_request(page):
# 每页的 url 不同
base_url = 'http://www.kfc.com.cn/kfccda/ashx/GetStoreList.ashx?op=cname'

data = {
'cname': '北京',
'pid': '',
'pageIndex': page,
'pageSize': '10'
}
data = urllib.parse.urlencode(data).encode('utf-8') # 编码 encode

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

request = urllib.request.Request(url=base_url, headers=headers, data=data)
return request


# 2.获取响应的数据
def get_content(request):
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
return content


# 3.下载数据到本地
def down_load(page, content):
with open('kfc_' + str(page) + '.json', 'w', encoding='utf-8') as fp:
fp.write(content)


if __name__ == '__main__':
start_page = int(input('请输入起始的页码: '))
end_page = int(input('请输入结束的页码: '))

# 循环遍历
for page in range(start_page, end_page + 1):
# 请求对象的定制
request = creat_request(page)

# 获取网页数据
content = get_content(request)

# 下载数据
down_load(page, content)

2.7 URLError\HTTPError

  • HTTPError 类是 URLError 类的子类
  • 导入的包 urllib.error.HTTPErrorurllib.error.URLError
  • 通过 urllib 发送请求的时候又可能会发送失败,这时候若想让代码更加健壮,可以通过 try-except 进行异常捕获
  • 代码示例:CSDN:https://blog.csdn.net/qq_48108092/article/details/126097408
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# https://blog.csdn.net/qq_48108092/article/details/126097408
import urllib.request
import urllib.error

url = 'https://blog.csdn.net/qq_48108092/article/details/126097408'

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

try:
request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
print(content)
except urllib.error.HTTPError: # 假设写错url, 就会报这个错
print('系统正在升级...')
except urllib.error.URLError:
print('我都说了, 系统正在升级...') # url 根本不存在?

适用场景:数据采集的时候,需要绕过登录,然后进入到某个页面

  • 个人信息页面是 utf-8, 但是还是报错编码错误, 因为没有进入到个人信息页面, 而是跳转到了登陆页面,那么登录页面不是 utf-8 所以报错
  • 什么情况下访问不成功?
    • 因为请求头的信息不够, 所以不成功
    • 如果有登录之后的 cookie, 那么我们就可以携带着 cookie 进入到登录后的任何页面
    • 请求头中还有个参数 'referer': 'https://weibo.cn/'
      • 这个可以用于判断当前路径是不是由上一个路径进来的, 一般情况下是做图片的防盗链

2.9 Handler 处理器

  • 不能定制请求头:urllib.request.urlopen(url)
  • 可以定制请求头:urllib.request.Request(url,headers,data)
  • 定制更高级的请求头:Handler
    • 动态 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
# 需求: 使用 handler 访问百度, 获取网页源码
import urllib.request

url = 'http://www.baidu.com'

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

request = urllib.request.Request(url=url, headers=headers)

# handler build_opener open
# 1. 获取 handler 对象
handler = urllib.request.HTTPHandler()

# 2. 通过 handler 获取 opener 对象
opener = urllib.request.build_opener(handler)

# 3. 调用 open 方法
response = opener.open(request)

content = response.read().decode('utf-8')
print(content)

2.10 代理服务器

  • 代理常用功能
    • 突破自身ip访问限制,访问国外站点
    • 访问一些单位或团体内部资源
      • 某大学FTP(前提是该代理地址在该资源的允许访问范围之内),使用教育网内地址免费代理服务器,就可以用于对教育网开房的各类FTP下载上传,以及各类资料查询共享等服务
    • 提高访问速度
      • 扩展:通常代理服务器都设置一个较大的硬盘缓冲区,当有外界的信息通过时,同时也将其保存到缓冲区中,当其他用户再访问相同的信息时,则直接由缓冲区取出信息,传给用户,以提高访问速度
    • 隐藏真实ip
  • 代码配置代理

创建Reuqest对象

创建ProxyHandler对象

用handler对象创建opener对象

使用opener.open函数发送请求

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
import urllib.request

url = 'https://www.baidu.com/s?wd=ip'

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69',
'Cookie': 'BDUSS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BDUSS_BFESS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BIDUPSID=B26B49048A0C9099B1456DF64F0279FF; PSTM=1684546051; BAIDUID=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BD_UPN=12314753; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; BD_CK_SAM=1; PSINO=5; BAIDUID_BFESS=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BA_HECTOR=ag01ag2g8k2hak81242k81001ihcet51p; ZFY=Y15oe:ADDl8fuenODkzShBnht8X9gotT2rS2ZtSq05IQ:C; B64_BOT=1; sug=3; sugstore=0; ORIGIN=2; bdime=21111; ab_sr=1.0.1_MWIwMmYyMmFmM2YxN2QyZGJjMTQ4YTRlNDU5NDBhY2YzNDY0OGIyNDFjZDcyOGIxNjBlNzZlZTIwYWUxMTk3MjBkNTMxMDhjOTU3ZmRmOWNlZDA3MjEwMjg1MzZlMzZkMmEyNWIzMjUxNWVlODE1YjRhOTAyYzllZTgzZDk0Yzc1MDg0YWE5ZTRlOWJkYzk1Yjg5N2M4ZTk3NDRjNWQ3MmNlOGI1ZGIyNGMxMzkzZDY4ZWQyNTA1MWIxZjZkOGEy; RT="z=1&dm=baidu.com&si=bee5a4dd-dfb0-43a8-8226-878c0904dd53&ss=ln47d4cw&sl=0&tt=0&bcn=https%3A%2F%2Ffclog.baidu.com%2Flog%2Fweirwood%3Ftype%3Dperf&ul=1xd&hd=1xx"; H_PS_PSSID=39323_39353_39399_39396_39407_39097_39412_39436_39358_39308_39375_39233_39406_26350_39219_22158_39427; COOKIE_SESSION=0_0_0_0_1_0_1_0_0_0_0_0_0_0_3_0_1695967725_0_1695967722%7C1%230_0_1695967722%7C1'
}

# 请求对象定制
request = urllib.request.Request(url=url, headers=headers)

# 模拟浏览器访问服务器
# response=urllib.request.urlopen(request)

# 使用代理 ip 进行访问, 以字典形式存在
proxies = {
'http': '221.4.241.198:9091'
}
# handler build_opener open
handler = urllib.request.ProxyHandler(proxies=proxies)
opener = urllib.request.build_opener(handler)
response = opener.open(request)

# 获得响应信息
content = response.read().decode('utf-8')

# 保存本地
with open('proxy.html', 'w', encoding='utf-8') as fp:
fp.write(content)

解析

之前在urlib的学习中,我们能将网页的网页源码爬取下来。但是我们我们仅仅需要其中的部分数据,此时就需要引入新的概念——解析。

目前使用最多的解析方法包括xpath、JsonPath、BeautifulSoup等。

参考:Python爬虫的解析(学习于b站尚硅谷)_知乎云烟的博客-CSDN博客

1. xpath

1.1 xpath 插件安装

  • 如果是在 google 浏览器上

    • 就直接将网盘里的 xpath.zip 拖到扩展程序里面

    1695974655310

    1695974736550

    • 然后打开任意一个网页,按住快捷键 ctrl+shift+x 即可出现调试工具

    1695974754513

  • 如果是在 edge 浏览器上,参考教程:在Edge中使用Xpath——更改快捷键_edge xpath_鹤行川.的博客-CSDN博客

    • 由于Xpath的快捷键 Ctrl+Shift+X 已经被一个叫做Web选择的功能占用(这个功能可以复制不让复制的页面内容!!震惊,才知道!),所以先下载下来修改快捷键后的 xpath 版本

      1695975202488

    • 然后打开 edge 的扩展功能,同时开启开发人员模式,解压后拖入即可,使用快捷键 ctrl+alt+X 即可打开调试窗口,注意插件的文件不要乱移位置

      1695975389083

      1695985804035

1.2 xpath 基本使用

1.2.1 lxml 库的安装

  • 还需要安装一个库 lxml 才能用:

    pip install lxml -i https://pypi.douban.com/simple

  • 这个库的安装路径需要在你目前项目所用的 python 解释器的目录下边

1695975654857

  • 输入命令进行安装
1
2
cd Scripts
pip install lxml

1695975901027

1695976053768

  • 导入验证:from lxml import etree

1.2.2 xpath 解析

  • 解析什么
    • 本地文件
      • etree.parse('XX.html')
    • 服务器响应的数据
      • etree.HTML(response.read().decode('utf‐8')
      • 实际这种情况用的多
  • xpath 基本语法

1695976852724

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

# xpath 解析
# 1. 本地文件 etree.parse('XX.html')
# 2. 服务器响应的数据 etree.HTML(response.read().decode('utf‐8') 实际这种情况用的多

# 1. xpath 解析本地文件
tree = etree.parse('01_xpath.html')

# 补充: xpath 语法
# (1) tree.xpath('xpath路径') `//`代表子孙, `/`代表孙子
# eg1. 查找 ul 下面的 li
li_list = tree.xpath('//body/ul/li') # 此处也可以写 //body//li
print(len(li_list)) # 判断列表的长度

# (2) 谓词查询 `//div[@id]` `//div[@id="maincontent"]`
# eg2.1 查找所有有 id 属性的 li 标签
# test() 可用于获取标签中的内容
li_list = tree.xpath('//ul/li[@id]/text()')
print(li_list) # ['北京', '上海']
print(len(li_list)) # 2
# eg2.2 查找id为l1的li标签, 注意引号的问题
li_list = tree.xpath('//ul/li[@id="l1"]/text()')
print(li_list) # ['北京']

# (3) 属性查询
# eg3. 查找id为l1的li标签的class的属性值
li = tree.xpath('//ul/li[@id="l1"]/@class')

# (4) 模糊查询(用的不多)
# eg4.1 查询id中包含l的li标签
li_list = tree.xpath('//ul/li[contains(@id, "l")]/text()')
print(li_list) # ['北京', '上海']
# eg4.2 查询id的值以c开头的li标签
li_list = tree.xpath('//ul/li[starts-with(@id, "c")]/text()')

1.3 获取百度页面的 百度一下 四个字

  • 先获取页面的源码,找到 百度一下 四个字的所在位置

<span class="s_btn_wr"><input type="submit" id="su" value="百度一下" class="bg s_btn">

1695978716209

  • 用xpath插件进行调试,可以找到合适的获得我们想要的数据的路径

1695979216574

  • 代码实现
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
# 1. 获取网页的源码
# 2. 解析 解析的是服务器响应的文件, 用 etree.HTML()
# 3. 打印

import urllib.request

# 1. 获取网页源码
url = 'https://www.baidu.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69',
'Cookie': 'BDUSS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BDUSS_BFESS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BIDUPSID=B26B49048A0C9099B1456DF64F0279FF; PSTM=1684546051; BAIDUID=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BD_UPN=12314753; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; BD_CK_SAM=1; PSINO=5; BAIDUID_BFESS=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BA_HECTOR=ag01ag2g8k2hak81242k81001ihcet51p; B64_BOT=1; sug=3; sugstore=0; ORIGIN=2; bdime=21111; RT="z=1&dm=baidu.com&si=bee5a4dd-dfb0-43a8-8226-878c0904dd53&ss=ln47d4cw&sl=0&tt=0&bcn=https%3A%2F%2Ffclog.baidu.com%2Flog%2Fweirwood%3Ftype%3Dperf&ul=1xd&hd=1xx"; COOKIE_SESSION=0_0_0_0_1_0_1_0_0_0_0_0_0_0_3_0_1695967725_0_1695967722%7C1%230_0_1695967722%7C1; ZFY=jKLYva:AwW:AlQpwrTh6Gt4SREDbR7NGeps2ei6L3zLXg:C; H_PS_PSSID=39323_39353_39399_39396_39407_39097_39412_39436_39358_39308_39375_39233_39406_26350_39219_22158_39427; baikeVisitId=50aa753e-979e-40e9-b6dc-b1730a994990; ab_sr=1.0.1_MDJlMTU2MDk2ZWFlYTMyNmQ2Njc5OWJjYzIwOGJjZDE2MjUwOWQyOTMxMDc5N2I2ZmJhZjczMjkyZjg5YzQ0MjAxYzUyZGEwZDU5MGY5NmZmNmMzZDgwNDkwOGQ5YTBmNTg2MGQ5ZjE0ZWVkNmUxMjUzZGY3NDU4MzZhYTU3NjMzYmVmOTg1NzZkNTQ4OTJkZjlhNThkZjAxNzExMTBjOTAxODlmODI3Nzg3ZGQwNGVkMzc1M2JkOWI5NTNlNmQ2'
}
# 1.1 请求对象的定制
request = urllib.request.Request(url=url, headers=headers)
# 1.2 模拟浏览器访问服务器
response = urllib.request.urlopen(request)
# 1.3 获取网页源码
content = response.read().decode('utf-8')
# print(content)

# 2. 解析网页源码, 获取我们想要的数据
from lxml import etree

# 2.1 解析服务器响应的文件
tree = etree.HTML(content)
# 2.2 获取想要的数据 用那个xpath插件调试寻找正确的路径 xpath的返回值是一个列表类型的数据, 所以加上[0], 即可得到纯正的四个大字
result = tree.xpath('//input[@id="su"]/@value')[0] # 百度一下

print(result)

1.4 站长素材(含懒加载、如何下载其中的高清图)

  • 网址:https://sc.chinaz.com/

  • xpath 调试获取图片的地址和alt值,但是这里好像返回的和页面中的不太一样,最后输出的结果为空,还是直接打印出 content 之后用 ctrl+F 查找吧

    1695984242783

    1695984267881

//div[@class='item masonry-brick']/img/@data-original

1695983936153

//div[@class='item masonry-brick']/img/@alt

1695983993986

  • 代码实现
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
# 1. 请求对象的定制
# 2. 获取网页源码
# 3. 下载

# 需求: 下载前十页的图片
# 第一页的地址
# https://sc.chinaz.com/tupian/taikongkexuetupian.html

# 第二页的地址
# https://sc.chinaz.com/tupian/taikongkexuetupian_2.html

# 分析: 除了第一页和其他页不一样, 其他页的末尾都是 _page
import urllib.request
from lxml import etree

# 1. 请求对象的定制
def creat_request(page):
if page == 1:
url = 'https://sc.chinaz.com/tupian/taikongkexuetupian.html'
else:
# 每页的 url 不同
url = 'https://sc.chinaz.com/tupian/taikongkexuetupian_' + str(page) + '.html'
print(url)

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

request = urllib.request.Request(url=url, headers=headers)
return request


# 2.获取网页的源码
def get_content(request):
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
# print(content)
return content


# 3.下载数据到本地
def down_load(content):
# 下载图片, 需要图片地址(在网页源码中获取)和文件的名字(在 alt 中获取)
# 那么就需要用到 xpath 进行解析
tree = etree.HTML(content)
# 获取 alt 属性
name_list = tree.xpath('//div[@class="item"]/img/@alt')
# 获取图片地址, 此处注意有的设计图片的网站会进行懒加载, 原先属性名可能是src2,需要等待你滑倒那个位置才会变成src,所以要用src2去获取
src_list = tree.xpath('//div[@class="item"]/img/@data-original')
# print(len(src_list)) # 40
# print(len(name_list)) # 40

# 遍历下载每一张图片
for i in range(len(name_list)):
name = name_list[i]
src = src_list[i]
# print(name, src) # 冬日唯美星空图片 //scpic2.chinaz.net/files/default/imgs/2023-03-01/0b5c5cf3cc6bb41d_s.jpg
# 注意返回的地址前面省略了 https: 需要补上才可以下载
url = 'https:' + src
print(name, url)

urllib.request.urlretrieve(url, filename='./img/' + name + '.jpg')


if __name__ == '__main__':
start_page = int(input('请输入起始的页码: '))
end_page = int(input('请输入结束的页码: '))

# 遍历
for page in range(start_page, end_page + 1):
# 1. 请求对象的定制
request = creat_request(page)

# 2. 获取网页的源码
content = get_content(request)

# 下载数据
down_load(content)

2. JsonPath

JsonPath 适用于解析网页源码的返回值为Json数据的网站

比如打开“淘票票”:https://dianying.taobao.com/,按F12 打开检查,点到网络。然后点击“淘票票”中的城市,会得到一个网络包,发现它是一个Json数据。后面我们将爬取该数据包存储的淘票票支持的城市

2.1 基本介绍

2.1.1 安装及使用

jsonpath 只能解析本地文件,不能解析服务器响应的文件

  • pip 安装:pip install jsonpath

1695985926957

  • jsonpath 的使用:
    • 导入 import jsonpath
    • obj = json.load(open('json文件', 'r', encoding='utf-8'))
    • ret = jsonpath.jsonpath(obj, 'jsonpath语法')

2.1.2 基本语法(与 xpath 对比)

参考:JSONPath-简单入门-CSDN博客

1695985322714

2.1.3 基本使用

  • 已知有如下的json文件
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
{
"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
}
}
}
  • 用jsonpath代码实现爬取数据:

1695986122236

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
import json
import jsonpath

# 读取本地的 json 文件
obj = json.load(open('04_jsonpath_store.json', 'r', encoding='utf-8'))
print(obj)

# 1. 爬取书店所有书的作者, 如果只要特定的第几本书, 可以这样写, 如第一本书: '$.store.book[0].author'
author_list = jsonpath.jsonpath(obj, '$.store.book[*].author')
print(author_list) # ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien']

# 2. 所有的作者
author_list = jsonpath.jsonpath(obj, '$..author')
print(author_list)

# 3. store 下面的所有的元素
tag_list = jsonpath.jsonpath(obj, '$.store.*')
print(tag_list)

# 4. store 里面所有东西的 price
price_list = jsonpath.jsonpath(obj, '$.store..price')
print(price_list) # [8.95, 12.99, 8.99, 22.99, 19.95]

# 5. 第三个书
book = jsonpath.jsonpath(obj, '$..book[2]')
print(book)
# [{'category': 'fiction', 'author': 'Herman Melville', 'title': 'Moby Dick', 'isbn': '0-553-21311-3', 'price': 8.99}]

# 6. 最后一本书
book = jsonpath.jsonpath(obj, '$..book[(@.length-1)]')
print(book)
# [{'category': 'fiction', 'author': 'J. R. R. Tolkien', 'title': 'The Lord of the Rings', 'isbn': '0-395-19395-8', 'price': 22.99}]

# 7. 前两本书
print('-------')
# 注意这里[0,1]之间不可以有空格
book_list = jsonpath.jsonpath(obj, '$..book[0,1]')
# 或者 book_list = jsonpath.jsonpath(obj, '$..book[:2]')
print(book_list)

# 8. 过滤出所有包含版本号 isbn 的书(条件过滤, 需要加个问号)
book_list = jsonpath.jsonpath(obj, '$..book[?(@.isbn)]')
print(book_list)

# 9. 哪本书超过了10块钱
book_list = jsonpath.jsonpath(obj, '$..book[?(@.price>10)]')
print(book_list)

2.2 JsonPath 解析淘票票

  • 网址:https://dianying.taobao.com/

  • 需求:获取所有的能买票的城市信息

    • 找接口,看下会不会有些反爬,请求地址:

      https://dianying.taobao.com/cityAction.json?activityId&_ksTS=1695987569488_108&jsoncallback=jsonp109&action=cityAction&n_s=new&event_submit_doGetAllRegion=true

      1695987733171

    • 访问一下,发现没给我们数据,应该是做了限制,不仅仅只校验UA,待会在请求头里我们还要给它点东西

      1695987705920

    • 复制所有请求头,尝试下

  • 代码实现

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
import urllib.request

url = 'https://dianying.taobao.com/cityAction.json?activityId&_ksTS=1695987569488_108&jsoncallback=jsonp109&action=cityAction&n_s=new&event_submit_doGetAllRegion=true'

headers = {
'Accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01',
# 'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Bx-V': '2.5.3',
'Cache-Control': 'no-cache',
'Cookie': 't=7cec2d4496f4b4f1e67f2b3781af9f07; cookie2=1a7dff465bd9981f135e2c3fa2c17538; v=0; _tb_token_=e17995433ee3e; cna=IJqeHFQN328BASQOBHM61WKq; xlly_s=1; tfstk=d499nsXEs20geUMNDShngOneRDnntdKZjF-7nZbgGeLpzFnNi5AMMrQpVc7igf8v9ETFsN9v0-IXcEoNoclHbhWVh40kkYxwbMatr4VCK8uh3tgorW2f_YB2ySUCsTrU--aUYRvMHGG4F89vkS6fX1QYnwex8mjO6dx1J4g5Qo9keZM_raI01DnLgS51YN4Rrb1..; l=fBIl_TtePNX1FeifBOfwnurza77OhIRAguPzaNbMi9fP_JCH5QTlW1HPceLMCnGVFsewR3rNfwdWBeYBqC2sjqj4axom45HmnmOk-Wf..; isg=BGBg3JJQEbUhTK2xJ5kiT7IFMW4yaUQz8Y4DhdpxQnsO1QD_gnjDw3rrbX3V5fwL',
'Pragma': 'no-cache',
'Referer': 'https://dianying.taobao.com/',
'Sec-Ch-Ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Microsoft Edge";v="116"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69',
'X-Requested-With': 'XMLHttpRequest'
}

request = urllib.request.Request(url=url, headers=headers)
response = urllib.request.urlopen(request)

content = response.read().decode('utf-8')
print(content) # 获取到的 json 文件前面有 jsonp109({"returnCode":"0","ret...
# 也就是 jsonp, jsonp 是跨域的一种解决方案, 但是我们不要前面的这个以及末尾对应的圆括号
# 利用 split 对获取到的 json 串进行开头和末尾的切割
content = content.split('(')[1].split(')')[0]

with open('05_jsonpath_ticket.json', 'w', encoding='utf-8') as fp:
fp.write(content)

import json
import jsonpath

# 将前面下载下来的json数据的本地打开文件, 然后再用jsonpath进行筛选想要的数据
obj = json.load(open('05_jsonpath_ticket.json', 'r', encoding='utf-8'))

city_list = jsonpath.jsonpath(obj, '$..regionName')
print(city_list) # 成功获取所有的城市
  • 成功获取到数据

1695988630074

  • 然后用 jsonpath 对保存到本地的json文件进行解析,筛选我们想要的所有的城市列表:

1695988930026

3. BeautifulSoup(即bs4)

和 xpath 是同一重量级的技术

3.1 基本使用

3.1.1 简介

  • 和 lxml 一样,是一个 html 的解析器,主要功能也是解析和提取数据

  • 优缺

    • 缺点:效率没有 lxml 高

    这缺点感觉有点不太可

    • 优点:接口设计人性化,使用方便

3.1.2 安装及创建

  • 安装:pip install bs4

1695989570174

  • 导入:from bs4 import BeautifulSoup
  • 创建对象:
    • 服务器响应的文件生成对象:soup = BeautifulSoup(response.read().decode(), 'lxml')
    • 本地文件生成对象:soup = BeautifulSoup(open('1.html'), 'lxml')

3.1.3 节点定位

1695990102206

1695990133905

3.1.4 节点信息

1695990148101

3.1.5 代码演示

  • html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<div>
<ul>
<li id="l1">张三</li>
<li id="l2">李四</li>
<li>王五</li>
<a href="" class="a1">thr</a>
<span>hhh</span>
</ul>
</div>
<a href="" title="a2">百度</a>

<div id="d1">
<span>
hahaha
</span>
</div>

<p id="p1" class="p1">hehehe</p>
</body>
  • py
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
from bs4 import BeautifulSoup

# 通过解析本地文件来学习bs4的基础语法
# 记得指定编码格式, 默认打开的文件编码格式是 gbk
soup = BeautifulSoup(open('06_bs4.html', encoding='utf-8'), 'lxml')
# print(soup)

# (一) 节点定位
# 1. 根据标签名字查找节点
# 找到的是第一个符合条件的数据
print(soup.a) # <a href="">thr</a>
# 获取标签的属性和属性值
print(soup.a.attrs) # {'href': ''}

# 2. bs4 的一些函数
# 2.1 find
# 2.1.1 返回第一个符合条件的数据
print(soup.find('a')) # <a href="">thr</a>
# 2.1.2 根据属性的值找到对应的标签对象
print(soup.find('a', title='a2'))
# 2.1.3 由于 class 与关键字冲突, 因此加个下划线以示区别
print(soup.find('a', class_='a1'))

# 2.2 find_all
# 2.2.1 返回包含所有符合条件的标签对象的列表
print(soup.find_all('a')) # [<a class="a1" href="">thr</a>, <a href="" title="a2">百度</a>]
# 2.2.2 如果想获取的是多个标签的数据, 需要在find_all参数中添加列表的数据
print(soup.find_all(['a', 'span']))
# 2.2.3 limit 参数可以查找前几个数据
print(soup.find_all('li', limit=2)) # [<li>张三</li>, <li>李四</li>]

# 2.3 select(推荐, 但一般情况下会结合上面两种使用)
# 2.3.1 select 方法返回的是一个列表 并且返回多个数据
print(soup.select('a')) # [<a class="a1" href="">thr</a>, <a href="" title="a2">百度</a>]
# 2.3.2 可以通过 . 代表 class, 用类选择器的方法进行筛选
print(soup.select('.a1')) # <a class="a1" href="">thr</a>]
# 2.3.3 也可以用属性选择器进行筛选和查找(用的多些)
print(soup.select('#l1')) # [<li id="l1">张三</li>]
# 2.3.4.1 查找 li 标签中有 id 属性的标签
print(soup.select('li[id]')) # [<li id="l1">张三</li>, <li id="l2">李四</li>]
# 2.3.4.2 查找 li 标签中 id 为 l2 的标签
print(soup.select('li[id="l2"]')) # [<li id="l2">李四</li>]
# 2.3.5 层级选择器 获取div下面的li的方式
# 2.3.5.1 后代选择器
print(soup.select('div li')) # [<li id="l1">张三</li>, <li id="l2">李四</li>, <li>王五</li>]
# 2.3.5.2 子代选择器 注意: 很多的计算机编程语言中, 如果不加空格不会输出内容, 但在bs4中, 不会报错, 也会显示内容
print(soup.select('div > ul > li'))
# 2.3.2.3 找到a标签和li标签所有的对象
print(soup.select('a,li')) # [<li id="l1">张三</li>, <li id="l2">李四</li>, <li>王五</li>, <a class="a1" href="">thr</a>, <a href="" title="a2">百度</a>]

# (二) 节点信息
# 1. 获取节点内容
obj = soup.select('#d1')[0] # 返回一个列表
# 如果标签对象中只有内容, 那么 string 和 get_text() 都可以使用
# 但是如果标签对象中除了内容还有标签, 那么 string 就获取不到数据, 而 get_text() 是可以获取数据的
# 一般情况下推荐 get_text()
print(obj.string) # None
print(obj.get_text()) # hahaha

# 2. 节点的属性
# 注意 select 返回的是列表!!
obj = soup.select('#p1')[0]
# name 是标签的名字
print(obj.name) # p
# attrs 是将属性值作为字典返回
print(obj.attrs) # {'id': 'p1', 'class': ['p1']}

# 3. 获取节点属性 3中方式
obj = soup.select('#p1')[0]
print(obj.attrs.get('class')) # ['p1'] 推荐这种
print(obj.get('class')) # ['p1']
print(obj['class']) # ['p1']

3.2 bs4 爬取星巴克数据

主要学习如何抓取服务器响应的文件

把菜单部分的图片以及产品的名字抓取并保存:

https://www.starbucks.com.cn/menu/

  • 抓接口,看下只有一页,比较容易,找到接口:

    https://www.starbucks.com.cn/menu/

1696036608147

  • 先写 xpath 语法,再改成 bs4

1696037282802

  • 抓取图片有点难,需要分析并拼接url

1696038072885

1696038098710

  • 代码示例:
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
import urllib.request

url = 'https://www.starbucks.com.cn/menu/'

response = urllib.request.urlopen(url)
content = response.read().decode('utf-8')
# print(content) # 没做反爬, 直接拿到源码

# 进行解析
from bs4 import BeautifulSoup

# 1. 加载服务器响应文件
soup = BeautifulSoup(content, 'lxml')

# 2. xpath 语法: //ul[@class="grid padded-3 product"]//strong/text()
name_list = soup.select('ul[class="grid padded-3 product"] strong')

for name in name_list:
print(name.string)

# 爬取图片
pic_list = soup.select('ul[class="grid padded-3 product"] div[class="preview circle"]')
for pic in pic_list:
# 大佬的答案, 好强!!
completePicUrl = 'https://www.starbucks.com.cn'+pic.attrs.get('style').split('url("')[1].split('")')[0]
print(completePicUrl)

Selenium

中文意思:[化学]硒

1. Selenium

1.1 介绍

  • 什么是 Selenium
    • 是一个用于 Web 应用程序测试的工具
    • 其测试直接运行在浏览器中,就像真正的用户在操作一样
    • 支持通过各种 driver 驱动真实浏览器完成测试
    • 也支持无界面浏览器操作
  • 为什么使用 selenium
    • 模拟浏览器功能,自动执行网页中的js代码,实现动态加载
  • 缺点:原生selenium有点慢,之后加东西使它效率变快
  • 代码演示

本次将要演示urllib获取京东的网页源码,从而说明使用的urllib获取京东的网页源码会缺失秒杀的一些数据,进而引入下一节将要使用的selenium

1
2
3
4
5
6
7
import urllib.request

url = "https://www.jd.com/"

response = urllib.request.urlopen(url)
content = response.read().decode('utf-8')
print(content)

1696400628402

1696400643464

使用的urllib获取京东的网页源码,搜索秒杀中的数据,发现确实是少了秒杀的内容,因此,下一节将学习并说明selenium能驱动真实浏览器去获取数据,不会缺少内容

1.2 基本使用

1.2.1 安装 selenium

  • 谷歌浏览器

    • 驱动地址下载:http://chromedriver.storage.googleapis.com/index.html

      1696401211274

    • 谷歌驱动和谷歌浏览器版本之间的映射表:http://blog.csdn.net/huilan_same/article/details/51896672

    • 查看谷歌浏览器版本:谷歌浏览器右上角–>帮助–>关于

      貌似最高才 114,我这 116 的它还没有,暂时放弃

    1696401112871

    • pip install selenium
  • 使用 Edge 浏览器

    • 同理,先看浏览器版本号(帮助与反馈->关于),再去 https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ 中下载

      1696401400568

      1696401510188

    • 解压文件:

    1696401624552

    • 放在代码目录下并安装:

    注意selenium版本和urllib3版本不兼容的问题!

    弹幕大佬建议:在selenium后面加个==3.141.0,否则后面会因为下载的selenium版本过高操作不同,个人认为高版本不好用,用法还得自己找

    1696401889003

    1696401875922

1.2.2 版本兼容问题解决

  • 由于版本不兼容的问题,这里还需要改下,我改了两个位置
    • selenium 改成 3.141.0 版本
    • urllib3 改成 1.2.6.2 版本
  • 修改参考如下步骤:

1696402920101

1.2.3 代码演示

  • 访问百度
1
2
3
4
5
6
7
8
9
10
11
12
# 1. 导入 selenium
from selenium import webdriver

# 2. 创建浏览器操作对象
path = "msedgedriver.exe" # 驱动文件的路径
browser = webdriver.Edge(path) # 创建浏览器

# 3. 访问网站
# 访问地址
url = 'https://www.baidu.com'
# 浏览器打开网址
browser.get(url)

1696403107071

  • 访问京东并获取带有秒杀界面的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 导入 selenium
from selenium import webdriver

# 2. 创建浏览器操作对象
path = 'msedgedriver.exe' # 驱动文件的路径
browser = webdriver.Edge(path) # 创建浏览器

# 3. 访问网站
url = 'https://www.jd.com'
browser.get(url)
# page_source 获取网页源码
content = browser.page_source
print(content)

1696403258751

1.3 元素定位

如果我们需要使用程序在百度(https://www.baidu.com/)中输入“周杰伦”,然后点击“百度一下”,会跳到一个新的页面。其中,使用程序找到“百度一下”的过程称为元素定位

1.3.1 元素定位的定义与方法

1696403449710

1.3.2 代码演示

不过这是老版本的,新版本好像把这些都结合成一个方法了

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
from selenium import webdriver

# 创建浏览器操作对象
path = 'msedgedriver.exe' # 驱动文件的路径
browser = webdriver.Edge(path) # 创建浏览器

# 3. 访问网站
url = 'https://www.baidu.com'
browser.get(url)

# 元素定位
# 1. 根据 id 找到对象 <input type="submit" id="su" value="百度一下" class="bg s_btn">
button = browser.find_element_by_id('su')
print(button) # <selenium.webdriver.remote.webelement.WebElement (session="f8da6a0d36bcf1b411dbbd04fb181d99", element="1F0CAAC086FFF88546C0873E34B579E2_element_6")>

# 2. 根据标签属性的属性值来获取对象 <input id="kw" name="wd" class="s_ipt" value="" maxlength="255" autocomplete="off">
button = browser.find_element_by_name('wd')
print(button) # selenium.webdriver.remote.webelement.WebElement (session="0ec1958b0e548f1ff0d1c001c0b604f2", element="857A99072AE0926284925BCCD1F8543E_element_5")>

# 3. 根据 xpath 语句获取对象
button = browser.find_elements_by_xpath('//input[@id="su"]')
print(button) # [<selenium.webdriver.remote.webelement.WebElement (session="e8f80626515e815272fc55fb2e8bf237", element="663F3737137A55001C2D941F6E5BCB8F_element_6")>]

# 4. 根据标签的名字来获取对象
button = browser.find_elements_by_tag_name('input')
print(button)

# 5. 使用 bs4 的语法来获取对象
button = browser.find_elements_by_css_selector('#su')
print(button)

# 6. 根据链接的文本来获取对象
button = browser.find_element_by_link_text('新闻')
print(button) # <selenium.webdriver.remote.webelement.WebElement (session="9f4847fd6b8da07b16d46fa265b7047c", element="E9A97B94E3D31089587AAF9916E657BF_element_25")>

1.4 元素信息

1.4.1 访问元素信息

  • 获取元素属性: .get_attribute('class')
  • 获取元素文本:text
  • 获取标签名:tag_name

1.4.2 代码演示

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

path = 'msedgedriver.exe'
browser = webdriver.Edge(path)

url = 'http://www.baidu.com'
browser.get(url)

# selenium 访问元素信息
input = browser.find_element_by_id('su')
# 1. 获取元素属性
print(input.get_attribute('class')) # bg s_btn
# 2. 获取标签名
print(input.tag_name) # input
# 3. 获取元素文本
a = browser.find_element_by_link_text('新闻')
print(a.text) # 新闻

1.5 selenium 的交互

1.5.1 交互

  • 点击:click()
  • 输入:send_keys()
  • 后退操作:browser.back()
  • 前进操作:browser.forword()
  • 模拟JS滚动: js='document.documentElement.scrollTop=100000'   browser.execute_script(js)
  • 执行js代a码 获取网页代码:page_source
  • 退出:browser.quit()

1.5.2 代码演示

本次需要通过程序使浏览器使用百度搜索“周杰伦”,然后点到第2页,再使用一下后退、前进操作,然后再滚动到页末

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
from selenium import webdriver

# 创建浏览器对象
path = 'msedgedriver.exe'
browser = webdriver.Edge(path)

# url
url = 'http://www.baidu.com'
browser.get(url)

import time
time.sleep(2) # 睡眠两秒

# 获取文本框的对象
input = browser.find_element_by_id('kw')
# 在文本框中输入文本
input.send_keys('周杰伦')
time.sleep(2)

# 获取点击按钮
button = browser.find_element_by_id('su')
# 点击按钮
button.click()
time.sleep(2)

# 滑倒顶部
js_bottom = 'document.documentElement.scrollTop=100000'
browser.execute_script(js_bottom)
time.sleep(2)

# 获取下一页的按钮
next = browser.find_element_by_xpath('//a[@class="n"]')
# 点击下一页
next.click()
time.sleep(2)

# 回到上一页
browser.back()
time.sleep(2)

# 回去
browser.forward()
time.sleep(3)

# 退出
browser.quit()

2. Phantomjs

在前面的学习中,发现Selenium,每次执行过程中都需打开浏览器、关闭浏览器、中间还有一堆操作,这是因为它有页面,而页面里面会有js、css等等很多文件,因此打开页面会导致代码的性能很慢

因此提出Phantomjs、Chrome handless,目前Phantomjs已逐渐淘汰,这里就不学了我

2.1 介绍

  • 是一个无界面浏览器
  • 支持页面元素查找,js 的执行等
  • 由于不进行 css 和 gui 渲染,运行效率比真实的浏览器要快的多

2.2 如何使用 Phantomjs

  • 获取Phantomjs.exe文件路径path
  • browser= webdriver.PhantomJs(path)
  • browser.get(url)

扩展:保存屏幕快照:browser.save_screenshot(‘baidu.png’)

3. Chrome handless

  • 基本配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from selenium import webdriver  # 导入selenium库
from selenium.webdriver.chrome.options import Options # 导入浏览器设置相关的类

# 无可视化界面设置
chrome_options = Options()
# 使用无头模式
chrome_options.add_argument('--headless')
# 禁用GPU,防止无头模式出现莫名的BUG
chrome_options.add_argument('--disable-gpu')

path = r'C:\Program Files\Google\Chrome\Application\chrome.exe'
chrome_options.binary_location = path

browser = webdriver.Chrome(chrome_options=chrome_options)
browser.get('http://www/baidu.com/')

requests

只属于 python?其他编程语言没有?

1. 基本使用

  • 文档

    • 官方文档: https://requests.readthedocs.io/projects/cn/zh_CN/latest
    • 快速上手 https://requests.readthedocs.io/projects/cn/zh_CN/latest/user/quickstart.html
  • 安装

    • pip install requests

    1696039537783

  • response 的属性以及类型

类型 models.Response
r.text 获取网站源码
r.encoding 访问或定制编码方式
r.url 获取请求的url
r.content 响应的字节类型
r.status_code 响应的状态码
r.headers 响应的头信息
  • 代码示例
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
import requests

url = 'http://www.baidu.com'
response = requests.get(url=url)

# 一个类型和六个属性
# Response 类型, 和 urllib 不一样(HTTPResponse类型)
print(type(response)) # <class 'requests.models.Response'>

# 六个属性
# 1. encoding 设置响应的编码格式
response.encoding = 'utf-8'

# 2. text 以字符串形式返回网页的源码
print(response.text)

# 3. 获取请求的 url
print(response.url)

# 4. 返回的是二进制的数据
print(response.content) # b'<!DOCTYPE html>\r\n<!--STATUS OK--...

# 5. 返回响应的状态码
print(response.status_code) # 200

# 6. 获取响应头信息
print(response.headers) # {'Cache-Control': 'private, no-cache, no-store...

2. get 请求

  • 总结
    • 参数使用 params 传递
    • 参数无需 urlencode 编码
    • 不需要请求对象的定制
    • 请求资源路径中的?可以加也可以不加
  • 代码示例
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
# urllib VS requests

# 1. urllib
# 1.1 一个类型六个方法
# 1.2 get 请求
# 1.3 post 请求
# 1.4 ajax 的 get 请求
# 1.5 ajax 的 post 请求
# 1.6 cookie 登录
# 1.7 代理

# 2. request
# 2.1 一个类型六个属性
# 2.2 get 请求
# 2.3 post 请求
# 2.4 代理
# 2.5 cookie 验证码

import requests

url = 'http://www.baidu.com/s?'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69',
'Cookie': 'BDUSS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BDUSS_BFESS=E5yeFZUSFRyTUxzUEhVbXhzUkZOV3lyeDlGLUMyVWFranBDNlRiV21tTWlnRDVrSVFBQUFBJCQAAAAAAAAAAAEAAAB~ymGB06O7qGNhbzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACLzFmQi8xZkT; BIDUPSID=B26B49048A0C9099B1456DF64F0279FF; PSTM=1684546051; BAIDUID=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BD_UPN=12314753; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; delPer=0; BD_CK_SAM=1; PSINO=5; BAIDUID_BFESS=C70948D5D545EED0115F390D9D6C8143:SL=0:NR=10:FG=1; BA_HECTOR=ag01ag2g8k2hak81242k81001ihcet51p; B64_BOT=1; RT="z=1&dm=baidu.com&si=bee5a4dd-dfb0-43a8-8226-878c0904dd53&ss=ln47d4cw&sl=0&tt=0&bcn=https%3A%2F%2Ffclog.baidu.com%2Flog%2Fweirwood%3Ftype%3Dperf&ul=1xd&hd=1xx"; COOKIE_SESSION=0_0_0_0_1_0_1_0_0_0_0_0_0_0_3_0_1695967725_0_1695967722%7C1%230_0_1695967722%7C1; ZFY=jKLYva:AwW:AlQpwrTh6Gt4SREDbR7NGeps2ei6L3zLXg:C; H_PS_PSSID=39323_39353_39399_39396_39407_39097_39412_39436_39358_39308_39375_39233_39406_26350_39219_22158_39427; baikeVisitId=cff92722-9312-4f8d-8d48-c7ba4ffa672e; sug=3; sugstore=0; ORIGIN=2; bdime=21111; H_PS_645EC=30ackYSFvlTVDOWoTBZDrGzNnqQmMHFCdPeK0%2BLRZ1OYuKcXF7ps3bKsk04'
}
data = {
'wd': '北京'
}

# requests.get 的参数有三个
# url:请求资源路径 params 参数 kwargs:字典
response = requests.get(url=url, params=data, headers=headers)
content = response.text # 注意这是属性
print(content)

3. post 请求

  • 抓取百度翻译 https://fanyi.baidu.com/sug

1696041178762

  • 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = 'https://fanyi.baidu.com/sug'

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

data = {
'kw': 'math'
}

# requests.post 该句使用到的参数说明:
# url请求地址 data请求参数 kwargs字典
response = requests.post(url=url, data=data, headers=headers)

content = response.text

import json
obj = json.loads(content.encode('utf-8'))
print(obj) # {'errno': 0, 'data': [{'k': 'math', 'v': 'n. 数...
  • 总结
    • post请求 是不需要编解码
    • post请求的参数是data
    • 不需要请求对象的定制

4. 代理

1
2
3
4
5
6
# 设置其他ip
proxy={
'http':'58.20.184.187:9091'
}

response = requests.get(url=url, params=data, headers=headers,proxies=proxy)
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
"""
cookie登陆古诗文网(含验证码)
"""
# 通过登陆 然后进入到主页面

# 通过找登陆接口我们发现 登陆的时候需要的参数很多
# __VIEWSTATE: wzavkIiUpeGeXT-Gu4jEWSBcHAneSt4SJdDa3y/PEP5sDZuLEWgE1r37kEQzlJ/pVVbYYMe7vrMvtm3NUmkX2KGAuPYULzyiZDcfhry5nbmFCtGY/RrDbqJIDMu0KDOYRMeQRs/Xwv2vH/1ZpkEoSK0lGoA0=
# __VIEWSTATEGENERATOR: C93BE1AE
# from: http://so.gushiwen.cn/user/collect.aspx
# email: cney6tcn@linshiyouxiang.net
# pwd: 8YW8GYET78933ETR
# code: 32GV
# denglu: 登录

# 我们观察到_VIEWSTATE __VIEWSTATEGENERATOR code是一个可以变化的量

# 难点:(1)_VIEWSTATE __VIEWSTATEGENERATOR 一般情况看不到的数据 都是在页面的源码中
# 我们观察到这两个数据在页面的源码中 所以我们需要获取页面的源码 然后进行解析就可以获取了
# (2)验证码

import requests

# 这是登陆页面的url地址
url = 'https://so.gushiwen.cn/user/login.aspx?from=http://so.gushiwen.cn/user/collect.a'

headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) Ap-pleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36 Edg/115.0.1901.200',
}
response = requests.get(url=url, headers=headers)
content = response.text
print(content) # 测试代码,验证能否获取网页源码

scrapy

1. srcapy 安装

1.1 什么是 srcapy

  • scray 是一个为了爬取网站数据,提取结构性数据而编写的应用框架。可以应用在包括数据挖掘、信息处理或存储历史数据等一系列的程序中。   
  • 什么是结构性数据?
    • 结构性就是类似的具有相同特征的东西,里面的数据就是结构性数据。   
    • 选中一本书进行定位(即在某本书处打开检查),发现这些书的信息有一个相同的结构,比如书名都在结构“/html/body/div[6]/div/div[2]/div[2]/ul/li/div/h3/a”下,它们具有相同的结构,这就是结构性的例子。至于结构性数据,比如书名就是该结构下的数据。
  • 优点
    • 爬取速度快
    • 代码简单好用

1.2 srcapy 的安装

  • 安装命令:pip install scrapy

2. 基本使用

  • 使用步骤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. 创建爬虫的项目     
scrapy startproject 项目名字
注意:项目的名字不允许使用数字开头 也不能包含中文
2. 创建爬虫文件
要在spiders文件夹中去创建爬虫文件

进入spiders文件夹:cd 项目的名字\项目的名字\spiders,
- 本次演示使用的命令为 cd scrapy_baidu_091\scrapy_baidu_091\spiders

创建爬虫文件
scrapy genspider 爬虫的名字 要爬取的网页
eg:scrapy genspider baidu http://www.baidu.com
3. 运行爬虫代码
scrapy crawl 爬虫的名字
eg:scrapy crawl baidu
注:在运行爬虫程序时,需注释掉文件“setting.py”中的“ROBOTSTXT_OBEY = True”,
即不遵守君子协议
  • 代码演示

补充知识

Day2 - 4.requests模块巩固深入案例之破解百度翻译_哔哩哔哩_bilibili

  • ajax 技术可以实现动态页面局部刷新文件类型一般为xhr或fetch

1696077406694

1696077448796

  • 如果直接访问页面url,请求获取的静态页面缺少一些的局部数据,可以考虑所抓取的数据并不是通过 url 请求到的,可能是由 ajax 动态加载请求到的,可以进行下列方式验证:

1696078327549

1. 模拟登录

爬取基于某些用户的用户信息

  • cookie:用来让服务器端记录客户端的相关状态

    • 不建议用手动cookie处理,即:通过f12里的抓包工具获取cookie值,将该值封装到 headers 中
    1
    2
    3
    headers = {
    'Cookie': 'xxx'
    }
    • 自动处理
      • cookie 值的来源?===> 去页面抓包,找到响应头信息中包含 Set-Cookie 字段的请求
      • session 的会话对象:可以进行请求的发送;如果请求过程中产生了 cookie,则该 cookie 会被自动存储/携带在该 session 对象中;那么之后就可以用这个已经存储了cookie的session对象发起请求
  • 自动处理 cookie 进行模拟登录的流程

    • 创建一个 session 对象:requests.Session()
    • 使用 session 对象进行模拟登录 post 请求的发送(cookie 就会被存储在session中)
    • session对象对个人主页对应的get请求进行发送(携带了 cookie)

2. sign 反爬-js 逆向

【爬虫实战】有道翻译的JS逆向技巧,透彻讲解教你如何破解sign参数!_哔哩哔哩_bilibili

  • 断点方式
    • xhr 断点 发包位置 加密参数之后断点
      • 对通用参数进行处理 往上找加密点
    • dom 断点 执行某一个事件 加密参数之前断点
      • 往下找加密点
  • 网页加密

3. 浏览器调试 Dev Tools

开发常用浏览器 Chrom,firefox

这里主要介绍 Chrom

【浏览器调试工具精讲】Chrome Dev Tools精讲,前端必看!_哔哩哔哩_bilibili

3.1 各个 Tab 介绍

  • 打开 Dev Tool
    • 菜单>更多工具>开发者工具
    • 快捷键:F12
  • 打开命令菜单:ctrl+shift+P
  • 常用的 Tab
    • Element
    • Console
    • Source
    • Network
    • Application

3.2 控制台(Console)

  • 快捷键:ctrl + Shift + J
  • 控制台输入:
    • $_ 可以返回上一条语句的执行结果
  • $0 可以返回上一个选择的DOM节点,以此类推,$1 就是上一个,$2 就是上上一个

3.3 JS 调试

  • 在 js 代码的某一行写上 debugger,回到页面运行时就会暂停在那行

1696155905278

1696155935075

  • 也可以直接点行号进行调试
    • 右侧 watch 部分还可以监测某一个变量

1696156009157

  • 其他加断点方式

1696156367090

1696156516180

3.4 Network

  • 记住跳转页面前的上一个页面的请求,需要勾选 Preserve log

1696156691181

4. 案例:抓取网易云评论

这个案例很完整,建议好好学!

3 5 综合训练 抓取网易云音乐评论信息(6)_哔哩哔哩_bilibili

https://music.163.com/#/song?id=1325905146

4.1 分析接口

  • 在f12抓包工具里的 XHR 选项下,可以找到获取评论的 post 接口:https://music.163.com/weapi/comment/resource/comments/get?csrf_token=

1696408535195

  • 可以看到需要有两个参数,但都是加密的

我们需要找到其 没加密之前是咋样的?加密的过程是咋样的?最后在程序里模拟其加密过程,加密完后再请求

1696408515949

  • 发起程序 选项卡下有个请求调用堆栈,可以在这里查看所调用的 js:

1696408952124

4.2 逆向 js

  • 尝试点击最上面的 js,也就是最后一次调用的 js,然后对代码进行分析

1696413885869

1696414122424

1696414266868

  • 找到加密的js,回去对其进行分析

1696414586163

1696414966073

  • 找到加密的函数,设断点,再进行分析

1696415569684

1696415827300

1696416034803

  • 继续执行,验证猜想

1696416158876

  • 得出结论:

    • 我们要的两个参数:

      • params:encText
      • encSecKey:encSecKey
    • 都是由这个函数进行生成的:

      var bKC6w = window.asrsea(JSON.stringify(i8a), bvh7a(["流泪", "强"]), bvh7a(Re1x.md), bvh7a(["爱心", "女孩", "惊恐", "大笑"]));

4.3 对加密过程进行分析

  • 接下来分析下这个加密过程,直接 ctrl+f 找下这个 window.asrsea 是啥?除了这句话之外,整个代码里只有下面这个地方有这个参数:window.asrsea = d

1696416743521

  • 对 d 这个函数,结合加密语句进行分析

var bKC6w = window.asrsea(JSON.stringify(i8a), bvh7a(["流泪", "强"]), bvh7a(Re1x.md), bvh7a(["爱心", "女孩", "惊恐", "大笑"]));

1
2
3
4
5
6
7
8
function d(d, e, f, g) {  # d: 数据 e: 010001 f: 很长的一个定值 g: '0CoJUm6Qyw8W8jud'
var h = {}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}

可以发现,参数的 d 就是数据,后面的我们可以通过 console.log 获取其内容

1696416995573

1696417194452

1696418007641

1696418150552

4.4 编写代码,得到结果

太厉害了这个,好难

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
# 需求:
# 1. 找到未加密的参数 # window.arsea(参数, xxx, xxx, xxx)
# 2. 想办法把参数进行加密(必须参考网易的逻辑), params => encText, encSecKey => encSecKey
# 3. 请求到网易, 拿到评论信息

from Crypto.Cipher import AES
from base64 import b64encode
import requests
import json

url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
# 请求方式是 post
data = { # 通过 js 源码, 我们分析出来了真实的参数
"rid": "R_SO_4_1325905146",
"threadId": "R_SO_4_1325905146",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token": ""
}

# 服务于d的
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
g = '0CoJUm6Qyw8W8jud'
e = '010001'
i = "SClpUDdZIpmWGncw" # 手动固定的


def get_encSecKey(): # 由于 i 固定, 那么 encSecText 就是固定的, c() 函数的结果就是固定的
encSecKey = "b40a2c971903961570378115316e7173a0d2a13532ebf67854866ffd90606842830f9713f2dbcb2db23e52c5ea5b9e48f1fed259ec15a82ab3d38228c90d88ced7885e69153a7cf4f0628983c0b427f15d4955f5afc34e0c332ca12cf47f359b7a68a5ab29bb774cd985638a733b824987a4548f8969fe2516e3de67b101426a"
return encSecKey

def get_params(data): # 默认这里接收到的是字符串
first = enc_params(data, g)
second = enc_params(first, i)
return second # 返回的就是 params


# 转化成 16 的倍数, 为下方的 AES 加密算法服务
def to_16(data):
pad = 16 - len(data) % 16
data += chr(pad) * pad
return data


def enc_params(data, key): # 加密过程(最恶心的部分, 要还原js源码里的函数b)
# 引入 AES 包后, 接下来进行 AES 加密
iv = "0102030405060708"
data = to_16(data)
aes = AES.new(key=key.encode("utf-8"), IV=iv.encode('utf-8'), mode=AES.MODE_CBC)
bs = aes.encrypt(data.encode('utf-8')) # 加密, 要求加密的内容的长度必须是16的倍数(而且补齐还是有逻辑的!), 涉及到AES加密的原理
return str(b64encode(bs), "utf-8") # 转化成字符串返回


# 处理加密过程
# var bKC6w = window.asrsea(JSON.stringify(i8a), bvh7a(["流泪", "强"]), bvh7a(Re1x.md), bvh7a(["爱心", "女孩", "惊恐", "大笑"]));
"""
function a(a = 16) { # 返回随机的16位字符串
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1) # d 从 0-15, 循环 16 次, 产生 16 个随机的字母或数字
e = Math.random() * b.length, # 随机数 假设1.2345
e = Math.floor(e), # 取整 假设1
c += b.charAt(e); # 去字符串中的xxx位置: 由上面假设, 此处就是 b
return c
}
function b(a, b) { # 参数a是要加密的内容,
var c = CryptoJS.enc.Utf8.parse(b) # 所以 b 就是密钥
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a) # e是数据
, f = CryptoJS.AES.encrypt(e, c, { # AES加密算法? 这里的c就是加密的密钥
iv: d, # AES加密里 iv 是偏移量
mode: CryptoJS.mode.CBC # mode 是模式, 表示这里用的是 CBC 模式进行的加密
});
return f.toString()
}
function c(a, b, c) { # c里面不产生随机数
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
function d(d, e, f, g) { # d: 数据 e: 010001 f: 很长的一个定值 g: '0CoJUm6Qyw8W8jud'
var h = {} # 空对象
, i = a(16); # i 就是一个16位的随机值, 可以把i设置成定值, 那么 encSecKey 就也是定的
h.encText = b(d, g), # 分析函数b, 可以得出g是密钥
h.encText = b(h.encText, i), # 返回的就是 params, i也是密钥
h.encSecKey = c(i, e, f), # 返回的就是 encSecKey
# 分析 encSecKey, 参数e和f都是定死的, i是随机的, 如果此时固定i, 那么从c的函数中可以看出其不产生随机数, 那么最后得到的 encSecKey 一定也是定死的
return h

# 现在分析 params, 进行了两次的加密
# 数据+g => b => 第一次加密的结果+i => b = params
}
"""


if __name__ == '__main__':
# 发送请求, 得到评论
resp = requests.post(url, data={
"params": get_params(json.dumps(data)),
"encSecKey": get_encSecKey()
})
print(resp.text)
  • 结果,成功:

1696420016729

5. python 并发编程

5.1 简介

  • 引入并发,就是为了提升程序的运行速度
  • 程序提速的方法:
    • 单线程串行:不加改造的程序
    • 多线程并发:py 的 threading 模块
    • 多 CPU 并行:multiprocessing
    • 多机器并行:hadoop/hive/spark

1697174911332

  • python 对并发编程的支持
    • 多线程:threading,利用 CPU 和 IO 可以同时执行的原理,让 CPU 不会干巴巴等待 IO 完成
    • 多进程:multiprocessing,利用多核 CPU 的能力,真正的并行执行任务
    • 异步 IO:asyncio,在单线程利用 CPU 和 IO 同时执行的原理,实现函数异步执行
    • 使用 Lock 对资源加锁,防止冲突访问
    • 使用 Queue 实现不同线程/进程之间的数据通信,实现生产者(边爬取)-消费者(边解析)模式
    • 使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果
    • 使用 subprocess 启动外部程序的进程,并进行输入输出交互

5.2 如何选择多线程多进程多协程

python 并发编程有三种方式:多线程 Thread、多进程 Process、多协程Coroutine

5.2.1 什么是 CPU 密集型计算、IO 密集型计算

1697175455216

5.2.2 多线程多进程多协程的对比

1697175588626

5.2.3 如何选择对应技术

  • python 速度慢的原因:
    • 动态类型语言,边解释边执行
    • GIL(全局解释器锁),无法利用多核CPU并发执行

1697175628421

5.3 python 利用多线程加速爬虫

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

urls = [
f"https://www.cnblogs.com/#p{page}"
for page in range(1, 50+1)
]


def craw(url):
r = requests.get(url)
print(url, len(r.text))

craw(urls[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
27
28
29
30
31
32
33
34
35
36
37
38
import blog_spider
import threading
import time

def single_thread():
print("single_thread begin")
for url in blog_spider.urls:
blog_spider.craw(url)
print("single_thread end")


def multi_thread():
print("multi_thread begin")
threads = []
for url in blog_spider.urls:
threads.append( # 创建多线程, 每个线程传入函数和函数所需参数
threading.Thread(target=blog_spider.craw, args=(url,))
)

for thread in threads: # 启动
thread.start()

for thread in threads: # 等待结束
thread.join()

print("multi_thread end")


if __name__ == '__main__':
start = time.time()
single_thread()
end = time.time()
print("single_thread cost: ", end-start, "seconds")

start = time.time()
multi_thread()
end = time.time()
print("multi_thread cost: ", end - start, "seconds")

5.4 实现生产者消费者模式的多线程爬虫

5.4.1 多组件的 Pipeline 技术架构

  • 复杂的事情一般会分很多中间步骤一步步完成

1697178699318

5.4.2 生产者消费者爬虫的架构

1697178748014

5.4.3 多线程数据通信的 queue.Queue

  • queue.Queue 可以用于多线程之间的、线程安全的数据通信
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
import queue
import blog_spider
import time
import random
import threading


def do_craw(url_queue:queue.Queue, html_queue:queue.Queue):
"""
生产者
"""
while True:
url = url_queue.get()
html = blog_spider.craw(url)
html_queue.put(html)
print(threading.current_thread().name, f"craw {url}",
"url_queue.size=", url_queue.qsize())
time.sleep(random.randint(1, 2))


def do_parse(html_queue:queue.Queue, fout):
while True:
html = html_queue.get()
results = blog_spider.parse(html)
for result in results:
fout.write(str(result) + "\n")
print(threading.current_thread().name, f"results {results}",
"html_queue.size=", html_queue.qsize())
time.sleep(random.randint(1, 2))


if __name__ == '__main__':
url_queue = queue.Queue()
html_queue = queue.Queue()

for url in blog_spider.urls:
url_queue.put(url)

# 创建三个生产者线程
for idx in range(3):
t = threading.Thread(target=do_craw, args=(url_queue, html_queue),
name=f"craw{idx}")
t.start()

# 创建两个消费者线程
fout = open("02.data.txt", "w")
for idx in range(2):
t = threading.Thread(target=do_parse, args=(html_queue, fout),
name=f"parse{idx}")
t.start()

5.5 线程安全问题以及 Lock 解决方案

  • 线程安全:指某个函数、函数库在多线程环境中被调用时,能够正确的处理多个线程之间的共享变量,使程序功能正确完成

  • 线程不安全:由于线程的执行随时会发生切换,造成不可预料的结果

  • Lock 用于解决线程安全问题lock = threading.Lock()

    • 用法一:try-finally 模式
    • 用法二:with 模式
  • 代码示例

    • 错误示范:
    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
    import threading
    import time

    class Account:
    def __init__(self, balance):
    self.balance = balance


    def draw(account, amount):
    if account.balance >= amount:
    time.sleep(0.1)
    print(threading.current_thread().name,
    "取钱成功")

    account.balance -= amount
    print(threading.current_thread().name,
    "余额: ", account.balance)

    else:
    print(threading.current_thread().name,
    "取钱失败, 余额不足")


    if __name__ == '__main__':
    account = Account(1000)
    ta = threading.Thread(name="ta", target=draw, args=(account, 800))
    tb = threading.Thread(name="tb", target=draw, args=(account, 800))

    ta.start()
    tb.start()

    1697246248156

    • 正确示范:加上 lock
    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
    import threading
    import time

    lock = threading.Lock()

    class Account:
    def __init__(self, balance):
    self.balance = balance


    def draw(account, amount):
    with lock:
    if account.balance >= amount:
    time.sleep(0.1)
    print(threading.current_thread().name,
    "取钱成功")

    account.balance -= amount
    print(threading.current_thread().name,
    "余额: ", account.balance)

    else:
    print(threading.current_thread().name,
    "取钱失败, 余额不足")


    if __name__ == '__main__':
    account = Account(1000)
    ta = threading.Thread(name="ta", target=draw, args=(account, 800))
    tb = threading.Thread(name="tb", target=draw, args=(account, 800))

    ta.start()
    tb.start()

    1697246351713

5.6 好用的线程池 ThreadPoolExecutor

  • 线程池的原理

1697246476661

  • 使用线程池的好处
    • 提升性能:减去大量新建、终止线程的开销,重用了线程资源
    • 适用场景:适合处理突发性大量请求或需要大量线程完成任务,但实际任务处理时间较短
    • 防御功能:能有效避免系统因为创建线程过多,而导致系统负荷过大相应变慢等问题