javascript

JavaScript 执行

在 Web 自动化中,有些操作仅通过页面元素操作难以实现,需要借助 JavaScript 来完成。DrissionPage 提供了强大的 JavaScript 执行能力,可以在页面上下文中运行 JavaScript 代码,实现复杂的操作和数据提取。本章将详细介绍如何在 DrissionPage 中使用 JavaScript 来扩展自动化脚本的能力。

JavaScript 执行基础

基本使用方法

DrissionPage 中执行 JavaScript 非常简单,通过页面对象的 run_js 方法即可:

from DrissionPage import ChromiumPage

page = ChromiumPage()
page.get('https://example.com')

# 执行简单的JavaScript代码
result = page.run_js('return document.title;')
print(f'页面标题: {result}')

# 执行多行JavaScript
result = page.run_js('''
    const heading = document.querySelector('h1');
    if (heading) {
        return heading.textContent;
    } else {
        return 'No heading found';
    }
''')
print(f'页面标题: {result}')

返回值处理

run_js 方法会自动将 JavaScript 返回值转换为 Python 对象:

# 返回字符串
text = page.run_js('return "Hello World";')  # 返回 Python 字符串

# 返回数字
number = page.run_js('return 42;')  # 返回 Python 整数

# 返回布尔值
flag = page.run_js('return true;')  # 返回 Python 布尔值 True

# 返回数组
array = page.run_js('return [1, 2, 3, 4];')  # 返回 Python 列表 [1, 2, 3, 4]

# 返回对象
obj = page.run_js('return {name: "John", age: 30};')  # 返回 Python 字典 {'name': 'John', 'age': 30}

# 返回 DOM 元素 (会转换为序列化对象)
element_info = page.run_js('return document.querySelector("button");')

不需要返回值的执行

如果不需要获取返回值,可以省略 return 语句:

# 不带返回值的JavaScript执行
page.run_js('''
    document.querySelector('input').value = 'test input';
    document.querySelector('button').click();
''')

与页面元素交互

操作元素

JavaScript 可以直接操作页面元素,执行点击、输入等操作:

# 使用JavaScript点击按钮
page.run_js('''
    document.querySelector('#submit-button').click();
''')

# 使用JavaScript填写表单
page.run_js('''
    const username = document.querySelector('#username');
    const password = document.querySelector('#password');
    username.value = 'user123';
    password.value = 'pass456';
    document.querySelector('form').submit();
''')

注入参数

run_js 方法支持将 Python 值作为参数传递给 JavaScript 代码:

# 传递单个参数
username = 'user123'
page.run_js('document.querySelector("#username").value = arguments[0];', username)

# 传递多个参数
username = 'user123'
password = 'pass456'
page.run_js('''
    document.querySelector('#username').value = arguments[0];
    document.querySelector('#password').value = arguments[1];
''', username, password)

# 使用字典参数
user_data = {'username': 'user123', 'password': 'pass456'}
page.run_js('''
    const data = arguments[0];
    document.querySelector('#username').value = data.username;
    document.querySelector('#password').value = data.password;
''', user_data)

数据提取

提取页面信息

JavaScript 可以轻松提取页面中的各种信息:

# 提取页面标题和URL
page_info = page.run_js('''
    return {
        title: document.title,
        url: window.location.href,
        domain: window.location.hostname,
        readyState: document.readyState
    };
''')
print(f"页面标题: {page_info['title']}")
print(f"页面URL: {page_info['url']}")

# 提取元数据
meta_data = page.run_js('''
    const metaTags = document.querySelectorAll('meta');
    const result = {};
    
    metaTags.forEach(tag => {
        const name = tag.getAttribute('name') || tag.getAttribute('property');
        if (name) {
            result[name] = tag.getAttribute('content');
        }
    });
    
    return result;
''')
print(f"页面描述: {meta_data.get('description')}")
print(f"Open Graph标题: {meta_data.get('og:title')}")

提取表格数据

使用 JavaScript 可以高效地提取网页中的表格数据:

