
Playwright 抓取网页时,响应可能为文本(HTML/JSON)或二进制(图片、字体、JS/CSS),直接调用 response.text() 并以 UTF-8 写入会导致 UnicodeDecodeError;应先判断响应类型,再选择 response.text() 或 response.body(),并动态命名文件避免覆盖。
playwright 抓取网页时,响应可能为文本(html/json)或二进制(图片、字体、js/css),直接调用 `response.text()` 并以 utf-8 写入会导致 `unicodedecodeerror`;应先判断响应类型,再选择 `response.text()` 或 `response.body()`,并动态命名文件避免覆盖。
在使用 Playwright 进行 Web 抓取时,一个常见误区是假设所有 HTTP 响应都是可读的 UTF-8 文本。实际上,现代网页包含大量非文本资源:PNG/JPEG 图片(以 0x89 0x50 0x4E 0x47 开头)、字体文件、压缩脚本、SVG、甚至二进制 WebAssembly 模块。当代码无差别调用 response.text() 并尝试用 "w" 模式写入文件时,Playwright 会强制解码二进制内容为字符串——一旦遇到 0x89(PNG 魔数首字节),就会触发 UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89。
✅ 正确做法是:区分响应类型,按需处理。核心策略如下:
-
优先检查响应状态与内容类型
使用response.ok确保请求成功,并通过response.headers.get("content-type", "")获取 MIME 类型(如"text/html","application/json","image/png"); -
区分写入模式
- 文本类(
text/,application/json,application/javascript等)→ 使用response.text()+"w"模式 +utf-8编码; - 二进制类(
image/,font/,application/octet-stream等)→ 使用response.body()+"wb"模式(二进制写入);
- 文本类(
-
避免文件覆盖
不要硬编码"copy.txt",而应基于 URL 路径、资源类型或哈希生成唯一文件名; -
善用
resource_type辅助判断response.request.resource_type(如"document","script","image","fetch")比 headers 更稳定,尤其在服务端未正确设置Content-Type时。
以下是优化后的健壮示例代码:
import os
import re
from playwright.sync_api import sync_playwright
URL = "https://www.booking.com/hotel/it/hotelnordroma.en-gb.html?aid=304142&checkin=2025-05-15&checkout=2025-05-16#map_opened-map_trigger_header_pin"
OUTPUT_DIR = "scraped_responses"
def sanitize_filename(url: str) -> str:
"""从 URL 提取安全文件名,保留扩展名,长度限制 + 防止路径遍历"""
name = re.sub(r"[^a-zA-Z0-9._-]", "_", url.split("/")[-1])
name = name[:100] or "unknown"
if not os.path.splitext(name)[1]:
name += ".bin"
return name
def handle_response(response):
# 跳过失败响应
if not response.ok:
return
content_type = response.headers.get("content-type", "").lower()
resource_type = response.request.resource_type
file_name = sanitize_filename(response.url)
file_path = os.path.join(OUTPUT_DIR, file_name)
# 创建输出目录
os.makedirs(OUTPUT_DIR, exist_ok=True)
try:
if resource_type == "document" or "text/" in content_type or "json" in content_type or "javascript" in content_type:
# 文本类响应:HTML、JSON、JS、CSS 等
text_content = response.text()
with open(file_path, "w", encoding="utf-8") as f:
f.write(text_content)
print(f"[TEXT] Saved {file_name} ({len(text_content)} chars)")
else:
# 二进制类响应:图片、字体、二进制资源等
binary_content = response.body()
with open(file_path, "wb") as f:
f.write(binary_content)
print(f"[BINARY] Saved {file_name} ({len(binary_content)} bytes)")
except Exception as e:
print(f"[ERROR] Failed to save {file_name}: {e}")
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.on("response", handle_response)
page.goto(URL, wait_until="networkidle")
browser.close()
if __name__ == "__main__":
main()
? 关键注意事项:
- ⚠️
response.text()内部已做字符编码自动推断(依据Content-Type的charset或 BOM),但仅适用于文本响应;对二进制调用必报错。 - ⚠️
response.body()返回bytes,永远不要用"w"模式写入——否则会触发TypeError: a bytes-like object is required, not 'str'。 - ⚠️
wait_for_timeout(1000)不可靠,推荐wait_until="networkidle"确保资源加载完成。 - ? 若你实际只需提取某类数据(如页面 JSON API 响应),建议结合
page.route()或过滤response.url正则匹配,而非捕获全部响应——既提升性能,也避免海量无用文件。
总结:Web 抓取不是“一招鲜”,而是对网络协议细节的尊重。掌握 response.body() / response.text() 的适用边界、Content-Type 与 resource_type 的语义差异,是写出健壮爬虫的第一道门槛。











