---
# System prepended metadata

title: Python爬虫
tags: [learn]

---

###### tags: `learn`

# Python爬虫

首先作为前提，我们需要知道什么是可以爬取的，什么是不能爬取的（爬虫写得好，牢饭吃到老）。那么要怎么确认呢？

网站主页面的 /robots.txt 可以查看。比如：
![](https://i.imgur.com/m4gCidL.png)
![](https://i.imgur.com/I3rWbtE.png)

---
## 1. 爬虫概述

### 1.1 第一个爬虫程序

```
# 需求，用程序模拟浏览器，输入一个网址，从该网址钟获取资源或者内容！
from urllib.request import urlopen  # 打开网址的组件！

url = 'https://www.baidu.com'
response = urlopen(url)  # 打开网址得到响应！

print(response.read().decode('utf-8')) # 解码选择看原本的read()返回字符的前几行就有！
with open("mybaidu.html", mode='w') as f:
    f.write(response.read().decode("utf-8")) # 读取到网页的页面源代码
print("爬取完毕！")
```
> 这样我们就保存了爬下来的网页！然后右键该文件打开即可打开我们爬取的页面！
> 为什么？因为==我们爬取下来的就是网页的源代码==！！！！

---

### 1.2 Web请求过程剖析

- 服务器渲染：在服务器端，将搜索信息和html进行整合，返回给浏览器；
- 客户端渲染：在客户端，第一次请求只拿到html，第二次拿到搜索信息且将搜索的信息和html整合。特点：在页面源代码中，看不到数据！

> 那么对于客户端渲染类型，需要如何去获得我们想要的数据？
> 想法也很简单，只要能获取url请求，就有我们想要的数据了！这里引入**抓包工具**。

- 抓包工具：在网页直接按F12可以查看当前网页状态，其中的`Network(网络)`里就有我们的请求！可以通过这里找到！
- ![](https://i.imgur.com/yNC75x8.png)


---

### 1.3 Http协议Hyper Text Transfer Protocol

（计算机网络知识了）超文本协议

==请求==
```
请求行 -> 请求方式(get/post) 请求url地址 协议
请求头 -> 放一些需要的附加信息

请求体 -> 放请求参数
```
请求方式：
- get：显式提交（只是查询...）
- post：隐式提交（可以自己选择的，要修改的）

:::success
请求头里面的重要信息：
1. User-Agent：请求载体的身份标识（用啥发的请求）
2. Referer：防盗链（这次请求是从哪来的？反爬用）
3. cookie：本地字符串数据信息（用户信息，反爬token）

![](https://i.imgur.com/kE3HIes.png)

:::

==响应==
```
状态行 -> 协议 状态码(400, 302...)
响应头 -> 客户端需要的附加信息

响应体 -> 返回客户端真正需要的内容（html, json...）
```

:::warning
响应头的重要信息：
1. cookie：本地字符串数据信息（用户信息，反爬token）
2. 各种奇妙字符串（常以token字样出现）
:::

---

### 1.4 requests入门

- requests比上面的urlopen更加好用！！

> 什么时候用get()?
> 但凡是把网页地址直接copy下来的都是直接用get()！

```
'''
requests入门：比url更方便！

1. get()方法：直接能够在网页地址栏搜到的东西

'''
import requests

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

MyHeaders = {
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0'
}

response = requests.get(url，headers=MyHeaders) # 打开网址获得响应
print(response) # 获得访问状态，返回为200表示成功访问
print(response.text) # 获得网页源代码！
response.close()
```

> 在使用爬虫时候经常会因为被网页辨别为使用程序进行自动爬虫而被拒绝，也就是 `User-Agent`没有设置！前面说到，`User-Agent`在请求头里面，所以我们在申请访问的时候修改请求头就可以处理掉它这个反爬！

---

#### 例子：爬取百度翻译结果

![](https://i.imgur.com/WADTyDH.png)
> 在我输入pig后，POST的sug文件返回了标准的查询结果的JSON文件！
> 那么对于POST方法，可见它不像get()方法，没有办法直接在网页地址栏看到，要如何解决？

![](https://i.imgur.com/fyTHtQE.png)

```
'''
2. POST方法
'''

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

word = input("输入您想查询的单词：")
data_dict = {
    "kw":word
}

resp = requests.post(url=url, data=data_dict)
print(resp.json()) # 返回json文件

resp.close()
---
输入您想查询的单词：multiprocessing
{'errno': 0, 'data': [{'k': 'multiprocessing', 'v': 'n. 多重处理，多处理（技术）'}]}
```

> post方法需要将发送数据放在字典中，通过形参`data`进行传递！

---

#### 例子：爬取豆瓣排行榜

![](https://i.imgur.com/2FLxr4w.png)
![](https://i.imgur.com/aBpFCoe.png)
> 用的是get方法！且要简单封装的参数也都在里面！

![](https://i.imgur.com/ejXVpo5.png)
> 找到我们想要的东西了！可以开始写了

```
url = 'https://movie.douban.com/typerank'
# 太长了，重新封装

param = {
    'type':'16',
    'interval_id': '100:90',
    'action': '',
    'start': 0,
    'limit': 20
}

headers = {'User-Agent':
           'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0'
           }

response = requests.get(url, params=param, headers=headers)
print(response.url) 

// 输出：https://movie.douban.com/typerank?type=16&interval_id=100%3A90&action=&start=0&limit=20
// 输出和我们一开始的长网页一模一样！

print(response.request.headers)

// {'User-Agent': 'python-requests/2.28.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
// （这里是我添加headers之前的结果！）可以发现我们默认的User-Agent是Python而不是浏览器，所以很有可能被拦截了！那么很简单，修改我们的默认User-Agent不就好了吗

response.close() # 关掉response，不然访问太多会被你堵死...
```

---

## 2. 数据解析概述 - re, bs4, Xpath(三者可互换！)

如何从杂乱的html中提取我们想要的东西？

### 2.1 re模块：正则表达式（之前学过啊！）

![](https://i.imgur.com/J4aTDQN.png)
![](https://i.imgur.com/YBFbOhT.png)
> 贪婪匹配：找尽可能长的匹配内容；
> 惰性匹配：找尽可能短的匹配内容。
- 实际上会发现我们在爬虫基本只用惰性匹配！（因为你想要的内容不一定，但是它们一定可以以惰性匹配表示！）
---
**RE模块的使用**

```
"""
第二章：数据解析：re,Bs4, Xpath
1. re模块
"""

import re

# findall：匹配字符串中所有符合正则的内容，返回列表
results = re.findall("\d+", "my phone number： 18696573201, 09060073210")
print(results)

---

# finditer：匹配字符串所有符合内容，返回迭代器！  = C++的 sregex_iterator
it = re.finditer("\d+", "my phone number： 18696573201, 09060073210")
print(*it)

for i in it:
    print(i.group())

---

# search:返回第一个查询到的match对象, = C++的 regex_search
s = re.search("\d+", "my phone number： 18696573201, 09060073210")
print(s) # 可发现返回的也是一个Match对象！

---

# match：对比查找正则式与输入是否一致！
s = re.match("\d+", "my phone number： 18696573201, 09060073210")
print(s)

------------------------------------------------------------------

--- re.findall()
['18696573201', '09060073210']

--- *(re.finditer())

<re.Match object; span=(17, 28), match='18696573201'> <re.Match object; span=(30, 41), match='09060073210'>

--- re.finditer() -> group()

18696573201
09060073210

--- re.search()

<re.Match object; span=(17, 28), match='18696573201'>

--- re.match()

None
```
- 会发现Python和`C++`一样，finditer返回的迭代器（很像`C++`的`sregex_iterator`,且迭代器中内容都是`Match`对象！
- 提取迭代器内对象也和`C++`基本一样，一个for循环，但是这里需要用`.group()`来得到匹配内容，而不是`C++`的`.result()`
- 实际上无论是方法，还是返回类型（Match对象），Python和C++都是基本完全一致的！

:::success
有时候我们希望对某个正则式进行多次查找，但是每一次写查找时候都打一遍正则式就很麻烦！
所以我们可以将正则式模板以 `re.compile()` 进行保存并调用！

```
'''
正则式的预加载
'''

obj = re.compile(r'\d+')
ret = obj.finditer("my phone number： 18696573201, 09060073210")
print(*ret)

---

<re.Match object; span=(17, 28), match='18696573201'> 
<re.Match object; span=(30, 41), match='09060073210'>
```
> 通过正则式的加载，我们可以保存一个`Pattern`对象，它在保存了正则规矩的同时也可以调用前面的各种方法！
:::

---
**正则内容的提取** 

上面将正则匹配内容讲完了，下面有个问题：如果我想要的内容只是正则式里面的一部分怎么办？
- 实际上这个问题很实在，我们用正则去匹配一长串东西，结果想要的一定只是一部分，而很大的一部分都是为了能匹配到它而存在的！所以要如何抽取出来正则式里的真正要点？

看下面的例子：
```
'''
正则内容的提取
'''

s = "<div class='jay'><span id='1'>韦君豪</span></div>" \
    "<div class='jyaus'><span id='2'>liiuuuuu</span></div>" \
    "<div class='jaris'><span id='3'>韦豪</span></div>" \
    "<div class='Okays'><span id='4'>杨威</span></div>" \
    "<div class='Newer'><span id='5'>米娅</span></div>"

obj = re.compile(r"<div class='.*?'><span id='\d'>.*?</span></div>"， re.S)
# re.S表示.匹配时候也可以匹配换行符！！！

result = obj.finditer(s)
for iter in result:
    print(iter.group())
    
--- 结果

<div class='jay'><span id='1'>韦君豪</span></div>
<div class='jyaus'><span id='2'>liiuuuuu</span></div>
<div class='jaris'><span id='3'>韦豪</span></div>
<div class='Okays'><span id='4'>杨威</span></div>
<div class='Newer'><span id='5'>米娅</span></div>
```
:::danger
问：如何从上面的整个正则式里面提取我想要的类别，id以及名字？
答：使用 `(?P<name>正则式)` 给正则式内容赋名为name，然后使用 `.group(name)` 或者 `.groups()` 直接提取！
```
'''
提取正则内容：方法：给使用正则的部分 以 (?P<组名>正则式) 命名，然后在 .group()里填写组名！
'''
obj = re.compile(r"<div class='(?P<class_name>.*?)'><span id='(?P<id>\d)'>(?P<name>.*?)</span></div>", re.S)
# re.S表示.匹配时候也可以匹配换行符！！！

result = obj.finditer(s)
for it in result:
    print(it.groups())
    
--- 结果
('jay', '1', '韦君豪')
('jyaus', '2', 'liiuuuuu')
('jaris', '3', '韦豪')
('Okays', '4', '杨威')
('Newer', '5', '米娅')
```
:::

:::success
实战1： 爬取豆瓣电影排行榜的图片与名字

```
"""
https://movie.douban.com/top250

右键豆瓣页面源代码可以发现排行榜的内容都在源代码里面！所以思路：
1. 提取页面源代码
2. 在源代码中正则查询
3. 保存！
"""
import requests
import re
import os


# 1. 提取源代码

url = 'https://movie.douban.com/top250'
header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0'
}
resp = requests.get(url, headers=header)
page_content = resp.text
resp.close()


# 2. 解析正则式

query = re.compile(
    r'<img width="100" alt="(?P<name>.*?)" src="(?P<fig>.*?)" class="">',
    re.S)
results = query.finditer(page_content)
os.makedirs(os.path.join(os.getcwd(),"SaveFigs"), exist_ok=True)
save_path = os.path.join(os.getcwd(), "SaveFigs")
for i, each in enumerate(results):
    with open(os.path.join(save_path, "path_{}.jpg".format(i)), 'wb') as f:
        url_img = each.group('fig')
        temp_resp = requests.get(url_img)
        f.write(temp_resp.content)
        print("完成第 {} 张图片爬虫！".format(i))
        temp_resp.close()
```
:::

---

### 2.2 BS4解析：美丽的汤_4

#### 2.2.1 html语言基础

```
<h1>wjh</h1>
<h2 align="center"> Wei JUNHAO </h2>

<!--# h1:标签-->
<!--# align:属性-->
<!--# center:属性值-->


<标签 属性="属性值">
    被标记的内容
</标签>


<body text="green" bgcolor="#eee">
    See My Freaking Color！
</body>
```

---
#### 2.2.2 BeautifulSoup - 根据html语言特性提取

我们可以通过实例化BeautifulSoup对象来对html语言进行分层提取。例子看下面吧，简单易懂。总之，html是分级对称且重要内容多在属性值里面。我们可以通过BS的find_all()和get()去逐步逼近我们的范围！

```
'''
bs4：提取我们所需要的东西！
bs4就是力用html语言的对称性，从n级标题开始一次一次逼近我们需要的内容！
当然我认为bs4能做的事情，re也能做。

思路：
1. 拿到主页面源代码，并在其中提取到子页面的链接地址 href；
2. 找到源代码中的图片下载地址， 如：
    <img src="http://kr.shanghai-jiuxin.com/file/2020/1031/d7de3f9faf1e0ecdea27b73139fc8d3a.jpg" />
3. 下载图片！
'''

from bs4 import BeautifulSoup
import requests
import time
import os

url = 'https://www.umei.cc/bizhitupian/weimeibizhi/'
resp = requests.get(url)
resp.encoding = 'utf-8' # 处理乱码

# 将源代码交给bs
# 后面的html.parser的意思是：
#   丢给BeautifulSoup的内容应该是html格式，我们用这句话表示前面的东西就是html文件！
main_page = BeautifulSoup(resp.text, "html.parser")
resp.close()

# 一步一步往下找，逼近得到图片url
section = main_page.find('div', class_="swiper-wrapper after").find_all('img')

# 访问url，存储内容！
os.makedirs(os.path.join(os.getcwd(), 'BS4爬取图片'), exist_ok=True)
save_path = os.path.join(os.getcwd(), 'BS4爬取图片')
for it in section:
    temp_url = it.get('data-src')
    filename = temp_url.split('/')[-1]
    with open(os.path.join(save_path, filename), mode='wb') as f:
        img_resp = requests.get(temp_url)
        f.write(img_resp.content)
        img_resp.close()
        print("下载完成图片: {}".format(filename))
        # 消停一下免得被ban了！
        time.sleep(0.5)
print("All Over!")
```
> find()和find_all()处理标题等级，get()精确到属性！
> 总之BS相对简单易懂，只需要好好检查网页源代码对准即可。

---

### 2.3 Xpath

#### 2.3.1 Xpath入门

**Xpath：比re和bs都好！而且简单~！**

- Xpath：在xml文档中搜索内容的一门语言！
- html是xml的一个子集，二者十分相似！

:::success
Xpath是基于XML文件特性搜寻：父节点->子节点->子节点的子节点...->所需属性！
相比之下：
- re使用正规表达式，不考虑XML和HTML语言特性，粗暴但有效；
- BS使用HTML语言特性，按n级标题以及属性进行`find(标题)`以及`get(属性)`；
- Xpath利用XML的父子节点（类树状结构）查找！

```
pip install lxml 
```
:::


```
# 1. xml语言

from lxml import etree

xml = """
<book>
    <id>1</id>
    <name>从百草园到三味书屋</name>
    <price>12</price>
    <author>
        <nick>鲁迅</nick>
        <nick>周树人</nick>
    </author>
</book>
"""

tree = etree.XML(xml)
result = tree.xpath("/book/name/text()") # 用 text() 拿文本！

print(*result)

---
Xpath寻找： book -> name -> text() : 取得对象文本：书名！

>> 从百草园到三味书屋


-----------------

result = tree.xpath("/book/author/nick/text()") # 用 text() 拿文本！

print(*result)

---

鲁迅 周树人
```
> 有时候可能有的子节点有公共的祖先但不是父节点，且它们同名，为了一下都检索出来，我们使用 ： `//` 表示中间可以有多层子节点（也就是说不一定就是直接的连续目录！）
> 或者如果两个子节点的父亲的父亲相同，但是父节点不相同，我们可以用通配符：`*`

例子：
```
子1： book/name/author/author1:周树人
子2：book/name/bookname/author1:鲁迅

它两都是author1目录下，且父父节点相同，我们可以用下面的语句：

result = tree.xpath("/book/name/*/author1") # 通配符！
```
> 那么 `//` 和通配符 `*` 有什么差别呢？
- `*` 只表示一级目录；
- `//` 表示一至多级目录（和re正则一个意思！）

:::info
上面演示了如何在多级目录里找到目录中最后的值。但是和上面的html文件一样，很多数据有值和属性，属性是在某一级目录里面的而非在值里！如何取得属性？看下面！

```
from lxml import etree

xml = """
<book>
    <id>1</id>
    <name>从百草园到三味书屋</name>
    <price>12</price>
    <author>
        <nick country="China" sex="man">鲁迅</nick>
        <nick country="中国" sex="男">Lu Xun</nick>
    </author>
</book>
"""

# 2. 取得某个属性！
tree = etree.XML(xml)
result = tree.xpath("/book/author/nick/@country")
print(result)

---

['China', '中国']
```

> 查找属性和目录一样要/，且用`@属性`来表示你要的属性！
:::

> 这里的例子一样看看代码就好！其实还是比较简单的！

---

## 3. requests 进阶 & 实战

进阶内容：
1. 模拟浏览器登录 -> 处理cookie；
2. 防盗链处理 -> 抓取梨视频数据；
3. 代理 -> 防止被封IP。（危险）

### 3.1 处理cookie(通常是登录信息)

很多信息是不登录就没法获得的，这些信息要求我们在用requests时也要写入自己的登录信息！怎么办？
- 登录 -> 得到返回的cookie -> 带着cookie进行后续操作！
- 使用 session 进行请求。
    - session：一连串的请求，并且在这个过程中, cookie不会丢失！

```
"""
session用法和requests是一模一样的！！！有get()和post()！
"""
import requests

session = requests.session()

url = '要登陆的网站'
data = {
    "LoginName": "登录名",
    "password": "密码"
}

resp = session.post(url, data=data)  # 具体是get还是post需要F12抓包
print(resp.cookies)
```

---

### 3.2 处理防盗链

防盗链：通俗地说，我们访问到一个资源，肯定是从它的上一级页面逐步逼近的。如果在爬虫请求时直接请求到资源，就会被认为是程序爬虫而被拒绝！这就是防盗链。
- 如何应对防盗链：请求头里的Referer表示的是到达当前页面的上一个页面，把它和User-Agent一起加入headers让别人认为是正常用户！

爬取梨视频的代码见下面：
```
"""
----------------------------
requests处理防盗链

1. 拿到contid
2. 拿到videostatus返回json -> 拿到srcurl
3. 修正srcurl
4. 下载视频
"""

url = 'https://www.pearvideo.com/video_1765735'
contid = url.split('_')[1]

videoStatus = f'https://www.pearvideo.com/videoStatus.jsp?contId={contid}&mrd=0.8976572629490815'

headers = {
    "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0',
    # 防盗链：溯源 -> 确认你从哪里到达的这个页面以反爬！总是在请求头的referer！
    "Referer": url
}
resp = requests.get(videoStatus, headers=headers)
# print(resp.json()) # 检查返回的内容找到srcUrl
srcurl = resp.json()['videoInfo']['videos']['srcUrl']
systemTime = resp.json()['systemTime']

'''
真实链接：
https://video.pearvideo.com/mp4/third/20220620/cont-1765735-10887340-165228-hd.mp4
爬取链接（网页方修改）：video_link_without_deal
https://video.pearvideo.com/mp4/third/20220620/1656643112952-10887340-165228-hd.mp4

> 注意到中间： cont_1765735 被换成了systemTime！它们用这个和溯源（Referer）反爬。
> 所以我们现在把中间的systemTime替换成 cont_{contid} 就好了！
'''

srcurl = srcurl.replace(systemTime, f'cont-{contid}')
# print(srcurl) # 确认是否正确获得url！正确了！


# 下载视频
with open(os.path.join(os.getcwd(), "BS4爬取图片", '梨视频.mp4'), mode='wb') as f:
    f.write(requests.get(srcurl).content)

# 成辣！
```

---

### 3.3 代理！

这个很容易进橘子，小心！
百度：免费代理IP，就能找到一些可用IP！

---

### 3.4 综合训练：爬取网易云音乐评论

:::danger
最后再来做！
:::

---

## 4. 线程&进程&协程加速！（看隔壁文件）

```
async def func1():
    print("Hi! I am good.")
    await asyncio.sleep(2)
    print("Hi! I am good.")


async def func2():
    print("Are you good?")
    await asyncio.sleep(4)
    print("YES! I am good.")


async def func3():
    print("Are you Okay???")
    await asyncio.sleep(3)
    print("No! I am not good.")


async def main():
    task_list = [
        asyncio.create_task(func1()),
        asyncio.create_task(func3()),
        asyncio.create_task(func2()),
    ]
    await asyncio.wait(task_list)


if __name__ == "__main__":
    t1 = time.time()
    asyncio.run(main())
    t2 = time.time()
    print(t2-t1)
```
> 协程模板！

---
### aiohttp & aiofiles：协程爬虫&文件写入

见下面的例子：爬取西游记

```
'''
爬取小说！爬取百度小说的西游记！


# http://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"4306063500"}    -> 所有章节名称与cid
# http://dushu.baidu.com/api/pc/getChapterContent?data={"book_id":"4306063500","cid":"4306063500|1569782244","need_bookinfo":1}
# -> 某一章节的内容！我们所需要的！

也就是说，我们可以通过所有章节的cid来拼接出来下面的网页并请求！

1. 同步操作：访问getCatalog 拿到所有章节的cid和名称；
2. 异步操作：访问getChapterContent 下载所有文章内容
'''
import os
import time
import json
import asyncio
import aiohttp
import aiofiles
import requests


async def getCatalog(url, b_id):
    """
    这个操作应该是同步且只做一次的。目的是返回所有章节名称与cid
    """
    tasks = []
    resp = requests.get(url)
    dic = resp.json()
    for each in dic['data']['novel']['items']: # 对应每一个章节的内容！
        # print(each['title'], '\t', each['cid']) # 确实无误！
        title = each['title']
        cid = each['cid']
        tasks.append(asyncio.create_task(aiodownload(cid, b_id, title)))
    await asyncio.wait(tasks)


async def aiodownload(cid, b_id, title):
    data = {
        'book_id': b_id,
        'cid': '{}|{}'.format(b_id, cid),
        'need_bookinfo': 1
    }
    data = json.dumps(data)
    url = 'http://dushu.baidu.com/api/pc/getChapterContent?data={}'.format(data)

    # 异步请求用aiohttp
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            dic = await resp.json()
            os.makedirs(os.path.join(os.getcwd(), '西游记'), exist_ok=True)
            save_path = os.path.join(os.getcwd(), '西游记')
            async with aiofiles.open(os.path.join(save_path, title), mode='w', encoding='utf-8') as f:
                await f.write(dic['data']['novel']['content'])


if __name__=="__main__":
    book_id = '4306063500'
    url = 'http://dushu.baidu.com/api/pc/getCatalog?data={"book_id":"' + book_id + '"}'
    t1 = time.time()
    asyncio.run(getCatalog(url, book_id))
    t2 = time.time()
    print("下载完成！耗时：", t2 - t1)
```

- 写下来个人认为的规律：
- 1. async 就是指我这个函数用得上协程（等待别的），总之我进行网页，文件等io操作的话就能用的上！
- 2. await 写在io操作前面，就是表示我需要等待你这个操作！
- 3. 创建aiohttp.CilentSession() 和 session.get() 都用了 with 语句，好处就是它会自动关闭连接！当然你会发现，除了这里需要前面跟上一个 async 之外，其他的和普通语句都是一模一样的！并且，你也可以不用with语句而是直接： `session = aiohttp.CilentSession()`, 但这样你容易忘记关闭就惨了！所以还是推荐能用with就用with！
- 4. 这其实就是个协程模板！

---

### 爬取视频！

:::warning
1. 视频网站工作原理

`<video src="源路径">视频名</video>`  -> 这样的写法很理想，但是很少出现！因为大的视频加载很慢！

一般的视频网站：

    用户上传 -> 服务器端转码(将视频处理为不同清晰度) -> 服务器端切片(单个文件拆成多个小文件)
    用户点播，拉动进度条 -> 服务器端加载对应的前后的切片文件即可！(不用从头到尾下载完)

如何爬取视频？
    下载多个切片文件并记录顺序与存放路径 -> 拼接！

M3U(8) txt json => 文本！

![](https://i.imgur.com/cPwJcYQ.png)
> 对于M3U8文件，关注：
> 1. 加密方法；
> 2. 视频ts切片文件！
- 也就是说，当你拿到M3U8文件时，这个任务就基本是完成了！

因此：抓取视频步骤（难）：
**1. 找到M3U8文件**
**2. 通过M3U8下载到ts文件**
**3. 通过各种手段(不一定是编程)合并ts文件！**
:::


![](https://i.imgur.com/oQN6n0x.png)
- m3u8文件！

---
## 5. selenium模块-自动化！
### selenium简单介绍
selenium：自动化模块
    当我们无法在源代码里面看到需要的资源时，我们就会用F12抓包然后寻找想要的资源。
    但是，有时候发送的数据贼难解密啊！我能不能不用requests，而是：

- 让程序直接连接到浏览器上，并且让浏览器自己完成加密等复杂操作，我们直接拿结果！

根据这个需求，selenium：自动化工具，就可以被利用！

selenium：打开浏览器，然后像人一样去操作浏览器，再从selenium中直接提取信息！

**环境搭建**：
```
1. pip install selenium
    2. 下载浏览器驱动：https://github.com/mozilla/geckodriver/releases
        将解压缩的浏览器驱动 chromedriver 放在python解释器的目录

检查是否已经安装成功：（会弹出浏览器就成功了！）
    from selenium import webdriver
    
    browser = webdriver.Firefox()
```

[
python3用Selenium驱动火狐浏览器GeckoDriver安装教程
](https://blog.csdn.net/zhi_neng/article/details/118109430)

:::success
对于之前那些需要交互去动态获得数据的例子，用selenium有奇效！
但是，selenium也有缺点：因为是自动化控制，容易被反爬！
:::

---

### selenium各种操作

- 点击：右键检查 -> 找到对应按钮的元素 -> 右键copy Xpath -> 使用Xpath模拟点击！（会用到`from selenium.webdriver.common.by import By`）

```
from selenium.webdriver.common.by import By

webdriver = Firefox()

webdriver.get('https://www.lagou.com')

el = webdriver.find_element(By.XPATH,'/html/body/div[10]/div[1]/div[2]/div[2]/div[1]/div/ul/li[1]/a')
el.click() # 点击

time.sleep(2) // selenium慢，缓一会让浏览器加载！

// 注意上面的find_element操作里面的 By.XPATH是selenium另一个部分的！
// find_element_by_手段() 方法在selenium4种已被丢弃！
```

- 输入：
1. 同上右键检查找到输入框后复制xpath；
2. 使用find_element(By.XPATH, 'xpath')找到输入框；
3. 使用send_keys("内容", Keys.ENTER)输入内容且按回车（不能输入字符回车，会被识别成字符串！）
代码：
```
from selenium.webdriver.common.keys import Keys

webdriver.find_element(By.XPATH, '//*[@id="search_input"]').send_keys('python', Keys.ENTER)

```
- 成功！
![](https://i.imgur.com/U7ErVjY.png)

---

接下来就可以开始爬虫辣！

- 右键检车观察页面：
- ![](https://i.imgur.com/BHzxJJZ.png)
- 发现每一块对应的一个 `item_10RTO` 块，那么我们要全部爬取的话一个一个获得就行！
:::danger
这里有个问题：我本来从上面的母模块爬取的，然后使用for循环一个个获得（视频也是），但是显示的是我爬取的数据是一个不可迭代的selenium的WebElement对象！可以认为这个写法已经不合适了吧！
:::

最后的代码以及结果：
```
'''
查找存放数据的位置，进行数据提取

睡眠三秒以防还没刷新就开始操作了！
'''
time.sleep(3)
with open("Python工资一览.csv", mode='w', encoding='utf-8') as f:
    csv_writer = csv.writer(f)
    for i in range(1, 16):
        li = webdriver.find_element(By.XPATH, '/html/body/div/div[2]/div/div[2]/div[3]/div/div[1]/div[{}]'.format(i))
        job_name = li.find_element(By.CLASS_NAME, 'p-top__1F7CL').text
        company = li.find_element(By.CLASS_NAME, 'company-name__2-SjF').text
        job_salary = li.find_element(By.CLASS_NAME, 'money__3Lkgq').text
        print('职位：', job_name, '公司：', company, '薪水：', job_salary)
        csv_writer.writerow([job_name, company, job_salary])
```

![](https://i.imgur.com/p0xdJsB.png)

---

**窗口之间的切换**

- 如何点击元素并且打开新窗口，切换到新窗口？
- 在selenium中，新窗口是默认不切换过来的！
- `web.switch_to.window(新窗口)`进行窗口切换!
- 一般而言我们都是希望去最新打开的页面，也就是： `web.window_handles[-1]`

> 对新窗口进行爬虫操作...

- 操作完成！接下来：
- 1. 关掉这个新窗口： `web.close()`;
- 2. 就算关掉窗口，目前的selenium还是聚焦于这个窗口的，所以要转移窗口回去！
- 也就是：`web.switch_to.window(web.window_handles[0])`
- 或理解为切换回默认页面：`web.switch_to.default_content()`

:::info
利用selenium打开页面进行爬取后关闭页面的代码见下面：
```
import time
from selenium.webdriver import Firefox
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys


web = Firefox()
web.get('https://www.lagou.com')
web.find_element(By.XPATH, '//*[@id="cboxClose"]').click()

time.sleep(2)

web.find_element(By.XPATH, '//*[@id="search_input"]').send_keys("python", Keys.ENTER)

time.sleep(4)

web.find_element(By.XPATH, '/html/body/div/div[2]/div/div[2]/div[3]/div/div[1]/div[1]/div[1]/div[1]/div[1]/a').click()

# 产生新窗口，如何进入新窗口并提取？
# 在selenium中，新窗口是默认不切换过来的！
# 所以需要进行窗口切换： web,switch_to.window(窗口)
# 窗口是什么？是我们web对象处理的最后一个窗口！即： web.window_handles[-1]
web.switch_to.window(web.window_handles[-1])

# 在新窗口提取内容
time.sleep(2)
require = web.find_element(By.CLASS_NAME, 'job-detail').text
print(require)

# 关掉这个窗口：注意，就算关掉了，selenium也是聚焦于这个页面的！所以要手动切换页面！
web.close()
web.switch_to.window(web.window_handles[0]) # 切换回最初的界面！用下面这一排也一样！
web.switch_to.default_content()
```
:::

有时候，我们爬取页面中内嵌的部分（如播放器等）会遇上iframe框架，这时候：
> 处理iframe(很多时侯就是嵌在网页内的播放器页面等)：
>     先拿到iframe -> selenium切换到iframe！ -> 后续操作

![](https://i.imgur.com/pExeHDt.png)

```

web.get("https://www.91kanju.com/vod-play/541-2-1.html")

# 处理页面中的播放器框架 iframe
iframe = web.find_element(By.XPATH, '//*[@id="player_iframe"]')
# 切换到iframe！
web.switch_to.frame(iframe)

print(web.title)

---

《越狱第1季》第第01集集在线观看－欧美剧－91看剧网

成功拿到！
```

---

**无头浏览器**

前面发现跑`selenium`都会伴随着浏览器动作，但是对我们而言这肯定不是想要的！我们想要的只有数据，如何做到？

- 下面的简单代码好理解，并且你直接粘过来就行！！！
```
from selenium.webdriver.firefox.options import Options

# 准备参数配置！
opt = Options()
opt.add_argument("--headless")
opt.add_argument('--disable-gpu')

web = Firefox(options=opt)

... 后续操作！
```

---

**超级鹰处理验证码**

遇到验证码怎么办！！？

> 超级鹰：成熟的验证码破解工具！其实是一个图像分析的深度学习啦哈哈哈哈哈
> 
> 收费！

- 百度超级鹰就完事了哈。

---

