pyhon爬虫—爬取道客巴巴文档(全面解析)

1 解析网页

1.1 网页加载

打开道客巴巴官网后,我们选择一篇文章《“多元互动对话式”劳动技术课教学策略.doc》,通过阅览文章的过程我们可以发现,进入文章预览页面后,网页只会加载文章的前5页内容,如果文章内容页数较多超过了5页,剩下的内容就需要通过点击“继续阅读”来预览文章剩下页数的内容,并且文章内容加载采用了懒加载的方式,在页面滚动到文章指定页数的时候,才会加载后续页数的内容,这也是前端为优化页面加载速度而常用的操作。
继续阅读
懒加载

1.2 网络请求

通过以上解析,其实就可推断网页的数据是动态加载。通过浏览器抓包工具获取网络请求,可以看到一组有getebt开头的网络请求,其请求了后缀为.ebt的文件数据,这种文件并不常见,有可能是加密过的自定义格式的文件。另外,6个请求地址没有规律可循,在循环抓取环节,这也是一个需要考虑解决的问题。因为是二进制数据,预览也只能看到一堆乱码,无法提取有价值的信息。
抓包

1.3 网页源码

看完请求信息,我们再来看看网页的源代码,从中可看到文章中的每一页的可预览内容都是通过canvas标签绘制出的图案。
网页结构
canvas是html中的画布元素,它没有自己的行为,但是定义了一个 API 支持脚本化绘图操作,让我们可以使用javascript代码来绘制出所需图案。如我们所熟知的echarts数据可视化图表库中的各种图表就是通过canvas绘制出来的,并且图表库中的图案可以以图片的格式保存下来。
保存图片

1.4 小结

  1. 根据浏览器的抓包工具获取到动态数据来看,这可能是是一组加密过的数据,先不考虑其是否能解密成功和解密过程所耗费的时间,因为文档的预览是canvas绘制的图案,所以解密出来的数据也有可能只是一些canvas的绘制参数,可能还会需要将数据进一步转化才能得到我们直接在网页上预览到的效果,所以直接请求数据再解密这一方法,无论从时间和结果上来看,都不太理想。
  2. 我们通过echarts数据可视化图表库中的图表实例得知,canvas绘制的图案是可以保存为图片的,那道客巴巴文章的预览内容既然是通过canvas绘制的,自然也可以将其保存为图片。因为文章预览数据是动态加载的,从而无法通过直接请求网页源代码的方式获取数据,所以最终决定使用selenium控制浏览器行为的方式对道客巴巴文档进行爬取。

2 编写代码

2.1 环境配置

名称建议版本
python3.9
selenium4.23.0

2.2 进入网页

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# 浏览器驱动配置
options = Options()
# 程序结束时,保持浏览器窗口开启状态
options.add_experimental_option('detach', True)

# 浏览器驱动实例
driver = webdriver.Chrome(options=options)

# 进入网页
driver.get('https://www.doc88.com/p-6923896489622.html')

2.3 继续阅读

根据1.1的网页加载分析,在文章页数大于5时,需要点击“继续阅读”开加载预览剩下的页数。我们利用selenium进入对应文章网页后,通过id获取“继续阅读”按钮的元素对象,并模拟点击,加载剩余数据。

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

# 模拟点击“继续阅读”按钮
try:
    continueButton = driver.find_element(By.ID, 'continueButton')
    continueButton.click()
except NoSuchElementException:
    pass

2.4 懒加载问题

根据1.1的网页加载分析,网页数据使用了懒加载,文章每一页需要都滚动到指定位置才会加载出来。针对这种情况,我们往往会使用javascript代码来控制页面进行上下滚动,来触发数据的加载。

// javascript 代码控制页面上下滚动

const speed = 50 // 滚动速度(越大越快)
window.scrollTo(0, document.body.scrollHeight)
let top = document.documentElement.scrollTop || document.body.scrollTop;
const timeTop = setInterval(() => {
    document.body.scrollTop = document.documentElement.scrollTop = top -= speed;
    if (top <= 0) {
      clearInterval(timeTop);
    }
});

滚动触发数据加载

但是通过多次测试会发现,仅通过上下滚动来处发数据加载的方法存在明显的问题:

  1. 控制页面滚动的速度太快的话,有些页面并不会触发加载,并且这些页面还是不确定的,如果无法定位并且移动到未加载的页面,页面就一直不会加载。
  2. 循环滚动多次或将滚动速度调慢不仅会影响爬虫的效率,而且仍旧可能出现第一种问题。

