解锁豆瓣高清海报(一) 深度爬虫与requests进阶之路
前瞻
PosterBandit
这个脚本能够根据用户指定的日期,爬取你看过的影视最高清的海报,然后使用 PixelWeaver.py
自动拼接成指定大小的长图。
你是否发现直接从豆瓣爬取下来的海报清晰度很低? 使用 .pic .nbg img
CSS 选择器,在 我看过的影视
界面找到图片元素并直接爬取(爬取深度为 0),甚至不用太重视 time.sleep()
。好处是速度超快,坏处是这样爬取的海报都是被压缩过的,画质很差。任何一个脚本小子都不会屈服于这样的质量。
想要爬取最高清的海报,答案一定是增加爬取深度!
脚本地址:
项目地址: Gazer
PosterBandit.py
使用方法
- 克隆或下载项目代码。
- 安装依赖:
pip install requests
,或者克隆项目代码后pip install -r requirements.txt
- 修改脚本内部的常量
DEFAULT_POSTER_PATH
,设置默认保存路径。 - 修改主函数处的
poster_save_path
保存路径。 - 修改主函数处的起始日期
target_date_1
和截止日期target_date_2
。 - 填写你的
cookies
。 - 运行脚本
PosterBandit
。
注意
- 起止日期不要写错, 否则判断逻辑会出错。
- 可能依然无法避免 418 错误, 日志中会输出保存失败的图片链接。为了后续拼接海报数据完整性, 建议自行补充保存失败的图片并规范命名(如 2024-12-31)。
示例:
target_date_1 = "2024-12-1" # TODO 填写起始日期target_date_2 = "2024-12-31" # TODO 填写截止日期
准备工作 - JavaScript 动态加载?
首先测试豆瓣海报相关页面是否是通过 JavaScript 动态加载的。在浏览器上设置“不允许网站使用 JavaScript”,刷新豆瓣界面,页面几乎全部正常加载。这很奇怪,和我之前做的脚本使用 requests 打印 raw HTML 得出用户相关信息以及影视相关信息都是使用 JS 动态加载的结论相悖。
先不管了,总之经过测试,完全可以只用 requests 爬取影视海报。😿
脚本构思
- 默认从第 1 页开始爬取,根据输入的起止日期参数,依次检查每页的所有条目是否在指定日期之间,如果是,爬取该条目的海报图片,如果不是,停止爬取;(
requests
) - 根据输入的长宽(长x张, 宽x张)参数,将海报拼接为长图(
pillow
/open CV
) - 自动计算爬取耗时,包括每条爬取耗时和总耗时,并在完成时输出。(
time
)
开始纸上谈兵
先以影视为例,书籍后面再核对元素选择器是否需要修改(做到书籍爬取的时候需要在开头增加选择书/影函数)。
构造请求来翻页,可以绕过使用选择器寻找 “下一页” 的元素。
首先访问 https://movie.douban.com/mine?status=collect
,观察不同页的载荷。
第 1 页
载荷 / payload
start: 0
sort: time
rating: all
mode: grid
type: all
filter: all
请求网址 (GET)
https://movie.douban.com/people/665544778/collect?start=0&sort=time&rating=all&mode=grid&type=all&filter=all
第 2 页
载荷 / payload
start: 15
sort: time
rating: all
mode: grid
type: all
filter: all
请求网址 (GET)
https://movie.douban.com/people/665544778/collect?start=15&sort=time&rating=all&mode=grid&type=all&filter=all
第 3 页
载荷 / payload
start: 30
sort: time
rating: all
mode: grid
type: all
filter: all
请求网址 (GET)
https://movie.douban.com/people/665544778/collect?start=30&sort=time&rating=all&mode=grid&type=all&filter=all
结论:
要获取不同页数的 URL,只需要改变 URL 中的 start=0
参数,第 x 页 URL 的 start
参数是 (x - 1) * 15
。
代码实现方案
使用广度优先搜索 (Breadth-First Search, BFS):一种常用的爬虫策略,先访问同一层级的所有页面,然后再访问下一层级的页面。最大爬取深度为 3,下面我在括号中标记了爬取深度。
-
构造不同页数的 URL,默认从第 1 页开始爬取。
-
以默认第一页或指定的页数作为爬取的起始页 (Level 0),找到所有包含电影条目的 div 元素,最大为 15 个。 ▶️
get_movie_elements
电影条目 CSS 选择器:
#content > div.grid-16-8.clearfix > div.article .item.comment-item
-
在电影条目的 div 元素内找到对应的日期元素和具体条目链接 ▶️
get_movie_info
日期 CSS 选择器:
#content div.info span.date
具体条目 CSS 选择器:
#content div.article div.pic > a
检查是否在指定的起止日期参数之间 ▶️
compare_date
-
进入具体条目链接 (Level 1),找到清晰的海报列表链接 ▶️
get_poster_url
crawl_link
海报列表链接 CSS 选择器:
#mainpic > a
-
进入海报列表页 (Level 2),找到默认的第一张海报 ▶️
crawl_link
第一张海报 CSS 选择器:
#content > div > div.article > ul > li:nth-child(1) > div.cover > a > img
-
进入未压缩的最终海报的链接 (Level 3) ▶️
get_poster_url
最终海报 CSS 选择器:
#content div.article div.cover > a
-
下载图片保存到指定路径,创建文件夹名称,根据日期定义,如
2024_1_1_2024_12_31
▶️create_folder
save_poster
文件结构
Gazer/
├── DoubanGaze/
│ ├── data/
│ │ └── poster/
│ │ └── 2024_1_1_2025_1_31/
│ └── src/
│ └── PosterBandit.py
└── ...
代码实践
find
, find_all
, select
, 和 select_one
几个方法的辨析
先来说说 find
和 find_all
,它们是一对,都是基于标签名和属性来查找元素:
-
find(name, attrs, recursive, string, **kwargs)
- 用途: 查找 第一个 匹配条件的 标签。
- 参数:
name
: 标签名,比如'div'
,'img'
,'a'
。attrs
: 一个字典,包含属性的键值对,比如{'class': 'title', 'id': 'myImage'}
。recursive
: 一个布尔值,表示是否递归查找所有子孙标签,默认为True
。string
: 查找包含特定文本的标签。**kwargs
: 可以直接写属性名作为参数,比如class_='title'
,id='myImage'
。
- 返回值: 如果找到,返回一个
Tag
对象;如果没找到,返回None
。
-
find_all(name, attrs, recursive, string, limit, **kwargs)
- 用途: 查找 所有 匹配条件的 标签。
- 参数:
name
,attrs
,recursive
,string
,**kwargs
: 和find
相同。limit
: 一个整数,限制返回的结果数量。
- 返回值: 返回一个
ResultSet
对象,它是一个包含所有匹配标签的列表。
举个例子:
<html>
<body><div class="movie"><img src="poster1.jpg" class="poster" id="poster1"><p class="title">电影1</p></div><div class="movie"><img src="poster2.jpg" class="poster" id="poster2"><p class="title">电影2</p></div>
</body>
</html>
from bs4 import BeautifulSoupsoup = BeautifulSoup(html_doc, 'html.parser')# 查找第一个 class 为 "movie" 的 div 标签
first_movie_div = soup.find('div', class_='movie')# 查找所有 class 为 "movie" 的 div 标签
all_movie_divs = soup.find_all('div', class_='movie')# 查找第一个 src 属性为 "poster1.jpg" 的 img 标签
first_poster = soup.find('img', src='poster1.jpg')# 查找所有 class 为 "poster" 的 img 标签
all_posters = soup.find_all('img', class_='poster')# 查找所有包含文本 "电影" 的 p 标签
movie_titles = soup.find_all('p', string='电影') #注意这个用法, 和class_='title'是不一样的, 一个是找文本内容, 一个是找属性内容
再来说说 select
和 select_one
,它们是另一对,都是基于 CSS 选择器来查找元素:
-
select_one(selector)
- 用途: 使用 CSS 选择器查找 第一个 匹配的 标签。
- 参数:
selector
: 一个字符串,表示 CSS 选择器。
- 返回值: 如果找到,返回一个
Tag
对象;如果没找到,返回None
。
-
select(selector)
- 用途: 使用 CSS 选择器查找 所有 匹配的 标签。
- 参数:
selector
: 一个字符串,表示 CSS 选择器。
- 返回值: 返回一个列表,包含所有匹配的标签。
CSS 选择器的优点:
- 简洁灵活: CSS 选择器语法非常强大,可以非常灵活地定位元素。
- 与前端开发衔接: 如果你熟悉前端开发,使用 CSS 选择器会非常顺手。
举个例子 (继续用上面的 HTML):
# 查找第一个 class 为 "movie" 的 div 标签
first_movie_div = soup.select_one('div.movie')# 查找所有 class 为 "movie" 的 div 标签
all_movie_divs = soup.select('div.movie')# 查找第一个 id 为 "poster1" 的 img 标签
first_poster = soup.select_one('img#poster1')# 查找所有 class 为 "poster" 的 img 标签
all_posters = soup.select('img.poster')# 查找所有 class 为 "movie" 的 div 标签下的 p 标签
movie_titles = soup.select('div.movie p')
总结一下:
方法 | 用途 | 基于 | 返回值 |
---|---|---|---|
find | 查找第一个匹配的标签 | 标签名和属性 | Tag 对象 或 None |
find_all | 查找所有匹配的标签 | 标签名和属性 | ResultSet 对象 (标签列表) |
select_one | 查找第一个匹配的标签 | CSS 选择器 | Tag 对象 或 None |
select | 查找所有匹配的标签 | CSS 选择器 | 列表 (包含所有匹配的标签) |
什么时候用哪个?
- 简单情况: 如果只是根据简单的标签名和属性查找,
find
和find_all
就足够了。 - 复杂情况: 如果需要根据复杂的条件查找,或者你更熟悉 CSS 选择器,那么
select
和select_one
更合适。 - 只要一个结果: 如果你确定只需要一个结果,或者只关心第一个结果,就用
find
或select_one
。 - 需要所有结果: 如果你需要所有匹配的结果,就用
find_all
或select
。
为什么 headers
中的 "cookies": cookies
要改成 "Cookie": cookies
?
这是因为在 HTTP 请求的头部信息中,用于传递 Cookie 的字段名是 Cookie
(注意首字母大写),而不是 cookies
。
- 服务器端识别的是
Cookie
这个字段名。 当服务器收到你的请求时,它会去解析Cookie
字段,获取你发送的 Cookie 信息。如果你写成cookies
,服务器就无法正确识别和处理你的 Cookie 了。 - 这是 HTTP 协议的规定。 就像你写信要按照固定的格式写地址一样,HTTP 协议也规定了请求头和响应头中各个字段的名称和格式,
Cookie
字段就是其中之一。
所以,为了让服务器能够正确识别你发送的 Cookie,我们必须使用 "Cookie": cookies
。
关于在 div
元素内部继续查找的选择器,有两种选择:
1. 只针对 div
内部编写选择器 (相对路径):
- 这种方式更简洁,也更符合当前的代码逻辑。
- 选择器直接从当前
div
元素的子元素开始写。 - 例如,如果当前
div
元素是movie_element
,那么movie_element.select_one("div.info > ul > li:nth-child(3)")
就表示选择当前div
元素内部div.info > ul
下的第三个li
元素。
2. 从 #content
开始编写选择器 (绝对路径):
- 这种方式也是可以的,但是没有必要,也更繁琐。
- 选择器需要从
#content
开始,写出完整的路径。 - 例如,
movie_element.select_one("#content > div.grid-16-8.clearfix > div.article .item.comment-item")
也能选择到相同的日期元素,但是这种写法很冗长,而且容易出错。而且我们已经通过movie_elements
缩小了范围, 没有必要继续从#content
开始了
Python 的函数参数传递规则 - 关于 page_id=1
参数位置:
把 page_id=1
这个参数放到任意参数前面会导致 IDE 提示错误,而放到最后就不会报错,这是因为 Python 的函数参数传递规则:
- 位置参数: 按照定义顺序传递的参数,必须按照顺序传入。
- 关键字参数: 通过参数名传递的参数,可以不按照顺序传入。
- 默认参数: 在函数定义时设置了默认值的参数,如果调用时没有传入该参数,则使用默认值。
规则:
- 位置参数必须在关键字参数前面。
- 默认参数必须在位置参数后面。
page_id=1
是一个默认参数,所以它必须放在位置参数cookies
,target_date_1
,target_date_2
,poster_save_path
后面,否则 IDE 会报错。
所以,只能把 page_id=1
放到最后。
关于 BeautifulSoup 解析:
是否总是使用 soup = BeautifulSoup(response.content.decode('utf-8'), 'html.parser')
? 是否可以只用 soup = BeautifulSoup(response, 'html.parser')
?
答案是:不建议。
response
对象默认是字节串,需要先解码成字符串,再交给BeautifulSoup
解析。- 如果你的 HTML 编码不是
utf-8
,需要使用正确的编码方式来解码(例如gbk
,iso-8859-1
等)。
建议:
- 始终使用
response.content.decode('utf-8', errors='ignore')
来解码,errors='ignore'
是为了忽略解码错误, 如果遇到无法解码的字符, 会忽略它, 不会报错。 - 最好在请求的时候设置正确的编码:
-
response = requests.get(target_link) response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, 'html.parser')
response.apparent_encoding
会根据响应内容尝试识别正确的编码方式,并设置到response.encoding
中,这样response.text
会使用正确的编码来解码.
-
关于 while 循环中的日期比较:
- 在 while 循环中,已经有
if not compare_date(target_date_1, target_date_2, viewed_date_text): break
跳出循环,为什么还要在最后加上if not compare_date(target_date_1, target_date_2, viewed_date_text): break
? - 理解
break
的作用域- 第一个
if not compare_date(...) : break
是在for movie_element in viewed_movie_elements:
循环内部,它只能跳出当前的for
循环。 - 为了在所有页面都爬取完毕后跳出
while True
的循环,还需要在 while 循环的末尾加上if not compare_date(...) : break
。 - 但是需要注意的是:
viewed_date_text
有可能为空, 这会导致错误, 你需要设置一个默认值viewed_date_text = ""
- 第一个
418 I’m a teapot?? Bring 'em on!!
418 错误
在哪里开始碰到 418 错误?
在 get_poster_url
函数内部,当访问海报页面的时候,被豆瓣服务器拒绝了,并返回 418
错误。
之前所有的步骤,包括访问列表页和详情页,都是成功的 (200)。
错误信息:
418 Client Error: for url: https://movie.douban.com/subject/3586996/
418 Client Error: for url: https://movie.douban.com/subject/2373195/
418 Client Error: for url: https://movie.douban.com/subject/10449575/
418 错误解决方案
1. 理解 418 错误
- 状态码含义: 418 是一个 HTTP 状态码,全称是
I'm a teapot
,本意表示服务器是一个茶壶,而不是咖啡机,无法提供请求的服务。 - 反爬虫应用: 网站会故意返回 418 状态码,来识别和阻止爬虫的访问。
- 选择 418 的原因: 这是一个不常见的 HTTP 状态码,可以更好地迷惑和阻止爬虫程序。
2. 418 错误产生的原因
- 请求头不完整: 网站会检查 HTTP 请求头来判断是否是爬虫。
- User-Agent: 缺失或使用默认爬虫
User-Agent
,容易被识别为爬虫。 - 其他请求头:
Referer
等信息不完整或不正确,也可能被识别为爬虫。 - 访问频率过快: 短时间内大量访问同一页面,也会被认为是爬虫行为。
3. 解决 418 错误的思路:伪装成浏览器
-
核心思路: 伪装成正常的浏览器访问行为,绕过网站的反爬虫机制。
-
解决方案:
-
3.1. 使用
requests.Session()
管理 Cookies 和连接池requests.Session
是requests
库中用于发送 HTTP 请求的类,它可以自动管理 Cookies 和连接池。- Cookies 管理:
requests.Session
可以自动保存和发送 Cookies,确保你的每次请求都携带了正确的 Cookies,从而避免被豆瓣服务器识别为爬虫。- 当你的爬虫第一次访问豆瓣时,豆瓣服务器会返回一些 Cookies,这些 Cookies 可以用来标识你的身份。使用
requests.Session
,你可以确保你的每次请求都携带了正确的 Cookies。
- 连接池:
requests.Session
还可以管理连接池,从而提高你的爬虫的性能。- 当你使用
requests.Session
发送多个请求时,requests.Session
会自动重用连接,从而避免每次请求都建立新的连接,从而提高效率。
- HTTP Keep-Alive (Persistent Connections):
- HTTP Keep-Alive,也称为持久连接,是一种在 HTTP/1.1 中引入的机制,用于提高 HTTP 请求的性能。
- 传统 HTTP 请求: 每次请求都会建立新的 TCP 连接,请求完成后会断开连接。
- Keep-Alive: 使用 Keep-Alive,可以在一个 TCP 连接上发送多个 HTTP 请求和响应,从而避免每次请求都建立新的 TCP 连接。
- 正确使用
session
- 如果在
get_poster_url
函数内创建了新的session
, 每次调用get_poster_url
都会创建一个新的session
。 - 这会导致
session
的Keep-Alive
特性无法被利用,每次请求都会建立新的 TCP 连接。 - 最佳实践:你需要在
download_poster_images
中创建session
,并将session
作为参数传递给get_poster_url
函数。
- 如果在
-
3.2 使用重试机制
- 使用
requests.Session()
和Retry
,以确保每个请求都有重试机制。 - 代码:
retry = Retry(connect=3, backoff_factor=0.5)adapter = HTTPAdapter(max_retries=retry)session.mount('http://', adapter)session.mount('https://', adapter)
- 作用:
Retry(connect=3, backoff_factor=0.5)
创建一个Retry
对象,用于定义重试策略。connect=3
表示连接错误最多重试 3 次,backoff_factor=0.5
表示重试的间隔时间会以 0.5 为系数逐渐增加。HTTPAdapter(max_retries=retry)
创建一个HTTPAdapter
对象,用于将Retry
对象应用到requests.Session
对象中。session.mount('http://', adapter)
和session.mount('https://', adapter)
将HTTPAdapter
对象应用到http
和https
协议的请求中。- 目的: 当你的请求因为网络错误或者服务器错误而失败时,
requests
会自动重试,从而提高你的代码的健壮性。
- 捕获 HTTPError - 非 200 状态码的重试
- ** 捕获 418 错误的关键是捕获
requests
库抛出的HTTPError
异常,然后根据错误码进行重试。在对关键的图片下载的 418 异常中, 加入重试的机制. - 为什么只捕获
HTTPError
?requests
库在遇到非 200 状态码(例如 418)时会抛出requests.exceptions.HTTPError
异常, 这种异常是requests.exceptions.RequestException
的子类.- 我们只捕获
HTTPError
,是因为requests.exceptions.RequestException
可能会捕捉到其他类型的异常。我们希望只在遇到 HTTP 状态码错误时进行重试,而不是其他类型的异常。
2. 之前的重试机制针对连接错误
- 之前的重试机制 (通过
urllib3.util.retry.Retry
和requests.adapters.HTTPAdapter
实现) 确实是针对连接错误的:urllib3.util.retry.Retry
中的connect=3
参数表示针对连接错误(例如requests.exceptions.ConnectionError
或者 socket 错误)最多重试 3 次。- 连接错误通常是指无法建立网络连接的情况,例如 DNS 解析失败、服务器无响应、网络中断等。
requests.adapters.HTTPAdapter
将重试策略应用到 HTTP 请求中,仅当发生连接错误时才会触发重试。
3.
requests.exceptions.RequestException
的作用requests.exceptions.RequestException
是一个基类: 它包含了所有requests
库可能抛出的异常,例如:requests.exceptions.ConnectionError
: 连接错误(例如 DNS 解析失败、服务器无响应)。requests.exceptions.HTTPError
: HTTP 状态码错误(例如 404 Not Found、500 Internal Server Error, 也包括 418)。requests.exceptions.Timeout
: 请求超时错误。requests.exceptions.TooManyRedirects
: 重定向次数过多错误。- 其他
requests
库的异常.
2.
raise_for_status()
的作用response.raise_for_status()
的作用是: 检查 HTTP 响应的状态码。- 如果状态码是 200 (OK): 表示请求成功,代码会继续往下执行。
- 如果状态码不是 200 (例如 404, 418, 500 等): 表示请求失败,会抛出一个
requests.exceptions.HTTPError
异常。
- 之前的代码已经有
raise_for_status()
:- 所以原本就可以检测 HTTP 状态码错误,并在遇到错误的时候抛出
requests.exceptions.HTTPError
异常. - 但是,没有处理这个异常,所以之前的代码会因为异常而直接终止, 而不会重试
- 所以原本就可以检测 HTTP 状态码错误,并在遇到错误的时候抛出
- 增加
try...except HTTPError
的作用:- 增加
try...except HTTPError
结构是为了捕获raise_for_status()
抛出的HTTPError
异常。 - 捕获到这个异常后,可以执行一些错误处理逻辑,例如打印错误信息,然后重新发送请求 (即进入下一循环)。
- 所以,增加
try...except HTTPError
的核心作用就是让你的代码能够处理 HTTP 状态码错误,并进行重试。
- 增加
总结
- 捕获 418 错误: 需要使用
try...except HTTPError
来捕获 HTTP 状态码错误。 - 之前的重试机制: 针对连接错误,而非 HTTP 状态码错误。
requests.exceptions.RequestException
:- 是一个基类,捕获所有
requests
库可能抛出的异常,但是不包括HTTPError
, 因为raise_for_status()
会单独抛出HTTPError
。 - 现在主要用于捕捉下载图片时发生的异常。
- 是一个基类,捕获所有
- 清晰的错误处理:
- 使用
response.raise_for_status()
并且try...except HTTPError
来进行重试, 这能确保我们只在 HTTP 状态码错误的时候重试。
- 使用
- 使用
-
3.3 减慢请求速度
- 添加
time.sleep()
: 在每次请求之前,设置随机的time.sleep()
,可以降低爬虫的访问频率,从而减少被网站识别为爬虫的风险。time.sleep(random.randint(2, 5))
- 添加
-
3.4 使用更真实的
User-Agent
- 使用真实的浏览器
User-Agent
,让网站误认为我们是浏览器在访问。 - 代码示例:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",}
- 添加其他头: 添加常见的 HTTP 头,例如
Accept
,Accept-Language
,Referer
等, 添加浏览器常用的 header, 比如Upgrade-Insecure-Requests
,Sec-Fetch-User
,Sec-Fetch-Mode
,Sec-Fetch-Dest
,Sec-Fetch-Site
等.
- 使用真实的浏览器
-
-
3.5 动态获取
headers
- 定义一个返回
headers
的函数, 在while
循环里面调用函数来动态获取headers
。
- 定义一个返回
-
3.6 传递
headers
参数- 将
headers
作为参数传递到get_poster_url
函数中,让get_poster_url
函数内部的每一个请求都能够携带正确的headers
信息,包括User-Agent
、Cookie
、Referer
等。
- 将
-
3.7 细粒度控制请求
- 使用
session.get
访问海报列表页面,使得我们能够更细粒度地控制请求,从而成功避开了豆瓣的反爬虫机制。
- 使用
4. requests.get
和 session.get
的对比
-
requests.get
:requests.get
是requests
库中用于发送 HTTP GET 请求的函数。- 每次调用
requests.get
,都会建立一个新的 TCP 连接。 - 不会自动保存 Cookies,每次请求都需要手动传递 Cookies。
- 不会自动管理连接池。
-
session.get
:session.get
是requests.Session
对象中用于发送 HTTP GET 请求的方法。- 使用同一个
requests.Session
对象发送多个请求,可以重用同一个 TCP 连接,从而提高效率。 - 可以自动保存和发送 Cookies,从而保持登录状态。
- 会自动管理连接池。
-
何时使用
session.get
:- 需要保持登录状态的爬虫: 如果你的爬虫需要访问需要登录才能访问的页面,那么你需要使用
session.get
来管理 Cookies,保持登录状态。 - 需要发送多个请求的爬虫: 如果你的爬虫需要访问多个页面,那么你可以使用
session.get
来重用 TCP 连接,从而提高效率。 - 需要使用重试机制的爬虫: 如果你的爬虫需要使用重试机制来处理请求失败的情况,你可以在
requests.Session
对象中配置重试策略。 - 总之,在很多时候
session.get
都是更合适的选择。
- 需要保持登录状态的爬虫: 如果你的爬虫需要访问需要登录才能访问的页面,那么你需要使用
-
session.get
取决于爬虫深度吗?- **并不是完全取决于爬虫深度,**虽然深层次爬取需要更多请求,使用
session.get
效率会更高,但是并不是说,浅层次的爬取就不需要session.get
。 - 选择
session.get
还是requests.get
, 主要取决于你的爬虫的复杂度和具体需求。- 如果你只需要发送一次请求,那么使用
requests.get
就可以。 - 但是,如果你需要发送多个请求,或者需要管理 Cookies 或者重试机制,那么使用
session.get
是更合适的选择。
- 如果你只需要发送一次请求,那么使用
- **并不是完全取决于爬虫深度,**虽然深层次爬取需要更多请求,使用
5. 经验总结
- 遇到反爬机制强的网站,可以尝试在每一次更深层次爬取的时候,都带上构造好的
headers
。 - 反爬虫策略通常会对深层次的爬取进行更严格的限制,因为深层次的爬取通常会消耗更多的服务器资源。
优化写入速度
-
为什么写入图片会很慢?
- 原因:
- 同步 I/O: 现在的代码使用同步 I/O 来写入图片,这意味着程序会阻塞在写入操作上,直到写入完成才会继续执行。
- 磁盘写入速度: 磁盘写入速度通常比内存读写速度慢很多。
chunk_size
: 在代码中设置了chunk_size=8192
,每次读取 8KB 的数据进行写入。- **CPU 负载:**虽然CPU性能足够,但是如果频繁读取和写入大量小块数据,会增加CPU的负载。
- 结论:
- 同步 I/O 加上磁盘写入速度限制导致了写入图片的速度较慢。
- 原因:
-
如何优化写入速度?
- 使用更大的
chunk_size
:- 增加
chunk_size
可以减少读取和写入的次数,从而提高写入速度。 - 你可以尝试将
chunk_size
设置为 65536 (64KB) 或者更大。 - 但是:
chunk_size
过大也可能导致内存使用过高,你需要根据实际情况进行调整。
- 增加
- 使用异步 I/O:
- 使用异步 I/O 可以让程序在写入图片的同时,执行其他操作,从而提高程序的效率。
- 需要使用异步 I/O库,例如
asyncio
和aiohttp
,这将大大增加代码的复杂度。 - 需要异步处理,也需要修改
save_poster
的调用方式。
- 使用多线程或多进程:
- 使用多线程或多进程可以并发地进行多个写入操作,从而提高整体的写入速度。
- 但是: 多线程可能受到 GIL 的限制,多进程可能会增加系统开销。
- 使用
shutil.copyfileobj
:shutil.copyfileobj
可以更高效地将文件对象复制到磁盘,减少代码量。
- 使用更大的
iter_content(chunk_size=65536)
和 shutil.copyfileobj
在不同情况下的性能问题
在爬取少量图片时,iter_content(chunk_size=65536)
和 shutil.copyfileobj
的性能差异不大,甚至 shutil.copyfileobj
还可能略慢。
爬取更多图片时,应该选择哪个?
shutil.copyfileobj
的优势:- 更高效:
shutil.copyfileobj
使用了更高效的底层实现,可以减少 Python 代码的开销,避免频繁读写操作,从而在大量数据传输时表现更好。 - 更简洁:
shutil.copyfileobj
的代码更简洁,易于维护。 - 更稳定: 由于
shutil.copyfileobj
由 Python 官方维护, 可以保证其稳定性。
- 更高效:
iter_content(chunk_size=65536)
的局限:- Python 代码开销: 每次循环读取
chunk_size
大小的数据都需要进行 Python 代码的执行,这会增加 Python 代码的开销。 - 需要手动处理: 需要自己编写代码来处理读取到的数据,容易出错。
- Python 代码开销: 每次循环读取
- 建议:
- 在爬取更多图片时,
shutil.copyfileobj
是更稳定和更好的选择。 - 不需要自己处理分块的数据,从而简化你的代码,让代码更易于维护。
- 如果你不希望使用
shutil.copyfileobj
, 你可以尝试使用更大的chunk_size
,但是不建议这样操作。
- 在爬取更多图片时,
代码计时
增加计时器来计算每次爬取耗时
-
在哪里增加计时器?
- 核心问题: 需要决定计时器应该放在代码的哪个位置,才能准确地计算每次爬取的耗时。
- 方案:
- 在
download_poster_images
函数开始时启动计时器: 这样可以计算整个爬取过程的耗时。 - 在
download_poster_images
函数结束时停止计时器: 这样可以获取整个爬取过程的耗时。 - 在
while
循环开始时记录时间,在while
循环结束时记录时间: 了解每次循环的耗时。 - 在
for
循环开始时记录时间,在for
循环结束时记录时间: 了解每次处理电影条目的耗时。
- 在
- 选择:
- 将计时器放在
download_poster_images
函数的开始和结束处,这样可以计算整个爬取过程的耗时。 - 同时,将计时器放在
for
循环的开始和结束处, 从而得到单个条目的爬取时间。
- 将计时器放在
-
如何使用 Python 实现计时器?
- 使用
time
模块: Python 的time
模块提供了time()
函数,可以获取当前时间的时间戳(以秒为单位)。 - 代码示例:
import timestart_time = time.time() # 启动计时器# 执行一些代码end_time = time.time() # 停止计时器 elapsed_time = end_time - start_time # 计算耗时 print(f"耗时:{elapsed_time:.2f} 秒")
time.perf_counter()
:time.perf_counter()
返回性能计数器的值(以秒为单位),该计数器提供尽可能高的可用分辨率测量时间。- 这个方法通常用来测量时间间隔, 非常适合我们的情景。
- 它的原理是基于CPU的硬件计时器,因此具有非常高的精度,可以达到纳秒级别。
- 使用
time.perf_counter()
和 time.time()
的区别
-
time.time()
的特点:- 返回时间戳:
time.time()
返回的是当前时间的时间戳,即从 Unix 纪元(1970年1月1日00:00:00 UTC)到现在的秒数,是一个浮点数。 - 系统时间:
time.time()
获取的是系统的实时时间,可能会受到系统时间调整的影响,例如:时钟同步、手动调整时间等。 - 精度较低:
time.time()
的精度通常较低,可能只能达到毫秒级别,甚至秒级别,具体取决于操作系统的实现。
- 返回时间戳:
-
time.perf_counter()
的特点:- 返回性能计数器值:
time.perf_counter()
返回的是性能计数器的值,这是一个单调递增的计时器,不会受到系统时间调整的影响。 - 高精度:
time.perf_counter()
的精度通常比time.time()
高很多,可以达到纳秒级别,具体取决于 CPU 的硬件实现。 - 适用于测量时间间隔:
time.perf_counter()
主要用于测量代码执行的时间间隔,而不是测量绝对时间。 - 不受系统时间影响:
time.perf_counter()
不受系统时间调整的影响,可以提供更准确的计时结果。
- 返回性能计数器值:
-
time.perf_counter()
为什么比time.time()
好?- 核心问题: 为什么在测量代码执行时间时,
time.perf_counter()
通常比time.time()
更好? - 原因:
- 高精度:
time.perf_counter()
的精度比time.time()
高,可以提供更准确的计时结果。这对于测量执行时间较短的代码片段,尤其重要。 - 不受系统时间影响:
time.perf_counter()
不受系统时间调整的影响,可以提供更稳定的计时结果。这对于长时间运行的代码或者在不同环境下运行的代码,尤其重要。 - 单调递增:
time.perf_counter()
返回的值是单调递增的,这意味着它可以确保时间测量的顺序性,避免出现时间回溯的问题。
- 高精度:
- 结论:
- 在测量代码执行时间时,
time.perf_counter()
是更合适的选择,因为它提供了更高的精度、更稳定的结果,并且不受系统时间调整的影响。
- 在测量代码执行时间时,
- 核心问题: 为什么在测量代码执行时间时,
-
什么时候使用
time.time()
?- 获取当前时间: 如果你需要获取当前时间,例如:记录日志的时间、设置定时任务等,那么可以使用
time.time()
。 - 需要系统时间: 如果你的程序需要使用系统时间,并且对精度要求不高,那么可以使用
time.time()
。 - 例如: 需要获得当前的日期, 你可能需要
time.time()
结合datetime
来实现。
- 获取当前时间: 如果你需要获取当前时间,例如:记录日志的时间、设置定时任务等,那么可以使用
-
总结:
time.time()
: 用于获取当前时间,精度较低,可能会受到系统时间调整的影响。time.perf_counter()
: 用于测量时间间隔,精度高,不受系统时间调整的影响。- 在测量代码执行时间时,通常使用
time.perf_counter()
,因为它可以提供更高的精度和更稳定的结果。
下一篇
解锁豆瓣高清海报(二) 使用 OpenCV 拼接和压缩 😽
相关文章:
解锁豆瓣高清海报(一) 深度爬虫与requests进阶之路
前瞻 PosterBandit 这个脚本能够根据用户指定的日期,爬取你看过的影视最高清的海报,然后使用 PixelWeaver.py 自动拼接成指定大小的长图。 你是否发现直接从豆瓣爬取下来的海报清晰度很低? 使用 .pic .nbg img CSS 选择器,在 我…...
计算机组成原理——数据运算与运算器(二)
生活就像一条蜿蜒的河流,有时平静,有时湍急。我们在这条河流中前行,会遇到风雨,也会遇见阳光。重要的是,无论遇到什么,都要保持内心的平静与坚定。每一次的挫折,都是让我们成长的机会࿱…...

