基于Playwright与Asyncio的eBay电商数据高效爬虫实战

1. 项目概述与核心挑战

最近在做一个电商数据分析的项目,需要从eBay上抓取一批商品数据来做价格监控和趋势分析。一开始想着用Requests+BeautifulSoup这种经典组合应该能轻松搞定,结果刚跑起来没几分钟,IP就被限制了,页面返回的要么是验证码,要么就是各种奇怪的“人机验证”提示。这让我意识到,面对eBay这样的大型电商平台,传统的同步、简单请求的爬虫思路已经行不通了。它不仅有完善的动态加载机制(大量数据通过JavaScript异步请求获取),还有层层加码的反爬策略,包括但不限于请求频率检测、请求头校验、甚至对浏览器指纹的识别。

经过一番调研和尝试,我最终将技术栈锁定在了 Playwright Asyncio 上。这个组合听起来可能有点“新”,但实测下来,它确实是应对现代复杂反爬场景的一把利器。简单来说,Playwright是一个由微软开源的浏览器自动化库,它不像Selenium那样依赖WebDriver,而是直接通过CDP协议与Chromium、Firefox、WebKit等浏览器内核通信,速度更快,功能也更强大。最关键的是,它能完美模拟真实用户的浏览器环境,生成难以被简单识别的“人类”指纹。而Asyncio是Python的异步IO框架,它能让我们用同步代码的写法,去执行高并发的异步操作。想象一下,你开一个浏览器窗口(一个Playwright实例)去访问页面,等待页面加载是“阻塞”的;但如果你用Asyncio同时控制几十个浏览器页面(Page)去并发抓取,效率的提升是指数级的。

这个项目的目标很明确:构建一个稳定、高效、能绕过eBay常见反爬机制的自动化数据抓取工具。它不仅要能拿到数据,还要拿得“优雅”,不給目标服务器造成过大压力,符合基本的网络礼仪(比如尊重 robots.txt )。接下来,我会详细拆解整个实现过程,从环境搭建、核心逻辑到每一个避坑细节。

2. 技术选型与环境搭建

2.1 为什么是Playwright + Asyncio?

在动手之前,我们先深入聊聊为什么是这两个技术,而不是别的。市面上常见的爬虫方案大概有几类:

  1. Requests/httpx + 解析库(如BeautifulSoup, lxml) :这是最经典、最轻量的方案。优点是速度快、资源消耗小。但缺点在面对像eBay这样的网站时暴露无遗:无法执行JavaScript,拿不到动态渲染的内容;请求头和行为模式过于简单,极易被反爬系统识别为机器人。
  2. Selenium :老牌的浏览器自动化工具,可以完美解决JavaScript渲染问题。但它也有硬伤:速度慢(每个浏览器实例资源消耗大)、需要额外的WebDriver、在异步和高并发控制上比较笨重。
  3. Pyppeteer / Playwright :这两个都属于“无头浏览器”的现代自动化库。Pyppeteer是Puppeteer的Python版本,而Playwright可以看作是它的加强版和多浏览器支持版。我选择Playwright的主要原因有三点:
    • 多浏览器支持 :一套API支持Chromium、Firefox和WebKit,测试和兼容性更有保障。
    • 更强大的API :在元素选择、等待、网络拦截等方面提供了更直观易用的方法。
    • 更好的异步支持 :Playwright的Python API从一开始就为异步(Async)设计,与Asyncio的结合是天衣无缝的。

Asyncio 的引入,是为了解决I/O密集型任务(如网络请求、文件读写)的并发瓶颈。爬虫99%的时间都在等待网络响应,使用同步代码意味着你发起一个请求后,整个程序就卡在那里等,直到收到回复才能处理下一个。Asyncio允许你在等待一个请求时,去处理另一个已经返回的请求,用单线程就能实现极高的并发效率,这对于需要抓取大量列表页和详情页的电商爬虫来说,是性能飞跃的关键。

2.2 基础环境配置

工欲善其事,必先利其器。首先确保你有一个可用的Python环境(3.7+)。我强烈建议使用虚拟环境来管理项目依赖,避免包冲突。

# 创建项目目录并进入
mkdir ebay-crawler && cd ebay-crawler
# 创建虚拟环境(这里使用venv,你也可以用conda)
python -m venv venv
# 激活虚拟环境
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate

接下来安装核心依赖。除了 playwright ,我们还会用到 asyncio (Python内置)、 aiofiles 用于异步文件操作、 pandas 用于数据处理(非必须,但很方便)。

# 安装Playwright库
pip install playwright
# 安装Playwright所需的浏览器内核(这里选择Chromium,最常用)
playwright install chromium
# 安装其他辅助库
pip install aiofiles pandas

注意 playwright install 命令会下载浏览器二进制文件,体积较大(约100-200MB),请确保网络通畅。如果下载慢,可以考虑配置环境变量 PLAYWRIGHT_DOWNLOAD_HOST 使用国内镜像源。

安装完成后,可以写一个简单的脚本来测试Playwright是否能正常工作:

