multithreading

多线程与多标签页

随着网络自动化需求的增加,高效处理多个任务变得越来越重要。DrissionPage 提供了强大的多线程和多标签页支持,让您能够同时处理多个网页和任务,大幅提高工作效率。本教程将详细介绍如何在 DrissionPage 中使用多线程和多标签页功能。

多标签页操作

创建和管理标签页

ChromiumPage 支持多标签页操作,使您能够在同一个浏览器实例中打开和操作多个网页。

创建新标签页

from DrissionPage import ChromiumPage

# 创建页面对象
page = ChromiumPage()

# 打开初始页面
page.get('https://www.example.com')

# 创建新标签页
new_tab = page.new_tab()  # 创建空白标签页
new_tab.get('https://www.example.org')  # 导航到新网址

# 或者直接创建并导航
another_tab = page.new_tab('https://www.github.com')

获取和切换标签页

# 获取所有标签页
all_tabs = page.tabs

# 通过索引切换标签页
page.to_tab(0)  # 切换到第一个标签页

# 通过 tab_id 切换标签页
tab_id = all_tabs[1].tab_id
page.to_tab(tab_id)

# 获取当前活动标签页
current_tab = page.tab

关闭标签页

# 关闭指定标签页
page.close_tabs(1)  # 关闭索引为1的标签页
page.close_tabs([1, 2])  # 关闭多个标签页
page.close_tabs('other')  # 关闭除当前外的所有标签页
page.close_tabs('all')  # 关闭所有标签页

# 关闭当前标签页
page.close_current_tab()

标签页间数据共享

所有标签页共享相同的浏览器环境,包括 cookies、缓存和其他会话数据,这意味着一个标签页中的登录状态可以在其他标签页中使用。

# 在一个标签页中登录
page.get('https://example.com/login')
page.ele('#username').input('user')
page.ele('#password').input('pass')
page.ele('#login-button').click()
page.wait.load_complete()

# 在新标签页中访问需要登录的页面
new_tab = page.new_tab('https://example.com/profile')
# 新标签页会保持登录状态
username = new_tab.ele('#user-profile-name').text

多标签页操作实战案例

以下是一个使用多标签页同时监控多个网站的示例:

from DrissionPage import ChromiumPage
import time

# 创建页面对象
page = ChromiumPage()

# 监控的网站列表
sites = [
    'https://example.com',
    'https://example.org',
    'https://example.net',
]

# 为每个网站创建一个标签页
tabs = []
for site in sites:
    tab = page.new_tab(site) if site != sites[0] else page
    tabs.append(tab)

# 定期检查每个网站的更新
while True:
    for i, tab in enumerate(tabs):
        page.to_tab(tab)
        # 检查是否有新内容
        update_element = tab.ele('#latest-update')
        if update_element:
            print(f"网站 {sites[i]} 有新更新: {update_element.text}")
    
    # 等待一段时间再检查
    time.sleep(300)  # 5分钟检查一次

多线程操作

虽然多标签页可以在同一个浏览器实例中打开多个页面,但它们仍然在同一个线程中运行。要实现真正的并行处理,您需要使用多线程。

基本多线程爬虫

以下是一个使用多线程爬取多个页面的基本示例:

from DrissionPage import ChromiumPage
import threading
import queue

# 创建一个队列来存储要爬取的 URL
url_queue = queue.Queue()
# 创建一个队列来存储结果
result_queue = queue.Queue()

# 添加 URL 到队列
for i in range(1, 11):
    url_queue.put(f"https://example.com/page/{i}")

# 定义爬虫工作函数
def crawler_worker():
    # 每个线程创建自己的页面对象
    page = ChromiumPage()
    
    while not url_queue.empty():
        try:
            # 从队列获取 URL
            url = url_queue.get(timeout=1)
            
            # 访问页面
            page.get(url)
            
            # 获取数据
            title = page.ele('h1').text
            content = page.ele('#content').text
            
            # 将结果添加到结果队列
            result_queue.put({
                'url': url,
                'title': title,
                'content': content
            })
            
            # 标记任务完成
            url_queue.task_done()
        except queue.Empty:
            break
        except Exception as e:
            print(f"处理 {url} 时出错: {e}")
            url_queue.task_done()
    
    # 关闭浏览器
    page.quit()

# 创建并启动多个爬虫线程
threads = []
for i in range(4):  # 创建4个线程
    thread = threading.Thread(target=crawler_worker)
    thread.daemon = True
    thread.start()
    threads.append(thread)

# 等待所有任务完成
url_queue.join()

# 等待所有线程结束
for thread in threads:
    thread.join()

# 处理结果
results = []
while not result_queue.empty():
    results.append(result_queue.get())

print(f"爬取了 {len(results)} 个页面")

使用线程池

Python 的 concurrent.futures 模块提供了更简单的线程池接口:

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

# 要爬取的 URL 列表
urls = [f"https://example.com/page/{i}" for i in range(1, 11)]

