本文档介绍如何开发一个针对政务网站新闻栏目的自动化爬虫工具。该工具基于 Playwright 框架,能够模拟浏览器行为,抓取指定栏目的新闻列表和详情内容,并导出为结构化数据文件。
🎯 核心技术方案
技术选型
| 技术组件 | 选择理由 |
|---|---|
| Playwright | 支持动态页面渲染,可模拟真实浏览器行为,绕过简单的反爬机制 |
| Python | 开发效率高,生态丰富,易于打包分发 |
| 正则表达式 | 轻量级解析,无需额外依赖 |
| PyInstaller | 打包为独立可执行文件,方便非技术用户使用 |
核心设计思路
- 浏览器模拟:使用无头浏览器模拟真实用户访问,避免被识别为爬虫
- 路径自适应:支持开发环境和打包后环境的路径自动切换
- 模块化设计:将功能拆分为独立模块,便于维护和扩展
- 容错机制:请求失败自动跳过,避免单点故障影响整体
🏗️ 架构设计
text
┌─────────────────────────────────────┐
│ 主控制流程 │
├─────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 浏览器管理器 │ │ 页面请求模块 │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 列表页解析器 │ │ 详情页解析器 │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 数据存储模块 │ │ 分页处理器 │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────┘
💻 关键技术实现
1. 打包环境路径处理
问题:打包后的程序需要正确找到 Playwright 浏览器驱动
解决方案:
python
if getattr(sys, 'frozen', False):
# 打包后的运行环境
base_path = sys._MEIPASS
else:
# 开发环境
base_path = os.path.dirname(os.path.abspath(__file__))
# 设置浏览器路径环境变量
os.environ['PLAYWRIGHT_BROWSERS_PATH'] = os.path.join(base_path, "ms-playwright")
原理:sys.frozen 在 PyInstaller 打包后为 True,sys._MEIPASS 指向临时解压目录
2. 浏览器反检测
关键配置:
python
self.browser = self.p.chromium.launch(
headless=True,
args=['--disable-blink-features=AutomationControlled'] # 禁用自动化特征
)
self.context = self.browser.new_context(
user_agent='Mozilla/5.0', # 模拟真实浏览器UA
viewport={'width': 1920, 'height': 1080} # 设置窗口大小
)
3. 分页URL构造模式
通用方案:分析网站分页参数规律
python
def build_page_url(base_url, page, page_size):
"""
构造分页URL
常见参数名:page, p, pageNo, pageNum
常见参数名:pageSize, limit, rows
"""
return f"{base_url}?page={page}&pageSize={page_size}"
4. 列表页解析策略
使用正则表达式提取链接:
python
def parse_list(html):
# 匹配a标签中的href和文本内容
pattern = r'<a\s+href="([^"]+)"[^>]*>([^<]+)</a>'
matches = re.findall(pattern, html)
# 去重处理
seen = set()
results = []
for href, title in matches:
if href not in seen:
seen.add(href)
results.append({
'title': title.strip(),
'url': normalize_url(href) # 相对路径转绝对路径
})
return results
注意:政务网站常使用 .shtml 扩展名,可作为特征匹配
5. 详情页信息提取
python
def parse_detail(html):
data = {}
# 标题提取(常见标签:h1, .title, .news-title)
title = re.search(r'<h1[^>]*>([\s\S]*?)</h1>', html)
data['title'] = title.group(1).strip() if title else ""
# 时间提取(正则匹配日期格式)
time_match = re.search(r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})', html)
data['time'] = time_match.group(1) if time_match else ""
# 来源提取(常见标识:来源、责任编辑、信息来源)
source_match = re.search(r'来源[::]\s*([^<>\n]+)', html)
data['source'] = source_match.group(1).strip() if source_match else ""
# 内容提取(定位到正文容器)
content_match = re.search(r'<div\s+class="[^"]*content[^"]*">(.*?)</div>', html, re.DOTALL)
data['content'] = content_match.group(1).strip() if content_match else ""
return data
6. 数据存储设计
python
def save_csv(data, filename):
"""使用CSV格式存储,UTF-8-BOM编码支持中文"""
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
# 写入表头
writer.writerow(['标题', '发布时间', '来源', '链接', '正文内容'])
# 写入数据
for item in data:
writer.writerow([
item.get('title'),
item.get('time'),
item.get('source'),
item.get('url'),
item.get('content')
])
📦 打包部署方案
PyInstaller 打包命令
bash
pyinstaller --onefile \
--name "新闻爬虫工具" \
--icon "icon.ico" \
--add-data "浏览器驱动路径;ms-playwright" \
--hidden-import "playwright" \
--hidden-import "playwright.sync_api" \
main.py
路径配置说明
--add-data:将 Playwright 浏览器驱动打包进程序- 源路径:
%LOCALAPPDATA%\ms-playwright(Windows) - 目标路径:
ms-playwright(程序内引用路径)
打包后文件结构
text
dist/
└── 新闻爬虫工具.exe # 可执行文件
用户无需安装 Python 环境即可运行
🔧 常见问题解决方案
1. 浏览器驱动自动安装
python
def auto_install_browser():
"""首次运行时自动安装浏览器驱动"""
try:
# 尝试启动浏览器,失败则安装
with sync_playwright() as p:
p.chromium.launch(headless=True)
except:
# 调用Playwright内置安装命令
subprocess.run(["playwright", "install", "chromium"])
2. 请求频率控制
python
import time
import random
# 随机延迟,模拟人类行为
time.sleep(random.uniform(1, 3))
3. 异常重试机制
python
def fetch_with_retry(page, url, max_retries=3):
for i in range(max_retries):
try:
page.goto(url, timeout=30000)
return page.content()
except Exception as e:
if i == max_retries - 1:
raise
time.sleep(2 ** i) # 指数退避
📊 数据清洗建议
常见数据问题处理
python
def clean_text(text):
"""清洗文本中的特殊字符"""
if not text:
return ""
# 去除空白字符
text = re.sub(r'\s+', ' ', text)
# 去除HTML标签
text = re.sub(r'<[^>]+>', '', text)
# 去除控制字符
text = re.sub(r'[\x00-\x1f\x7f]', '', text)
return text.strip()