用 requests 做数据抓取,遇到 JS 渲染的页面直接抓瞎。用 Selenium 吧,启动慢、内存占用高,打包成 EXE 后体积爆炸。
Playwright 算是目前比较均衡的选择:启动快、API 现代、自动等待省心。但当你把它和 PyInstaller 打包到一起时,坑才开始出现。
这篇文章记录一个完整的流程:从 Playwright 抓取脚本,到打包成双击运行的 EXE,中间踩过的坑和解决办法。
一、为什么选 Playwright 而不是 requests + BeautifulSoup?
requests + BeautifulSoup 的方案在静态页面时代没问题。但现在大部分网站的数据都是 Ajax 加载,页面源码里根本看不到表格内容。加上登录态、滑块验证这些反爬机制,纯 HTTP 请求方案基本出局。
Playwright 的优势:
- 真正的浏览器内核,能执行 JavaScript,自动等待元素加载
- 内置的自动重试和智能等待,不用手动写
time.sleep() - 支持多浏览器(Chromium、Firefox、WebKit),测试兼容性方便
- 录制功能可以自动生成代码,快速上手
代价也很明显:打包后的体积会大很多。后面会详细说。
二、核心抓取逻辑:一个完整的示例
先上代码,再拆解思路。
python
# spider.py
import asyncio
import csv
import os
from datetime import datetime
from playwright.async_api import async_playwright
class OrderSpider:
def __init__(self, username, password):
self.username = username
self.password = password
self.results = []
self.output_dir = os.path.join(os.path.dirname(__file__), "output")
os.makedirs(self.output_dir, exist_ok=True)
async def run(self):
async with async_playwright() as p:
# 启动浏览器,headless=True 表示无界面模式
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
page = await context.new_page()
try:
await self._login(page)
await self._fetch_orders(page)
self._save_to_csv()
print(f"抓取完成,共 {len(self.results)} 条数据")
finally:
await browser.close()
async def _login(self, page):
"""模拟登录流程"""
await page.goto("https://example.com/login")
# 等待页面加载完成
await page.wait_for_selector('input[name="username"]')
await page.fill('input[name="username"]', self.username)
await page.fill('input[name="password"]', self.password)
# 点击登录按钮
await page.click('button[type="submit"]')
# 等待登录后的页面元素出现,确认登录成功
await page.wait_for_selector(".dashboard-header", timeout=10000)
print("登录成功")
async def _fetch_orders(self, page):
"""翻页抓取订单数据"""
page_num = 1
while True:
print(f"正在抓取第 {page_num} 页...")
# 等待表格加载
await page.wait_for_selector("table.order-list tbody tr")
rows = await page.query_selector_all("table.order-list tbody tr")
for row in rows:
cells = await row.query_selector_all("td")
if len(cells) >= 5:
order_data = {
"order_id": await cells[0].inner_text(),
"buyer": await cells[1].inner_text(),
"amount": await cells[2].inner_text(),
"status": await cells[3].inner_text(),
"create_time": await cells[4].inner_text(),
"fetch_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
self.results.append(order_data)
# 检查是否有下一页
next_btn = await page.query_selector("a.next-page")
if not next_btn:
break
is_disabled = await next_btn.get_attribute("class")
if "disabled" in (is_disabled or ""):
break
await next_btn.click()
# 等待页面刷新
await page.wait_for_load_state("networkidle")
page_num += 1
def _save_to_csv(self):
"""结果保存为 CSV"""
if not self.results:
print("没有数据需要保存")
return
filename = os.path.join(
self.output_dir,
f"orders_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=[
"order_id", "buyer", "amount", "status",
"create_time", "fetch_time"
])
writer.writeheader()
writer.writerows(self.results)
print(f"数据已保存至: {filename}")
if __name__ == "__main__":
spider = OrderSpider(username="your_username", password="your_password")
asyncio.run(spider.run())
几个关键设计点
1. 异步写法
Playwright 支持同步和异步两种 API。批量抓取时,异步能显著提升效率。上面用的是 async_playwright,配合 asyncio.run() 执行。
2. 智能等待
wait_for_selector 和 wait_for_load_state 是核心。不要用 time.sleep() 硬等,既慢又不稳定。Playwright 会自动轮询直到元素出现或超时。
3. 异常处理
实际生产环境中,网络波动、页面结构变化、反爬机制都会导致失败。建议加上:
- 单页重试机制(失败自动重试 3 次)
- 异常数据跳过,不中断整体流程
- 抓取日志记录,方便排查问题
4. 数据存储
小批量用 CSV 足够。如果数据量大或需要后续分析,可以接 SQLite 或 MySQL。这里用 utf-8-sig 编码是为了让 Excel 打开中文不乱码。
三、打包成 EXE:最大的坑在这里
写脚本容易,打包才是噩梦的开始。
3.1 基础打包命令
r
pip install pyinstaller
pyinstaller -F spider.py
-F 表示打包成单个文件。执行完会在 dist 目录生成 spider.exe。
但这时候直接运行,大概率会报错:
rust
Executable doesn't exist at C:\Users\xxx\AppData\Local\ms-playwright\chromium-xxx\chrome-win\chrome.exe
3.2 问题根源:浏览器没打包进去
Playwright 默认把浏览器安装在用户目录的 .local-browsers 文件夹里,PyInstaller 根本不知道这些文件的存在,自然不会打包。
解决方案:把浏览器"焊死"在程序里。
步骤如下:
csharp
# 1. 设置环境变量,让浏览器安装到 Python 包目录下
set PLAYWRIGHT_BROWSERS_PATH=0
# 2. 安装需要的浏览器(这里只装 Chromium,减小体积)
playwright install chromium
# 3. 打包,带上 playwright 的 driver 目录
pyinstaller -F spider.py --add-data "venv\Lib\site-packages\playwright\driver;playwright/driver"
3.3 体积优化
按上面的方式打包,生成的 exe 大约 300-400MB。如果目标用户带宽有限,可以尝试:
- 只打包用到的浏览器 :上面已经用了
playwright install chromium,如果不需要 Firefox 和 WebKit,就别装 - 用
--onedir代替--onefile:虽然分发麻烦点(一个文件夹),但启动速度更快,体积也能小一些 - UPX 压缩 :
pyinstaller支持--upx-dir参数,用 UPX 压缩可执行文件
csharp
pyinstaller -D spider.py --upx-dir "C:\upx" --add-data "venv\Lib\site-packages\playwright\driver;playwright/driver"
3.4 路径处理
打包后,程序内部的路径会变。建议加一个工具函数统一处理:
python
# utils.py
import sys
import os
def get_resource_path(relative_path):
"""获取资源文件的绝对路径,兼容开发环境和打包后的环境"""
if hasattr(sys, '_MEIPASS'):
# PyInstaller 打包后的临时目录
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
所有读取文件的地方都用这个函数,避免打包后找不到资源。
3.5 反病毒软件误报
PyInstaller 打包的程序经常被 Windows Defender 误报为病毒。这是因为它的自解压执行模式跟某些恶意软件类似。
缓解方案:
- 用
--onedir模式,误报率比--onefile低 - 给 exe 做代码签名(需要购买证书,个人开发者成本较高)
- 引导用户手动添加到白名单
四、进阶:做成可配置的工具
上面的代码硬编码了用户名密码,实际交付时肯定不行。可以改成从配置文件读取:
lua
# config.py
import json
import os
def load_config():
config_path = os.path.join(os.path.dirname(__file__), "config.json")
if not os.path.exists(config_path):
return {}
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
# config.json
{
"username": "",
"password": "",
"headless": true,
"max_pages": 10,
"output_format": "csv"
}
打包时把 config.json 一起带上,用户只需要改配置文件就能用,不用碰代码。
五、另一种思路:低代码方案
如果你不想折腾代码、打包、环境配置这一套,或者需要给非技术人员使用,可以考虑低代码的自动化方案。
市面上有一些工具能把抓取流程可视化编排,直接导出成可执行程序,不用写一行代码。比如蓝印RPA支持 API 触发、定时执行、自定义界面、打包成 EXE 分发,还能设置授权和加密分享。数据全部保存在本地,不上传云端,适合对隐私有要求的场景。
这类工具的优势是:
- 不用写代码,拖拽式编排流程
- 一键打包成独立程序,对方电脑不用装任何环境
- 支持定时任务和 API 调用,方便集成到现有系统
- 个人使用没有时长限制,中小企业也能低成本上手
如果你只是偶尔有抓取需求,或者需要快速交付一个工具给别人用,这种方案比从零写代码省很多时间。
六、总结
| 环节 | 关键点 |
|---|---|
| 技术选型 | Playwright 适合 JS 渲染页面,requests 适合静态页面 |
| 抓取逻辑 | 异步 + 智能等待 + 异常处理 |
| 打包 | 设置 PLAYWRIGHT_BROWSERS_PATH=0,用 --add-data 带上浏览器 |
| 体积 | 只装需要的浏览器,考虑 --onedir 或 UPX 压缩 |
| 交付 | 配置文件化,降低使用门槛 |
整个过程最耗时的不是写抓取逻辑,而是打包和测试兼容性。建议每改一步就打包测试一次,别等到最后才发现问题。