# 定义爬虫函数
def crawl_page(url):
    try:
        page = ChromiumPage()
        page.get(url)
        
        # 获取数据
        title = page.ele('h1').text
        content = page.ele('#content').text
        
        # 关闭浏览器
        page.quit()
        
        return {
            'url': url,
            'title': title,
            'content': content
        }
    except Exception as e:
        print(f"处理 {url} 时出错: {e}")
        return None

# 使用线程池并行爬取
start_time = time.time()
with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(crawl_page, urls))

# 过滤掉失败的结果
results = [r for r in results if r is not None]

print(f"爬取了 {len(results)} 个页面,耗时 {time.time() - start_time:.2f} 秒")

资源共享和竞争条件

在多线程环境中,需要注意资源共享和竞争条件问题。如果多个线程需要写入同一个文件或数据结构,应使用锁来避免冲突:

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

# 要爬取的 URL 列表
urls = [f"https://example.com/page/{i}" for i in range(1, 11)]

# 创建一个锁
file_lock = threading.Lock()

# 定义爬虫函数
def crawl_page(url):
    try:
        page = ChromiumPage()
        page.get(url)
        
        # 获取数据
        title = page.ele('h1').text
        content = page.ele('#content').text
        
        # 使用锁安全地写入文件
        with file_lock:
            with open('results.csv', 'a', newline='', encoding='utf-8') as f:
                writer = csv.writer(f)
                writer.writerow([url, title, content])
        
        # 关闭浏览器
        page.quit()
        
        return True
    except Exception as e:
        print(f"处理 {url} 时出错: {e}")
        return False

# 准备 CSV 文件
with open('results.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.writer(f)
    writer.writerow(['URL', '标题', '内容'])

# 使用线程池并行爬取
with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(crawl_page, urls))

# 计算成功数
success_count = sum(1 for r in results if r)
print(f"成功爬取了 {success_count} 个页面")

多标签页和多线程结合使用

在某些情况下,您可能希望结合使用多标签页和多线程,以获得最佳性能。例如,每个线程可以管理多个标签页:

from DrissionPage import ChromiumPage
import threading
import queue

# 创建一个队列来存储要爬取的 URL 分组
url_queue = queue.Queue()

# 将 URL 分组添加到队列
url_groups = [
    ["https://example.com/page/1", "https://example.com/page/2", "https://example.com/page/3"],
    ["https://example.com/page/4", "https://example.com/page/5", "https://example.com/page/6"],
    ["https://example.com/page/7", "https://example.com/page/8", "https://example.com/page/9"],
]
for group in url_groups:
    url_queue.put(group)

# 定义爬虫工作函数
def crawler_worker():
    # 每个线程创建自己的浏览器实例
    page = ChromiumPage()
    
    while not url_queue.empty():
        try:
            # 从队列获取 URL 组
            urls = url_queue.get(timeout=1)
            
            # 为每个 URL 创建标签页
            tabs = []
            for i, url in enumerate(urls):
                if i == 0:
                    # 第一个 URL 使用初始标签页
                    page.get(url)
                    tabs.append(page)
                else:
                    # 其他 URL 创建新标签页
                    tab = page.new_tab(url)
                    tabs.append(tab)
            
            # 处理每个标签页
            for tab in tabs:
                page.to_tab(tab)
                # 等待页面加载
                page.wait.load_complete()
                # 获取数据
                title = page.ele('h1').text
                print(f"线程 {threading.current_thread().name} 爬取了 {page.url}: {title}")
            
            # 关闭额外的标签页
            page.close_tabs('other')
            
            # 标记任务完成
            url_queue.task_done()
        except queue.Empty:
            break
        except Exception as e:
            print(f"处理时出错: {e}")
            url_queue.task_done()
    
    # 关闭浏览器
    page.quit()

# 创建并启动多个爬虫线程
threads = []
for i in range(3):  # 创建3个线程
    thread = threading.Thread(target=crawler_worker, name=f"Crawler-{i}")
    thread.daemon = True
    thread.start()
    threads.append(thread)

# 等待所有任务完成
url_queue.join()

# 等待所有线程结束
for thread in threads:
    thread.join()

print("所有任务完成")

性能优化建议

在使用多线程和多标签页时,请考虑以下性能优化建议:

  1. 适当的线程数量:线程数量不应过多,通常设置为 CPU 核心数的 2-3 倍为宜。过多的线程会导致资源竞争增加。

  2. 每个线程的标签页数量:每个浏览器实例打开的标签页不宜过多,通常 3-5 个为宜,避免浏览器性能下降。

  3. 使用无头模式:对于不需要可视化的任务,使用无头模式可以减少资源消耗:

    from DrissionPage import ChromiumOptions, ChromiumPage
    
    co = ChromiumOptions()
    co.set_headless(True)
    page = ChromiumPage(co)
    
  4. 禁用图片加载:禁用图片可以加快页面加载速度:

    co = ChromiumOptions()
    co.set_no_imgs(True)
    page = ChromiumPage(co)
    
  5. 设置合理的超时时间:根据网络情况设置合理的超时时间,避免因等待超时导致的线程阻塞:

    co = ChromiumOptions()
    co.set_timeouts(page_load=10, script=10)
    page = ChromiumPage(co)
    
  6. 资源释放:确保在不再需要时关闭浏览器,释放资源:

    # 关闭标签页
    page.close_tabs('other')
    
    # 关闭浏览器
    page.quit()
    
  7. 错误处理:妥善处理异常,避免因一个页面的错误导致整个任务失败。

