DrissionPage避坑指南:从安装到多标签页管理的完整教程

Python中的DrissionPage详解 DrissionPage是一个创新的Python库,它巧妙地融合了driver和session的功能,为Web自动化操作提供了强大的支持。尤其在处理需要登录的网站爬虫任务时,DrissionPage简化了原本复杂的流程,无需深入分析网络数据包或JavaScript代码,开发者可以通过简洁的代码实现自动化的登录过程。这不仅提高了开发效率,还降低了出错的可能性。版权声明:本博客内容为原创,转载请保留原文链接及作者信息。参考文章DrissionPage官方文档。 阅读详情

DrissionPage实战避坑指南:从零构建高效自动化与数据采集方案

如果你正在寻找一个能同时驾驭浏览器自动化与高效网络请求的Python工具,那么DrissionPage很可能就是你期待已久的答案。这个库巧妙地将Selenium的浏览器控制能力与Requests的轻量级请求特性融为一体,让你在面对复杂网页交互和数据采集任务时,能够灵活切换策略,游刃有余。不过,任何强大的工具在初次接触时都可能遇到一些“坑”,这篇文章就是为你准备的实战指南。我会结合自己多次项目实践中的经验,带你从环境配置一路走到高级的多标签页管理,帮你避开那些常见的陷阱,快速上手并发挥出DrissionPage的真正威力。

1. 环境搭建与首次启动的常见陷阱

万事开头难,一个顺畅的起步能省去后面无数麻烦。DrissionPage的安装虽然简单,但第一次启动浏览器时,很多人都会卡在第一步。

1.1 安装与基础依赖检查

安装DrissionPage本身只需要一行命令,但确保你的Python环境是3.6或更高版本。我建议使用虚拟环境来管理依赖,避免与其他项目产生冲突。

# 创建并激活虚拟环境(以venv为例)
python -m venv drission_env
source drission_env/bin/activate  # Linux/Mac
# 或
drission_env\Scripts\activate  # Windows

# 安装DrissionPage
pip install DrissionPage

注意:如果你之前安装过旧版本,想要升级到最新版,可以使用pip install DrissionPage --upgrade。但要注意,不同大版本之间API可能有变化,升级后最好测试一下现有代码。

安装完成后,不要急着写复杂的脚本。先来一个最简单的测试,看看基础环境是否正常:

from DrissionPage import ChromiumPage

page = ChromiumPage()
page.get('https://www.baidu.com')
print(f"页面标题: {page.title}")

如果这段代码能正常运行并打印出百度首页的标题,那么恭喜你,最基础的环境已经没问题了。但实际情况中,很多人会在这里遇到第一个坑——浏览器路径问题。

1.2 浏览器路径配置的两种策略

当看到BrowserNotFoundError或类似的错误时,不要慌张。这通常意味着DrissionPage没有在你的系统中自动找到Chrome或Edge浏览器。解决这个问题有两种思路,我根据不同的使用场景来分别说明。

场景一:固定开发环境,一劳永逸

如果你的开发环境相对固定,比如就在自己的笔记本电脑上工作,那么将浏览器路径永久配置到DrissionPage的配置文件中是最方便的选择。DrissionPage提供了一个命令行工具dp来完成这个配置:

# Windows系统,假设Chrome安装在默认位置
dp -p "C:\Program Files\Google\Chrome\Application\chrome.exe"

# Mac系统
dp -p "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"

# Linux系统
dp -p "/usr/bin/google-chrome-stable"

执行这个命令后,DrissionPage会在用户目录下创建或更新配置文件,以后每次启动都会使用这个路径。你可以通过以下代码验证配置是否生效:

from DrissionPage import ChromiumOptions

# 查看当前配置的浏览器路径
co = ChromiumOptions()
print(f"当前浏览器路径: {co.browser_path}")

场景二:动态环境或临时测试

有时候你可能需要在不同的机器上运行代码,或者只是想临时测试一下某个功能,不想修改全局配置。这时候可以在代码中动态指定浏览器路径:

from DrissionPage import ChromiumPage, ChromiumOptions

# 创建配置对象并设置浏览器路径
co = ChromiumOptions().set_browser_path(r"D:\MyChrome\chrome.exe")

# 使用这个配置创建页面对象
page = ChromiumPage(co)
page.get('https://www.example.com')

这种方法的好处是灵活,不会影响其他项目或用户的配置。我个人的习惯是,在团队协作的项目中,将浏览器路径作为配置项从外部文件读取,这样每个开发者都可以根据自己的环境进行调整。

1.3 浏览器启动模式的选择与优化

DrissionPage默认会启动一个带界面的浏览器,这在调试阶段非常有用,你可以直观地看到自动化脚本的执行过程。但在生产环境或需要批量处理时,无头模式(Headless)是更好的选择,因为它不显示界面,节省系统资源,运行速度也更快。

from DrissionPage import ChromiumPage, ChromiumOptions

# 配置无头模式
co = ChromiumOptions().headless(True)

# 还可以设置其他常用选项
co.set_argument('--no-sandbox')  # 在Linux服务器上运行时可能需要
co.set_argument('--disable-gpu')  # 某些环境下禁用GPU加速
co.set_argument('--disable-dev-shm-usage')  # 解决共享内存问题

page = ChromiumPage(co)

除了无头模式,你还可以根据需求配置其他启动参数:

配置项方法适用场景
无痕模式.incognito(True)每次启动都是全新会话,不读取历史记录和cookies
访客模式.set_argument('--guest')类似无痕,但更彻底,不创建用户数据目录
指定用户数据目录.set_paths(user_data_path='path')保留登录状态、扩展程序等个性化设置
设置代理.set_proxy('http://proxy:port')需要通过代理访问网络
禁用图片加载.set_argument('--blink-settings=imagesEnabled=false')提高页面加载速度,减少流量消耗

我在实际项目中经常根据任务类型选择不同的配置组合。比如做数据采集时,我会使用无头+无痕模式,确保每次都是干净的会话;而在测试需要登录的网站自动化时,我会指定用户数据目录来保持登录状态。

2. 元素定位:从基础到高级的精准查找策略

元素定位是网页自动化的核心,也是新手最容易感到困惑的地方。DrissionPage提供了一套非常灵活且强大的定位语法,比传统的XPath和CSS选择器更加直观。

2.1 基础定位语法详解

让我们从一个简单的HTML示例开始,逐步理解各种定位方式:

<!-- 假设这是我们要操作的页面结构 -->
<div id="main-container">
    <form class="login-form">
        <input type="text" id="username" name="user" placeholder="请输入用户名">
        <input type="password" id="password" name="pass">
        <button type="submit" class="btn-primary">登录</button>
        <a href="/forgot" class="link">忘记密码?</a>
    </form>
    <div class="news-list">
        <h3>最新消息</h3>
        <ul>
            <li class="news-item" data-id="101">第一条新闻</li>
            <li class="news-item" data-id="102">第二条新闻</li>
            <li class="news-item special" data-id="103">特别推荐</li>
        </ul>
    </div>
</div>

按属性定位是最直接的方式:

# 按id定位 - 最精确的方式
username_input = page.ele('#username')
# 或者使用属性语法
username_input = page.ele('@id=username')

# 按class定位 - 注意class可能有多个值
login_form = page.ele('.login-form')
submit_button = page.ele('@class=btn-primary')

# 按其他属性定位
password_input = page.ele('@type=password')
forgot_link = page.ele('@href=/forgot')

按文本内容定位在处理动态内容时特别有用:

# 精确文本匹配
login_button = page.ele('text=登录')
forgot_text = page.ele('text=忘记密码?')

# 模糊文本匹配(包含)
special_news = page.ele('text:特别')
# 这也会匹配到"特别推荐"

# 文本开头匹配
latest_news = page.ele('text^=最新')
# 匹配以"最新"开头的元素

# 文本结尾匹配
password_label = page.ele('text$=密码')
# 匹配以"密码"结尾的元素

2.2 复杂条件组合与相对定位

实际项目中,页面结构往往比示例复杂得多。DrissionPage支持多种条件的组合查询,让你能精准定位到目标元素。

多条件"与"关系:当需要同时满足多个条件时

# 查找class为news-item且data-id为103的元素
special_item = page.ele('@@class=news-item@@data-id=103')

# 查找type为submit且class包含btn的元素
submit_btn = page.ele('@@type=submit@@class:btn')

多条件"或"关系:当满足任一条件即可时

# 查找id为username或name为user的输入框
user_input = page.ele('@|id=username@|name=user')

# 查找文本包含"新闻"或"消息"的元素
news_elements = page.eles('@|text:新闻@|text:消息')

链式查找与相对定位:这是DrissionPage的一大亮点

# 先找到表单,再在表单内查找元素
form = page.ele('.login-form')
username = form.ele('#username')  # 在form元素内查找

# 更简洁的链式写法
password = page.ele('.login-form').ele('@type=password')

# 查找兄弟元素
first_news = page.ele('.news-item')
second_news = first_news.next('tag:li')  # 下一个li兄弟元素
third_news = second_news.next('tag:li')

# 查找父元素
news_list = first_news.parent('tag:ul')
container = news_list.parent('tag:div')

2.3 处理动态加载与等待机制