综合多方面因素考虑,我最后决定先使用JavaScript代码定位每一页的元素并跳转到指定页面的位置,然后判断页面数据是否加载,未加载则等待加载,加载完成后者返回对象信息。首先,因为文章每页的元素id=page_页数,所以我们需要获取到文章的总页数。如下图所示,总页数在头元素中已经给出,我们通过xpath就可顺利获取。

总页数

通过以下JavaScript代码,我们可以滚动到指定元素的位置。

// JavaScript代码滚动到指定元素位置
// 如果网络不好,可能会报错:Cannot read properties of null (reading 'scrollIntoView')
// 建议按需求添加延迟代码,等待元素加载
document.getElementById("page_5").scrollIntoView()

滚动到指定页面

如果网络不好,不建议使用javascript代码跳转到指定位置,但可以使用selenium模拟网站的翻页行为以达到同样的效果。

from selenium.webdriver import Keys

# 获取页数跳转输入框对象
pageNumInput = driver.find_element(By.ID, 'pageNumInput')
# 全选输入框内容
pageNumInput.send_keys(Keys.CONTROL, 'a')
# 输入页数
pageNumInput.send_keys(str(5))
# 回车
pageNumInput.send_keys(Keys.ENTER)

跳转到指定页面位置后,我们需要等待页面数据的加载,如何判断数据是否加载成功大家可以发挥想象,我采用的方法是通过加载前后canvas标签元素属性的变化来判断的。通过下面给出的html代码观察就可以发现加载前后canvas标签元素属性是不同的,加载后canvas标签上会多出4个属性值,分别是fs、lz、width和height。width和height是canvas标签的初始值,可直接排除,剩下fs和lz可以任意使用。

<!-- 加载前 -->
<canvas class="inner_pkage" zoom="1" ls="0" ss="0" id="page_1"></canvas>

<!-- 加载后 -->
<canvas class="inner_page" zoom="1" ls="1" ss="0" id="page_1" fs="0" lz="1" width="1212" height="682"></canvas>

有了上述准备后,我们就可以开始整合代码,用于解决懒加载问题了。

import time
from selenium import webdriver
from selenium.webdriver import Keys
from selenium.webdriver.common.by import By
from selenium.common import NoSuchElementException
from selenium.webdriver.chrome.options import Options

# 浏览器驱动配置
options = Options()
# 程序结束时,保持浏览器窗口开启状态
options.add_experimental_option('detach', True)

# 浏览器驱动实例
driver = webdriver.Chrome(options=options)

# 进入网页
driver.get('https://www.doc88.com/p-6923896489622.html')

# 模拟点击“继续阅读”按钮
try:
    continueButton = driver.find_element(By.ID, 'continueButton')
    continueButton.click()
except NoSuchElementException:
    pass

# 总页数
page_xpath = '//meta[@property="og:document:page"]'
page = int(driver.find_element(By.XPATH, page_xpath).get_attribute("content"))

# 页数跳转输入框对象
pageNumInput = driver.find_element(By.ID, 'pageNumInput')

# 循环抓取
for num in range(1, page + 1):
    # 页面元素id
    canvas_id = f"page_{num}"

    # JavaScript代码滚动到指定元素位置
    # 如果网络不好,可能会报错:Cannot read properties of null(reading 'scrollIntoView')
    # 建议按需求添加延迟代码,等待元素加载
    driver.execute_script(f'document.getElementById("{canvas_id}").scrollIntoView()')

    # 通过selenium模拟控制翻页滚动到指定元素位置(更稳定,建议使用)
    # pageNumInput.send_keys(Keys.CONTROL, 'a')
    # pageNumInput.send_keys(str(num))
    # pageNumInput.send_keys(Keys.ENTER)

    while True:
        # 通过canvas标签上的lz属性判断数据是否加载完成
        canvas = driver.find_element(By.ID, canvas_id)
        lz = canvas.get_attribute("lz")

        if lz is not None:
            # 数据加载完成,获取元素对象
            print(f'成功获取{canvas_id}:', canvas)

            # 跳出循环
            break

        # 数据未加载,等待1s后继续循环判断
        print(f'{canvas_id}尚未加载,正在重新获取...')
        time.sleep(1)

跳转等待加载

2.4 图片下载