# 从表格中提取数据
table_data = page.run_js('''
    const table = document.querySelector('#data-table');
    if (!table) return null;
    
    const headers = [];
    const headerCells = table.querySelectorAll('thead th');
    
    // 提取表头
    headerCells.forEach(cell => {
        headers.push(cell.textContent.trim());
    });
    
    // 提取数据行
    const rows = [];
    const bodyRows = table.querySelectorAll('tbody tr');
    
    bodyRows.forEach(row => {
        const rowData = {};
        const cells = row.querySelectorAll('td');
        
        cells.forEach((cell, index) => {
            if (index < headers.length) {
                rowData[headers[index]] = cell.textContent.trim();
            }
        });
        
        rows.push(rowData);
    });
    
    return rows;
''')

# 处理提取的表格数据
if table_data:
    print(f"提取了 {len(table_data)} 行数据")
    for row in table_data[:3]:  # 只显示前3行
        print(row)

提取动态加载的内容

对于动态加载的内容,JavaScript 可以监控并等待内容加载完成:

# 等待动态内容加载并提取
data = page.run_js('''
    // 定义超时时间
    const maxWaitTime = 10000; // 10秒
    const startTime = Date.now();
    
    // 返回一个Promise,等待目标元素出现或超时
    return new Promise((resolve, reject) => {
        const checkElement = () => {
            const element = document.querySelector('#dynamic-data');
            
            if (element && element.children.length > 0) {
                // 元素已加载,提取数据
                const items = [];
                const itemElements = element.querySelectorAll('.item');
                
                itemElements.forEach(item => {
                    items.push({
                        id: item.getAttribute('data-id'),
                        name: item.querySelector('.name').textContent,
                        value: item.querySelector('.value').textContent
                    });
                });
                
                resolve(items);
            } else if (Date.now() - startTime > maxWaitTime) {
                // 超时
                resolve({error: '等待超时', elapsed: Date.now() - startTime});
            } else {
                // 继续等待
                setTimeout(checkElement, 200);
            }
        };
        
        // 开始检查
        checkElement();
    });
''')

# 处理结果
if isinstance(data, dict) and 'error' in data:
    print(f"错误: {data['error']}")
else:
    print(f"成功提取 {len(data)} 个动态项目")

修改页面内容

改变元素样式

JavaScript 可以直接修改页面元素的样式和属性:

# 修改元素样式使其高亮显示
page.run_js('''
    const element = document.querySelector('#target-element');
    if (element) {
        // 保存原始样式
        const originalBg = element.style.backgroundColor;
        const originalBorder = element.style.border;
        
        // 应用高亮样式
        element.style.backgroundColor = 'yellow';
        element.style.border = '2px solid red';
        element.style.padding = '5px';
        
        // 3秒后恢复原始样式
        setTimeout(() => {
            element.style.backgroundColor = originalBg;
            element.style.border = originalBorder;
        }, 3000);
    }
''')

添加和删除元素

可以使用 JavaScript 动态添加或删除页面元素:

# 添加新元素
page.run_js('''
    // 创建新元素
    const newDiv = document.createElement('div');
    newDiv.id = 'injected-content';
    newDiv.innerHTML = '<h3>这是动态添加的内容</h3><p>此内容由DrissionPage通过JavaScript注入</p>';
    newDiv.style.padding = '10px';
    newDiv.style.border = '1px solid blue';
    newDiv.style.margin = '10px';
    
    // 添加到页面
    document.body.appendChild(newDiv);
''')

# 删除元素
page.run_js('''
    // 删除特定元素
    const elementsToRemove = document.querySelectorAll('.ad-banner, .popup, .cookie-notice');
    elementsToRemove.forEach(element => {
        if (element && element.parentNode) {
            element.parentNode.removeChild(element);
        }
    });
''')

修改表单行为

JavaScript 可以修改表单默认行为,例如取消提交动作并获取表单数据:

# 拦截表单提交并获取数据
form_data = page.run_js('''
    // 获取表单
    const form = document.querySelector('#checkout-form');
    
    if (!form) return {error: '表单不存在'};
    
    // 保存原始的onsubmit处理函数
    const originalOnsubmit = form.onsubmit;
    
    // 返回Promise以等待表单提交
    return new Promise(resolve => {
        // 替换提交处理函数
        form.onsubmit = function(event) {
            // 阻止表单提交
            event.preventDefault();
            
            // 收集表单数据
            const formData = new FormData(form);
            const data = {};
            
            for (let [key, value] of formData.entries()) {
                data[key] = value;
            }
            
            // 恢复原始处理函数
            form.onsubmit = originalOnsubmit;
            
            // 返回收集的数据
            resolve(data);
        };
        
        // 模拟点击提交按钮
        form.querySelector('button[type="submit"]').click();
    });
''')