现代网页大量使用JavaScript动态加载内容,如果元素还没加载出来就去操作,肯定会出错。DrissionPage内置了智能的等待机制,但了解其工作原理能帮你写出更健壮的代码。

内置等待:ele()和eles()方法默认会等待10秒

# 默认等待10秒,如果10秒内元素出现就返回,否则超时
try:
    element = page.ele('#dynamic-content')
    print("元素找到了!")
except TimeoutError:
    print("元素在10秒内没有出现")

自定义等待时间:

from DrissionPage import ChromiumPage

page = ChromiumPage(timeout=30)  # 设置全局超时时间为30秒

# 或者针对单个查找设置超时
element = page.ele('#slow-loading-element', timeout=15)

显式等待特定条件:有时候我们需要等待更复杂的条件

# 等待页面标题包含特定文本
page.wait.title_change(to='控制面板')

# 等待URL变化
page.wait.url_change(contains='dashboard')

# 等待元素显示出来
element = page.ele('#loading')
element.wait.displayed(timeout=20)

# 等待元素被删除(比如加载动画消失)
loading = page.ele('#loading-spinner')
loading.wait.deleted()

我在处理一个电商网站的数据采集时,遇到过商品列表分页加载的情况。页面不会一次性加载所有商品,而是滚动到底部时加载更多。这时候就需要结合滚动和等待:

# 滚动到页面底部触发加载
page.scroll.to_bottom()

# 等待新元素出现
page.wait.eles_loaded('tag:div@@class=product-item', timeout=5)

# 如果等待失败,可能已经加载完毕,继续处理
products = page.eles('tag:div@@class=product-item')

2.4 定位失败的原因分析与调试技巧

即使经验丰富的开发者,也难免遇到定位失败的情况。下面是一些常见问题及解决方法:

问题1:元素在iframe中

# 先定位到iframe元素
iframe = page.ele('tag:iframe@@id=content-frame')

# 在iframe内部查找元素
# 方法1:直接链式查找(DrissionPage会自动处理iframe切换)
inner_element = iframe.ele('.some-class')

# 方法2:获取iframe的页面对象
iframe_page = iframe.inner_page
element = iframe_page.ele('#target')

问题2:元素在shadow DOM中

# 对于开放的shadow-root
shadow_host = page.ele('#shadow-host')
shadow_root = shadow_host.shadow_root
shadow_element = shadow_root.ele('.inside-shadow')

# 对于闭合的shadow-root,可能需要执行JavaScript
js_script = """
return document.querySelector('#shadow-host').shadowRoot.querySelector('.target');
"""
element = page.run_js(js_script)

问题3:XPath定位的注意事项

虽然DrissionPage支持XPath,但XPath对页面结构变化非常敏感。我建议优先使用DrissionPage的自定义语法,只在必要时使用XPath:

# 不推荐 - 对结构变化敏感
elements = page.eles('xpath://div[@class="container"]/div[2]/span[3]')

# 推荐 - 更健壮的定位方式
elements = page.eles('tag:div@@class=container').ele('tag:div', index=1).eles('tag:span')[2]

调试技巧:当定位失败时,可以逐步缩小范围

# 1. 先确认页面是否加载完成
print(f"当前URL: {page.url}")
print(f"页面标题: {page.title}")

# 2. 查看页面HTML结构(调试时使用,生产环境慎用)
with open('debug.html', 'w', encoding='utf-8') as f:
    f.write(page.html)

# 3. 尝试更宽泛的定位,逐步精确
all_divs = page.eles('tag:div')
print(f"找到{len(all_divs)}个div元素")

# 4. 使用浏览器开发者工具辅助
# 在代码中暂停,手动在浏览器中检查
import time
time.sleep(10)  # 给你时间在浏览器中按F12检查

3. 多标签页管理与并发操作实战

多标签页操作是DrissionPage相比传统自动化工具的一大优势。你可以同时控制多个标签页,实现真正的并发操作,这在数据采集和自动化测试中非常有用。

3.1 标签页的基本操作

让我们从一个简单的多标签页示例开始:

from DrissionPage import ChromiumPage
import time

page = ChromiumPage()

# 打开第一个标签页访问百度
page.get('https://www.baidu.com')
print(f"标签页1标题: {page.title}")

# 新建标签页(会在后台打开,不会自动切换焦点)
page.new_tab('https://www.github.com')
print(f"当前标签页数量: {page.tabs_count}")

# 获取所有标签页ID
tab_ids = page.tab_ids
print(f"标签页IDs: {tab_ids}")

# 切换到第二个标签页
tab2 = page.get_tab(tab_ids[1])
tab2.get('https://www.python.org')
print(f"标签页2标题: {tab2.title}")

# 切回第一个标签页进行操作
tab1 = page.get_tab(tab_ids[0])
tab1.ele('#kw').input('DrissionPage')
tab1.ele('#su').click()
time.sleep(2)
print(f"标签页1新标题: {tab1.title}")

这个例子展示了标签页的基本操作,但在实际项目中,我们通常需要更精细的控制。

3.2 高级标签页管理策略

场景一:批量打开多个页面并收集数据

假设我们需要从多个新闻网站收集头条新闻:

from DrissionPage import ChromiumPage
from concurrent.futures import ThreadPoolExecutor
import threading

class NewsCollector:
    def __init__(self):
        self.page = ChromiumPage()
        self.lock = threading.Lock()
        self.results = {}
    
    def collect_from_site(self, url, site_name):
        """在每个标签页中收集数据"""
        # 新建标签页
        tab = self.page.new_tab(url)
        
        # 等待页面加载
        tab.wait.doc_loaded()
        
        # 根据网站结构提取数据
        if site_name == 'news_site1':
            title = tab.ele('tag:h1@@class=headline', timeout=5).text
        elif site_name == 'news_site2':
            title = tab.ele('tag:div@@id=main-title', timeout=5).text
        else:
            title = tab.title
        
        # 关闭标签页
        tab.close()
        
        # 线程安全地保存结果
        with self.lock:
            self.results[site_name] = title
        
        return title
    
    def run(self, sites):
        """并发收集所有站点"""
        with ThreadPoolExecutor(max_workers=3) as executor:
            futures = []
            for url, name in sites.items():
                future = executor.submit(self.collect_from_site, url, name)
                futures.append(future)
            
            # 等待所有任务完成
            for future in futures:
                try:
                    future.result(timeout=30)
                except Exception as e:
                    print(f"收集数据时出错: {e}")
        
        return self.results

# 使用示例
collector = NewsCollector()
sites = {
    'news_site1': 'https://news.site1.com',
    'news_site2': 'https://news.site2.com',
    'news_site3': 'https://news.site3.com'
}

results = collector.run(sites)
for site, title in results.items():
    print(f"{site}: {title}")

场景二:保持登录状态的多标签页操作

有些操作需要在登录状态下进行,我们可以利用用户数据目录来保持会话:

from DrissionPage import ChromiumPage, ChromiumOptions
import json

class MultiTabManager:
    def __init__(self, user_data_dir='./browser_data'):
        # 配置使用特定的用户数据目录
        co = ChromiumOptions().set_paths(
            user_data_path=user_data_dir,
            local_port=9222
        )
        self.page = ChromiumPage(co)
        self.tabs = {}  # 存储标签页对象
        self.tab_configs = {}  # 存储标签页配置
    
    def create_tab_with_config(self, tab_id, url, config=None):
        """创建带有特定配置的标签页"""
        if config is None:
            config = {}
        
        # 记录配置
        self.tab_configs[tab_id] = {
            'url': url,
            'config': config,
            'created_at': time.time()
        }
        
        # 创建新标签页
        tab = self.page.new_tab(url)
        self.tabs[tab_id] = tab
        
        # 应用配置
        if 'viewport' in config:
            tab.set.window.size(*config['viewport'])
        
        if 'cookies' in config:
            for cookie in config['cookies']:
                tab.cookies.set(cookie)
        
        return tab
    
    def execute_in_tab(self, tab_id, func, *args, **kwargs):
        """在指定标签页中执行函数"""
        if tab_id not in self.tabs:
            raise ValueError(f"标签页 {tab_id} 不存在")
        
        tab = self.tabs[tab_id]
        
        # 切换到该标签页
        tab.set.activate()
        
        # 执行函数
        return func(tab, *args, **kwargs)
    
    def close_tab(self, tab_id):
        """关闭指定标签页"""
        if tab_id in self.tabs:
            self.tabs[tab_id].close()
            del self.tabs[tab_id]
            del self.tab_configs[tab_id]
    
    def save_state(self, filepath='tab_state.json'):
        """保存标签页状态(便于恢复)"""
        state = {
            'current_url': self.page.url,
            'tabs': {}
        }
        
        for tab_id, tab in self.tabs.items():
            state['tabs'][tab_id] = {
                'url': tab.url,
                'title': tab.title,
                'config': self.tab_configs.get(tab_id, {})
            }
        
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(state, f, ensure_ascii=False, indent=2)
    
    def cleanup(self):
        """清理所有标签页"""
        for tab_id in list(self.tabs.keys()):
            self.close_tab(tab_id)
        self.page.quit()

# 使用示例
manager = MultiTabManager()

# 创建多个标签页用于不同任务
manager.create_tab_with_config(
    'dashboard',
    'https://example.com/dashboard',
    config={'viewport': (1920, 1080)}
)