import asyncio
from playwright.async_api import async_playwright

async def test_playwright():
    async with async_playwright() as p:
        # 启动浏览器,headless=True表示无头模式(不显示GUI)
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        await page.goto('https://www.example.com')
        print(await page.title())
        await browser.close()

asyncio.run(test_playwright())

如果成功打印出“Example Domain”,那么恭喜你,环境搭建成功。

3. 核心爬虫架构设计

一个健壮的爬虫不应该是一堆线性脚本的堆砌,而应该有一个清晰的结构。我们的爬虫主要分为以下几个模块:

  1. 配置管理模块 :集中管理User-Agent、代理IP、请求延迟、重试次数等所有可配置项。
  2. 浏览器上下文管理模块 :负责创建和管理Playwright的BrowserContext。Context可以隔离cookie、缓存等,模拟不同的“会话”,对于需要多账号或隔离环境的场景很有用。
  3. 页面抓取与解析模块 :核心业务逻辑。负责导航到目标URL,执行必要的页面操作(如滚动、点击),等待元素加载,并提取结构化数据。
  4. 异步任务调度模块 :利用Asyncio的 asyncio.create_task asyncio.gather 等机制,高效地并发执行多个抓取任务。
  5. 数据存储模块 :将抓取到的数据异步保存到文件(如JSON、CSV)或数据库中。
  6. 反爬对抗与日志模块 :集成随机延迟、请求头轮换、代理IP池等策略,并记录详细的运行日志,便于监控和调试。

整个程序的流程可以概括为: 主函数创建一批异步抓取任务 -> 任务池并发执行 -> 每个任务独立管理自己的浏览器页面进行抓取和解析 -> 结果汇总并保存

4. 实战:eBay商品列表页抓取

eBay的商品搜索列表页(例如搜索“iphone 13”)是我们数据的主要入口。这个页面包含了商品标题、价格、链接等基础信息,也是我们获取详情页URL的来源。

4.1 分析页面结构与反爬策略

在写代码之前,一定要先用浏览器开发者工具(F12)手动分析目标页面。

  1. 网络请求 :查看“Network”标签,特别是XHR/Fetch请求,看看商品数据是直接嵌入在HTML中,还是通过额外的API接口获取。eBay的商品列表数据通常是首屏直接渲染在HTML里,但翻页或滚动加载更多时,会触发API请求。我们的策略是优先从初始HTML中解析,如果不行再考虑模拟API。
  2. 元素定位 :使用“检查”工具,查看商品卡片的HTML结构。找到包含标题、价格、链接的稳定CSS选择器。注意,eBay的类名可能含有随机字符串,要寻找那些语义化或结构稳定的父元素作为锚点。
  3. 反爬观察 :尝试快速刷新页面,或使用简单的Python脚本请求页面,观察是否会触发验证码。注意查看 robots.txt https://www.ebay.com/robots.txt ),尊重其中关于爬取频率和禁止目录的规定。

4.2 实现列表页抓取函数

下面是一个抓取eBay搜索列表页的核心函数。我们假设数据直接从初始HTML中加载。