print("表单数据:", form_data)

浏览器控制

操作浏览器历史

JavaScript 可以控制浏览器的历史导航:

# 浏览器历史前进后退
page.run_js('history.back();')  # 后退
page.wait.load_complete()

page.run_js('history.forward();')  # 前进
page.wait.load_complete()

# 获取浏览历史长度
history_length = page.run_js('return history.length;')
print(f"历史记录长度: {history_length}")

操作存储

JavaScript 可以操作浏览器的本地存储和会话存储:

# 保存数据到localStorage
page.run_js('''
    localStorage.setItem('user_preferences', JSON.stringify({
        theme: 'dark',
        fontSize: 'large',
        notifications: true
    }));
''')

# 读取localStorage数据
preferences = page.run_js('return JSON.parse(localStorage.getItem("user_preferences"));')
print(f"用户偏好设置: {preferences}")

# 清除特定localStorage数据
page.run_js('localStorage.removeItem("user_preferences");')

# 操作sessionStorage
page.run_js('sessionStorage.setItem("session_id", "12345");')
session_id = page.run_js('return sessionStorage.getItem("session_id");')
print(f"会话ID: {session_id}")

控制页面滚动

JavaScript 可以控制页面滚动位置:

# 滚动到页面底部
page.run_js('window.scrollTo(0, document.body.scrollHeight);')

# 滚动到页面顶部
page.run_js('window.scrollTo(0, 0);')

# 滚动到特定元素
page.run_js('''
    const element = document.querySelector('#target-section');
    if (element) {
        element.scrollIntoView({behavior: 'smooth', block: 'center'});
    }
''')

# 获取当前滚动位置
scroll_position = page.run_js('''
    return {
        x: window.pageXOffset || document.documentElement.scrollLeft,
        y: window.pageYOffset || document.documentElement.scrollTop
    };
''')
print(f"当前滚动位置: X={scroll_position['x']}, Y={scroll_position['y']}")

实用场景

处理复杂的动态内容

JavaScript 非常适合处理动态加载的复杂内容:

from DrissionPage import ChromiumPage
import time
import json

def scrape_dynamic_content(url, target_selector, max_wait=30):
    page = ChromiumPage()
    page.get(url)
    
    print("等待动态内容加载...")
    
    # 使用JavaScript等待和提取动态内容
    result = page.run_js(f'''
        const maxWait = {max_wait * 1000};
        const checkInterval = 500;
        let elapsed = 0;
        
        return new Promise((resolve) => {
            function checkContent() {{
                const container = document.querySelector('{target_selector}');
                
                if (container && container.children.length > 0) {{
                    // 内容已加载提取数据
                    const items = [];
                    const itemElements = container.querySelectorAll('.item');
                    
                    itemElements.forEach(item => {{
                        items.push({{
                            id: item.getAttribute('data-id'),
                            title: item.querySelector('.title')?.textContent?.trim() || '',
                            description: item.querySelector('.description')?.textContent?.trim() || '',
                            imageUrl: item.querySelector('img')?.src || '',
                            link: item.querySelector('a')?.href || ''
                        }});
                    }});
                    
                    resolve({{
                        status: 'success',
                        count: items.length,
                        items: items
                    }});
                }} else if (elapsed >= maxWait) {{
                    // 超时
                    resolve({{
                        status: 'timeout',
                        message: `等待内容加载超时 (${maxWait/1000}秒)`
                    }});
                }} else {{
                    // 继续等待
                    elapsed += checkInterval;
                    setTimeout(checkContent, checkInterval);
                }}
            }}
            
            // 开始检查
            checkContent();
        }});
    ''')
    
    if result.get('status') == 'success':
        print(f"成功提取 {result['count']} 个项目")
        return result['items']
    else:
        print(f"提取失败: {result.get('message')}")
        return []

# 使用函数提取动态内容
items = scrape_dynamic_content('https://example.com/dynamic-page', '#content-container')

# 保存结果
with open('dynamic_content.json', 'w', encoding='utf-8') as f:
    json.dump(items, f, ensure_ascii=False, indent=2)

绕过复杂的反爬机制

JavaScript 可以帮助绕过一些基本的反爬机制:

# 模拟正常的用户行为
def bypass_detection(page):
    # 随机滚动
    page.run_js('''
        function randomScroll() {
            const maxScrolls = 5 + Math.floor(Math.random() * 10);
            let scrollCount = 0;
            
            function doScroll() {
                if (scrollCount >= maxScrolls) return;
                
                const scrollAmount = 100 + Math.floor(Math.random() * 400);
                window.scrollBy(0, scrollAmount);
                scrollCount++;
                
                setTimeout(doScroll, 500 + Math.random() * 1000);
            }
            
            doScroll();
        }
        
        randomScroll();
    ''')
    
    # 等待一段时间,让滚动完成
    time.sleep(5)
    
    # 模拟鼠标移动
    page.run_js('''
        function simulateMouseMovement() {
            const events = 20;
            let count = 0;
            
            function moveRandom() {
                if (count >= events) return;
                
                const x = Math.floor(Math.random() * window.innerWidth);
                const y = Math.floor(Math.random() * window.innerHeight);
                
                const event = new MouseEvent('mousemove', {
                    view: window,
                    bubbles: true,
                    cancelable: true,
                    clientX: x,
                    clientY: y
                });
                
                document.dispatchEvent(event);
                count++;
                
                setTimeout(moveRandom, 100 + Math.random() * 200);
            }
            
            moveRandom();
        }
        
        simulateMouseMovement();
    ''')
    
    # 等待鼠标移动完成
    time.sleep(3)
    
    return page

# 使用函数绕过检测
page = ChromiumPage()
page.get('https://example.com/protected-page')
bypass_detection(page)

# 现在尝试提取内容
content = page.ele('#content').text
print("成功获取内容:", content[:100] + "...")

处理弹窗和对话框

JavaScript 可以预先设置处理程序来应对各种弹窗:

# 处理各种弹窗
page.run_js('''
    // 处理alert/confirm/prompt对话框
    window.alert = function(message) {
        console.log('Alert拦截: ' + message);
        return undefined;
    };
    
    window.confirm = function(message) {
        console.log('Confirm拦截: ' + message);
        return true;  // 总是返回确认
    };
    
    window.prompt = function(message, defaultValue) {
        console.log('Prompt拦截: ' + message);
        return 'AutomatedResponse';  // 返回自动回复
    };
    
    // 监控并自动关闭模态框
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes && mutation.addedNodes.length > 0) {
                for (let node of mutation.addedNodes) {
                    if (node.classList && 
                        (node.classList.contains('modal') || 
                         node.classList.contains('popup') || 
                         node.classList.contains('dialog'))) {
                        
                        console.log('检测到弹窗,尝试关闭');
                        
                        // 尝试点击关闭按钮
                        const closeButton = node.querySelector('.close, .dismiss, .cancel, [data-dismiss="modal"]');
                        if (closeButton) {
                            closeButton.click();
                        } else {
                            // 如果没有找到关闭按钮,尝试隐藏弹窗
                            node.style.display = 'none';
                        }
                    }
                }
            }
        });
    });
    
    // 开始监控DOM变化
    observer.observe(document.body, { childList: true, subtree: true });
''')

# 执行可能触发弹窗的操作
page.ele('#action-button').click()

高级技巧

与元素对象结合使用

JavaScript 也可以在元素对象上执行,仅影响特定元素:

# 获取元素
form_element = page.ele('form#registration')

# 在特定元素上下文中执行JavaScript
form_data = form_element.run_js('''
    // 在此上下文中,this 引用的是form元素
    const inputs = this.querySelectorAll('input, select, textarea');
    const data = {};
    
    inputs.forEach(input => {
        if (input.name) {
            data[input.name] = input.value;
        }
    });
    
    return data;
''')

print("表单字段:", form_data)

# 修改特定元素
table_element = page.ele('table#data')
table_element.run_js('''
    // 添加CSS类来高亮表格
    this.classList.add('highlighted');
    
    // 为所有表格行添加鼠标悬停效果
    const rows = this.querySelectorAll('tr');
    rows.forEach(row => {
        row.addEventListener('mouseover', function() {
            this.style.backgroundColor = '#f0f0f0';
        });
        row.addEventListener('mouseout', function() {
            this.style.backgroundColor = '';
        });
    });
''')

注入和使用外部库

可以注入和使用外部 JavaScript 库:

# 注入jQuery库
page.run_js('''
    function loadScript(url, callback) {
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = url;
        script.onload = callback;
        document.head.appendChild(script);
    }
    
    // 检查jQuery是否已加载
    if (typeof jQuery === 'undefined') {
        return new Promise(resolve => {
            loadScript('https://code.jquery.com/jquery-3.6.0.min.js', () => {
                resolve('jQuery加载完成');
            });
        });
    } else {
        return 'jQuery已存在';
    }
''')

# 等待库加载完成
time.sleep(1)

# 使用jQuery操作页面
data = page.run_js('''
    // 使用jQuery选择器
    const items = [];
    $('.product-item').each(function() {
        items.push({
            id: $(this).data('id'),
            name: $(this).find('.product-name').text(),
            price: $(this).find('.product-price').text(),
            rating: $(this).find('.rating').data('value')
        });
    });
    
    return items;
''')

print(f"提取了 {len(data)} 个产品信息")

创建持久的页面函数

为了重复使用一些功能,可以在页面上下文中定义持久的函数:

# 定义持久可用的页面帮助函数
page.run_js('''
    // 定义在window对象上使函数全局可用
    window.dpHelper = {
        // 提取表格数据
        extractTable: function(selector) {
            const table = document.querySelector(selector);
            if (!table) return null;
            
            const headers = [];
            const rows = [];
            
            // 提取表头
            table.querySelectorAll('thead th').forEach(th => {
                headers.push(th.textContent.trim());
            });
            
            // 提取数据行
            table.querySelectorAll('tbody tr').forEach(tr => {
                const row = {};
                tr.querySelectorAll('td').forEach((td, index) => {
                    if (index < headers.length) {
                        row[headers[index]] = td.textContent.trim();
                    }
                });
                rows.push(row);
            });
            
            return {headers, rows};
        },
        
        // 等待元素出现
        waitForElement: function(selector, timeout = 10000) {
            const startTime = Date.now();
            
            return new Promise((resolve, reject) => {
                function check() {
                    const element = document.querySelector(selector);
                    
                    if (element) {
                        resolve(true);
                    } else if (Date.now() - startTime > timeout) {
                        resolve(false);
                    } else {
                        setTimeout(check, 200);
                    }
                }
                
                check();
            });
        },
        
        // 高亮显示元素
        highlight: function(selector) {
            const elements = document.querySelectorAll(selector);
            elements.forEach(el => {
                const originalBackground = el.style.backgroundColor;
                const originalBorder = el.style.border;
                
                el.style.backgroundColor = 'yellow';
                el.style.border = '2px solid red';
                
                setTimeout(() => {
                    el.style.backgroundColor = originalBackground;
                    el.style.border = originalBorder;
                }, 2000);
            });
            
            return elements.length;
        }
    };
''')

# 使用已定义的帮助函数
# 提取表格
table_data = page.run_js('return dpHelper.extractTable("#data-table");')
if table_data:
    print(f"表格列: {table_data['headers']}")
    print(f"行数: {len(table_data['rows'])}")

# 等待元素出现
element_appeared = page.run_js('return dpHelper.waitForElement(".dynamic-content");')
if element_appeared:
    print("动态元素已出现")
else:
    print("等待超时,元素未出现")

# 高亮元素
highlighted_count = page.run_js('return dpHelper.highlight(".highlight-me");')
print(f"高亮了 {highlighted_count} 个元素")

小结

JavaScript 执行功能极大地扩展了 DrissionPage 的能力,使其不仅能通过 Python 操作页面元素,还能利用 JavaScript 实现更复杂的功能。通过结合两种语言的优势,可以:

  1. 提取复杂的动态内容 - 等待和捕获动态加载的数据
  2. 执行高级页面操作 - 修改页面行为、拦截事件
  3. 获取浏览器接口数据 - 访问 localStorage、cookie 等
  4. 增强页面交互 - 实现自定义的交互效果
  5. 注入辅助函数 - 在页面上下文中添加持久的帮助功能

在实际应用中,合理结合 DrissionPage 的原生 Python API 和 JavaScript 执行功能,可以构建更强大、灵活的自动化解决方案。

在下一章中,我们将学习 DrissionPage 的异常处理和调试技巧,帮助你构建更稳健的自动化脚本。

使用 Hugo 构建
主题 StackJimmy 设计