manager.create_tab_with_config(
    'monitor',
    'https://example.com/monitor',
    config={'viewport': (1200, 800)}
)

# 在各个标签页中执行任务
def check_alerts(tab):
    """检查监控告警"""
    alerts = tab.eles('.alert-item')
    return [alert.text for alert in alerts]

def get_stats(tab):
    """获取仪表板统计"""
    stats = {}
    stat_elements = tab.eles('.stat-card')
    for elem in stat_elements:
        key = elem.ele('.stat-label').text
        value = elem.ele('.stat-value').text
        stats[key] = value
    return stats

# 并发执行(实际项目中可以用线程池)
alerts = manager.execute_in_tab('monitor', check_alerts)
stats = manager.execute_in_tab('dashboard', get_stats)

print(f"告警信息: {alerts}")
print(f"统计信息: {stats}")

# 保存状态以便下次恢复
manager.save_state()

# 清理
manager.cleanup()

3.3 标签页间的通信与数据同步

在多标签页应用中,经常需要在不同标签页之间传递数据或同步状态。DrissionPage虽然不直接提供标签页间通信的API,但我们可以通过一些技巧实现:

import json
import time
from DrissionPage import ChromiumPage

class TabCommunicator:
    def __init__(self):
        self.page = ChromiumPage()
        self.shared_data = {}
    
    def share_data_via_localstorage(self, key, value, target_tab_id=None):
        """通过localStorage共享数据"""
        # 将数据存储到当前标签页的localStorage
        js_code = f'localStorage.setItem("{key}", {json.dumps(value)})'
        self.page.run_js(js_code)
        
        # 如果指定了目标标签页,也同步过去
        if target_tab_id:
            target_tab = self.page.get_tab(target_tab_id)
            target_tab.run_js(js_code)
    
    def get_shared_data(self, key, tab_id=None):
        """从localStorage获取共享数据"""
        if tab_id:
            tab = self.page.get_tab(tab_id)
            js_code = f'return localStorage.getItem("{key}")'
            result = tab.run_js(js_code)
        else:
            js_code = f'return localStorage.getItem("{key}")'
            result = self.page.run_js(js_code)
        
        if result:
            return json.loads(result)
        return None
    
    def broadcast_message(self, message_type, data):
        """向所有标签页广播消息"""
        message = {
            'type': message_type,
            'data': data,
            'timestamp': time.time()
        }
        
        js_code = f"""
        // 创建自定义事件
        const event = new CustomEvent('tabMessage', {{
            detail: {json.dumps(message)}
        }});
        // 触发事件
        window.dispatchEvent(event);
        """
        
        # 向所有标签页广播
        for tab_id in self.page.tab_ids:
            try:
                tab = self.page.get_tab(tab_id)
                tab.run_js(js_code)
            except:
                continue  # 某些标签页可能已关闭
    
    def setup_message_listener(self, tab_id, callback):
        """设置消息监听器"""
        tab = self.page.get_tab(tab_id)
        
        # 注入监听代码
        listener_js = """
        window.addEventListener('tabMessage', function(event) {
            // 将消息发送回Python端
            window.tabMessageCallback(event.detail);
        });
        """
        tab.run_js(listener_js)
        
        # 设置回调函数
        def handle_message(detail):
            callback(detail)
        
        # 这里需要将回调函数暴露给JavaScript
        # 实际实现可能需要更复杂的桥接机制
        
    def coordinated_action(self, action_func, tab_ids=None):
        """协调多个标签页执行同步动作"""
        if tab_ids is None:
            tab_ids = self.page.tab_ids
        
        results = {}
        
        # 第一步:准备阶段
        self.broadcast_message('prepare', {'action': 'start'})
        time.sleep(1)  # 给各标签页准备时间
        
        # 第二步:执行阶段
        for tab_id in tab_ids:
            tab = self.page.get_tab(tab_id)
            try:
                result = action_func(tab)
                results[tab_id] = {'status': 'success', 'result': result}
            except Exception as e:
                results[tab_id] = {'status': 'error', 'error': str(e)}
        
        # 第三步:清理阶段
        self.broadcast_message('cleanup', {'action': 'complete'})
        
        return results

# 使用示例
communicator = TabCommunicator()

# 打开多个标签页
communicator.page.get('https://example.com/page1')
communicator.page.new_tab('https://example.com/page2')
communicator.page.new_tab('https://example.com/page3')

# 共享数据
communicator.share_data_via_localstorage('session_token', 'abc123xyz')
communicator.share_data_via_localstorage('user_prefs', {'theme': 'dark', 'lang': 'zh'})

# 在所有标签页中获取共享数据
for tab_id in communicator.page.tab_ids:
    token = communicator.get_shared_data('session_token', tab_id)
    prefs = communicator.get_shared_data('user_prefs', tab_id)
    print(f"标签页 {tab_id}: token={token}, prefs={prefs}")

# 定义要在各标签页执行的动作
def collect_page_info(tab):
    """收集页面信息"""
    info = {
        'url': tab.url,
        'title': tab.title,
        'load_time': tab.run_js('return performance.timing.loadEventEnd - performance.timing.navigationStart'),
        'element_count': len(tab.eles('tag:*'))
    }
    return info

# 协调执行
results = communicator.coordinated_action(collect_page_info)
for tab_id, result in results.items():
    print(f"标签页 {tab_id}: {result}")

3.4 性能优化与资源管理

同时操作多个标签页会消耗大量系统资源,合理的资源管理至关重要:

import psutil
import time
from DrissionPage import ChromiumPage, ChromiumOptions

class ResourceAwareTabManager:
    def __init__(self, max_tabs=10, memory_limit_mb=2048):
        self.max_tabs = max_tabs
        self.memory_limit = memory_limit_mb * 1024 * 1024  # 转换为字节
        self.page = None
        self.active_tabs = {}
        self.tab_usage = {}  # 跟踪标签页资源使用
        
    def start(self):
        """启动浏览器,配置资源限制"""
        co = ChromiumOptions()
        
        # 限制浏览器资源使用
        co.set_argument('--max-old-space-size=4096')  # 限制JavaScript堆内存
        co.set_argument('--disable-background-timer-throttling')
        co.set_argument('--disable-backgrounding-occluded-windows')
        co.set_argument('--disable-renderer-backgrounding')
        
        self.page = ChromiumPage(co)
        
    def check_system_resources(self):
        """检查系统资源使用情况"""
        memory = psutil.virtual_memory()
        cpu_percent = psutil.cpu_percent(interval=1)
        
        return {
            'memory_percent': memory.percent,
            'memory_available': memory.available,
            'cpu_percent': cpu_percent
        }
    
    def can_create_tab(self):
        """检查是否可以创建新标签页"""
        if len(self.active_tabs) >= self.max_tabs:
            return False, "达到最大标签页数量限制"
        
        resources = self.check_system_resources()
        
        if resources['memory_percent'] > 85:
            return False, "内存使用率过高"
        
        if resources['cpu_percent'] > 90:
            return False, "CPU使用率过高"
        
        return True, "可以创建新标签页"
    
    def create_tab_with_limits(self, url, tab_id=None):
        """创建带有资源限制的标签页"""
        can_create, reason = self.can_create_tab()
        if not can_create:
            raise RuntimeError(f"无法创建标签页: {reason}")
        
        if tab_id is None:
            tab_id = f"tab_{len(self.active_tabs) + 1}"
        
        # 创建新标签页
        tab = self.page.new_tab(url)
        
        # 记录创建时间和初始状态
        self.active_tabs[tab_id] = {
            'tab': tab,
            'created_at': time.time(),
            'last_activity': time.time(),
            'url': url
        }
        
        # 设置标签页特定的资源限制
        self._apply_tab_limits(tab)
        
        return tab_id, tab
    
    def _apply_tab_limits(self, tab):
        """应用标签页资源限制"""
        # 限制JavaScript执行时间
        tab.set.timeouts(script=30)  # 脚本超时30秒
        
        # 禁用不必要的功能以节省资源
        tab.run_js("""
        // 禁用图片(需要时再加载)
        const images = document.getElementsByTagName('img');
        for (let img of images) {
            img.loading = 'lazy';
        }
        
        // 禁用自动播放视频
        const videos = document.getElementsByTagName('video');
        for (let video of videos) {
            video.autoplay = false;
        }
        """)
    
    def monitor_and_cleanup(self):
        """监控并清理不活跃的标签页"""
        current_time = time.time()
        tabs_to_close = []
        
        for tab_id, info in self.active_tabs.items():
            # 检查标签页是否超时未活动(30分钟)
            if current_time - info['last_activity'] > 1800:
                tabs_to_close.append(tab_id)
                continue
            
            # 检查标签页内存使用(通过执行JavaScript估算)
            try:
                memory_estimate = info['tab'].run_js("""
                if (window.performance && window.performance.memory) {
                    return window.performance.memory.usedJSHeapSize;
                }
                return 0;
                """)
                
                # 如果单个标签页使用超过200MB,考虑关闭
                if memory_estimate > 200 * 1024 * 1024:
                    print(f"标签页 {tab_id} 内存使用过高: {memory_estimate / (1024*1024):.2f}MB")
                    tabs_to_close.append(tab_id)
            
            except:
                # 标签页可能已经崩溃或关闭
                tabs_to_close.append(tab_id)
        
        # 关闭选中的标签页
        for tab_id in tabs_to_close:
            self.close_tab(tab_id)
        
        # 如果系统资源仍然紧张,关闭更多标签页
        resources = self.check_system_resources()
        if resources['memory_percent'] > 90:
            # 按最近活动时间排序,关闭最老的标签页
            sorted_tabs = sorted(
                self.active_tabs.items(),
                key=lambda x: x[1]['last_activity']
            )
            
            for tab_id, _ in sorted_tabs[:2]:  # 关闭2个最老的
                if tab_id not in tabs_to_close:
                    self.close_tab(tab_id)
                    break
    
    def close_tab(self, tab_id):
        """关闭标签页并清理资源"""
        if tab_id in self.active_tabs:
            try:
                self.active_tabs[tab_id]['tab'].close()
            except:
                pass  # 标签页可能已经关闭
            
            del self.active_tabs[tab_id]
            
            if tab_id in self.tab_usage:
                del self.tab_usage[tab_id]
    
    def optimize_performance(self):
        """执行性能优化"""
        # 清理浏览器缓存
        for tab_id, info in self.active_tabs.items():
            try:
                info['tab'].run_js("""
                // 清理不必要的定时器
                const maxId = setTimeout(() => {}, 0);
                for (let i = maxId; i > 0; i--) {
                    clearTimeout(i);
                    clearInterval(i);
                }
                
                // 触发垃圾回收(如果浏览器支持)
                if (window.gc) {
                    window.gc();
                }
                """)
            except:
                pass
        
        # 压缩页面资源
        self.page.run_js("""
        // 尝试压缩DOM(实际效果有限,但可以尝试)
        if (document.body) {
            document.body.style.willChange = 'auto';
            setTimeout(() => {
                document.body.style.willChange = '';
            }, 100);
        }
        """)
    
    def get_usage_report(self):
        """获取使用情况报告"""
        resources = self.check_system_resources()
        
        report = {
            'system': {
                'memory_used_percent': resources['memory_percent'],
                'cpu_used_percent': resources['cpu_percent'],
                'active_tabs': len(self.active_tabs),
                'max_tabs': self.max_tabs
            },
            'tabs': {}
        }
        
        for tab_id, info in self.active_tabs.items():
            report['tabs'][tab_id] = {
                'url': info['url'],
                'age_seconds': time.time() - info['created_at'],
                'inactive_seconds': time.time() - info['last_activity'],
                'status': 'active'
            }
        
        return report