import asyncio
import random
import logging
from typing import List, Dict
from urllib.parse import quote_plus
from playwright.async_api import Page, BrowserContext

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class EbayListCrawler:
    def __init__(self, context: BrowserContext):
        self.context = context
        # 基础URL,关键词和页数通过参数传入
        self.base_url = "https://www.ebay.com/sch/i.html"
        # 一些常用的请求头,可以放到配置文件中
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Accept-Language': 'en-US,en;q=0.9',
            'Accept-Encoding': 'gzip, deflate, br',
        }

    async def fetch_list_page(self, keyword: str, page_num: int = 1, max_retries: int = 3) -> List[Dict]:
        """
        抓取指定关键词和页数的商品列表
        """
        items = []
        page: Page = None
        try:
            # 1. 创建新页面
            page = await self.context.new_page()
            # 2. 设置请求头(Playwright支持在上下文中设置,也可以在goto时设置)
            await page.set_extra_http_headers(self.headers)
            
            # 3. 构造URL
            # eBay的搜索URL参数:_nkw是关键词,_pgn是页码
            encoded_keyword = quote_plus(keyword)
            url = f"{self.base_url}?_nkw={encoded_keyword}&_pgn={page_num}"
            logger.info(f"正在抓取列表页: {url}")

            # 4. 导航到页面,并等待主要内容加载完成
            # `wait_until='networkidle'` 会等待页面网络活动变得很少时再继续,确保动态内容加载完毕
            response = await page.goto(url, wait_until='networkidle', timeout=60000)
            if not response or response.status != 200:
                logger.warning(f"页面加载失败,状态码: {getattr(response, 'status', 'Unknown')}")
                return items

            # 5. 加入随机延迟,模拟人类浏览,避免请求过于规律
            await asyncio.sleep(random.uniform(1.0, 3.0))

            # 6. 模拟滚动,触发可能的懒加载(如果需要)
            await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
            await asyncio.sleep(random.uniform(0.5, 1.5)) # 等待滚动后内容加载

            # 7. 定位并提取商品元素
            # 使用更稳定的容器选择器,避免依赖易变的类名
            # 这里的选择器需要根据eBay实际页面结构调整
            item_selector = "li.s-item"  # 这是一个示例,eBay的列表项通常有`s-item`类
            await page.wait_for_selector(item_selector, state='attached', timeout=10000)
            
            item_elements = await page.query_selector_all(item_selector)
            logger.info(f"找到 {len(item_elements)} 个商品元素")

            for item_element in item_elements:
                try:
                    # 提取商品信息
                    title_elem = await item_element.query_selector(".s-item__title")
                    price_elem = await item_element.query_selector(".s-item__price")
                    link_elem = await item_element.query_selector("a.s-item__link")
                    
                    title = await title_elem.inner_text() if title_elem else "N/A"
                    # 价格可能包含货币符号和范围,需要清洗
                    price_raw = await price_elem.inner_text() if price_elem else "N/A"
                    price = self._clean_price(price_raw)
                    link = await link_elem.get_attribute("href") if link_elem else "N/A"
                    item_id = link.split('/')[-1].split('?')[0] if link != "N/A" else "N/A" # 从链接提取商品ID

                    if title != "N/A" and link != "N/A": # 过滤无效条目
                        items.append({
                            'item_id': item_id,
                            'title': title.strip(),
                            'price': price,
                            'url': link,
                            'keyword': keyword,
                            'page_num': page_num
                        })
                except Exception as e:
                    logger.debug(f"解析单个商品时出错: {e}")
                    continue # 跳过这个商品,继续下一个

        except Exception as e:
            logger.error(f"抓取列表页 {keyword} 第 {page_num} 页时发生错误: {e}")
            # 这里可以加入重试逻辑
        finally:
            if page:
                await page.close() # 非常重要!及时关闭页面释放资源
        return items

    @staticmethod
    def _clean_price(price_str: str) -> str:
        """清洗价格字符串,移除货币符号和多余空格,只保留数字和点"""
        import re
        # 匹配数字、点和逗号(用于千分位)
        match = re.search(r'[\d,]+\.?\d*', price_str.replace(',', ''))
        return match.group(0) if match else "N/A"

关键点解析与避坑指南:

  1. wait_until='networkidle' :这个参数非常关键。 'load' 只等待HTML加载完成,而 'networkidle' 会等待至少500ms内没有超过2个网络连接,这对于等待JavaScript发起的API请求(如图片、商品数据)完成至关重要。
  2. 随机延迟 ( asyncio.sleep(random.uniform(...)) ) :这是最基本的“礼貌”爬虫行为。固定的延迟间隔是机器行为的明显特征。随机化能有效降低被识别风险。
  3. 页面关闭 ( finally 块中的 await page.close() ) :Playwright的每个Page对象都会消耗内存。在异步并发环境中,如果不及时关闭已完成任务的页面,会导致内存泄漏,最终程序崩溃。 这是一个极易忽视但至关重要的细节。
  4. 选择器的稳定性 :避免使用包含长随机字符串的类名(如 class_=”s-item__title__123abc” )。优先使用部分匹配( css=a[class*=”s-item__link”] )或基于DOM结构的稳定路径(如 li.s-item > a )。最好定期检查选择器是否依然有效。
  5. 错误处理与重试 :网络请求和页面解析充满不确定性。必须用 try...except 包裹核心逻辑,并对可恢复的错误(如超时、元素未找到)进行重试。上面的代码框架中留下了重试逻辑的插入点。

5. 深入详情页与异步并发调度

获取到商品链接列表后,下一步就是并发地抓取每个商品的详情页,获取更丰富的信息,如商品描述、卖家信息、物流政策、用户评价等。

5.1 详情页抓取实现

详情页的结构更复杂,信息更分散。我们需要更有耐心地定位和提取。