SpringBoot+Vue的理解(含axios/ajax)-前后端交互前端篇
文章目录 引言SpringBootThymeleafVueSpringBootSpringBootVue(前端)axios/ajaxVue作用响应式动态绑定单页面应用SPA前端路由 前端路由URL和后端API URL的区别前端路由的数据从哪里来的 Vue和只用三件套axios区别 关于地址栏url和axios请求不一致VueJSPS…...

【AI】DeepSeek 概念/影响/使用/部署
在大年三十那天,不知道你是否留意到,“deepseek”这个词出现在了各大热搜榜单上。这引起了我的关注,出于学习的兴趣,我深入研究了一番,才有了这篇文章的诞生。 概念 那么,什么是DeepSeek?首先百…...

javascript-es6 (二)
函数进阶 函数提升 函数提升与变量提升比较类似,是指函数在声明之前即可被调用 好处:能够使函数的声明调用更灵活 函数提升出现在 相同作用域 当中 //可调用函数 fn()//后声明函数 function fn() {console.log(可先调用再声明) } 注意:函数表…...

供应链系统设计-供应链中台系统设计(十四)- 清结算中心设计篇(三)
关于清结算中心的设计,我们之前的两篇文章中,对于业务诉求的好的标准进行了初步的描述,如果没有看的同学可以参考一下两篇文章进行了解,这样更有利于理解本篇的内容。链接具体如下: 供应链系统设计-供应链中台系统设计…...
【自学笔记】MySQL的重点知识点-持续更新
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 MySQL重点知识点MySQL知识点总结一、数据库基础二、MySQL的基本使用三、数据类型四、触发器(Trigger)五、存储引擎六、索引七、事务处理八、…...