# 使用示例
manager = ResourceAwareTabManager(max_tabs=5, memory_limit_mb=1024)
manager.start()

try:
    # 创建多个标签页
    tab1_id, tab1 = manager.create_tab_with_limits('https://example.com/data1')
    tab2_id, tab2 = manager.create_tab_with_limits('https://example.com/data2')
    
    # 定期监控和清理
    import schedule
    import threading
    
    def monitor_task():
        while True:
            manager.monitor_and_cleanup()
            report = manager.get_usage_report()
            print(f"监控报告: {report['system']}")
            time.sleep(300)  # 每5分钟检查一次
    
    # 在后台线程中运行监控
    monitor_thread = threading.Thread(target=monitor_task, daemon=True)
    monitor_thread.start()
    
    # 主任务
    def process_data(tab, url):
        tab.get(url)
        # ... 处理数据
        return {'processed': True}
    
    result1 = process_data(tab1, 'https://example.com/process')
    result2 = process_data(tab2, 'https://example.com/analyze')
    
    # 定期优化性能
    schedule.every(30).minutes.do(manager.optimize_performance)
    
    # 运行一段时间
    time.sleep(3600)  # 运行1小时
    
finally:
    # 清理所有标签页
    for tab_id in list(manager.active_tabs.keys()):
        manager.close_tab(tab_id)

4. 实战案例:构建一个健壮的网页数据采集系统

现在让我们把这些知识整合起来,构建一个完整的、健壮的网页数据采集系统。这个系统将展示如何在实际项目中使用DrissionPage的各种高级功能。

4.1 系统架构设计

我们的数据采集系统需要处理以下需求:

  1. 支持多种网站结构
  2. 处理JavaScript渲染的内容
  3. 管理会话和cookies
  4. 错误处理和重试机制
  5. 并发采集提高效率
  6. 数据验证和清洗