class EbayDetailCrawler:
    def __init__(self, context: BrowserContext):
        self.context = context

    async def fetch_item_detail(self, item_url: str, item_id: str) -> Dict:
        """
        抓取单个商品详情页信息
        """
        detail_info = {'item_id': item_id, 'url': item_url}
        page: Page = None
        try:
            page = await self.context.new_page()
            # 可以设置更详细的请求头,模拟从列表页跳转过来的行为
            await page.set_extra_http_headers({
                'Referer': 'https://www.ebay.com/',
                'Sec-Fetch-Dest': 'document',
                'Sec-Fetch-Mode': 'navigate',
            })
            
            logger.info(f"正在抓取详情页: {item_id}")
            response = await page.goto(item_url, wait_until='networkidle', timeout=45000)
            
            if response.status != 200:
                logger.warning(f"详情页 {item_id} 加载失败,状态码: {response.status}")
                return detail_info
            
            await asyncio.sleep(random.uniform(2.0, 4.0)) # 详情页浏览时间更长

            # --- 提取各种信息,这里需要根据实际页面调整选择器 ---
            # 1. 商品描述 (可能在div#desc_div中,或者需要点击“描述”标签)
            desc_selector = "div.d-item-desc" # 示例选择器
            try:
                # 有些描述是折叠的,需要点击展开
                show_desc_button = await page.query_selector("button[aria-label*='description']")
                if show_desc_button:
                    await show_desc_button.click()
                    await page.wait_for_selector(desc_selector, state='visible', timeout=5000)
                description_elem = await page.wait_for_selector(desc_selector, timeout=10000)
                detail_info['description'] = await description_elem.inner_text() if description_elem else "N/A"
            except Exception as e:
                logger.debug(f"获取描述失败 {item_id}: {e}")
                detail_info['description'] = "N/A"

            # 2. 卖家信息
            seller_selector = "div.seller-persona a" # 示例
            seller_elem = await page.query_selector(seller_selector)
            detail_info['seller_name'] = await seller_elem.inner_text() if seller_elem else "N/A"
            detail_info['seller_feedback'] = await page.get_attribute(".seller-feedback span", "innerText") or "N/A"

            # 3. 物流信息
            shipping_selector = "div.shipping span" # 示例
            shipping_elem = await page.query_selector(shipping_selector)
            detail_info['shipping_cost'] = await shipping_elem.inner_text() if shipping_elem else "N/A"

            # 4. 商品规格 (通常在一个表格里)
            spec_items = await page.query_selector_all("div.ux-layout-section--features table tr")
            specs = {}
            for row in spec_items:
                cols = await row.query_selector_all("td")
                if len(cols) >= 2:
                    key = await cols[0].inner_text()
                    value = await cols[1].inner_text()
                    specs[key.strip(': ')] = value.strip()
            detail_info['specifications'] = specs

            # 5. 图片URLs (可能有多张)
            img_elements = await page.query_selector_all("div.ux-image-carousel img")
            img_urls = []
            for img in img_elements:
                src = await img.get_attribute("src")
                if src and src.startswith('http'):
                    # 有时是缩略图,可以尝试获取高清图URL(规律需分析)
                    img_urls.append(src)
            detail_info['image_urls'] = img_urls

            logger.info(f"成功抓取详情页: {item_id}")

        except asyncio.TimeoutError:
            logger.error(f"抓取详情页 {item_id} 超时")
        except Exception as e:
            logger.error(f"抓取详情页 {item_id} 时发生未知错误: {e}")
        finally:
            if page:
                await page.close()
        return detail_info

5.2 异步并发调度器

这是整个爬虫的“发动机”。我们需要一个管理器来并发执行大量的详情页抓取任务,同时控制并发度,避免对目标服务器造成DDoS攻击,也避免本地资源耗尽。

import aiofiles
import json
from asyncio import Semaphore

class CrawlerScheduler:
    def __init__(self, max_concurrent_tasks: int = 10):
        # 使用信号量(Semaphore)控制最大并发数
        self.semaphore = Semaphore(max_concurrent_tasks)
        self.detail_crawler = None # 将在主函数中初始化

    async def crawl_detail_worker(self, item: Dict, output_file: str):
        """
        一个工作协程,负责抓取一个商品详情
        """
        async with self.semaphore: # 控制并发
            # 每次抓取前等待一个随机短时间,进一步分散请求
            await asyncio.sleep(random.uniform(0.1, 0.5))
            detail_data = await self.detail_crawler.fetch_item_detail(item['url'], item['item_id'])
            # 合并列表页基础信息
            full_data = {**item, **detail_data}
            # 异步保存到文件
            await self._save_to_jsonl(full_data, output_file)

    async def _save_to_jsonl(self, data: Dict, filepath: str):
        """异步追加数据到JSON Lines格式文件"""
        async with aiofiles.open(filepath, 'a', encoding='utf-8') as f:
            await f.write(json.dumps(data, ensure_ascii=False) + '\n')

    async def schedule_detail_crawls(self, item_list: List[Dict], output_file: str):
        """
        调度并发抓取所有商品详情
        """
        logger.info(f"开始调度 {len(item_list)} 个详情页抓取任务,最大并发数: {self.semaphore._value}")
        tasks = []
        for item in item_list:
            # 为每个商品创建一个异步任务
            task = asyncio.create_task(self.crawl_detail_worker(item, output_file))
            tasks.append(task)
        
        # 等待所有任务完成,并收集结果(或异常)
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # 检查是否有任务因异常失败
        error_count = sum(1 for r in results if isinstance(r, Exception))
        if error_count:
            logger.warning(f"详情页抓取完成,有 {error_count} 个任务失败")
        else:
            logger.info("所有详情页抓取任务已完成")

并发控制的核心:信号量 (Semaphore) asyncio.Semaphore(10) 创建了一个计数为10的信号量。 async with self.semaphore: 确保了同一时间最多只有10个 crawl_detail_worker 协程在执行 fetch_item_detail 这个I/O密集型操作。这既保护了目标网站,也防止我们自己的程序打开过多浏览器页面导致内存溢出。

6. 高级反爬策略与稳定性优化

到了这一步,一个基本的爬虫已经可以运行了。但要让它能7x24小时稳定运行,还需要添加更多“盔甲”。

