python爬虫:基于异步Playwright并发爬取豆瓣电影排行榜(豆瓣Chart页面),包含完整源码

免责声明

本项目仅供学习和研究使用,使用者应遵守相关法律法规,任何由使用者因违法使用所产生的后果,与本项目无关。爬虫违法违规案例

目录

免责声明

话不多说,先看效果

(一)使用的包

(二)页面分析

(三)大致构想

(四)进行爬取

(1)打开浏览器

(2)自定义页面

(3)打开页面

(4)提取并构造链接

(5)并发爬虫

(6)提取数据

(7)保存数据

(五)完整代码


话不多说,先看效果

playwright异步爬取豆瓣Chart页面

爬取目标:豆瓣电影排行榜icon-default.png?t=N7T8https://movie.douban.com/chart,如果你希望爬取豆瓣电视剧 豆瓣电影页面,可以去看我昨天发布的博客:使用异步Playwright并发爬取豆瓣电视剧tv和电影explore页面,Python实现-CSDN博客icon-default.png?t=N7T8https://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

(三)大致构想

通过上面的页面分析,我们需要做到的有以下几步:

  1. 打开豆瓣Chart页面
  2. 提取到“分类排行榜”的链接
  3. 构造新的链接:从interval_id=100:90interval_id=10:0
  4. 所有链接保存为json文件
  5. 提取之前保存的所有链接,通过playwright打开链接,
  6. 进入页面后,通过提取到xhr文件,将需要下滑加载数据的次数计算出来
  7. 不断下滑,直到加载次数与计算的次数相符

(四)进行爬取

同样是使用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()

 程序核心,实现了以下功能:

  1. 提取并构造链接
  2. 保存所有构造的链接
  3. 提取已经保存的链接
  4. 利用并发爬取内容
  5. 如果存在未爬取的链接,则重新调用“打开页面”函数,继续爬取

(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基础的人学习,不过你都看到这了,应该有基础吧?),数据量也比较大,同样非常推荐用数据库进行储存。

(五)完整代码

【免费】使用python的异步库playwright进行爬取豆瓣电影排行榜Chart页面的数据资源-CSDN文库icon-default.png?t=N7T8https://download.csdn.net/download/btaworld/89292423

完整代码已经上传到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博客icon-default.png?t=N7T8https://blog.csdn.net/btaworld/article/details/138496824

最后,运行py文件,就能愉快得爬取豆瓣chart页面所有电影信息。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值