import json
import time
import logging
import hashlib
from datetime import datetime
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from DrissionPage import WebPage, ChromiumOptions
from DrissionPage.common import Keys

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('crawler.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

@dataclass
class CrawlResult:
    """爬取结果数据类"""
    url: str
    title: str
    content: str
    extracted_data: Dict[str, Any]
    status: str  # success, error, retry
    error_message: Optional[str] = None
    timestamp: str = None
    content_hash: str = None
    
    def __post_init__(self):
        if self.timestamp is None:
            self.timestamp = datetime.now().isoformat()
        if self.content_hash is None and self.content:
            self.content_hash = hashlib.md5(self.content.encode()).hexdigest()

class WebsiteConfig:
    """网站配置类"""
    
    def __init__(self, name: str, config: Dict):
        self.name = name
        self.config = config
        
        # 解析配置
        self.login_required = config.get('login_required', False)
        self.login_selectors = config.get('login_selectors', {})
        self.data_selectors = config.get('data_selectors', {})
        self.pagination = config.get('pagination', {})
        self.wait_selectors = config.get('wait_selectors', [])
        self.js_to_execute = config.get('js_to_execute', [])
        self.request_headers = config.get('headers', {})
        self.timeout = config.get('timeout', 30)
        
    def get_selector(self, key: str, default=None):
        """获取选择器,支持多种格式"""
        selector = self.data_selectors.get(key, default)
        if isinstance(selector, dict):
            # 支持多种选择器类型
            return selector.get('selector', default)
        return selector

class RobustWebCrawler:
    """健壮的网页爬虫"""
    
    def __init__(self, config_path: str = 'website_configs.json'):
        self.configs = self._load_configs(config_path)
        self.sessions = {}  # 存储不同网站的会话
        self.results_cache = {}  # 缓存结果
        
    def _load_configs(self, config_path: str) -> Dict[str, WebsiteConfig]:
        """加载网站配置"""
        try:
            with open(config_path, 'r', encoding='utf-8') as f:
                config_data = json.load(f)
            
            configs = {}
            for name, config in config_data.items():
                configs[name] = WebsiteConfig(name, config)
            
            logger.info(f"已加载 {len(configs)} 个网站配置")
            return configs
        except FileNotFoundError:
            logger.warning(f"配置文件 {config_path} 不存在,使用空配置")
            return {}
        except json.JSONDecodeError as e:
            logger.error(f"配置文件格式错误: {e}")
            return {}
    
    def _get_session(self, website_name: str) -> WebPage:
        """获取或创建网站会话"""
        if website_name not in self.sessions:
            config = self.configs.get(website_name)
            
            # 创建浏览器选项
            co = ChromiumOptions()
            
            # 根据配置设置选项
            if config:
                if config.request_headers:
                    co.set_headers(config.request_headers)
                
                # 设置超时
                co.set_timeouts(
                    base=config.timeout,
                    page_load=config.timeout * 2,
                    script=config.timeout
                )
            
            # 创建WebPage会话(支持模式切换)
            session = WebPage(addr_or_opts=co)
            self.sessions[website_name] = session
            
            # 如果需要登录,执行登录
            if config and config.login_required:
                self._login(session, config)
            
            logger.info(f"为网站 {website_name} 创建新会话")
        
        return self.sessions[website_name]
    
    def _login(self, session: WebPage, config: WebsiteConfig):
        """执行登录流程"""
        try:
            logger.info(f"正在登录网站: {config.name}")
            
            # 访问登录页面
            login_url = config.config.get('login_url')
            if not login_url:
                logger.error(f"网站 {config.name} 需要登录但未配置登录URL")
                return False
            
            session.get(login_url)
            
            # 等待登录表单加载
            for selector in config.wait_selectors:
                session.ele(selector, timeout=10)
            
            # 填写登录表单
            login_selectors = config.login_selectors
            if 'username' in login_selectors:
                session.ele(login_selectors['username']).input(
                    config.config.get('credentials', {}).get('username', '')
                )
            
            if 'password' in login_selectors:
                session.ele(login_selectors['password']).input(
                    config.config.get('credentials', {}).get('password', '')
                )
            
            # 点击登录按钮
            if 'submit' in login_selectors:
                session.ele(login_selectors['submit']).click()
            
            # 等待登录完成
            time.sleep(3)
            
            # 检查登录是否成功
            success_selector = config.config.get('login_success_selector')
            if success_selector:
                try:
                    session.ele(success_selector, timeout=10)
                    logger.info(f"网站 {config.name} 登录成功")
                    return True
                except:
                    logger.error(f"网站 {config.name} 登录失败")
                    return False
            
            return True
            
        except Exception as e:
            logger.error(f"登录网站 {config.name} 时出错: {e}")
            return False
    
    def _extract_data(self, session: WebPage, config: WebsiteConfig) -> Dict[str, Any]:
        """提取页面数据"""
        data = {}
        
        for key, selector_config in config.data_selectors.items():
            try:
                if isinstance(selector_config, dict):
                    # 复杂选择器配置
                    selector = selector_config.get('selector')
                    selector_type = selector_config.get('type', 'text')
                    multiple = selector_config.get('multiple', False)
                    attribute = selector_config.get('attribute')
                    default = selector_config.get('default')
                    
                    if not selector:
                        data[key] = default
                        continue
                    
                    if multiple:
                        elements = session.eles(selector)
                        if selector_type == 'text':
                            values = [ele.text for ele in elements]
                        elif selector_type == 'attribute' and attribute:
                            values = [ele.attr(attribute) for ele in elements]
                        elif selector_type == 'html':
                            values = [ele.html for ele in elements]
                        else:
                            values = [ele.text for ele in elements]
                        data[key] = values
                    else:
                        element = session.ele(selector)
                        if selector_type == 'text':
                            value = element.text
                        elif selector_type == 'attribute' and attribute:
                            value = element.attr(attribute)
                        elif selector_type == 'html':
                            value = element.html
                        else:
                            value = element.text
                        data[key] = value
                
                else:
                    # 简单选择器配置
                    element = session.ele(selector_config)
                    data[key] = element.text
                    
            except Exception as e:
                logger.warning(f"提取数据字段 {key} 时出错: {e}")
                data[key] = None
        
        return data
    
    def _handle_pagination(self, session: WebPage, config: WebsiteConfig) -> List[Dict[str, Any]]:
        """处理分页"""
        all_data = []
        pagination = config.pagination
        
        if not pagination:
            # 没有分页,直接提取当前页数据
            data = self._extract_data(session, config)
            if data:
                all_data.append(data)
            return all_data
        
        max_pages = pagination.get('max_pages', 10)
        next_selector = pagination.get('next_selector')
        page_param = pagination.get('page_param')
        base_url = pagination.get('base_url', session.url)
        
        current_page = 1
        
        while current_page <= max_pages:
            try:
                logger.info(f"正在处理第 {current_page} 页")
                
                # 提取当前页数据
                page_data = self._extract_data(session, config)
                if page_data:
                    all_data.append(page_data)
                
                # 检查是否有下一页
                if next_selector:
                    # 通过"下一页"按钮翻页
                    try:
                        next_button = session.ele(next_selector, timeout=5)
                        if next_button and not next_button.attr('disabled'):
                            next_button.click()
                            # 等待页面加载
                            session.wait.doc_loaded()
                            current_page += 1
                            continue
                        else:
                            break  # 没有下一页或按钮被禁用
                    except:
                        break  # 找不到下一页按钮
                
                elif page_param and base_url:
                    # 通过URL参数翻页
                    current_page += 1
                    next_url = f"{base_url}?{page_param}={current_page}"
                    session.get(next_url)
                    session.wait.doc_loaded()
                
                else:
                    break  # 没有配置翻页方式
                    
            except Exception as e:
                logger.error(f"处理第 {current_page} 页时出错: {e}")
                break
        
        return all_data
    
    def crawl(self, website_name: str, url: str, use_cache: bool = True) -> CrawlResult:
        """爬取单个页面"""
        # 检查缓存
        cache_key = f"{website_name}:{url}"
        if use_cache and cache_key in self.results_cache:
            cached_result = self.results_cache[cache_key]
            # 检查缓存是否过期(1小时)
            cache_time = datetime.fromisoformat(cached_result.timestamp)
            if (datetime.now() - cache_time).total_seconds() < 3600:
                logger.info(f"使用缓存结果: {url}")
                return cached_result
        
        config = self.configs.get(website_name)
        session = self._get_session(website_name)
        
        result = CrawlResult(
            url=url,
            title="",
            content="",
            extracted_data={},
            status="pending"
        )
        
        try:
            # 访问页面
            logger.info(f"开始爬取: {url}")
            session.get(url)
            
            # 等待必要元素加载
            if config and config.wait_selectors:
                for selector in config.wait_selectors:
                    session.ele(selector, timeout=config.timeout)
            
            # 执行JavaScript(如果需要)
            if config and config.js_to_execute:
                for js in config.js_to_execute:
                    session.run_js(js)
                    time.sleep(1)  # 给JS执行时间
            
            # 获取页面信息
            result.title = session.title
            result.content = session.html
            
            # 提取数据
            if config:
                if config.pagination:
                    # 处理分页
                    all_page_data = self._handle_pagination(session, config)
                    result.extracted_data = {
                        'pages': len(all_page_data),
                        'data': all_page_data
                    }
                else:
                    # 单页数据提取
                    result.extracted_data = self._extract_data(session, config)
            
            result.status = "success"
            logger.info(f"成功爬取: {url}")
            
        except Exception as e:
            result.status = "error"
            result.error_message = str(e)
            logger.error(f"爬取 {url} 时出错: {e}")
        
        finally:
            result.timestamp = datetime.now().isoformat()
            if result.content:
                result.content_hash = hashlib.md5(result.content.encode()).hexdigest()
            
            # 缓存结果
            if result.status == "success":
                self.results_cache[cache_key] = result
        
        return result
    
    def crawl_multiple(self, tasks: List[Dict], max_workers: int = 3) -> List[CrawlResult]:
        """并发爬取多个页面"""
        results = []
        
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            # 提交所有任务
            future_to_task = {}
            for task in tasks:
                website_name = task.get('website')
                url = task.get('url')
                use_cache = task.get('use_cache', True)
                
                if not website_name or not url:
                    logger.warning(f"跳过无效任务: {task}")
                    continue
                
                future = executor.submit(self.crawl, website_name, url, use_cache)
                future_to_task[future] = task
            
            # 收集结果
            for future in as_completed(future_to_task):
                task = future_to_task[future]
                try:
                    result = future.result(timeout=300)  # 5分钟超时
                    results.append(result)
                except Exception as e:
                    logger.error(f"任务执行失败: {task}, 错误: {e}")
                    results.append(CrawlResult(
                        url=task.get('url', ''),
                        title="",
                        content="",
                        extracted_data={},
                        status="error",
                        error_message=str(e)
                    ))
        
        return results
    
    def export_results(self, results: List[CrawlResult], format: str = 'json'):
        """导出结果"""
        if format == 'json':
            data = [asdict(r) for r in results]
            filename = f"crawl_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            logger.info(f"结果已导出到: {filename}")
            return filename
        
        elif format == 'csv':
            import csv
            filename = f"crawl_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
            
            # 收集所有可能的字段
            all_fields = set()
            for result in results:
                all_fields.update(result.extracted_data.keys())
            
            fieldnames = ['url', 'title', 'status', 'timestamp'] + list(all_fields)
            
            with open(filename, 'w', encoding='utf-8', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()
                
                for result in results:
                    row = {
                        'url': result.url,
                        'title': result.title,
                        'status': result.status,
                        'timestamp': result.timestamp
                    }
                    row.update(result.extracted_data)
                    writer.writerow(row)
            
            logger.info(f"结果已导出到: {filename}")
            return filename
        
        else:
            logger.error(f"不支持的导出格式: {format}")
            return None
    
    def cleanup(self):
        """清理资源"""
        for website_name, session in self.sessions.items():
            try:
                session.quit()
                logger.info(f"已关闭网站 {website_name} 的会话")
            except:
                pass
        
        self.sessions.clear()
        self.results_cache.clear()

# 配置文件示例
SAMPLE_CONFIG = {
    "news_site": {
        "login_required": False,
        "data_selectors": {
            "title": {
                "selector": "tag:h1",
                "type": "text"
            },
            "articles": {
                "selector": ".article-list .article-item",
                "type": "text",
                "multiple": True
            },
            "publish_date": {
                "selector": ".publish-time",
                "type": "attribute",
                "attribute": "datetime"
            },
            "author": ".author-name"
        },
        "pagination": {
            "next_selector": ".next-page",
            "max_pages": 5
        },
        "wait_selectors": [".article-list"],
        "timeout": 30
    },
    "ecommerce_site": {
        "login_required": True,
        "login_url": "https://example.com/login",
        "login_selectors": {
            "username": "#username",
            "password": "#password",
            "submit": "tag:button@@type=submit"
        },
        "credentials": {
            "username": "your_username",
            "password": "your_password"
        },
        "data_selectors": {
            "product_name": ".product-title",
            "price": ".price",
            "description": ".product-description",
            "images": {
                "selector": ".product-image",
                "type": "attribute",
                "attribute": "src",
                "multiple": True
            }
        },
        "wait_selectors": [".product-details"],
        "js_to_execute": [
            "window.scrollTo(0, document.body.scrollHeight);",
            "setTimeout(() => {}, 1000);"
        ]
    }
}

# 使用示例
def main():
    # 1. 保存示例配置
    with open('website_configs.json', 'w', encoding='utf-8') as f:
        json.dump(SAMPLE_CONFIG, f, indent=2)
    
    # 2. 创建爬虫实例
    crawler = RobustWebCrawler('website_configs.json')
    
    try:
        # 3. 定义爬取任务
        tasks = [
            {
                'website': 'news_site',
                'url': 'https://example-news.com/latest',
                'use_cache': True
            },
            {
                'website': 'ecommerce_site',
                'url': 'https://example-store.com/product/123',
                'use_cache': False
            }
        ]
        
        # 4. 执行爬取
        logger.info("开始执行爬取任务...")
        results = crawler.crawl_multiple(tasks, max_workers=2)
        
        # 5. 分析结果
        success_count = sum(1 for r in results if r.status == 'success')
        error_count = sum(1 for r in results if r.status == 'error')
        
        logger.info(f"爬取完成: 成功 {success_count} 个, 失败 {error_count} 个")
        
        # 6. 导出结果
        if success_count > 0:
            export_file = crawler.export_results(results, format='json')
            logger.info(f"结果已导出到: {export_file}")
            
            # 打印成功的结果摘要
            for result in results:
                if result.status == 'success':
                    print(f"\nURL: {result.url}")
                    print(f"标题: {result.title}")
                    print(f"提取数据量: {len(result.extracted_data)} 个字段")
                    if 'articles' in result.extracted_data:
                        print(f"文章数量: {len(result.extracted_data['articles'])}")
        
        # 7. 性能监控
        print("\n=== 性能统计 ===")
        for website_name, session in crawler.sessions.items():
            # 可以添加更多的性能统计信息
            print(f"网站 {website_name}: 会话已建立")
    
    finally:
        # 8. 清理资源
        crawler.cleanup()
        logger.info("爬虫已关闭")

if __name__ == "__main__":
    main()

4.2 错误处理与重试机制

在实际的爬虫项目中,网络不稳定、网站反爬、页面结构变化等问题是家常便饭。一个健壮的爬虫必须有完善的错误处理和重试机制。

import random
import time
from functools import wraps
from datetime import datetime, timedelta
from DrissionPage.errors import ElementNotFoundError, BrowserConnectError

class RetryStrategy:
    """重试策略类"""
    
    def __init__(self, max_retries=3, base_delay=1, max_delay=30, 
                 retry_exceptions=None, exponential_backoff=True):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.exponential_backoff = exponential_backoff
        
        if retry_exceptions is None:
            self.retry_exceptions = (
                ElementNotFoundError,
                BrowserConnectError,
                TimeoutError,
                ConnectionError,
                Exception  # 谨慎使用,可能会掩盖真正的问题
            )
        else:
            self.retry_exceptions = retry_exceptions
        
        self.retry_stats = {
            'total_attempts': 0,
            'successful_attempts': 0,
            'failed_attempts': 0,
            'retry_count': 0
        }
    
    def calculate_delay(self, attempt):
        """计算重试延迟"""
        if self.exponential_backoff:
            delay = min(self.base_delay * (2 ** attempt), self.max_delay)
        else:
            delay = self.base_delay
        
        # 添加随机抖动,避免所有重试同时发生
        jitter = random.uniform(0, delay * 0.1)
        return delay + jitter
    
    def should_retry(self, exception, attempt):
        """判断是否应该重试"""
        if attempt >= self.max_retries:
            return False
        
        if isinstance(exception, self.retry_exceptions):
            return True
        
        return False
    
    def record_attempt(self, success):
        """记录尝试结果"""
        self.retry_stats['total_attempts'] += 1
        if success:
            self.retry_stats['successful_attempts'] += 1
        else:
            self.retry_stats['failed_attempts'] += 1
    
    def get_stats(self):
        """获取统计信息"""
        return self.retry_stats.copy()

def retry_on_failure(strategy=None):
    """重试装饰器"""
    if strategy is None:
        strategy = RetryStrategy()
    
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            attempt = 0
            
            while attempt <= strategy.max_retries:
                try:
                    if attempt > 0:
                        strategy.retry_count += 1
                        delay = strategy.calculate_delay(attempt - 1)
                        print(f"第 {attempt} 次重试,等待 {delay:.2f} 秒...")
                        time.sleep(delay)
                    
                    result = func(*args, **kwargs)
                    strategy.record_attempt(True)
                    return result
                    
                except Exception as e:
                    last_exception = e
                    attempt += 1
                    
                    if strategy.should_retry(e, attempt):
                        print(f"尝试 {attempt}/{strategy.max_retries} 失败: {e}")
                        continue
                    else:
                        strategy.record_attempt(False)
                        raise
            
            # 所有重试都失败了
            strategy.record_attempt(False)
            raise last_exception
        
        return wrapper
    return decorator

class SmartCrawlerWithRetry:
    """带智能重试的爬虫"""
    
    def __init__(self):
        self.retry_strategy = RetryStrategy(
            max_retries=3,
            base_delay=2,
            max_delay=60,
            exponential_backoff=True
        )
        self.circuit_breaker = CircuitBreaker(
            failure_threshold=5,
            recovery_timeout=300  # 5分钟
        )
    
    @retry_on_failure()
    def fetch_page_with_retry(self, url, session=None):
        """带重试的页面获取"""
        if session is None:
            from DrissionPage import WebPage
            session = WebPage()
        
        try:
            # 使用断路器保护
            if not self.circuit_breaker.allow_request(url):
                raise CircuitBreakerOpenError(f"断路器已打开: {url}")
            
            success = session.get(url, retry=0, timeout=30)
            if not success:
                raise ConnectionError(f"无法访问页面: {url}")
            
            # 验证页面是否正常加载
            self._validate_page(session)
            
            return session
            
        except Exception as e:
            self.circuit_breaker.record_failure(url)
            raise
    
    def _validate_page(self, session):
        """验证页面是否正常加载"""
        # 检查是否有错误页面
        error_indicators = [
            'error', '404', 'not found', '无法访问',
            'access denied', 'forbidden', '服务器错误'
        ]
        
        page_text = session.text.lower()
        for indicator in error_indicators:
            if indicator in page_text:
                raise PageValidationError(f"页面包含错误指示: {indicator}")
        
        # 检查是否有反爬机制
        if 'please enable javascript' in page_text.lower():
            raise AntiScrapingError("页面需要JavaScript")
        
        # 检查页面结构是否完整
        if len(session.html) < 100:
            raise PageValidationError("页面内容过短")
    
    def smart_crawl(self, url, extractors, max_depth=2):
        """智能爬取,支持深度爬取和自适应策略"""
        results = []
        visited = set()
        
        def crawl_recursive(current_url, depth):
            if depth > max_depth or current_url in visited:
                return
            
            visited.add(current_url)
            
            try:
                # 获取页面
                session = self.fetch_page_with_retry(current_url)
                
                # 提取数据
                page_data = self._extract_with_adaptation(session, extractors)
                page_data['url'] = current_url
                page_data['depth'] = depth
                results.append(page_data)
                
                # 提取链接继续爬取
                if depth < max_depth:
                    links = self._extract_links(session)
                    for link in links[:5]:  # 限制每页只爬取5个链接
                        if link not in visited:
                            # 随机延迟,避免请求过于频繁
                            time.sleep(random.uniform(1, 3))
                            crawl_recursive(link, depth + 1)
                            
            except Exception as e:
                print(f"爬取 {current_url} 失败: {e}")
                # 根据错误类型调整策略
                self._adapt_strategy(e)
        
        crawl_recursive(url, 0)
        return results
    
    def _extract_with_adaptation(self, session, extractors):
        """自适应数据提取"""
        data = {}
        
        for key, extractor in extractors.items():
            try:
                # 尝试主要选择器
                value = self._extract_with_selector(session, extractor['primary'])
                data[key] = value
                
            except ElementNotFoundError:
                # 如果主要选择器失败,尝试备用选择器
                if 'fallback' in extractor:
                    try:
                        value = self._extract_with_selector(session, extractor['fallback'])
                        data[key] = value
                        print(f"使用备用选择器提取 {key}")
                    except:
                        data[key] = None
                else:
                    data[key] = None
                    
                # 记录选择器失败,用于后续优化
                self._record_selector_failure(key, extractor['primary'])
        
        return data
    
    def _extract_with_selector(self, session, selector_config):
        """使用选择器提取数据"""
        if isinstance(selector_config, str):
            element = session.ele(selector_config, timeout=10)
            return element.text
        
        elif isinstance(selector_config, dict):
            selector = selector_config.get('selector')
            selector_type = selector_config.get('type', 'text')
            multiple = selector_config.get('multiple', False)
            
            if multiple:
                elements = session.eles(selector, timeout=10)
                if selector_type == 'text':
                    return [ele.text for ele in elements]
                elif selector_type == 'attribute':
                    attr = selector_config.get('attribute', 'href')
                    return [ele.attr(attr) for ele in elements]
                else:
                    return [ele.text for ele in elements]
            else:
                element = session.ele(selector, timeout=10)
                if selector_type == 'text':
                    return element.text
                elif selector_type == 'attribute':
                    attr = selector_config.get('attribute', 'href')
                    return element.attr(attr)
                elif selector_type == 'html':
                    return element.html
                else:
                    return element.text
        
        raise ValueError(f"无效的选择器配置: {selector_config}")
    
    def _extract_links(self, session):
        """提取页面中的链接"""
        links = []
        try:
            link_elements = session.eles('tag:a')
            for link in link_elements:
                href = link.attr('href')
                if href and href.startswith('http'):
                    links.append(href)
        except:
            pass
        
        return links
    
    def _adapt_strategy(self, error):
        """根据错误类型调整策略"""
        if isinstance(error, (ElementNotFoundError, PageValidationError)):
            # 页面结构可能已变化,增加等待时间
            self.retry_strategy.base_delay = min(
                self.retry_strategy.base_delay * 1.5, 
                self.retry_strategy.max_delay
            )
            print(f"调整基础延迟为: {self.retry_strategy.base_delay}秒")
        
        elif isinstance(error, (BrowserConnectError, ConnectionError)):
            # 网络问题,减少并发数或增加重试次数
            self.retry_strategy.max_retries += 1
            print(f"增加最大重试次数为: {self.retry_strategy.max_retries}")
    
    def _record_selector_failure(self, key, selector):
        """记录选择器失败,用于机器学习优化"""
        # 这里可以记录到数据库或文件,用于后续分析
        with open('selector_failures.log', 'a', encoding='utf-8') as f:
            timestamp = datetime.now().isoformat()
            f.write(f"{timestamp},{key},{selector}\n")

class CircuitBreaker:
    """断路器模式,防止连续失败"""
    
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failures = {}
        self.state = {}  # 'closed', 'open', 'half-open'
        self.last_failure_time = {}
    
    def allow_request(self, service):
        """是否允许请求"""
        if service not in self.state:
            self.state[service] = 'closed'
            self.failures[service] = 0
            return True
        
        if self.state[service] == 'open':
            # 检查是否应该尝试恢复
            if service in self.last_failure_time:
                time_since_failure = time.time() - self.last_failure_time[service]
                if time_since_failure > self.recovery_timeout:
                    self.state[service] = 'half-open'
                    return True
            return False
        
        return True
    
    def record_failure(self, service):
        """记录失败"""
        if service not in self.failures:
            self.failures[service] = 0
        
        self.failures[service] += 1
        self.last_failure_time[service] = time.time()
        
        if self.failures[service] >= self.failure_threshold:
            self.state[service] = 'open'
            print(f"断路器打开: {service}")
    
    def record_success(self, service):
        """记录成功"""
        self.failures[service] = 0
        self.state[service] = 'closed'

# 自定义异常类
class PageValidationError(Exception):
    pass

class AntiScrapingError(Exception):
    pass

class CircuitBreakerOpenError(Exception):
    pass

# 使用示例
def example_usage():
    crawler = SmartCrawlerWithRetry()
    
    # 定义数据提取器
    extractors = {
        'title': {
            'primary': 'tag:h1',
            'fallback': 'tag:h2'
        },
        'content': {
            'primary': '.article-content',
            'fallback': '.content'
        },
        'author': {
            'primary': '.author-name',
            'fallback': '.byline'
        },
        'publish_date': {
            'primary': '.publish-time@@datetime',
            'fallback': '.date'
        },
        'tags': {
            'primary': {
                'selector': '.tag-list .tag',
                'type': 'text',
                'multiple': True
            },
            'fallback': {
                'selector': '.tags a',
                'type': 'text',
                'multiple': True
            }
        }
    }
    
    try:
        # 爬取单个页面
        session = crawler.fetch_page_with_retry('https://example.com/article/123')
        data = crawler._extract_with_adaptation(session, extractors)
        print(f"提取的数据: {data}")
        
        # 深度爬取
        results = crawler.smart_crawl(
            'https://example.com/blog',
            extractors,
            max_depth=2
        )
        
        print(f"共爬取 {len(results)} 个页面")
        
        # 查看重试统计
        stats = crawler.retry_strategy.get_stats()
        print(f"重试统计: {stats}")
        
    except Exception as e:
        print(f"爬取失败: {e}")
    
    finally:
        # 保存选择器失败记录
        print("选择器失败记录已保存到 selector_failures.log")

# 在实际项目中,你可能会这样使用
class ProductionCrawler:
    def __init__(self):
        self.smart_crawler = SmartCrawlerWithRetry()
        self.results_db = []  # 这里可以用数据库替换
        
    def run_daily_crawl(self, start_urls):
        """每日爬取任务"""
        all_results = []
        
        for url in start_urls:
            try:
                print(f"开始爬取: {url}")
                
                # 使用智能爬取,深度为3
                results = self.smart_crawler.smart_crawl(
                    url=url,
                    extractors=self.get_extractors_for_url(url),
                    max_depth=3
                )
                
                all_results.extend(results)
                
                # 保存到数据库
                self.save_to_database(results)
                
                # 生成报告
                self.generate_report(results)
                
                # 避免请求过于频繁
                time.sleep(random.uniform(5, 10))
                
            except CircuitBreakerOpenError:
                print(f"断路器已打开,跳过: {url}")
                continue
                
            except Exception as e:
                print(f"爬取 {url} 时发生严重错误: {e}")
                # 发送警报
                self.send_alert(f"爬取失败: {url}", str(e))
                continue
        
        return all_results
    
    def get_extractors_for_url(self, url):
        """根据URL返回相应的提取器配置"""
        # 这里可以根据URL模式返回不同的提取器
        if 'news' in url:
            return {
                'title': {'primary': '.news-title'},
                'content': {'primary': '.article-body'},
                # ... 其他字段
            }
        elif 'blog' in url:
            return {
关于网页自动化工具DrissionPage进行爬虫的使用方法 一个基于 python 的网页自动化工具,它既能控制浏览器,也能收发数据包,还能把两者合而为一。可兼顾浏览器自动化的便利性和 requests 的高效率,可以跨 iframe 查找元素,无需切入切出 阅读详情

相关推荐

DrissionPage实战:Python网页自动化与XPath定位避坑指南

网页自动化是提升数据采集和流程效率的核心技术,其原理在于模拟浏览器行为或直接处理网络请求,实现对网页的交互与数据提取。在Python生态中,Selenium曾是这一领域的标准工具,但其配置复杂、启动缓慢的问题常影响开发效率。DrissionPage作为新兴的国产库,通过融合Requests的高效与Selenium的动态渲染能力,提供了更优的技术解决方案,显著降低了自动化脚本的编写和维护门槛。它尤其适用于需要处理JavaScript动态加载内容的单页应用(SPA)、表单自动提交及页面监控等场景。本文聚焦于使用

weixin_33994429的博客 395

自动化测试框架:DrissionPage(1)——安装与设置

但从 3.0 版开始,作者另起炉灶,用 chromium 协议自行实现了 selenium 全部功能,从而摆脱了对 selenium 的依赖,功能更多更强,运行效率更高,开发更灵活。因此,DrissionPage设计初衷,是将它们合而为一,能够在不同须要时切换相应模式,并提供一种人性化的使用方法,提高开发和运行效率。安装过程也是遇到了很多麻烦,下载就花了很多时间,中间下载一直断,就一直运行安装命令,总算多次安装后安装完成了。说了这么多,相信很多人已经跃跃欲试了,怎么快速搭建这套框架,先要准备一些基础环境。

up1292的博客 2万+

DrissionPage实战:5分钟搞定ChromiumPage多标签页自动化管理(含常见问题排查)

本文详细介绍了如何使用DrissionPage的ChromiumPage模块高效管理多标签页自动化任务。通过实战代码演示了标签页的创建、获取、协同工作及性能优化技巧,并提供了常见问题排查方案,帮助开发者快速掌握这一强大的浏览器自动化工具。

weixin_28340315的博客 507

【Python爬虫实战】全面解析 DrissionPage:简化 Python 浏览器自动化的三种模式

在数据抓取与自动化测试领域,处理静态和动态网页的需求日益增加。传统的工具如requests和selenium虽然各有优势,但往往需要根据需求在二者之间切换,增加了代码的复杂度。则创新性地整合了这两种模式,提供了统一的 API,使得静态和动态网页的处理变得更加简洁高效。此外,的模块结合了Playwright的强大性能,专为处理复杂的动态网页而设计,让开发者可以在无头浏览器中高效执行 JavaScript、进行模拟操作,为网页自动化和数据抓取提供了理想的解决方案。

易辰的博客 8138

Python中DrissionPage的详细解析与实战

DrissionPage 是一个基于 Selenium 和 requests 的轻量级 Python 库,旨在简化网页操作和数据抓取。它为开发者提供了统一的 API,让用户可以方便地在浏览器模式和无浏览器模式之间切换,从而更高效地完成网页自动化任务。 本文将从 DrissionPage 的核心功能、安装配置、主要 API 以及实际应用场景等方面进行详细解析,帮助你全面掌握 DrissionPage 的使用方法。

fudaihb的博客 5704

Android架构中UseCase的设计原则与实战解析

本文深入解析Android架构中UseCase的设计原则与实战应用,探讨其在Domain Layer中的核心作用。通过对比ViewModel的臃肿问题,详细阐述UseCase的无状态原则、单一职责边界等设计铁律,并提供多种实现方式的代码示例,帮助开发者构建更清晰、高效的Android应用架构。

weixin_28432777的博客 431

【限时免费】 《DrissionPage的安装与使用教程》

《DrissionPage的安装与使用教程》 【免费下载链接】DrissionPage 基于python的网页自动化工具。既能控制浏览器,也能收发数据包。可兼顾浏览器自动化的便利性和requests的高效率。功能强大,内置无数人性化设计和便捷功能。语法简洁而优雅,代码量少。 ...

gitblog_01420的博客 1167

在线考试系统源码+数据库+word+ppt(高分毕业设计)

本系统通过Spring Cloud实现微服务拆分,将授权、考试、监控等功能解耦,既保证了系统的扩展性,也便于后期维护。基于OAuth2.0的单点登录,解决多服务统一认证问题;考试与练习模式分离,满足不同场景需求;集成Druid与Hystrix,保障系统监控与稳定性。

weixin_44355158的博客 737

【限时免费】 DrissionPage多标签页并行操作技术解析

DrissionPage多标签页并行操作技术解析 【免费下载链接】DrissionPage 基于python的网页自动化工具。既能控制浏览器,也能收发数据包。可兼顾浏览器自动化的便利性和requests的高效率。功能强大,内置无数人性化设计和便捷功能。语法简洁而优雅,代码量少。 ...

gitblog_01412的博客 647

YOLOv8中的C2f模块代码详解

在YOLOv8网络结构中,C2f模块(CSP Bottleneck with 2 Convolutions)是一个关键组件,用于实现跨阶段部分聚合

zzzyyy8的博客 3万+

Windows10 搭建 FTP 服务器

问题: 有这么一个需求,博主需要对班级同学的信息进行收集,同学的信息是一个个的文件, 正常是每个同学将自己的信息打包压缩通过 QQ 或者邮箱方式发送给博主, 博主一个个接收这些压缩包,然后进行解压,再统一放到一个文件夹下, 但是博主觉得这样子实在是太麻烦了,如果某一个同学的信息来回变动,就需要接收很多次,更改很多次, 另外如果消息过多还会产生漏掉某个同学的信息的问题。 解决方法: 如果可以使...

assiduous_me的博客 3965

《DrissionPage的安装与使用教程》

《DrissionPage的安装与使用教程》 【免费下载链接】DrissionPage 基于python的网页自动化工具。既能控制浏览器,也能收发数据包。可兼顾浏览器自动化的便利性和requests的高效率。功能强大,内置无数人性化设计和便捷功能。语法简洁而优雅,代码量少。 ...

gitblog_01429的博客 1350

VMware Workstation Pro 17 最新破解版安装教程(附永久激活密钥)

本文详细介绍了VMware Workstation Pro 17专业版的安装与配置指南,包括系统要求检查、官方版本安装流程、高级配置优化及常见问题排查。通过合规使用官方版本,开发者可以充分利用其强大的虚拟化能力,提升多平台开发与测试效率。

weixin_29327977的博客 3555

RPA入门:DrissionPage多标签页管理

本文介绍浏览器标签页管理的三个核心功能:1.get_tab()获取当前活动标签页,适用于操作可见页面;2.new_tab()创建新标签页,支持并行处理任务;3.latest_tab属性获取最新打开的标签页,可用于捕获跳转页或弹窗。三种方法都提供了Python代码示例,分别演示了基本用法和典型应用场景,帮助开发者灵活管理浏览器标签页。这些功能适用于需要多标签页协同工作的自动化测试和爬虫场景。

2301_80809484的博客 411

innovus中如何设置cts参数

上面是设置cts的一般设置,如果需要对某些skew group或者某些clock tree设置不一样的参数,如需要对clk1这个clock设置不一样的inverter,可以使用。需要查询已经设置过的ccopt参数,可以使用get_ccopt_property去查询,方法和set_ccopt_property一致。言归正传,cts的设置命令是set_ccopt_property,获取命令是get_ccopt_property。另外,set_ccopt_property提供一套命令查询方法。

芯冰乐 544

LVGL 组件对齐方式说明

本文说明 LVGL(以 v8 为例)中常用的 lv_align_t对齐选项的含义与用法,对应界面设计工具里下拉列表中的以及等命名。

摸鱼专家 131

Python网页自动化工具终极指南:DrissionPage快速上手教程

在当今数字化时代,网页自动化工具已成为提升工作效率的必备利器。DrissionPage作为一款基于Python的网页自动化工具,完美融合了浏览器控制和数据包收发能力,为开发者和数据分析师提供了前所未有的便利。这款Python浏览器控制工具不仅语法简洁优雅,还内置了无数人性化设计和便捷功能,让网页自动化变得简单高效。 ## 🚀 项目简介与核心优势 DrissionPage是一个功能强大的Pyt

gitblog_01140的博客 1003

RK3568实现蓝牙音箱功能

A2DP旨在通过蓝牙连接传输高质量的立体声音频流。它使用的基本压缩算法是SBC(Sub-Band Coding)来减小音频数据的大小,同时保持高音质,SBC压缩虽然效率较低,但是是必须支持的基本备用方案。A2DP还支持其他高级编解码器,例如AAC、aptX和LDAC,这些编解码器比SBC提供更好的音质,但这些编解码器的支持取决于设备本身的支持情况。注意A2DP协议要与AVRCP协议区分开。A2DP是传输音频的,而AVRCP是用来控制音频的。这里我用到的是RK3568 Android11。

leikko的博客 1175

DrissionPage避坑指南:从安装到高级功能的完整配置流程

本文详细介绍了Python网页自动化工具DrissionPage的完整配置流程与避坑指南。从环境准备、安装配置到高级功能实战,涵盖浏览器路径设置、元素定位策略、网络代理配置及性能优化等关键环节,帮助开发者高效部署并规避常见问题,实现无缝的网页自动化操作。

rgv23456789的博客 268

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

DrissionPage Cookie管理:跨会话保持登录状态的高级技巧

在Web自动化与数据爬取过程中,**Cookie(小型文本文件)** 是维持用户会话的核心机制。许多开发者面临以下痛点: - 重复登录导致IP被封或验证码拦截 - 分布式任务中Cookie同步困难 - 不同浏览器/会话间状态无法共享 - 复杂Cookie参数(如HttpOnly、Secure)处理不当 本文将系统讲解DrissionPage的Cookie全生命周期管理方案,通过12个实战案例带你...

gitblog_00718的博客 1684

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

从Selenium迁移到DrissionPage的5个实战技巧(附避坑指南)

本文为资深开发者提供了从Selenium迁移到DrissionPage的5个核心实战技巧与避坑指南。内容涵盖思维转换、API重构、性能优化及常见问题解决,重点介绍了如何利用DrissionPage简洁的API和独特的Session/Driver双模式切换,显著提升爬虫与自动化脚本的开发效率和运行性能。

weixin_28835789的博客 453

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

从Selenium迁移到DrissionPage:5个核心技巧与避坑指南

在Web自动化与数据抓取领域,Selenium作为经典工具,通过驱动真实浏览器模拟用户操作,能够处理复杂的JavaScript渲染与动态交互,但其性能开销和通信延迟是长期痛点。其核心原理是基于WebDriver协议与浏览器通信,虽然保证了兼容性,却牺牲了执行效率。为应对这一挑战,混合动力架构应运而生,它通过融合轻量级的HTTP请求与浏览器引擎,实现了任务执行路径的优化。这种模式在需要兼顾静态数据高速抓取与动态交互的爬虫及自动化测试场景中价值显著,能大幅提升脚本性能与可维护性。本文聚焦于DrissionPag

weixin_29698641的博客 241

DrissionPage项目中多标签页切换问题的解决方案

DrissionPage项目中多标签页切换问题的解决方案 【免费下载链接】DrissionPage 基于python的网页自动化工具。既能控制浏览器,也能收发数据包。可兼顾浏览器自动化的便利性和requests的高效率。功能强大,内置无数人性化设计和便捷功能。语法简洁而优雅,代码量少。 ...

gitblog_01409的博客 910

DrissionPage vs Selenium:自动化测试与数据抓取工具选型指南

在Web自动化领域,浏览器自动化是核心技术之一,它通过模拟用户操作实现网页交互自动化。其原理主要基于驱动真实浏览器或调用浏览器内核,与网页进行通信和渲染。这项技术的核心价值在于提升测试效率、实现数据自动采集以及简化重复性Web操作。在自动化测试、网络爬虫和RPA等应用场景中,工具选型直接影响项目效率。本文聚焦于DrissionPage和Selenium两大热门工具,深入对比它们在性能、资源消耗及功能特性上的差异。其中,DrissionPage凭借其独特的混合架构和高效的网络请求处理能力,在数据抓取场景中表现

cnmik42448的专栏 429

DrissionPage页面操作指南:标签页管理与iframe切换技巧

你是否还在为Selenium中标签页切换导致元素丢失而烦恼?是否因iframe嵌套层级复杂而陷入定位困境?作为一款强大的Python Web自动化工具,DrissionPage以其创新的API设计彻底重构了页面操作逻辑。本文将系统讲解标签页(Tab)全生命周期管理与iframe(内联框架)灵活操控的实战技巧,帮助你摆脱传统工具的桎梏,实现更优雅、高效的自动化脚本编写。 **读完本文你将掌握:**...

gitblog_01129的博客 1098

Python自动化新选择:DrissionPage实战指南

本文详细介绍了Python自动化工具DrissionPage的核心优势与实战应用。作为Selenium与Requests的融合方案,它通过无缝模式切换、极简定位语法和内置智能等待,解决了传统工具配置复杂、运行缓慢的痛点。文章提供了从安装配置、元素操作到高效数据抓取的完整指南,帮助开发者快速上手这一高效自动化新选择。

n8m7b6v5c4的博客 860
上一篇: PyQt5实战:用QThread解决界面卡死的3种常见场景(附完整代码)
下一篇: Android电池日志全解析:从healthd日志看懂手机充电那些事儿
ruby5
博客等级 码龄15年 29粉丝 893原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值