X86路由搭配rtl8367s交换机
x86软路由,买双网口就好。或者单网口主板,外加一个pcie千兆。 华硕h81主板戴尔i350-T2双千兆,做bridge下载,速度忽高忽低。 今天交换机到货,poe供电,还是网管,支持Qvlan及IGMP Snooping…...

Linux环境基础开发工具的使用(apt, vim, gcc, g++, gbd, make/Makefile)
目录 什么是软件包 Linux 软件包管理器 apt 认识apt 查找软件包 安装软件 如何实现本地机器和云服务器之间的文件互传 卸载软件 Linux编辑器 - vim vim的基本概念 vim下各模式的切换 vim命令模式下各指令汇总 vim底行模式个指令汇总 Linux编译器 - gcc/g gcc/g的作…...

多模态论文笔记——ViViT
大家好,这里是好评笔记,公主号:Goodnote,专栏文章私信限时Free。本文详细解读多模态论文《ViViT: A Video Vision Transformer》,2021由google 提出用于视频处理的视觉 Transformer 模型,在视频多模态领域有…...
搜索与图论复习1
1深度优先遍历DFS 2宽度优先遍历BFS 3树与图的存储 4树与图的深度优先遍历 5树与图的宽度优先遍历 6拓扑排序 1DFS: #include<bits/stdc.h> using namespace std; const int N10; int n; int path[N]; bool st[N]; void dfs(int u){if(nu){for(int i0;…...