实战案例:并行抓取新闻网站

以下是一个结合多线程和多标签页技术抓取多个新闻网站的完整示例:

from DrissionPage import ChromiumPage, ChromiumOptions
from concurrent.futures import ThreadPoolExecutor
import csv
import threading
import time
from urllib.parse import urlparse

# 新闻网站列表
news_sites = [
    "https://news.example.com",
    "https://articles.example.org",
    "https://blog.example.net",
    # 添加更多新闻网站...
]

# 创建一个锁用于文件写入
file_lock = threading.Lock()

# 定义爬虫函数
def crawl_news_site(site_url):
    # 配置浏览器选项
    co = ChromiumOptions()
    co.set_headless(True)  # 无头模式
    co.set_no_imgs(True)  # 禁用图片
    
    # 创建浏览器实例
    page = ChromiumPage(co)
    
    try:
        # 访问新闻网站首页
        page.get(site_url)
        page.wait.load_complete()
        
        # 获取网站域名作为标识
        domain = urlparse(site_url).netloc
        
        # 查找所有新闻链接
        news_links = []
        for link in page.eles('tag:a'):
            href = link.attr('href')
            if href and ('news' in href or 'article' in href):
                # 处理相对链接
                if href.startswith('/'):
                    href = f"{site_url}{href}"
                news_links.append(href)
        
        # 限制链接数量,避免过多
        news_links = news_links[:5]
        
        # 处理每个新闻链接
        results = []
        for i, link in enumerate(news_links):
            try:
                # 为每个链接创建标签页
                if i == 0:
                    page.get(link)
                    tab = page
                else:
                    tab = page.new_tab(link)
                
                # 等待页面加载
                tab.wait.load_complete()
                
                # 提取新闻标题和内容
                title = tab.ele('tag:h1').text
                
                # 不同网站可能有不同的内容选择器
                content_selectors = ['#article-content', '.news-body', 'tag:article']
                content = ""
                for selector in content_selectors:
                    content_elem = tab.ele(selector, timeout=1)
                    if content_elem:
                        content = content_elem.text
                        break
                
                # 如果找不到内容,尝试提取所有段落
                if not content:
                    paragraphs = tab.eles('tag:p')
                    content = '\n'.join([p.text for p in paragraphs])
                
                # 添加到结果
                results.append({
                    'url': link,
                    'title': title,
                    'content': content[:200] + '...' if len(content) > 200 else content
                })
                
                # 如果不是第一个标签页,关闭它
                if i > 0:
                    page.close_tabs(tab)
            
            except Exception as e:
                print(f"处理链接 {link} 时出错: {e}")
        
        # 安全写入结果到 CSV 文件
        with file_lock:
            with open('news_results.csv', 'a', newline='', encoding='utf-8') as f:
                writer = csv.writer(f)
                for result in results:
                    writer.writerow([domain, result['url'], result['title'], result['content']])
        
        return len(results)
    
    except Exception as e:
        print(f"处理网站 {site_url} 时出错: {e}")
        return 0
    
    finally:
        # 确保关闭浏览器
        page.quit()

# 准备 CSV 文件
with open('news_results.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.writer(f)
    writer.writerow(['网站', 'URL', '标题', '内容摘要'])

# 计时开始
start_time = time.time()

# 使用线程池并行爬取
with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(crawl_news_site, news_sites))

# 计算总抓取数量
total_news = sum(results)

# 打印结果
print(f"从 {len(news_sites)} 个网站共抓取了 {total_news} 条新闻")
print(f"总耗时: {time.time() - start_time:.2f} 秒")

总结

通过结合使用多线程和多标签页功能,DrissionPage 能够高效地并行处理多个网页任务。多线程允许您同时运行多个浏览器实例,而多标签页让您能够在一个浏览器实例中同时操作多个页面。

关键要点:

  1. 多标签页适合需要共享会话状态的任务,如保持同一登录状态
  2. 多线程适合彼此独立的任务,可以真正并行执行
  3. 结合两者使用可以最大化性能和效率
  4. 注意资源管理,及时释放不再需要的资源
  5. 妥善处理异常,确保一个任务的错误不会影响其他任务

通过合理使用这些技术,您可以显著提高爬虫和自动化任务的效率,减少运行时间。

使用 Hugo 构建
主题 StackJimmy 设计