免责声明
本项目仅供学习和研究使用,使用者应遵守相关法律法规,任何由使用者因违法使用所产生的后果,与本项目无关。爬虫违法违规案例
目录
话不多说,先看效果
playwright异步爬取豆瓣Chart页面
爬取目标:豆瓣电影排行榜https://movie.douban.com/chart,如果你希望爬取豆瓣电视剧 或豆瓣电影页面,可以去看我昨天发布的博客:使用异步Playwright并发爬取豆瓣电视剧tv和电影explore页面,Python实现-CSDN博客https://blog.csdn.net/btaworld/article/details/138366849
完整代码已经上传到github项目,如果只考虑使用,不在乎原理的话,直接访问github项目,按照里面的简陋使用方法进行操作即可,推荐看完这篇博客,理解后再去尝试
或访问下面链接进行资源下载:使用python的异步库playwright进行爬取豆瓣电影排行榜的数据资源-CSDN文库
(一)使用的包
# 测试用的playwright版本如下,一般使用最新的即可
pip install playwright==1.43.0
# 安装好后你需要运行playwright install来下载对应版本的浏览器
playwright install
(二)页面分析
- 这是主界面
- 我们需要爬取的目标在右上角
- 准确来说这些按钮的链接,对应的页面内容,这些页面之后再分析
- 首先看下按钮对应的html如何,很简洁,没有像豆瓣详情页那样为难人
- 可以看到我们需要的链接就在其中,不过这只是初步获取,后面还需要进行修改,如何修改?继续往下看
- 上面可以看出2个链接的差异,主要是:interval_id=90:80和interval_id=100:90,或许也有人细心注意到type_name=剧情片也是不同的,但经过测试,无大影响
- 现在来看如何提取出其中的数据,Chart页面的数据也是通过Ajax响应获取,同样使用浏览器进行网络抓包
- 筛选出xhr的响应,/top_list?type的响应就是我们需要的内容,如何加载更多?一直下滑到底
- 下滑到底后,又加载了一个/top_list?type链接的响应,这个过程是通过js代码控制的,如果有会js逆向的小伙伴可以尝试
- 那……需要加载多少次/top_list?type响应,才算加载完成?
- 在/top_list_count?type响应中,可以看到该页面有多少部电影信息,而每次/top_list?type响应都会产生20部电影的数据,如下图:
- 通过简单的计算:total // 20 就可以得到结果,如果total不是20的倍数需要+1
(三)大致构想
通过上面的页面分析,我们需要做到的有以下几步:
- 打开豆瓣Chart页面
- 提取到“分类排行榜”的链接
- 构造新的链接:从interval_id=100:90到interval_id=10:0
- 将所有链接保存为json文件
- 提取之前保存的所有链接,通过playwright打开链接,
- 进入页面后,通过提取到xhr文件,将需要下滑加载数据的次数计算出来
- 不断下滑,直到加载次数与计算的次数相符
(四)进行爬取
同样是使用2个类实现所有功能,父类FunChart实现全局设置和非爬虫功能的实现,子类PlaywrightChart(FunChart)用于实现浏览器自动化操作功能。
(1)打开浏览器
# 打开浏览器
async def open_browser(self): # 打开浏览器
"""
打开浏览器
"""
logging.info("打开浏览器中……")
if await self.isSaveChartLinks() and not await self.getChartLinks():
logging.info("数据已经爬取完成,无需再次运行,退出程序中……")
logging.info(
f"如果需要重新爬取数据,请删除{self.doubanWaitToSpider}文件夹中的chart.json文件"
)
return
async with async_playwright() as p:
if self.isLogin: # 如果选择登录,设置用户数据目录
self.browser = await p.chromium.launch_persistent_context(
user_data_dir=self.isLogin_user_data_path, # 用户数据目录
headless=self.isHeadless, # 是否无头模式
ignore_default_args=[
"--enable-automation"
], # 禁止弹出Chrome正在受到自动软件的控制的通知
)
else:
self.browser = await p.chromium.launch(
headless=self.isHeadless, # 是否无头模式
ignore_default_args=[
"--enable-automation"
], # 禁止弹出Chrome正在受到自动软件的控制的通知
)
logging.info(f"浏览器已经打开,即将打开页面")
await self.open_page() # 打开页面
-
await self.isSaveChartLinks() and not await self.getChartLinks() 功能是用于检测是否已经构造了所有页面,并以及爬取完成(后面有个功能,如果数据已经爬取完成就自动弹出链接)
-
之后就是playwright打开浏览器的常规操作,这里使用了chromium作为自动化浏览器,是否使用无头浏览器可以在父类的self.isHeadless控制
-
我添加了禁止弹出Chrome正在受到自动软件的控制的通知的功能,有需要可以自行添加其他功能
-
之后运行self.open_page()函数,这个是程序的核心
(2)自定义页面
# 获取一个新的页面
async def get_page(self, url):
"""
获取打开目标url的页面
"""
page = await self.browser.new_page()
if self.isLogin and self.isLogin_isWait:
if self.isLogin_wait_url:
await page.goto(self.isLogin_wait_url)
await asyncio.sleep(self.isLogin_wait_time)
await self.close_browser()
sys.exit()
await page.route(
"**/*.{png,jpg,jpeg,gif,svg,ico}", lambda route, request: route.abort()
) # 禁止加载图片
await page.route(
"**/*.woff2", lambda route, request: route.abort()
) # 禁止加载字体
# await page.route(
# "**/*.css", lambda route, request: route.abort()
# ) # 禁止加载CSS
# await page.route(
# "**/*.js", lambda route, request: route.abort()
# ) # 禁止加载JavaScript
await page.add_init_script( # 添加初始化脚本
'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'
)
await page.goto(url)
await page.wait_for_load_state("load") # 等待页面加载完成
# await page.wait_for_load_state("networkidle") # 等待网络空闲
await self.random_sleep()
return page
- 由于异步爬取过程中需要同时处理多个页面,将页面处理逻辑单独封装成一个函数。
- 这样做可以提高代码的可读性和可维护性,方便对页面进行整体的处理。
- 此页面我已经进行了一些处理(见注释),有需要可以自行修改。
(3)打开页面
# 打开页面
async def open_page(self):
"""
打开页面,获取数据页面
"""
if not await self.isSaveChartLinks(): # 如果没有保存数据,则获取数据
page = await self.get_page(self.url) # 打开页面
# 获取所有分类排行榜的链接和名称
all_elements = await page.query_selector_all(
".types span a"
) # 用css选择器获取所有分类排行榜的链接
all_links = [] # 存储所有链接
from urllib.parse import urljoin # urljoin方法,拼接url
import re # 正则表达式,用于替换url
for element in all_elements:
# /typerank?type_name=历史&type=4&interval_id=100:90&action=,需要替换interval_id的值
relative_url = await element.get_attribute("href") # 获取链接
for i in range(100, 0, -10): # 从100开始,每次减少10
re_url = re.sub( # 替换url
"interval_id=(\d+):(\d+)",
f"interval_id={i}:{i-10}",
relative_url,
)
full_url = urljoin(self.url, re_url) # 拼接url
all_links.append(full_url) # 存储拼接后的url
# 存储所有链接
await self.saveChartLinks(all_links) # 保存所有链接
await self.random_sleep() # 随机等待时间
await page.close() # 关闭页面
# 开始爬取所有链接
self.allSpiderLinks = await self.getChartLinks() # 获取所有链接
tasks = [
asyncio.ensure_future(self.spiderAll(link)) for link in self.allSpiderLinks
]
# 使用asyncio.gather并发地运行所有的任务
await asyncio.gather(*tasks)
self.allSpiderLinks = await self.getChartLinks() # 获取所有链接
if self.allSpiderLinks: # 如果还有未爬取的链接
await self.open_page()
程序核心,实现了以下功能:
- 提取并构造链接
- 保存所有构造的链接
- 提取已经保存的链接
- 利用并发爬取内容
- 如果存在未爬取的链接,则重新调用“打开页面”函数,继续爬取
(4)提取并构造链接
- 如果检测未储存所有需要爬取的所有链接,则需要提取并构造链接:
page = await self.get_page(self.url) # 打开页面
# 获取所有分类排行榜的链接和名称
all_elements = await page.query_selector_all(
".types span a"
) # 用css选择器获取所有分类排行榜的链接
- 首先打开豆瓣Chart页面,然后利用CSS选择器获取所有所有分类排行榜的元素
all_links = [] # 存储所有链接
from urllib.parse import urljoin # urljoin方法,拼接url
import re # 正则表达式,用于替换url
for element in all_elements:
# /typerank?type_name=历史&type=4&interval_id=100:90&action=,需要替换interval_id的值
relative_url = await element.get_attribute("href") # 获取链接
for i in range(100, 0, -10): # 从100开始,每次减少10
re_url = re.sub( # 替换url
"interval_id=(\d+):(\d+)",
f"interval_id={i}:{i-10}",
relative_url,
)
full_url = urljoin(self.url, re_url) # 拼接url
all_links.append(full_url) # 存储拼接后的url
- 然后,创建一个列表(用于储存所有链接),首先对之前提取的所有元素进行循环,获取每一个元素的url,再利用re构造所有链接,然后利用from urllib.parse import urljoin的链接拼接功能,将相对链接拼接位绝对链接,最后将绝对链接添加到列表中
# 存储所有链接
await self.saveChartLinks(all_links) # 保存所有链接
await self.random_sleep() # 随机等待时间
await page.close() # 关闭页面
- 最后保存所有构造的链接到json文件中,再关闭页面
- 构造的链接如下:
[
"https://movie.douban.com/typerank?type_name=剧情&type=11&interval_id=100:90&action=",
"https://movie.douban.com/typerank?type_name=剧情&type=11&interval_id=90:80&action=",
"https://movie.douban.com/typerank?type_name=剧情&type=11&interval_id=80:70&action=",
"https://movie.douban.com/typerank?type_name=剧情&type=11&interval_id=70:60&action=",
"https://movie.douban.com/typerank?type_name=剧情&type=11&interval_id=60:50&action=",
"https://movie.douban.com/typerank?type_name=剧情&type=11&interval_id=50:40&action=",
"https://movie.douban.com/typerank?type_name=剧情&type=11&interval_id=40:30&action=",
……
(5)并发爬虫
# 开始爬取所有链接
self.allSpiderLinks = await self.getChartLinks() # 获取所有链接
tasks = [
asyncio.ensure_future(self.spiderAll(link)) for link in self.allSpiderLinks
]
# 使用asyncio.gather并发地运行所有的任务
await asyncio.gather(*tasks)
- 首先,从await self.getChartLinks() # 获取所有链接
- 然后,为每一个链接创建一个异步任务,这个任务会调用self.spiderAll(link)方法。
- 所有的异步任务被存储在tasks列表中。
- 最后,使用asyncio.gather(*tasks)来并发地运行所有的任务。
(6)提取数据
提取数据的功能在async def spiderAll(self, link):中
# 开始爬取所有链接的数据
async def spiderAll(self, link):
"""
开始爬取所有链接的数据
"""
async with self.semaphore: # 限制并发数量
slip_count = -1 # 需要下滑加载数据的次数
load_count = 0 # 加载次数
# 处理response
async def on_request(response):
"""
获取Ajax数据,并进行处理+保存数据
"""
nonlocal slip_count, load_count # nonlocal声明,可以修改外部变量slip_count, load_count
if "/top_list_count?type" in response.url and response.status == 200:
# 提取出最大下滑次数
data = await response.json()
max_total = data.get("total", 0) # 最多爬取的数据量
slip_count = max_total / 20 # 每次加载20条数据,计算需要下滑的次数
# 判断是否为整数,不是则加1
if slip_count % 1 != 0:
slip_count = int(slip_count) + 1
if "/top_list?type=" in response.url and response.status == 200:
data = await response.json()
await self.saveData(data) # 保存爬取的数据
load_count += 1 # 加载次数+1
logging.info(f"开始爬取链接:{link}") # 开始爬取链接
page = await self.get_page(link) # 打开页面
page.on("response", on_request) # 监听response事件
await page.reload() # 重新加载页面,重新触发response事件
await page.wait_for_load_state("load") # 等待页面加载完成
# 不断下滑,直到没有新的数据
try:
while True:
# 不断下滑
await page.evaluate(
"window.scrollTo(0, document.body.scrollHeight)"
)
# await page.wait_for_load_state("networkidle") # 等待网络空闲
await page.wait_for_load_state("load") # 等待页面加载完成
await self.random_sleep() # 随机等待时间
if load_count == slip_count: # 如果加载次数等于下滑次数,退出循环
break
logging.info(f"爬取数据完成:{link}") # 爬取数据完成
self.allSpiderLinks.remove(link) # 移除已经爬取的链接
await self.saveChartLinks(self.allSpiderLinks) # 保存剩余的链接
except Exception as e:
logging.error(
f"爬取数据出现错误:{e}\n类型:{type(e)}\n堆栈跟踪:\n{traceback.format_exc()}"
)
finally:
await self.random_sleep() # 随机等待时间
await page.close() # 关闭页面,爬取下一个链接
- 这个功能实现了监听response事件,用于提取数据的功能
- 并使用
# 不断下滑 await page.evaluate( "window.scrollTo(0, document.body.scrollHeight)" )
实现了不断向下滑,滑到底部的功能
-
利用
if load_count == slip_count: # 如果加载次数等于下滑次数,退出循环 break
判断是否已经获取全部数据
-
再利用
logging.info(f"爬取数据完成:{link}") # 爬取数据完成 self.allSpiderLinks.remove(link) # 移除已经爬取的链接 await self.saveChartLinks(self.allSpiderLinks) # 保存剩余的链接
弹出已经爬取完成的链接
-
最后关闭页面
(7)保存数据
在 async def on_request(response):功能中,会筛选出目标链接的数据,然后调用await self.saveData(data) 进行筛选并保存数据
# 保存爬取的数据
async def saveData(self, data):
"""
保存爬取的数据
"""
for item in data:
id = item.get("id", None) # id
title = item.get("title", None) # 标题
douban_link = f"https://movie.douban.com/subject/{id}/" # 豆瓣链接
pic_normal = item.get("cover_url", None) # 普通图片链接
score = float(item.get("score", 0.0)) # 评分
score_num = int(item.get("vote_count", 0)) # 评分人数
genres = item.get("types", []) # 类型
country = item.get("regions", []) # 国家
release_date = item.get("release_date", None) # 上映日期
actors = item.get("actors", []) # 演员
item_type = "movie" # 类型
logging.info(f"id: {id},标题: {title}") # 同样需要自行完成数据储存的功能
我已经提取出一些数据,需要自行实现数据储存的功能(适合有python基础的人学习,不过你都看到这了,应该有基础吧?),数据量也比较大,同样非常推荐用数据库进行储存。
(五)完整代码
完整代码已经上传到github项目
这里只对主要功能,进行了分析,如果需要使用,可以去github项目上下载代码,里面有比较简陋的使用方法,但能用……该项目中的代码大多都写了注释,希望你能通过此博客理解其原理。
(六)使用教程
所有代码已经上传到CSDN,通过CSDN的资源绑定功能进行绑定本页面。
首先下载该页面顶部绑定的资源,免费的,也不需要积分,压缩宿包,解压玉里面的文件,就能获取所有代码,然后根据以下教程进行使用。
需要进行登入,程序默认使用登入功能(不登入ip非常容易被禁),需要在登入前提下进行爬取。如何登入:
首先准备一个豆瓣账号
然后,打开里面的py文件(只有一个py文件):
修改self.isLogin_isWait #打开页面后等待的值为True 修改self.isHeadless #是否无头模式 为False。
再运行(默认你已经搭建好环境),会弹出一个浏览器,打开豆瓣页面
接下来就进行登入豆瓣(及时登入,因为这个页面只会打开self.isLogin_wait_time =100 #等待时间,等待100秒),然后手动关闭浏览器
再修改self. isLogin_isWait#打开页面后等待的值为False(一定要修改,否则无法爬取内容)
再修改self.isHeadlace 为True(看个人意愿)
你需要在里面手动添加保存数据的方式。如果不会,可以看我的博客:使用Python存储数据-CSDN博客https://blog.csdn.net/btaworld/article/details/138496824
最后,运行py文件,就能愉快得爬取豆瓣chart页面所有电影信息。