【数据结构】初识链表
顺序表的优缺点 缺点: 中间/头部的插入删除,时间复杂度效率较低,为O(N) 空间不够的时候需要扩容。 如果是异地扩容,增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。 扩容可能会存在…...

第11章:根据 ShuffleNet V2 迁移学习医学图像分类任务:甲状腺结节检测
目录 1. Shufflenet V2 2. 甲状腺结节检测 2.1 数据集 2.2 训练参数 2.3 训练结果 2.4 可视化网页推理 3. 下载 1. Shufflenet V2 shufflenet v2 论文中提出衡量轻量级网络的性能不能仅仅依靠FLOPs计算量,还应该多方面的考虑,例如MAC(memory acc…...

deepseek+vscode自动化测试脚本生成
近几日Deepseek大火,我这里也尝试了一下,确实很强。而目前vscode的AI toolkit插件也已经集成了deepseek R1,这里就介绍下在vscode中利用deepseek帮助我们完成自动化测试脚本的实践分享 安装AI ToolKit并启用Deepseek 微软官方提供了一个针对AI辅助的插件,也就是 AI Toolk…...
深入理解Flexbox:弹性盒子布局详解
深入理解Flexbox:弹性盒子布局详解 一、Flexbox 的基本概念二、Flexbox 的核心属性1. display: flex2. flex-direction3. flex-wrap4. justify-content5. align-items6. flex 三、Flexbox 的实际应用1. 创建响应式三列布局2. 实现垂直居中3. 复杂布局的嵌套使用 四、…...
android Camera 的进化
引言 Android 的camera 发展经历了3个阶段 : camera1 -》camera2 -》cameraX。 正文 Camera1 Camera1 的开发中,打开相机,设置参数的过程是同步的,就跟用户实际使用camera的操作步骤一样。但是如果有耗时情况发生时,会…...

仿真设计|基于51单片机的氨气及温湿度检测报警
目录 具体实现功能 设计介绍 51单片机简介 资料内容 仿真实现(protues8.7) 程序(Keil5) 全部内容 资料获取 具体实现功能 (1)LCD1602液晶第一行显示当前的氨气值,第二行显示当前的温度…...

关于EDGE IMPULSE的使用与适配,包含如何学习部署在对应的板子
创建好账号后,可以打开主页新建一个工程 跳出这个选no就可以不用标label直接整张图训练,要更改可以去dashboard》labeling method改 然后在这个工程中选择添加自己的照片等数据,他支持这些格式的数据我们现在一般是用在openmv opencv yolo 等…...
【Python蓝桥杯备赛宝典】
文章目录 一、基础数据结构1.1 链表1.2 队列1.3 栈1.4 二叉树1.5 堆二、基本算法2.1 算法复杂度2.2 尺取法2.3 二分法2.4 三分法2.5 倍增法和ST算法2.6 前缀和与差分2.7 离散化2.8 排序与排列2.9 分治法2.10贪心法1.接水时间最短问题2.糖果数量有限问题3.分发时间最短问题4.采摘…...

数据结构 前缀中缀后缀
目录 前言 一,前缀中缀后缀的基本概念 二,前缀与后缀表达式 三,使用栈实现后缀 四,由中缀到后缀 总结 前言 这里学习前缀中缀后缀为我们学习树和图做准备,这个主题主要是对于算术和逻辑表达式求值,这…...

龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...

内窥镜检查中基于提示的息肉分割|文献速递-深度学习医疗AI最新文献
Title 题目 Prompt-based polyp segmentation during endoscopy 内窥镜检查中基于提示的息肉分割 01 文献速递介绍 以下是对这段英文内容的中文翻译: ### 胃肠道癌症的发病率呈上升趋势,且有年轻化倾向(Bray等人,2018&#x…...

MAZANOKE结合内网穿透技术实现跨地域图像优化服务的远程访问过程
文章目录 前言1. 关于MAZANOKE2. Docker部署3. 简单使用MAZANOKE4. 安装cpolar内网穿透5. 配置公网地址6. 配置固定公网地址总结 前言 在数字世界高速发展的今天,您是否察觉到那些静默增长的视觉数据正在悄然蚕食存储空间?随着影像记录成为日常习惯&…...