6.1 代理IP池集成

使用住宅代理或高质量的数据中心代理是绕过IP封锁最有效的方法之一。我们可以创建一个简单的代理管理器。

class ProxyManager:
    def __init__(self, proxy_list: List[str]):
        self.proxy_list = proxy_list
        self.current_index = 0

    def get_proxy(self) -> str:
        """简单轮询获取代理"""
        proxy = self.proxy_list[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.proxy_list)
        return proxy

    async def test_proxy(self, proxy_url: str) -> bool:
        """测试代理是否可用"""
        try:
            async with async_playwright() as p:
                # 启动浏览器时指定代理
                browser = await p.chromium.launch(
                    headless=True,
                    proxy={"server": proxy_url} # 格式如 "http://ip:port" 或 "socks5://ip:port"
                )
                page = await browser.new_page()
                # 访问一个测试网站,如谷歌
                response = await page.goto("https://www.google.com", timeout=15000)
                await browser.close()
                return response and response.status == 200
        except Exception:
            return False

在创建BrowserContext时使用代理:

proxy = proxy_manager.get_proxy()
browser_context = await browser.new_context(
    proxy={"server": proxy},
    viewport={'width': 1920, 'height': 1080}, # 设置一个常见的视口大小
    user_agent='...' # 可以在这里覆盖User-Agent
)

6.2 请求头与浏览器指纹随机化

固定的User-Agent和浏览器特征也是检测点。Playwright可以轻松模拟不同设备。

import random

def get_random_user_agent():
    """从预定义的列表中随机返回一个User-Agent"""
    agents = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...',
        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ...',
    ]
    return random.choice(agents)

# 在创建上下文或页面时使用
context = await browser.new_context(
    user_agent=get_random_user_agent(),
    # 还可以随机化其他属性,如时区、地理位置(需浏览器支持)
    # locale='en-US',
    # timezone_id='America/Los_Angeles',
)

6.3 智能重试与熔断机制

网络请求失败是常态。我们需要一个健壮的重试逻辑。

async def fetch_with_retry(page, url, max_retries=3, backoff_factor=1.5):
    """带指数退避的重试函数"""
    for attempt in range(max_retries):
        try:
            response = await page.goto(url, wait_until='networkidle', timeout=30000)
            if response.status in [200, 404]: # 404也是有效的响应,不需要重试
                return response
            elif response.status in [429, 503]: # 遇到频率限制或服务不可用,必须重试
                logger.warning(f"请求被限制(状态码{response.status}),第{attempt+1}次重试...")
            else:
                logger.error(f"请求失败,状态码{response.status}")
                break # 其他错误可能重试无效
        except (asyncio.TimeoutError, playwright._impl._api_types.Error) as e:
            logger.warning(f"请求异常({e}),第{attempt+1}次重试...")
        
        if attempt < max_retries - 1:
            # 指数退避:等待时间随重试次数增加
            wait_time = backoff_factor ** attempt + random.uniform(0, 1)
            logger.info(f"等待 {wait_time:.2f} 秒后重试")
            await asyncio.sleep(wait_time)
    
    logger.error(f"URL {url} 重试{max_retries}次后仍失败")
    return None

6.4 数据去重与增量抓取

对于长期运行的价格监控,我们只需要抓取新增或变价的商品。可以在存储时,以商品ID为主键进行去重,或者记录上次抓取的时间戳,只抓取最近更新的商品。

7. 完整流程整合与主函数

最后,我们把所有模块像拼积木一样组合起来,形成一个完整的、可配置的爬虫主程序。

import asyncio
import random
import logging
from typing import List
from playwright.async_api import async_playwright

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

async def main():
    # 配置参数
    KEYWORDS = ["iphone 13", "laptop"]
    MAX_LIST_PAGES = 3  # 每个关键词抓取前3页列表
    MAX_CONCURRENT_DETAILS = 8  # 详情页并发数
    OUTPUT_FILE = "ebay_items.jsonl"
    
    async with async_playwright() as p:
        # 1. 启动浏览器(可配置headless=False进行调试)
        browser = await p.chromium.launch(headless=True, slow_mo=100) # slow_mo可放慢操作,便于观察
        # 2. 创建浏览器上下文(可在此处配置代理、UA等全局设置)
        context = await browser.new_context(
            viewport={'width': 1280, 'height': 720},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...'
        )
        
        # 3. 初始化爬虫组件
        list_crawler = EbayListCrawler(context)
        detail_crawler = EbayDetailCrawler(context)
        scheduler = CrawlerScheduler(max_concurrent_tasks=MAX_CONCURRENT_DETAILS)
        scheduler.detail_crawler = detail_crawler # 注入依赖
        
        all_list_items = []
        
        # 4. 抓取所有关键词的列表页
        for keyword in KEYWORDS:
            logger.info(f"开始处理关键词: '{keyword}'")
            for page_num in range(1, MAX_LIST_PAGES + 1):
                items = await list_crawler.fetch_list_page(keyword, page_num)
                all_list_items.extend(items)
                logger.info(f"关键词 '{keyword}' 第 {page_num} 页找到 {len(items)} 个商品")
                # 列表页之间间隔稍长
                await asyncio.sleep(random.uniform(3.0, 6.0))
        
        logger.info(f"列表页抓取结束,共收集到 {len(all_list_items)} 个商品链接")
        
        # 5. 去重(根据item_id)
        unique_items = {}
        for item in all_list_items:
            unique_items[item['item_id']] = item
        unique_item_list = list(unique_items.values())
        logger.info(f"去重后剩余 {len(unique_item_list)} 个唯一商品")
        
        # 6. 并发抓取详情页
        await scheduler.schedule_detail_crawls(unique_item_list, OUTPUT_FILE)
        
        # 7. 清理资源
        await context.close()
        await browser.close()
    
    logger.info("所有抓取任务完成!")