在1.4中提到,canvas绘制的图案可转为图片,具体的方法步骤就是将canvas画布数据转base64字符串,再将base64解析为二进制数据后写入图片格式的文件中,最后得到图片。canvas元素对象内部提供了方法,通过javascript代码可以直接获取base64字符串。

// 通过javascript代码获取对应canvas的base64数据
document.getElementById("page_1").toDataURL()

canvas转base64

2.5 完整代码

最后通过python将获取到的base64字符串解析为二进制并保存为图片,至此已完成爬取道客巴巴文档的所有代码。

import time
import base64
from PIL import Image
from io import BytesIO
from selenium import webdriver
from selenium.webdriver import Keys
from selenium.webdriver.common.by import By
from selenium.common import NoSuchElementException
from selenium.webdriver.chrome.options import Options

# 浏览器驱动配置
options = Options()
# 程序结束时,保持浏览器窗口开启状态
options.add_experimental_option('detach', True)

# 浏览器驱动实例
driver = webdriver.Chrome(options=options)

# 进入网页
driver.get('https://www.doc88.com/p-6923896489622.html')

# 模拟点击“继续阅读”按钮
try:
    continueButton = driver.find_element(By.ID, 'continueButton')
    continueButton.click()
except NoSuchElementException:
    pass

# 总页数
page_xpath = '//meta[@property="og:document:page"]'
page = int(driver.find_element(By.XPATH, page_xpath).get_attribute("content"))

# 页数跳转输入框对象
pageNumInput = driver.find_element(By.ID, 'pageNumInput')

# 循环抓取
for num in range(1, page + 1):
    # 页面元素id
    canvas_id = f"page_{num}"

    # JavaScript代码滚动到指定元素位置
    # 如果网络不好,可能会报错:Cannot read properties of null(reading 'scrollIntoView')
    # 建议按需求添加延迟代码,等待元素加载
    # driver.execute_script(f'document.getElementById("{canvas_id}").scrollIntoView()')

    # 通过selenium模拟控制翻页(更稳定,建议使用)
    pageNumInput.send_keys(Keys.CONTROL, 'a')
    pageNumInput.send_keys(str(num))
    pageNumInput.send_keys(Keys.ENTER)

    while True:
        # 通过canvas标签上的lz属性判断数据是否加载完成
        canvas = driver.find_element(By.ID, canvas_id)
        lz = canvas.get_attribute("lz")

        if lz is not None:
            # 数据加载完成,获取元素对象
            script = f'return document.getElementById("{canvas_id}").toDataURL()'
            base64_str = driver.execute_script(script)

            # base64转图片
            head, body = base64_str.split(",")
            binary = base64.b64decode(body)
            image = Image.open(BytesIO(binary))
            image.save(f'{canvas_id}.png')
            print(f'图片{canvas_id},下载成功!')

            # 跳出循环
            break

        # 数据未加载,等待1s后继续循环判断
        print(f'{canvas_id}尚未加载,正在重新获取...')
        time.sleep(1)

完整代码演示

2.6 封装代码

import time
import base64
from PIL import Image
from io import BytesIO
from selenium import webdriver
from selenium.webdriver import Keys
from selenium.webdriver.common.by import By
from selenium.common import NoSuchElementException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.webdriver import WebDriver


