一个重复代码的案例
2026年4月,我检查jianfei-wechat v3的代码,发现一个让人头疼的问题:
微信模块有一套网页审查逻辑,小红书模块又有一套,微博模块还有一套。
功能都一样:打开浏览器,访问某个URL,截图,提取页面文本内容,检查是否有错误提示。但三个模块的实现完全不同:
- 微信模块用的是Playwright + Chrome,截图保存为PNG
- 小红书模块用的是Selenium + Firefox,截图保存为JPEG
- 微博模块用的是requests + BeautifulSoup,不截图,只提取HTML
结果就是:同样的"访问URL并验证"功能,三份代码,三个依赖,三种截图格式。
更麻烦的是维护。Playwright升级了,微信模块要改;Selenium出bug了,小红书模块要改;BeautifulSoup解析规则失效了(微博改版),微博模块要改。三个地方改同一件事。
这就是没有"全局化"的代价:重复实现,重复维护,重复踩坑。
什么是网页审查的全局化
网页审查的全局化,指的是把"访问URL → 验证内容 → 截图存档"这个流程,从各个平台模块中抽离出来,变成统一的基础设施。
全局化后,架构变成:
Before:
微信模块 → Playwright → 微信后台
小红书模块 → Selenium → 小红书后台
微博模块 → requests → 微博后台
After:
所有模块 → 全局网页审查服务 → 各平台后台
全局网页审查服务提供统一的API:
def review_page(url: str, check_rules: list) -> dict:
"""
访问URL,执行检查规则,返回结果。
Args:
url: 要审查的URL
check_rules: 检查规则列表,比如 [
{"type": "no_text", "text": "错误"}, # 页面不能包含"错误"二字
{"type": "has_element", "selector": "#success"}, # 页面必须包含#success元素
{"type": "screenshot"}, # 截图存档
]
Returns:
{
"status": "pass", # 或 "fail"
"screenshot_path": "/path/to/screenshot.png",
"page_text": "页面完整文本",
"errors": [] # 违反的规则列表
}
"""
各平台模块不再直接操作浏览器,而是调用 review_page(),传入平台特定的检查规则。
为什么要全局化
原因1:浏览器环境统一管理
不同平台对浏览器环境的要求不同:
微信:需要登录态,Cookie从微信开发者工具导出,导入到Playwright。 小红书:需要登录态,但Cookie有效期短(约2小时),需要定期重新登录。 微博:不需要登录也能查看大部分内容,但查看完整文章需要登录。
如果每个平台模块自己管理浏览器环境,会出现以下问题:
- Cookie管理混乱:微信的Cookie存在
wechat/cookies.json,小红书的存在xiaohongshu/cookies.pkl,格式不统一,无法共用。 - 浏览器实例重复:同时跑微信审查和小红书审查,会启动两个浏览器实例,占用双倍内存。
- 登录态失效处理不一致:微信Cookie失效后,微信模块会打印错误日志;小红书Cookie失效后,小红书模块会尝试自动重新登录。两套逻辑,两种用户体验。
全局化后,浏览器环境统一由"浏览器池"管理:
class BrowserPool:
def __init__(self):
self.browsers = {} # {platform: browser_instance}
self.cookies = {} # {platform: cookie_dict}
def get_browser(self, platform: str):
"""获取指定平台的浏览器实例,如果不存在则创建。"""
if platform not in self.browsers:
self.browsers[platform] = self._launch_browser(platform)
return self.browsers[platform]
def refresh_cookie(self, platform: str):
"""刷新指定平台的Cookie。"""
# 统一刷新逻辑
...
所有平台共用一个 BrowserPool,Cookie管理、浏览器实例管理、登录态刷新都在这一个地方处理。
原因2:截图格式统一
不同平台的截图需求不同:
- 微信:需要完整页面截图(从顶部滚到底部),因为微信后台的文章列表是长页面
- 小红书:只需要视口截图(当前可见区域),因为小红书的笔记详情页是单屏展示
- 微博:需要截图,但也要提取页面文本内容,因为微博的错误提示可能在文本里不在图片里
如果各平台自己截图,截图格式、命名规则、存储路径都不统一:
wechat/screenshots/20260613_142356_wechat_draft_list.png
xiaohongshu/imgs/notes/20260613-2B.png
weibo/screen/13-06-2026-1423.png
全局化后,截图格式统一:
screenshots/
2026-06-13/
wechat_draft_list.png
xiaohongshu_note_detail.png
weibo_article_view.png
命名规则统一:{date}/{platform}_{page_type}.png
存储路径统一:screenshots/{date}/
截图尺寸统一:默认1280x720,如果平台有特殊需求(比如小红书需要竖版截图),在检查规则里指定。
原因3:检查规则可复用
不同平台的检查规则,有很多是通用的。比如:
通用规则:
- 页面不能包含"错误"、"失败"、"异常"等文字
- 页面不能包含"登录已过期"、"请重新登录"等文字(说明Cookie失效)
- 页面加载时间不能超过10秒
平台特定规则:
- 微信:页面必须包含"草稿箱"三个字(确认进入了草稿箱页面)
- 小红书:页面必须包含"发布成功"四个字
- 微博:页面URL必须包含"/article/"(确认进入了文章详情页)
如果各平台自己实现检查逻辑,通用规则要写三遍。全局化后,通用规则写成函数,各平台通过配置引用:
# 全局检查规则库
GLOBAL_CHECK_RULES = {
"no_error_text": {
"type": "no_text",
"texts": ["错误", "失败", "异常"],
"description": "页面不能包含错误信息"
},
"cookie_valid": {
"type": "no_text",
"texts": ["登录已过期", "请重新登录"],
"description": "Cookie必须有效"
},
"load_time": {
"type": "max_load_time",
"max_seconds": 10,
"description": "页面加载不能超过10秒"
}
}
# 微信特定规则
WECHAT_CHECK_RULES = [
GLOBAL_CHECK_RULES["no_error_text"],
GLOBAL_CHECK_RULES["cookie_valid"],
{"type": "has_text", "text": "草稿箱", "description": "必须进入草稿箱"}
]
# 小红书特定规则
XIAOHONGSHU_CHECK_RULES = [
GLOBAL_CHECK_RULES["no_error_text"],
{"type": "has_text", "text": "发布成功", "description": "必须显示发布成功"}
]
原因4:失败重试策略统一
网页审查可能因为各种原因失败:网络超时、浏览器崩溃、页面结构变化、Cookie失效...
如果各平台自己实现重试,重试策略不统一:
- 微信模块:失败后立即重试,最多重试3次
- 小红书模块:失败后等待5秒再重试,最多重试5次
- 微博模块:失败不重试,直接报人工处理
全局化后,重试策略统一配置:
RETRY_POLICY = {
"max_retries": 3,
"retry_delay": 5, # 秒
"exponential_backoff": True, # 每次重试延迟翻倍
"retry_on": ["timeout", "browser_crash", "network_error"],
"no_retry_on": ["cookie_invalid", "page_structure_changed"] # 这些错误重试也没用
}
所有平台共用同一份重试策略,行为可预期。
全局化的实现方案
jianfei-wechat v4中,网页审查全局化的实现位于 v4/content_center/web_reviewer.py(规划中,当前v4还没完全实现全局化,部分模块还在用各自的逻辑)。
规划中的架构:
核心类:WebReviewer
class WebReviewer:
def __init__(self, browser_pool: BrowserPool):
self.browser_pool = browser_pool
self.screenshot_dir = Path("screenshots")
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
def review(self, url: str, platform: str, rules: list) -> dict:
"""执行网页审查。"""
browser = self.browser_pool.get_browser(platform)
page = browser.new_page()
try:
# 1. 访问URL
page.goto(url, timeout=10000)
# 2. 执行检查规则
errors = []
for rule in rules:
result = self._check_rule(page, rule)
if not result["pass"]:
errors.append(result["error"])
# 3. 截图
screenshot_path = self._take_screenshot(page, platform)
# 4. 返回结果
return {
"status": "pass" if not errors else "fail",
"screenshot_path": str(screenshot_path),
"page_text": page.inner_text("body"),
"errors": errors
}
finally:
page.close()
def _check_rule(self, page, rule: dict) -> dict:
"""检查单条规则。"""
if rule["type"] == "no_text":
for text in rule["texts"]:
if text in page.inner_text("body"):
return {"pass": False, "error": f"页面包含禁用文字:{text}"}
elif rule["type"] == "has_text":
if rule["text"] not in page.inner_text("body"):
return {"pass": False, "error": f"页面缺少文字:{rule['text']}"}
elif rule["type"] == "has_element":
if not page.query_selector(rule["selector"]):
return {"pass": False, "error": f"页面缺少元素:{rule['selector']}"}
return {"pass": True, "error": ""}
def _take_screenshot(self, page, platform: str) -> Path:
"""截图并存档。"""
date_str = datetime.now().strftime("%Y-%m-%d")
date_dir = self.screenshot_dir / date_str
date_dir.mkdir(exist_ok=True)
screenshot_path = date_dir / f"{platform}_{int(time.time())}.png"
page.screenshot(path=str(screenshot_path), full_page=True)
return screenshot_path
各平台模块的调用方式
全局化后,各平台模块不再直接操作浏览器,而是调用 WebReviewer:
微信模块(改造前):
# v3代码(不要这样做)
def check_wechat_draft(url):
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(url)
page.screenshot(path="wechat_draft.png")
browser.close()
微信模块(改造后):
# v4代码(正确做法)
def check_wechat_draft(url, web_reviewer: WebReviewer):
result = web_reviewer.review(
url=url,
platform="wechat",
rules=WECHAT_CHECK_RULES
)
if result["status"] == "fail":
log_error(f"微信草稿页面检查失败:{result['errors']}")
return False
# 截图路径存到文章记录里,方便后续查看
article.wechat_screenshot = result["screenshot_path"]
return True
全局化的挑战
挑战1:平台差异难以完全抽象
虽然大部分网页审查逻辑是通用的,但每个平台总有一些特殊需求:
- 小红书需要识别"图片仍在审核中"的状态,这需要通过图片元素的CSS类判断,不是简单的文字检查
- 微博需要处理弹窗(比如"是否继续查看"的确认框),自动点击"继续"
- 微信需要处理iframe(草稿箱在iframe里),切换到iframe才能操作
如果把这些特殊逻辑都塞进 WebReviewer,这个类会变得极其臃肿,违反单一职责原则。
解决方法:WebReviewer 只提供通用能力(访问URL、检查规则、截图),特殊逻辑通过"平台适配器"(Platform Adapter)实现:
class PlatformAdapter(ABC):
"""平台适配器抽象类。"""
@abstractmethod
def pre_check(self, page):
"""检查前的预处理,比如处理弹窗、切换iframe。"""
pass
@abstractmethod
def post_check(self, page, result: dict):
"""检查后的后处理,比如提取平台特定的信息。"""
pass
class WechatAdapter(PlatformAdapter):
def pre_check(self, page):
# 切换到草稿箱iframe
frame = page.frame_locator("#draft-iframe")
return frame
def post_check(self, page, result: dict):
# 提取草稿ID
result["draft_id"] = page.get_attribute("#draft-id", "data-id")
WebReviewer.review() 在执行检查规则前后,调用 PlatformAdapter 的钩子函数,实现平台特定逻辑。
挑战2:性能瓶颈
全局化后,所有网页审查请求都经过 WebReviewer,它可能成为性能瓶颈。
比如同时有10篇文章要发布,每篇发布后都要审查(确认是否发布成功),就是10个并发的网页审查请求。WebReviewer 如果只用一个浏览器实例串行处理,要等很久。
解决方法:浏览器池 + 请求队列。
class WebReviewer:
def __init__(self, browser_pool: BrowserPool, max_concurrent: int = 3):
self.browser_pool = browser_pool
self.max_concurrent = max_concurrent
self.semaphore = asyncio.Semaphore(max_concurrent) # 限制并发数
async def review_async(self, url: str, platform: str, rules: list) -> dict:
"""异步版本的review,支持并发。"""
async with self.semaphore: # 限制同时只有3个审查任务运行
browser = self.browser_pool.get_browser(platform)
page = await browser.new_page()
# ... 执行审查逻辑 ...
挑战3:截图存储成本
如果每篇文章的每个平台都截图,一天发10篇文章,每篇4个平台,就是40张截图。一张截图约200KB,一天就是8MB,一年就是2.9GB。
解决方法:
- 只保留最近30天的截图,更早的压缩存档或删除
- 失败才截图:如果审查通过,不截图(节省存储空间);如果审查失败,截图(用于排查问题)
- 截图压缩:统一保存为WebP格式,比PNG小30%
总结
网页审查全局化的本质:把"访问URL并验证"这件亃,从平台特定逻辑变成通用基础设施。
这样做的好处:
- 代码复用:通用逻辑只写一次,所有平台共用
- 统一管理:浏览器环境、Cookie、截图格式、重试策略,统一配置,统一维护
- 可测试性:
WebReviewer可以单独测试,不需要依赖具体平台 - 可扩展性:新增平台时,只需要写平台特定的检查规则和适配器,不需要重新实现网页审查逻辑
代价是实现复杂度。你需要设计好抽象层(哪些逻辑通用、哪些平台特定),处理好性能瓶颈,管理好存储成本。
但这个代价是值得的。当你的系统从支持1个平台扩展到支持4个平台,没有全局化的网页审查模块,代码会变成意大利面条——到处是重复逻辑,改一处漏一处。
全局化,才能规模化简洁。
文章信息
- 写作时间:2026-06-13
- 字数:约3300字
- 状态:草稿
- 下一步:配图生成、质量检查、发布到博客