if __name__ == "__main__":
    asyncio.run(main())

8. 常见问题、排查技巧与优化建议

在实际运行中,你肯定会遇到各种各样的问题。这里记录了我踩过的一些坑和解决方案。

8.1 元素找不到或超时

  • 问题 page.wait_for_selector 超时,或者 query_selector 返回 None
  • 排查
    1. 确认选择器 :用浏览器的开发者工具,在“元素”面板使用 Ctrl+F ,输入你的CSS选择器,看是否能唯一匹配到目标元素。eBay的页面结构可能因地区、A/B测试、登录状态不同而变化。
    2. 检查页面是否加载完全 :确保在尝试选择元素前,已经使用了足够的等待(如 wait_until='networkidle' )或等待了特定元素出现。
    3. 处理iframe :有些内容(如部分描述)可能嵌套在 <iframe> 里。你需要先定位到iframe,再切换到其内部文档进行查找: frame = await page.wait_for_selector('iframe.xxx'); content_frame = await frame.content_frame(); elem = await content_frame.query_selector(...)
    4. 处理动态内容 :有些内容需要交互(如点击“查看更多”)才会加载。在提取前模拟这些点击操作。
  • 优化 :使用更宽松的选择器(如属性包含 *= ),或组合多个选择器策略。增加超时时间,并做好异常处理,让程序能跳过当前失败的商品继续运行。

8.2 被屏蔽或弹出验证码

  • 问题 :页面返回验证码,或提示“访问受限”。
  • 排查
    1. 检查请求频率 :这是最常见的原因。立即大幅降低抓取速度,增加随机延迟的间隔。 MAX_CONCURRENT_DETAILS 不要设置过高(建议5-15之间)。
    2. 检查代理IP质量 :免费代理或低质量代理IP很可能已被eBay封禁。使用 ProxyManager.test_proxy 功能定期检测代理可用性。
    3. 检查浏览器指纹 :无头模式( headless=True )容易被检测。可以尝试使用 headless=False (会弹出可见浏览器窗口)或使用 playwright.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled']) 来隐藏自动化特征。Playwright本身已经做了很多反检测处理,但并非绝对。
  • 应对
    • 策略性暂停 :一旦检测到验证码,程序应记录下该任务,并暂停该IP或该会话一段时间(如半小时),然后换用新的上下文或代理重试。
    • 使用更真实的模拟 :在创建上下文时,设置 viewport user_agent locale timezone_id 等,并启用 ignore_https_errors=False (默认)以使用完整TLS栈。
    • 考虑CAPTCHA解决服务 :对于商业级项目,可能需要集成如2Captcha、Anti-Captcha等服务,但这会增加复杂性和成本。

8.3 内存占用过高或程序崩溃

  • 问题 :程序运行一段时间后内存持续增长,最终崩溃。
  • 原因 :这是异步爬虫,特别是使用无头浏览器时,最容易出现的问题。根本原因是资源未正确释放。
  • 解决
    1. 确保关闭Page :每个 fetch 函数中,必须在 finally 块或使用 async with 语句确保 await page.close() 被调用。
    2. 限制并发数 :通过 Semaphore 严格控制同时打开的页面数量。
    3. 定期重启BrowserContext :长时间运行后,浏览器上下文内可能会积累缓存和内存碎片。可以设定每抓取N个商品后,关闭当前 context 并创建一个新的。
    4. 监控资源 :使用 psutil 等库监控程序内存使用,达到阈值后主动清理或重启相关组件。

8.4 数据解析不准确或缺失

  • 问题 :价格、标题等字段抓取到的是空值或错误信息。
  • 排查
    1. 数据清洗逻辑 :检查 _clean_price 这类函数是否能处理所有价格格式(如 $1,234.56 $12.34 to $56.78 AU $12.34 )。
    2. 页面结构变体 :eBay有不同的商品展示模板(如拍卖、一口价、二手、新品)。你的选择器可能需要兼容多种变体。写更健壮的提取代码,例如先尝试选择器A,如果没找到再尝试选择器B。
    3. 编码问题 :确保保存文件时使用 utf-8 编码,以正确处理多语言字符。
  • 优化 :将解析逻辑模块化,每个字段的提取写成一个独立函数,便于测试和维护。定期运行测试用例,对比抓取结果和手动查看的结果是否一致。