class Doc88Spider(object):
    def __init__(self, url: str, timeout: int = 1, reconnect: int = 10, save_image: bool = True,
                 driver: WebDriver = None, headless: bool = True) -> None:
        """初始化

        :param url: 目标网址(必填)
        :param headless: 是否隐藏浏览器窗口(默认隐藏)
        :param timeout: 重连时间间隔(默认1s)
        :param reconnect: 失败重连次数(默认10次)
        :param save_image: 是否保存图片(默认保存)
        :param driver: 指定浏览器驱动(默认使用Chrome)
        """

        self.url = url
        self.headless = headless
        self.timeout = timeout
        self.reconnect = reconnect
        self.save_image = save_image
        self.driver = (driver, self.init_web_driver())[driver is None]

    def init_web_driver(self) -> WebDriver:
        """初始化浏览器驱动

        :return 返回浏览器驱动实例
        """

        # 浏览器驱动配置
        options = Options()

        if self.headless:
            # 隐藏浏览器
            options.add_argument('--headless')

        # 浏览器驱动实例
        driver = webdriver.Chrome(options=options)

        return driver

    def get_web_obj(self) -> dict:
        """获取网页对象

        :return 返回网页元素对象字典
        """

        # 打开网站
        self.driver.get(self.url)

        # 继续阅读按钮
        try:
            continueButton = self.driver.find_element(By.ID, 'continueButton')
            continueButton.click()
        except NoSuchElementException:
            pass

        # 页数跳转输入框
        pageNumInput = self.driver.find_element(By.ID, 'pageNumInput')

        # 总页数
        page_xpath = '//meta[@property="og:document:page"]'
        page = int(self.driver.find_element(By.XPATH, page_xpath).get_attribute("content"))

        return {
            "page": page,
            "pageNumInput": pageNumInput
        }

    def run_spider(self) -> None:
        """启动爬虫"""

        print("爬虫程序正在启动,请稍等...")

        web_obj = self.get_web_obj()

        page = web_obj['page']
        pageNumInput = web_obj['pageNumInput']

        # 循环爬取图片
        for num in range(1, page + 1):
            # 输入页数跳转至对应图片位置
            pageNumInput.send_keys(Keys.CONTROL, 'a')
            pageNumInput.send_keys(str(num))
            pageNumInput.send_keys(Keys.ENTER)

            # canvas元素id
            canvas_id = f"page_{num}"

            # 重连次数
            n = 0

            while n < self.reconnect:
                # 获取图片加载状态
                lz = self.driver.find_element(By.ID, canvas_id).get_attribute("lz")

                if lz is not None:
                    # 通过js代码,将canvas画布转为base64
                    script = f'return document.getElementById("{canvas_id}").toDataURL()'
                    base64_str = self.driver.execute_script(script)

                    if self.save_image:
                        # 下载图片
                        self.download_image(base64_str, canvas_id)
                    else:
                        print(f"已获取到数据{canvas_id}")

                    break

                n += 1
                # 数据未加载,等待1s后继续循环判断
                print(f"{canvas_id}尚未加载,正在重新获取({n})")
                time.sleep(self.timeout)

    @staticmethod
    def download_image(base64_str: str, filename: str = None):
        """将base64保存图片

        :param base64_str: base64字符串
        :param filename: 文件名
        """

        if filename is None:
            filename = int(time.time())

        head, body = base64_str.split(",")
        binary = base64.b64decode(body)
        image = Image.open(BytesIO(binary))
        image.save(f'{filename}.png')
        print(f'图片{filename},下载成功!')


if __name__ == '__main__':
    # url – 目标网址(必填)
    # timeout – 重连时间间隔(默认1s)
    # reconnect – 失败重连次数(默认10次)
    # save_image – 是否保存图片(默认保存)
    # headless – 是否隐藏浏览器窗口(默认隐藏)
    doc88Spider = Doc88Spider(
        url="https://www.doc88.com/p-9019970307413.html",
        timeout=1,
        reconnect=10,
        save_image=True,
        headless=True,
    )
    doc88Spider.run_spider()

3 结语

本文以我的视角出发,从网页解析到完成代码,整个过程都进行了详细的分析与解读,希望对各位读者有所帮助。

  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python可以使用多个库来爬取道客巴巴网站的js内容,其中比较常用的库包括urllib、requests和beautifulsoup等。 首先,我们可以使用urllib库中的urlopen函数来打开指定网页的链接,并读取该网页的内容。可以使用指定的url打开道客巴巴网页。接下来,我们需要解析网页中的js内容。 在这里,我们可以使用beautifulsoup库来解析网页的内容,并提取出我们需要的js内容。beautifulsoup提供了一种简单的方法来处理html或xml文件,并从中提取我们需要的信息。我们可以使用beautifulsoup的find_all函数找到所有的js标签,并从中提取出我们需要的内容。 然后,我们可以使用requests库来发送GET请求,并获取返回的内容。requests库提供了一种方便的方法来发送请求和处理响应。我们可以使用该库的get函数发送GET请求,并指定请求的url。然后,我们可以使用返回的响应对象的content属性来获取返回的内容。可以将返回的内容保存到一个文件中,以便之后使用。 最后,我们可以使用Python的文件操作函数来保存获取到的js内容。可以使用open函数打开一个文件,并将js内容写入到文件中。 综上所述,我们可以使用Python的urllib、requests和beautifulsoup等库来爬取道客巴巴网站的js内容。首先使用urllib库打开指定url,然后使用beautifulsoup库解析网页内容,并提取出js内容,接着使用requests库发送GET请求,获取返回的内容,并保存到一个文件中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值