DrissionPage与OCR实战:破解动态加密网站的数据采集方案
1. 项目概述与核心挑战
海关企业信息公示平台,对于做外贸数据、企业征信或者市场调研的朋友来说,绝对是个绕不开的“硬骨头”。这个平台承载了大量公开的企业注册、报关、信用等信息,价值不言而喻。但当你兴冲冲地打开页面,准备写几行Python脚本把数据“请”下来时,大概率会碰一鼻子灰。你会发现,传统的requests库配合BeautifulSoup或者lxml,在这里几乎寸步难行。页面要么是白屏一片,要么返回一堆你看不懂的加密字符串,要么就是弹出一个验证码让你证明“你是人”。这背后,正是平台部署的动态防御与混淆加密技术在发挥作用。
我最近接手的一个项目,核心需求就是从这类平台上稳定、高效地采集企业名录及关键经营信息。经过几轮“攻防”测试,最终敲定了一套混合方案:以DrissionPage作为浏览器自动化核心,突破动态脚本的检测与拦截;再结合轻量级OCR技术,专门对付那些以Base64图片形式混淆的关键数据(比如企业名称、统一社会信用代码)。这套组合拳打下来,不仅成功绕过了瑞数(一种常见的动态安全产品)等防御体系,还将采集的稳定性和准确率提升到了可工程化的水平。今天,我就把这套方案的实战细节、踩过的坑以及优化心得,毫无保留地分享出来。无论你是数据工程师、爬虫开发者,还是对反爬对抗技术感兴趣的朋友,相信都能从中找到可以直接“抄作业”的灵感和代码。
2. 目标平台防御机制深度拆解
在动手之前,我们必须像黑客一样,先搞清楚“敌人”的防御工事是怎么建的。盲目冲锋只会浪费时间和资源。通过对目标海关公示平台的多次探测与分析,我将其防御体系归纳为以下三个核心层面。
2.1 动态脚本加载与执行验证
这是第一道,也是最棘手的一道防线。平台并未直接返回渲染好的HTML,而是先返回一个包含大量混淆JavaScript代码的“壳”页面。这段代码的核心任务之一是计算生成一个或多个动态的Token或Cookie(例如__jsluid_s、__jsl_clearance_s等),这些值是后续请求能够获取真实数据的“门票”。
它的工作原理可以简单理解为:
- 首次请求:你的脚本请求目标URL,服务器返回一个包含复杂JS计算逻辑的HTML。
- 环境检测:这段JS会在浏览器环境中执行,它会检测一系列浏览器特征,如
window、document、navigator对象的属性和方法,甚至包括WebGL、Canvas指纹等。纯requests这类无头(headless)的HTTP库不具备这些环境。 - 动态计算:利用检测到的环境信息,结合一套算法(通常是混淆过的),计算出正确的Token。
- 设置凭证并重定向:JS将计算出的Token设置为Cookie,然后触发页面重定向或再次发起请求到真正的数据接口。
- 获取数据:携带了正确Token的请求才能从服务器拿到真实的JSON或HTML数据。
传统爬虫死在第一步到第二步。requests无法执行JS,因此永远算不出那个关键的Token,后续所有请求都会被服务器拒绝,返回的可能是一个错误页,或者永远在循环加载。
2.2 关键数据混淆与图片化
即便你突破了第一道防线,拿到了看似结构化的数据页面,第二个陷阱正在等着你。为了增加自动化采集的难度,平台会将最核心的字段进行混淆处理。最常见的手法就是文本图片化。
具体表现为:
- 你在网页上明明能看到清晰的企业名称“XX国际贸易有限公司”。
- 但查看网页源代码(HTML)时,对应的位置却不是文本,而是一个``标签,其
src属性是一串以data:image/png;base64,开头的很长很长的字符串。 - 这串字符串就是那张包含文字的图片的Base64编码。对于浏览器来说,它能正常解码并显示为图片;但对于想直接提取文本的爬虫来说,这成了一堵墙。你不能直接解析出文字,必须先处理图片。
这么做的目的很明确:大幅提高数据提取的成本。你要么用人眼去看,要么引入图像识别(OCR)技术。而OCR的准确率和速度,就成了新的挑战。
2.3 请求频率与行为指纹监控
任何公开平台都不会允许无限制的疯狂抓取。在动态防御之外,必定存在基于请求频率、IP地址、请求头完整性以及用户行为序列的监控。
- 频率限制:单位时间内过多请求,会触发IP暂时或永久封禁。
- 请求头校验:缺少
User-Agent、Referer、Accept-Language等标准浏览器头,或头信息明显是脚本生成的(如Python-requests),容易被识别。 - 行为指纹:正常的用户浏览是有逻辑的:先访问列表页,点击某一项进入详情页,停留一段时间,滚动页面。而爬虫的行为往往是:以固定时间间隔请求不同页面,不执行鼠标移动、点击等操作。高级防御系统可以构建行为指纹来区分人和机器。
注意:在实施采集方案时,必须将“友好性”和“可持续性”放在首位。我们的目标是获取公开数据,而非攻击或拖垮对方服务器。因此,模拟人类行为的延迟设置、合理的并发控制、以及使用高质量的代理IP池,是方案设计中不可或缺的伦理与技术环节。
3. 技术选型与方案设计思路
面对上述三重防御,单一技术很难通吃。我们需要一个融合了浏览器自动化、智能等待和图像识别的混合架构。下面是我经过多轮POC(概念验证)后确定的方案核心。
3.1 为什么是DrissionPage,而不是Selenium或Playwright?
浏览器自动化工具很多,Selenium是老牌王者,Playwright是后起之秀,那为什么选择相对小众的DrissionPage?
核心优势对比:
- 基于CDP协议,无驱动依赖:DrissionPage直接通过Chrome DevTools Protocol与浏览器通信。这意味着你不需要下载和管理与浏览器版本严格匹配的
chromedriver或geckodriver。对于需要频繁更新或部署在服务器环境的情况,这减少了巨大的维护成本。启动更快,连接更稳定。 - 更强的反检测能力:许多反爬系统会检测
window.navigator.webdriver属性。Selenium和早期Playwright需要额外参数来隐藏这个属性。而DrissionPage在设计上就更注重“隐身”,其默认启动的浏览器环境更接近真实用户,对于瑞数这类动态验证的绕过成功率,在我的实测中更高。 - 简洁的API与混合模式:DrissionPage的API设计非常Pythonic,学习成本低。更重要的是,它支持在同一个浏览器对象内,无缝切换“浏览器操作”和“requests-like的网络请求”模式。这意味着我们可以用浏览器环境搞定登录和Token获取,然后切换到更轻量、更快的网络请求模式来获取数据,效率提升显著。
一个简单的启动示例,展示了其简洁性:
from DrissionPage import ChromiumPage # 创建页面对象,可传递启动参数模拟更真实的浏览器 co = ChromiumPageOptions().auto_port() # 自动寻找可用端口 co.set_argument('--disable-blink-features=AutomationControlled') # 禁用自动化控制特征 co.set_user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...') # 设置UA page = ChromiumPage(addr_or_opts=co) page.get('https://目标网站') # 此时页面已加载,并完成了动态JS的执行和Token设置3.2 OCR引擎选型:平衡精度、速度与部署复杂度
解决了“进入”的问题,接下来要解决“提取”的问题——识别图片中的文字。OCR选择是关键,需要权衡几个因素:
- Tesseract:开源老将,免费,但对中文、复杂版面、小字体、低对比度图片的识别效果一般,且需要训练字库才能达到较好效果,配置稍显繁琐。
- PaddleOCR:百度开源,中文识别效果一流,精度高,但体积相对较大,依赖PaddlePaddle深度学习框架,在资源受限的服务器上部署和运行会有一定压力。
- ddddocr:本项目最终选择的方案。它是一个轻量级的开源OCR库,专为验证码、简单文本图片识别优化。
- 优点:体积极小(几MB),纯Python实现,无需复杂环境依赖,
pip install ddddocr即可使用。对于目标平台上那种清晰、字体规范的企业信息图片,识别准确率可达95%以上,速度极快。 - 缺点:对于极度扭曲、背景复杂的通用场景,可能不如PaddleOCR。
- 优点:体积极小(几MB),纯Python实现,无需复杂环境依赖,
- 商业化API:如百度、阿里、腾讯的OCR服务,精度最高,但有调用次数限制和费用。
选型结论:对于本项目这种特定场景(字体清晰、格式固定)下的图片文字提取,ddddocr在精度、速度和部署简易性上取得了最佳平衡。它让我们的采集脚本可以轻松地在任何有Python环境的机器上运行,无需关心GPU或复杂的深度学习环境。
3.3 整体架构设计流程图
为了让整个方案的逻辑更清晰,我将其核心流程绘制如下(文字描述):
初始化阶段:
- 配置DrissionPage浏览器,设置反检测参数和代理。
- 初始化ddddocr识别器。
突破动态防御(获取通行证):
- 使用DrissionPage的
ChromiumPage访问目标列表页URL。 - 页面自动执行JS,完成环境检测与Token计算。
- DrissionPage内部维护的Session此时已携带了有效的Cookie。
- 使用DrissionPage的
提取与解析列表:
- 等待列表页关键元素(如表格)加载完成。
- 从页面中解析出企业条目链接(通常是详情页的URL)。
详情页采集循环:
- 对于每个详情页链接: a.方式A(稳健优先):继续使用
ChromiumPage访问详情页,等待加载。 b.方式B(效率优先):利用DrissionPage的SessionPage模式,复用浏览器已获得的Cookie,发起一个轻量的HTTP GET请求获取详情页HTML。这种方式速度更快,资源占用更少。 c. 解析详情页HTML,定位包含Base64图片的标签。 d. 提取src属性中的Base64字符串,去除data:image/png;base64,前缀。 e. 将纯Base64字符串传递给ddddocr进行识别,得到文本。 f. 存储结构化数据(企业名、信用代码、地址等)。
- 对于每个详情页链接: a.方式A(稳健优先):继续使用
反反爬策略融入:
- 在每个请求间插入随机延迟(如2-5秒),模拟人工浏览。
- 使用代理IP池,定期更换IP,避免频率封锁。
- 监控采集状态,遇到异常(如验证码、封禁)时,触发相应的重试或报警机制。
4. 核心模块实现与代码详解
理论说再多,不如一行代码。接下来,我们深入到各个核心模块的实现细节。我会提供关键代码片段,并解释每一处设计背后的考量。
4.1 DrissionPage环境配置与启动优化
一个“像人”的浏览器环境是成功的第一步。以下配置是我经过多次测试后总结出的最佳实践。
from DrissionPage import ChromiumPage, ChromiumOptions import time import random def create_stealth_browser(): """ 创建一个用于反爬的隐身浏览器实例 Returns: ChromiumPage: 配置好的浏览器页面对象 """ co = ChromiumOptions() # 1. 禁用自动化控制特征(最关键的一步) co.set_argument('--disable-blink-features=AutomationControlled') # 2. 设置一个常见的、更新的User-Agent ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" co.set_argument(f'--user-agent={ua}') # 3. 其他优化参数 co.set_argument('--no-sandbox') # 在Docker或某些服务器环境下可能需要 co.set_argument('--disable-gpu') # 禁用GPU,适用于无头模式或服务器 # co.set_argument('--headless') # 如需无头模式,可取消注释。但某些网站对无头模式检测更严。 # 4. 实验性参数,用于排除一些可能暴露自动化的特性 co.set_experimental_option('excludeSwitches', ['enable-automation']) co.set_experimental_option('useAutomationExtension', False) # 5. 启动浏览器 # auto_port=True 自动寻找可用端口,避免冲突 browser = ChromiumPage(addr_or_opts=co, auto_port=True) # 6. 执行额外JS,覆盖可能被检测的navigator属性 stealth_js = """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """ browser.run_js(stealth_js) return browser # 使用示例 browser = create_stealth_browser()实操心得:
--disable-blink-features=AutomationControlled这个参数至关重要,它能移除浏览器中的“自动化”标志。是否使用--headless(无头模式)需要根据目标网站具体测试。有些网站在无头模式下防御更弱,有些则更强。建议在开发调试阶段使用有头模式,便于观察;部署时再切换为无头模式以节省资源。
4.2 动态Token的获取与Session维持
DrissionPage的强大之处在于,它处理完动态JS后,其内部的网络Session(SessionPage对象)会自动更新为携带有效Cookie的状态。我们可以直接复用这个Session。
def get_authenticated_session(list_url): """ 访问列表页,完成动态验证,并返回一个已认证的Session对象。 该Session可用于后续的快速HTTP请求。 Args: list_url (str): 目标列表页URL Returns: SessionPage: 已携带有效Cookie的会话对象,或None(如果失败) """ browser = None try: browser = create_stealth_browser() print(f"正在访问列表页以获取认证... {list_url}") browser.get(list_url) # 关键:等待页面完全加载,特别是等待可能存在的动态元素出现 # 这里以等待一个特定的、列表页加载后才会出现的元素为例,比如分页控件或表格行 # browser.wait.ele_displayed('tag:table', timeout=10) # 示例:等待表格出现 # 更通用的做法:等待一段时间,确保JS执行完毕。可以结合具体网站调整。 time.sleep(3 + random.uniform(1, 3)) # 随机等待,更拟人 # 检查是否成功(例如,通过判断页面标题或某个特定元素是否存在) # if "验证" in browser.title or browser.ele('tag:input@type=text'): # 简单判断是否有验证码输入框 # print("可能触发了验证码,需要人工干预或更复杂的处理。") # return None # 从浏览器对象中获取当前的Session对象 # 这个Session已经包含了浏览器当前的所有Cookie,包括动态生成的Token session_page = browser.get_session() print("动态认证成功,Session已就绪。") return session_page except Exception as e: print(f"获取认证Session时发生错误: {e}") return None finally: # 重要:获取Session后,可以关闭耗资源的浏览器对象 if browser: browser.quit()代码解析:
browser.get(list_url)触发了完整的页面加载和JS执行。time.sleep(3 + random.uniform(1, 3))是一个简单的等待策略。在实际项目中,强烈建议使用更智能的等待方式,例如browser.wait.ele_displayed()等待某个关键元素出现,这比固定等待更可靠、更高效。browser.get_session()是精髓所在。它返回一个SessionPage对象,这个对象不是一个浏览器,而是一个类似requests.Session的HTTP会话对象,但神奇的是,它“继承”了当前浏览器标签页的所有Cookie。这意味着后续我们可以用这个轻量的Session去请求其他页面,而无需再启动笨重的浏览器。
4.3 Base64图片的定位、提取与OCR识别
拿到详情页的HTML后,真正的挑战才开始。我们需要从一堆标签中找到那个隐藏着文字的图片。
import base64 import ddddocr from lxml import etree import re class ImageOCRProcessor: def __init__(self): """初始化OCR识别器""" # 创建ddddocr识别器实例,可以传入参数调整识别模式 self.ocr = ddddocr.DdddOcr(show_ad=False) # show_ad=False 关闭无关输出 def extract_base64_from_src(self, src_attr): """ 从img标签的src属性中提取纯净的Base64编码字符串。 Args: src_attr (str): 类似 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." Returns: str: 去除了头部的纯Base64字符串,或None """ # 正则匹配 data:image/[^;]+;base64, 之后的部分 pattern = r'data:image/[^;]+;base64,(.+)' match = re.match(pattern, src_attr) if match: return match.group(1) return None def ocr_from_base64(self, base64_str): """ 将Base64字符串解码为图片字节,并进行OCR识别。 Args: base64_str (str): 纯Base64编码字符串 Returns: str: 识别出的文本 """ try: # 1. Base64解码 img_bytes = base64.b64decode(base64_str) # 2. 调用ddddocr进行识别 text = self.ocr.classification(img_bytes) return text.strip() # 去除可能的空白字符 except Exception as e: print(f"OCR识别失败: {e}") return "" def parse_detail_page(self, html_content, xpath_img): """ 解析详情页HTML,识别指定XPath下的Base64图片。 Args: html_content (str): 详情页HTML xpath_img (str): 定位包含Base64图片的img标签的XPath表达式 Returns: dict: 识别出的字段键值对 """ result = {} try: tree = etree.HTML(html_content) # 假设企业名称和信用代码分别在不同的img标签里,用不同的XPath # 示例XPath,需要根据实际网页结构调整 # name_img_src = tree.xpath('//div[@class="company-name"]/img/@src')[0] # code_img_src = tree.xpath('//div[@class="credit-code"]/img/@src')[0] # 这里以通用方法示例:查找所有符合给定XPath的img img_elements = tree.xpath(xpath_img) for idx, img_ele in enumerate(img_elements): src = img_ele.get('src') if src and src.startswith('data:image'): pure_base64 = self.extract_base64_from_src(src) if pure_base64: recognized_text = self.ocr_from_base64(pure_base64) # 根据位置或属性判断字段名,这里简单用索引 field_name = f"field_{idx}" result[field_name] = recognized_text print(f"识别字段 {field_name}: {recognized_text}") else: result[f"field_{idx}"] = "Base64提取失败" else: # 如果不是Base64图片,可能是普通文本或网络图片,这里可以扩展处理 pass except Exception as e: print(f"解析详情页时发生错误: {e}") return result # 使用示例 processor = ImageOCRProcessor() # 假设已经从详情页获取到html # detail_html = session_page.get('详情页URL').html # data = processor.parse_detail_page(detail_html, '//td[@class="encrypted"]/img')代码解析与注意事项:
- 正则提取Base64:
data:image/png;base64,这个前缀是固定的格式,用正则可以稳健地提取出后面真正的编码数据。直接使用split(',')[-1]也可以,但正则更清晰。 - OCR调用:
ddddocr的API极其简单,classification方法接收图片字节,返回识别文本。对于简单的数字、英文、中文混合体(如信用代码、企业名),效果很好。 - XPath定位:这是整个环节中最需要人工介入和调试的部分。你必须使用浏览器的开发者工具(F12),仔细查看目标图片的DOM结构,找到能够唯一、稳定定位它的XPath。这个路径可能会随着网站改版而变化,因此代码需要一定的容错性,或者将XPath作为可配置项。
- 错误处理:网络请求、HTML解析、Base64解码、OCR识别每一步都可能出错,必须用
try...except包裹,并记录日志,避免因单条数据失败导致整个任务崩溃。
4.4 高效采集循环与状态管理
将上述模块组合起来,就构成了完整的采集流水线。这里的关键是稳健性和可恢复性。
import json import time from datetime import datetime class CustomsDataCrawler: def __init__(self, start_url, proxy_pool=None): self.start_url = start_url self.proxy_pool = proxy_pool # 代理IP池,可以是列表或获取函数 self.session = None self.ocr_processor = ImageOCRProcessor() self.data_list = [] def run(self): """主运行流程""" print(f"[{datetime.now()}] 采集任务开始") # 步骤1:获取认证Session self.session = get_authenticated_session(self.start_url) if not self.session: print("初始化Session失败,任务终止。") return # 步骤2:从列表页解析出所有详情页链接 detail_urls = self.parse_list_page(self.session) print(f"共发现 {len(detail_urls)} 个详情页待采集。") # 步骤3:遍历详情页进行采集 for idx, url in enumerate(detail_urls): print(f"\n正在采集第 {idx+1}/{len(detail_urls)} 项: {url}") try: # 随机延迟,模拟人工 delay = random.uniform(2, 5) time.sleep(delay) # 可选:定期更换代理(如果配置了代理池) # if self.proxy_pool and idx % 10 == 0: # new_proxy = self.get_proxy_from_pool() # self.session.proxies.update({'http': new_proxy, 'https': new_proxy}) # 使用轻量Session获取详情页HTML resp = self.session.get(url, timeout=15) if resp.status_code != 200: print(f"请求失败,状态码: {resp.status_code}") self.record_failure(url, f"HTTP {resp.status_code}") continue detail_html = resp.html # 解析详情页,识别图片文字 # 这里需要根据目标网站的实际结构,编写或配置XPath company_data = self.ocr_processor.parse_detail_page( detail_html, xpath_img='//div[contains(@class, "encrypt-field")]/img' # 示例XPath ) if company_data: company_data['source_url'] = url company_data['crawl_time'] = datetime.now().isoformat() self.data_list.append(company_data) print(f"成功采集: {company_data.get('field_0', 'N/A')}") # 每采集N条或每隔一段时间保存一次,防止数据丢失 if len(self.data_list) % 20 == 0: self.save_to_file() except Exception as e: print(f"采集 {url} 时发生异常: {e}") self.record_failure(url, str(e)) # 可以根据异常类型决定是否重试、暂停或终止 # 例如,遇到连接超时,可以休息更长时间后重试一次 # time.sleep(30) # ... 重试逻辑 ... # 步骤4:最终保存 self.save_to_file(final=True) print(f"\n[{datetime.now()}] 采集任务结束,共成功采集 {len(self.data_list)} 条数据。") def parse_list_page(self, session): """解析列表页,获取详情页链接列表。需要根据实际网页结构实现。""" # 示例:使用XPath或CSS选择器定位链接 # resp = session.get(self.start_url) # tree = etree.HTML(resp.html) # links = tree.xpath('//table//tr/td/a/@href') # return ['https://目标域名' + link for link in links] # 补全完整URL # 此处返回模拟数据 return [f"https://example.com/detail/{i}" for i in range(1, 101)] def save_to_file(self, final=False): """将数据保存到JSON文件""" filename = f"customs_data_{datetime.now().strftime('%Y%m%d_%H%M')}.json" with open(filename, 'w', encoding='utf-8') as f: json.dump(self.data_list, f, ensure_ascii=False, indent=2) suffix = "(最终)" if final else "(增量)" print(f"数据已保存至 {filename} {suffix}") def record_failure(self, url, reason): """记录失败的URL和原因,便于后续排查或重试""" with open('failed_urls.log', 'a', encoding='utf-8') as f: f.write(f"{datetime.now()}\t{url}\t{reason}\n") # 启动采集 if __name__ == '__main__': crawler = CustomsDataCrawler(start_url='https://目标网站/list') crawler.run()5. 实战避坑指南与高级优化
纸上得来终觉浅,绝知此事要躬行。在实际运行中,你会遇到各种各样预料之外的问题。下面是我踩过坑后总结出的经验。
5.1 动态元素等待策略优化
前面用了time.sleep,这是最差但最简单的方式。DrissionPage提供了更强大的等待方法。
from DrissionPage import ChromiumPage page = ChromiumPage() page.get(url) # 方式1:等待元素出现(推荐) # 等待最多10秒,直到ID为‘result’的元素出现 ele = page.wait.ele_displayed('#result', timeout=10) # 方式2:等待元素消失(例如等待“加载中”图标消失) page.wait.ele_disappear('.loading-mask', timeout=10) # 方式3:等待特定文本出现 page.wait.ele_displayed('tag:div@@text():查询结果', timeout=10) # 方式4:自定义等待条件 def table_has_rows(): rows = page.eles('tag:table tbody tr') return len(rows) > 5 # 等待表格至少有5行数据 page.wait.until(table_has_rows, timeout=15)最佳实践:结合使用wait方法和随机延迟。先用智能等待确保关键元素加载,然后在操作间加入小范围的随机延迟(time.sleep(random.uniform(0.5, 2))),这样既能保证稳定性,又能更好地模拟人类的不确定性。
5.2 OCR识别准确率提升技巧
ddddocr默认模型对于清晰图片效果很好,但如果遇到干扰线、轻微扭曲或字体特殊的情况,准确率会下降。
图片预处理:在将Base64字节传给OCR之前,可以先进行简单的图像处理。
import cv2 import numpy as np from io import BytesIO from PIL import Image def preprocess_image(img_bytes): """对图片进行预处理以提高OCR识别率""" # 将字节转换为PIL Image image = Image.open(BytesIO(img_bytes)) # 转换为OpenCV格式 (BGR) img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) # 示例处理1:转为灰度图 gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) # 示例处理2:二值化(阈值处理),适用于背景干净的图片 # _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 示例处理3:降噪(中值滤波) # denoised = cv2.medianBlur(gray, 3) # 将处理后的图片转换回字节 is_success, buffer = cv2.imencode(".png", gray) if is_success: return buffer.tobytes() return img_bytes # 处理失败则返回原图在OCR调用前使用:
processed_bytes = preprocess_image(img_bytes); text = ocr.classification(processed_bytes)多引擎投票:对于关键字段(如统一社会信用代码),可以结合
ddddocr和pytesseract(Tesseract的Python封装)进行识别,取两者一致的结果,或通过规则(如信用代码有固定格式)进行校验和纠正。自定义字典:如果目标字段的词汇范围有限(比如都是“有限公司”、“股份有限公司”、“集团”等),可以收集这些词汇,在识别后进行简单的字符串匹配和纠正。
5.3 会话失效与自动重连机制
动态Token通常有有效期。长时间采集时,Session可能会过期。我们需要检测这种状态并自动恢复。
def is_session_invalid(html_content): """通过页面特征判断Session是否失效""" invalid_indicators = [ "登录", "验证码", "会话过期", "请重新登录", "access denied", "403", "Unauthorized" ] content_lower = html_content.lower() for indicator in invalid_indicators: if indicator in content_lower: return True return False # 在采集循环中 resp = self.session.get(url) if resp.status_code == 403 or is_session_invalid(resp.html): print("检测到会话失效,正在尝试重新认证...") self.session = get_authenticated_session(self.start_url) # 重新获取Session if self.session: # 重新请求当前URL resp = self.session.get(url) else: print("重新认证失败,暂停任务。") break5.4 分布式与增量采集架构思考
当数据量巨大时,单机单线程采集会非常慢。可以考虑以下优化方向:
- 生产者-消费者模式:一个进程专门负责解析列表页,生成详情页URL队列(生产者)。多个工作进程/线程从队列中获取URL并进行详情页采集和OCR识别(消费者)。使用
Redis或RabbitMQ作为队列中间件。 - 增量采集:每次采集后,记录已成功采集的URL或企业ID。下次启动时,先拉取最新的列表,与已采集记录对比,只采集新增或更新的条目。这需要设计一个简单的状态数据库(如SQLite)。
- 异步并发:对于I/O密集型的网络请求,可以使用
asyncio+aiohttp库进行异步编程,大幅提升采集效率。但需要注意目标网站的并发承受能力,避免因请求过快被封。
6. 常见问题排查与解决方案实录
即使方案设计得再完美,运行时也总会遇到问题。这里记录了几个最典型的“坑”及其解决方法。
6.1 DrissionPage启动失败或连接超时
- 问题现象:
ChromiumPage初始化时报错,或get()页面时长时间无响应。 - 可能原因及解决:
- 端口冲突:使用
auto_port=True参数让DrissionPage自动选择可用端口。 - 浏览器路径问题:确保系统已安装Chrome或Chromium浏览器。可以通过
co.set_browser_path()指定浏览器可执行文件的绝对路径。 - 代理或网络问题:检查本地网络和代理设置。如果使用代理,确保在
ChromiumOptions中正确配置。 - 资源不足:服务器内存不足可能导致浏览器无法启动。考虑使用
co.set_argument('--headless')和co.set_argument('--disable-gpu')减少资源占用。
- 端口冲突:使用
6.2 页面加载成功但无法获取数据
- 问题现象:浏览器能打开页面,但通过
page.html或SessionPage获取的HTML内容不包含目标数据,可能只有一些框架代码。 - 可能原因及解决:
- 等待不足:动态内容尚未加载。必须使用
page.wait系列方法等待特定数据元素出现,而不是等待固定时间。 - iframe框架:数据可能嵌套在``标签内。需要使用
page.get_frame()方法切换到对应的frame再进行元素查找。 - 数据来自异步接口(XHR):数据可能是页面加载后通过Ajax请求获取的。你需要使用浏览器的“网络”面板(F12 -> Network)监控XHR/Fetch请求,找到真正的数据接口URL。然后,可以直接用
SessionPage去请求这个接口(通常是返回JSON),这比渲染整个页面更高效。
- 等待不足:动态内容尚未加载。必须使用
6.3 OCR识别结果乱码或错误率高
- 问题现象:识别出的文字包含大量乱码、错别字或根本无法阅读。
- 可能原因及解决:
- 图片质量问题:首先检查Base64解码后的图片是否清晰。可以在代码中临时将图片保存到本地查看。如果图片本身模糊或有严重干扰,需要联系上游,或尝试更强大的图像预处理(如超分辨率、去噪)。
- 语言问题:
ddddocr默认主要针对中英文。如果文字是其他语言(如俄语、阿拉伯语),效果会很差。Tesseract或PaddleOCR支持更多语言包。 - 字体特殊:某些网站使用特殊字体来显示数字(如字体“防爬体”)。这种情况下,通用OCR引擎几乎都会失败。解决方案有两种:一是找到该字体文件,进行映射还原;二是更高级的,使用深度学习模型针对该特定字体进行训练。这超出了轻量级方案的范畴。
- 非纯文本图片:如果图片是复杂的表格、图表,或者文字与背景颜色相近,
ddddocr可能不适用。需要考虑使用PaddleOCR的版面分析功能,或者专门针对表格识别的库。
6.4 触发频率限制或IP被封禁
- 问题现象:请求开始返回403/429错误码,或要求输入验证码。
- 可能原因及解决:
- 请求过快:这是最常见的原因。务必在请求间增加随机延迟。对于重要网站,延迟时间建议在3-10秒甚至更长。
- User-Agent单一:虽然我们设置了UA,但如果一直用同一个,也可能被标记。可以准备一个UA列表,每次请求随机选择一个。
- 必须使用代理IP池:对于大规模采集,这是标配。购买或自建代理IP服务,并在代码中实现IP的自动切换。注意区分HTTP(S)代理和SOCKS代理。
- 模拟更复杂的行为:除了延迟,还可以模拟鼠标移动、滚动页面等行为(DrissionPage支持
page.scroll等方法),让指纹更像真人。
这套基于DrissionPage与OCR的混合采集方案,本质上是在“道高一尺,魔高一丈”的对抗中寻找一个平衡点。它不追求暴力破解,而是通过模拟真实用户行为和智能解析,以相对友好和可持续的方式获取公开数据。技术是不断演进的,今天有效的方法,明天可能就会失效。因此,核心思路——即“分析防御机制,组合针对性工具,模拟人类行为,保持稳健友好”——比任何具体的代码都更重要。在实际项目中,还需要加入完善的日志系统、监控告警和异常处理机制,才能让采集任务7x24小时稳定运行。希望这篇深度剖析能为你打开一扇窗,在合规的前提下,更高效地获取所需的数据价值。