8.5 性能优化建议

  1. 复用BrowserContext,而非为每个任务创建新Browser :创建Browser实例开销巨大,而创建Page相对较小。整个主流程应只启动一个Browser,在其下创建多个Context或Page。
  2. 使用 page.route 进行网络拦截 :如果你只需要文本数据,可以拦截图片、字体、CSS等不必要的请求,大幅提升页面加载速度和减少带宽消耗。
    async def route_handler(route):
        if route.request.resource_type in ["image", "stylesheet", "font"]:
            await route.abort()
        else:
            await route.continue_()
    await page.route("**/*", route_handler)
    
  3. 批量保存数据 :不要每抓取一条数据就写一次文件。可以先将数据缓存在内存列表中,积累到一定数量(如100条)后,再批量异步写入文件,减少I/O操作。
  4. 分布式抓取 :如果数据量极大,单机性能成为瓶颈,可以考虑使用消息队列(如Redis + RQ或Celery)将抓取任务分发到多台机器上执行,每台机器运行独立的Playwright实例。这需要更复杂的架构设计。

构建一个用于生产环境的爬虫,就像是一场与目标网站反爬系统的持续博弈。没有一劳永逸的方案,核心在于 理解原理、保持灵活、尊重规则 。Playwright+Asyncio提供了强大的武器,但如何使用它,取决于你对目标网站的理解和工程化的细致程度。从简单的脚本开始,逐步添加错误处理、代理、并发控制、监控告警等模块,你的爬虫才会越来越健壮。最后再次强调,务必遵守 robots.txt ,合理控制请求频率,你的爬虫才能走得长远。

源码下载地址: https://pan.quark.cn/s/a4b39357ea24 Altium Designer 18是一款功能丰富的电子设计自动化工具,其集成了电路原理图绘制、PCB布局规划、三维视图展示、电路仿真分析以及生产文件生成等多项核心功能,为电子工程师们提供了一个全面的硬件设计解决方案。该软件的官方中文指导材料系统地阐述了从项目初始阶段到最终完成的全过程操作方法。 在“AD18 官方中文指导材料”中,使用者可以掌握以下核心内容要点: 1. **系统配置界面认知**:熟悉Altium Designer 18的工作平台,涵盖菜单选项、工具栏配置、工作区域安排,以及个性化工作界面的设定方法。 2. **项目创建流程**:掌握如何建立新的项目工程,包括工程参数配置、项目模板选取,以及项目文件的添加操作。 3. **电路图绘制技术**:学习电路原理图的绘制流程,包括元件库的维护管理、元件放置技巧、连接线路绘制、属性编辑操作,以及网络表的自动生成方法。 4. **元件库构建管理**:了解如何建立自定义元件库,执行元件的导入导出任务,以及利用Altium Designer自带的元件资源库。 5. **PCB布局设计方法**:掌握PCB设计的基本准则,如元件布局策略、布线技巧、层叠结构调整、布线规则优化,以及冲突检测短路处理技术。 6. **设计规范约束条件**:理解设定设计规范和电气约束条件的关键性,包括间距参数设定、焊盘尺寸规格、过孔尺寸要求等,以及运用规则检查进行设计验证的流程。 7. **三维模型整合应用**:学习如何将三维模型PCB设计内容相结合,以实现更为直观的机械配合验证。 8. **电路性能仿真技术**:掌握使用Altium Des...
源码链接: https://pan.quark.cn/s/a4b39357ea24 ### 接口测试知识要点说明 #### 一、接口测试的定义及分类 1. **概念阐释**: - 接口测试作为软件测试的关键环节,主要对系统之间的交互点进行验证。 - 它旨在核实接口的正确性、稳定性和功能性,保障各个系统组件能够依照预期执行交互操作。 2. **实施情境**: - 适用于多系统联合开发的环境。 - 适用于包含多个子系统的复杂应用系统开发过程。 3. **适用范围**: - 为其他系统提供支持的底层基础架构系统。 - 负责协调中心服务的系统架构。 4. **分类标准**: - **模块接口测试**:通常作为单元测试的一部分,适用于独立构建的功能模块。 - **Web接口测试**: - **服务端接口测试**:针对客户端服务器端之间的接口进行验证。 - **外部接口测试**:对第三方提供的接口进行测试,例如支付平台提供的授权登录接口。 5. **测试角度**: - **接口功能验证**:核实接口功能是否满足预期要求。 - **接口性能评估**:衡量接口的处理能力及响应时间。 - **接口稳定性考察**:检测接口在长时间运行中的表现情况。 - **接口安全检测**:确保接口能够抵御非法访问或数据篡改。 6. **测试手法**: - **参数细致测试**:深入测试接口的输入参数和输出结果。 - **场景模拟测试**:依据实际业务场景进行测试验证。 #### 二、接口测试的详细流程 1. **测试规划**: - 明确测试范围、目标及所需资源。 - 规划测试策略,选择适宜的工具和技术手段。 2. **测试用例设计**: - 依据需求文档和接口规格文档设计测试用例。 - 覆...
内容概要:本文研究基于遗传算法(GA)粒子群算法(PSO)相结合的无人机三维路径规划方法,旨在解决无人机在复杂三维环境中避障路径优化的关键问题。通过构建包含障碍物、威胁区域等约束的三维仿真环境,系统实现了GAPSO两种智能优化算法的Matlab代码,并对其在路径规划中的性能进行全面对比分析,重点关注路径长度、飞行安全性、算法收敛速度等核心指标。研究充分利用GA的全局搜索能力PSO的快速局部收敛特性,提出一种混合优化策略,有效克服单一算法易陷入局部最优或收敛缓慢的缺陷。文中不仅详细阐述了算法的设计流程、数学模型实现细节,还提供了完整的仿真结果,验证了所提混合方法在复杂动态场景下的优越性、有效性和鲁棒性,为无人机自主导航提供了可靠的理论依据和技术方案。; 适合人群:具备一定Matlab编程基础和优化算法理论知识,从事无人机系统设计、智能控制、路径规划、人工智能应用等相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于城市峡谷、山区、灾害救援等复杂三维环境中的无人机自主避障最优任务路径规划;②为智能优化算法(GA、PSO及其混合策略)的学习、性能对比工程化应用提供详实的实践案例和代码参考;③服务于高校科研教学、算法原型快速复现、以及在此基础上的进一步改进创新研究。; 阅读建议:建议读者结合所提供的Matlab代码进行仿真运行调试,深入理解两种算法的参数设置、适应度函数设计及路径生成机制,可通过修改环境地图、增加动态障碍物或调整优化目标函数等方式进行扩展性研究,以深化对算法本质和应用场景的理解。
内容概要:LTK8870S是一款单通道H桥有刷直流电机驱动器,支持6.5V至20V工作电压,具备3.0A峰值电流驱动能力,适用于打印机、家用电器及工业设备等机电一体化应用。该芯片采用PWM控制接口,可通过IN1和IN2逻辑输入实现电机正转、反转、滑行制动等多种工作模式,并支持快衰减和慢衰减两种电流衰减方式以优化电机控制性能。器件集成电流调节功能,通过VREF引脚和外部检测电阻设定限流值,有效控制电机电流并降低系统功耗。同时具备低功耗休眠模式,在IN1和IN2均为低电平时自动进入,显著节省能耗。LTK8870S还集成了多重保护机制,包括VM欠压锁定(UVLO)、过温保护(TSD)、过流保护(OCP)及自动故障恢复功能,提升系统可靠性。其ESOP-8封装带裸露焊盘,利于散热,符合无铅环保标准。; 适合人群:电子硬件工程师、电机控制系统开发者、嵌入式系统设计人员,尤其适用于从事电机驱动电路设计的中级技术人员。; 使用场景及目标:①用于控制中小型有刷直流电机的正反转调速;②应用于需要电流限制和节能休眠功能的电池供电设备;③作为工业自动化、智能家电或办公设备中的核心驱动模块;④帮助开发者理解H桥驱动、PWM调速、电流衰减模式及保护电路的设计原理。; 阅读建议:此资源技术细节丰富,建议结合典型应用电路图电气参数表进行硬件设计参考,重点关注PWM控制逻辑、电流调节设置及保护机制的实现方式,并在实际应用中配合示波器调试输出波形电流响应。
内容概要:本文提出了一种基于纳什谈判理论的风–光–氢多主体能源系统合作运行方法,旨在通过合作博弈框架协调风电、光伏氢能系统之间的运行决策,实现多能源主体在满足个体理性前提下的整体效益最优。研究构建了考虑可再生能源出力随机性氢能系统动态响应特性的多主体协同优化模型,采用纳什谈判机制设计利益分配规则,有效解决了各方在联合运行中的竞争协作矛盾。通过Matlab平台完成了模型编程求解仿真验证,结果表明该方法能够显著提升系统经济性、增强清洁能源消纳能力,并促进多主体间的合作意愿,为高比例可再生能源系统的市场化协同运行提供了理论依据技术路径。; 适合人群:具备电力系统分析、优化建模博弈论基础,从事新能源系统调度、综合能源系统规划运行、能源市场机制设计等方向的研究生、科研人员及工程技术人员。; 使用场景及目标:①研究风–光–氢多能互补系统的协同优化机制利益分配策略;②构建基于合作博弈的综合能源系统运行模型并进行仿真分析;③支撑硕士、博士论文复现及高水平学术论文撰写投稿。; 阅读建议:建议结合Matlab代码深入理解纳什谈判模型的数学建模过程,重点掌握目标函数构造、约束条件处理及均衡求解算法的实现细节,同时可尝试将其拓展至Shapley值等其他合作博弈方法的对比研究,以深化对多主体协同机制的理解。
源码下载地址: https://pan.quark.cn/s/2a998390025c 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源,包含带管理端功能的报名预约系统源代码。 微信小程序开发资源...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值