尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python自动化实战:OCR与爬虫技术实现发票验证码自动识别与批量查验

Python自动化实战:OCR与爬虫技术实现发票验证码自动识别与批量查验 1. 项目缘起为什么需要自动生成发票验证码在财务、报销、电商对账这些日常工作中处理发票是个绕不开的环节。很多企业内部系统或第三方发票查验平台为了防止恶意批量查询、保护数据安全都会设置验证码CAPTCHA这道门槛。想象一下财务同事月底要核对上百张发票每查一张都要手动输入一次扭曲的字母数字组合不仅效率低下还容易因看错而输入错误导致查询失败反复操作让人不胜其烦。这就是“发票验证码自动生成”这个需求最直接的来源。但这里有个关键的语义转换需要理解我们通常说的“验证码自动生成”在发票查验这个语境下往往不是指凭空创造出一个验证码而是指自动识别并填写查验页面上出现的验证码实现查询流程的自动化。其核心目标是绕过人工干预环节让程序能像人一样“看懂”图片里的验证码字符并自动填入表单完成整个查验请求。所以这个项目的本质是一个结合了网络爬虫、图像处理和模拟交互的Python自动化脚本。它要解决的问题非常具体自动登录或访问发票查验页面截获或下载页面上的验证码图片利用OCR光学字符识别技术识别出其中的字符然后将识别结果自动填充到输入框并提交表单最终获取发票的查验结果。对于有大量发票需要批量查验的企业或自由职业者来说这样一个工具能节省大量重复劳动时间将人力从枯燥的识别-输入循环中解放出来。2. 核心工具链选型与搭建要实现这个自动化流程我们需要一套可靠的Python工具组合。选型的原则是成熟、稳定、社区支持好并且能应对验证码可能出现的简单干扰。2.1 网络请求与页面操控Requests 与 Playwright/Selenium首先我们需要能模拟浏览器行为获取网页内容。requests库是处理HTTP请求的黄金标准简单直接。对于静态页面或者验证码图片URL直接暴露在HTML中的情况用requests获取页面源码再用BeautifulSoup或lxml解析出验证码图片的链接然后下载图片是最轻量高效的方案。import requests from bs4 import BeautifulSoup session requests.Session() # 1. 首先访问发票查验首页可能获取必要的cookies或token index_url https://inv-veri.example.com/ index_resp session.get(index_url) # 2. 解析页面找到验证码图片的img标签 soup BeautifulSoup(index_resp.text, html.parser) captcha_img_tag soup.find(img, idcaptchaImg) # 根据实际页面结构定位 captcha_url captcha_img_tag[src] if captcha_url.startswith(/): captcha_url fhttps://inv-veri.example.com{captcha_url} # 3. 下载验证码图片 captcha_resp session.get(captcha_url) with open(captcha.png, wb) as f: f.write(captcha_resp.content)但是很多现代网站的验证码是动态生成的图片链接可能带有一次性令牌token或者验证码本身是通过JavaScript动态渲染的。这时单纯的requests就力不从心了我们需要一个能执行JavaScript的“无头浏览器”。Playwright和Selenium是两大主流选择。我个人的偏好是Playwright。它由微软开发相比Selenium其API更现代、同步/异步支持都很好而且自带浏览器二进制文件无需单独配置驱动启动更快。在应对动态内容方面表现更稳定。from playwright.sync_api import sync_playwright def get_dynamic_captcha(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) # 无头模式不显示浏览器窗口 page browser.new_page() page.goto(https://inv-veri.example.com/) # 等待验证码图片元素加载 captcha_element page.locator(#captchaImg) captcha_element.wait_for(statevisible) # 截图验证码区域 captcha_element.screenshot(pathdynamic_captcha.png) browser.close()注意使用无头浏览器会消耗更多资源速度也相对较慢。在项目选型时应优先尝试用requests解决如果不行再升级到Playwright或Selenium。2.2 验证码识别引擎DDDDOCR 与 PaddleOCR下载到验证码图片后下一步就是识别。这里我们面对的不是通用的文字识别而是专门针对验证码的识别。验证码的特点是字体扭曲、带有干扰线或噪点但字符集通常仅限于数字和字母。DDDDOCR是这个领域的“网红”库它基于深度学习对各类简单的字符型验证码包括点选、滑块验证的缺口识别有非常好的效果。它最大的优点是开箱即用无需训练对于常见的干扰线、扭曲字体识别率很高。安装简单pip install ddddocr。import ddddocr ocr ddddocr.DdddOcr() with open(captcha.png, rb) as f: img_bytes f.read() captcha_text ocr.classification(img_bytes) print(f识别结果: {captcha_text})如果DDDDOCR对某些特定网站的风格验证码识别效果不佳或者你需要识别更复杂的验证码如中文汉字、印刷体数字等可以考虑PaddleOCR。它是百度开源的OCR系统功能强大支持多语言精度高但体积也更大部署稍复杂。对于发票验证码通常数字和英文居多DDDDOCR在轻量与效果上取得了很好的平衡是我首推的方案。# PaddleOCR示例需先安装 paddlepaddle 和 paddleocr from paddleocr import PaddleOCR ocr_engine PaddleOCR(use_angle_clsTrue, langen) # 使用英文模型 result ocr_engine.ocr(captcha.png, clsTrue) captcha_text .join([line[1][0] for line in result[0]]) # 提取识别文本 print(fPaddleOCR识别结果: {captcha_text})2.3 环境配置与依赖管理一个稳定的项目离不开清晰的环境。强烈建议使用虚拟环境venv或conda来隔离项目依赖。# 创建虚拟环境 python -m venv inv_venv # 激活虚拟环境 (Windows) inv_venv\Scripts\activate # 激活虚拟环境 (Mac/Linux) source inv_venv/bin/activate # 安装核心依赖 pip install requests beautifulsoup4 lxml pip install playwright playwright install chromium # 安装Playwright所需的浏览器 pip install ddddocr # 如需PaddleOCR安装命令较复杂请参考其官方文档将依赖写入requirements.txt文件便于复现环境requests2.31.0 beautifulsoup44.12.2 playwright1.40.0 ddddocr1.4.113. 实战构建一个完整的发票查验自动化脚本现在我们把各个模块串联起来构建一个能实际运行的脚本。我们假设目标网站是一个简单的发票查验平台验证码是静态图片。3.1 步骤拆解与代码实现整个流程可以分解为以下步骤初始化会话与获取初始页面使用requests.Session()保持会话状态携带cookies。定位并下载验证码解析HTML找到验证码图片的真实地址并下载。识别验证码使用DDDDOCR对下载的图片进行识别。构造并提交查询表单除了验证码通常还需要发票代码、发票号码、开票日期等信息。将这些数据和识别出的验证码一起以POST请求提交。解析与处理结果接收服务器返回的响应解析出发票查验结果如“真票”、“假票”、发票明细等。下面是一个整合的示例代码import requests from bs4 import BeautifulSoup import ddddocr import time import re class InvoiceVerifier: def __init__(self, base_url): self.base_url base_url self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) self.ocr ddddocr.DdddOcr() def fetch_captcha(self): 获取验证码图片并识别 # 1. 访问首页可能包含生成验证码的令牌 index_resp self.session.get(self.base_url) soup BeautifulSoup(index_resp.text, html.parser) # 2. 寻找验证码图片这里假设img标签的id是captcha_image # 实际项目中需要根据目标网站的具体HTML结构来调整 captcha_img soup.find(img, idcaptcha_image) if not captcha_img: # 尝试其他常见属性或CSS选择器 captcha_img soup.find(img, srcre.compile(rcaptcha, re.I)) if not captcha_img: raise ValueError(未在页面中找到验证码图片元素) captcha_src captcha_img.get(src) # 处理相对路径 if captcha_src.startswith(//): captcha_url https: captcha_src elif captcha_src.startswith(/): captcha_url self.base_url.rstrip(/) captcha_src else: captcha_url captcha_src # 3. 下载验证码图片 print(f正在下载验证码: {captcha_url}) captcha_resp self.session.get(captcha_url) captcha_image_bytes captcha_resp.content # 4. 识别验证码 captcha_text self.ocr.classification(captcha_image_bytes) # 清理识别结果只保留字母和数字 captcha_text re.sub(r[^a-zA-Z0-9], , captcha_text) print(f验证码识别结果: {captcha_text}) return captcha_text, captcha_image_bytes # 返回文本和图片字节可用于调试 def verify_invoice(self, invoice_code, invoice_number, invoice_date, captcha_text): 提交发票查验请求 verify_url self.base_url /verify # 假设查验接口路径 form_data { fpdm: invoice_code, # 发票代码 fphm: invoice_number, # 发票号码 kprq: invoice_date, # 开票日期 yzm: captcha_text # 验证码 # 可能还有其他隐藏字段如__VIEWSTATE等需要从首页提取 } print(f正在提交查验请求数据: {form_data}) resp self.session.post(verify_url, dataform_data) return resp.text # 返回HTML或JSON响应 def run(self, invoice_info_list): 批量查验入口 results [] for info in invoice_info_list: print(f\n开始查验发票: {info}) try: # 每次查验前最好重新获取一次验证码 captcha_text, _ self.fetch_captcha() time.sleep(1) # 礼貌性等待避免请求过快 result_html self.verify_invoice(info[code], info[number], info[date], captcha_text) # 这里需要根据实际返回的HTML结构解析结果 # 例如查找包含“查验成功”、“一致”等关键词的标签 results.append({info: info, result: result_html}) except Exception as e: print(f查验发票 {info} 时发生错误: {e}) results.append({info: info, error: str(e)}) return results if __name__ __main__: # 示例用法 verifier InvoiceVerifier(https://example-invoice-check.com) invoices [ {code: 144011999111, number: 12345678, date: 20230501}, # ... 更多发票 ] all_results verifier.run(invoices) for res in all_results: print(res)3.2 关键细节与避坑指南在实际编写和运行过程中你会遇到各种各样的问题。下面是我踩过的一些坑和对应的解决方案验证码URL是动态的或带有Token这是最常见的问题。解决方案是先用Session访问一次生成验证码的页面这个页面通常会在表单里设置一个隐藏的token或者验证码图片的URL本身包含一个随时间变化的参数。你需要先解析出这个token在后续下载验证码和提交表单时都带上它。务必保持Session的一致性让服务器认为这是同一个会话。识别准确率不高DDDDOCR虽然强大但并非万能。如果某个网站的验证码风格独特识别率低可以尝试图片预处理在识别前对图片进行灰度化、二值化、降噪等处理。OpenCV (pip install opencv-python) 是处理图像的好帮手。import cv2 import numpy as np def preprocess_image(image_bytes): # 将字节转换为numpy数组 nparr np.frombuffer(image_bytes, np.uint8) img cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE) # 转为灰度 # 二值化 _, img_bin cv2.threshold(img, 150, 255, cv2.THRESH_BINARY) # 降噪中值滤波 img_clean cv2.medianBlur(img_bin, 3) # 将处理后的图片转换回字节 _, img_encoded cv2.imencode(.png, img_clean) return img_encoded.tobytes() # 使用预处理后的图片进行识别 processed_bytes preprocess_image(captcha_image_bytes) captcha_text ocr.classification(processed_bytes)多引擎投票同时使用DDDDOCR和PaddleOCR或Tesseract进行识别取多数一致的结果或者对置信度高的结果进行选择。人工兜底与自建样本库对于识别失败的可以将图片保存下来人工识别后存入一个文件夹。积累一定数量后可以用这些图片微调一个简单的CNN模型或者直接建立一个“图-码”映射字典下次遇到相同图片直接返回结果。这对于固定样式的验证码特别有效。请求频率过快被屏蔽自动化脚本容易触发网站的反爬机制。必须加入随机延迟模拟人类操作间隔。import random import time time.sleep(random.uniform(1, 3)) # 随机等待1到3秒更高级的做法是监控响应状态码如果遇到403/429等自动延长等待时间或暂停一段时间。表单提交需要额外的隐藏字段很多基于传统框架如ASP.NET的网站表单里会有__VIEWSTATE、__EVENTVALIDATION等隐藏字段。这些字段必须在第一次GET页面时解析出来并随POST数据一起提交否则请求会失败。务必用BeautifulSoup仔细检查表单结构。验证码有效期有些验证码服务器端会设置有效期如60秒。如果你的脚本在下载验证码后处理时间过长才提交验证码可能已失效。解决方案是优化代码逻辑识别后立即提交并处理好重试机制即如果返回“验证码错误”自动重新获取并识别新验证码再提交。4. 进阶应对更复杂的验证码与反爬策略当目标网站升级了防御简单的字符验证码可能变成更复杂的类型如点选验证码、滑块验证码、旋转验证码等。此时我们的策略也需要升级。4.1 点选验证码的破解思路点选验证码如“请点击图中所有的XXX”是常见的进阶验证方式。对于这类验证码DDDDOCR也提供了相应的APIslide_match方法可用于滑块但点选需要识别物体位置。核心思路是目标识别使用目标检测模型如YOLO或专门的识别库识别出图片中所有需要点击的物体如“自行车”、“红绿灯”及其坐标。坐标计算将识别到的物体坐标转换为浏览器页面中的相对坐标。模拟点击使用Playwright或Selenium依次移动到这些坐标并执行点击操作。这个过程技术门槛较高需要一定的机器学习知识。一个取巧的思路是寻找该验证码提供商如极验、腾讯云验证码的公开破解方案或开源项目但请注意法律和道德边界仅用于学习测试勿用于非法用途。4.2 滑块验证码的自动化滑块验证码的核心是找到缺口位置。DDDDOCR的slide_match功能可以计算两张图片背景图和缺口图的匹配位置。import ddddocr det ddddocr.DdddOcr(detFalse, ocrFalse) with open(background.png, rb) as f: target_bytes f.read() with open(gap.png, rb) as f: background_bytes f.read() res det.slide_match(target_bytes, background_bytes, simple_targetTrue) print(res)它会返回一个字典包含缺口位置的x、y坐标。得到坐标后再用Playwright模拟鼠标拖动滑块即可。这里的关键是模拟人的拖动轨迹不能直接设置到终点而应该生成一个包含加速、减速、轻微抖动的移动轨迹数组然后使用page.mouse.move和page.mouse.down/up来模拟。4.3 使用Playwright应对动态渲染与高级交互当网站大量使用JavaScript动态加载验证码或者验证码是Canvas绘制时Playwright的优势就凸显出来了。它可以等待特定元素出现、与页面充分交互。from playwright.sync_api import sync_playwright def handle_js_captcha(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 调试时可设为False看过程 page browser.new_page() page.goto(https://target-site.com) # 等待并点击可能触发验证码的按钮 page.click(#query_button) # 等待验证码弹窗或区域出现 page.wait_for_selector(.captcha-modal, statevisible) # 假设验证码是canvas我们将其截图 captcha_canvas page.locator(#captchaCanvas) captcha_canvas.screenshot(pathjs_captcha.png) # ... 后续识别过程与之前类似 # 识别后在输入框输入并提交 page.fill(#captchaInput, recognized_text) page.click(#submitBtn) # 等待结果 page.wait_for_selector(.result-panel) result page.inner_text(.result-panel) print(result) browser.close()5. 项目优化与工程化思考一个能稳定运行的脚本只是开始。要让这个工具真正好用、可维护还需要考虑以下几点5.1 错误处理与日志记录健壮的程序必须能妥善处理各种异常网络超时、识别失败、页面结构变化、验证码错误等。使用try...except包裹关键步骤并记录详细的日志便于后期排查。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[logging.FileHandler(invoice_verifier.log), logging.StreamHandler()]) logger logging.getLogger(__name__) try: captcha_text ocr.classification(img_bytes) if len(captcha_text) ! 4: # 假设验证码是4位 logger.warning(f识别结果长度异常: {captcha_text}) raise ValueError(识别结果无效) except Exception as e: logger.error(f验证码识别失败: {e}, exc_infoTrue) # 触发重试或人工处理流程5.2 配置化与可扩展性不要将发票信息、网站URL等硬编码在脚本里。应该使用配置文件如config.yaml或config.ini或命令行参数来管理。# config.yaml target_website: base_url: https://inv-veri.example.com captcha_img_selector: #captchaImg form_fields: fpdm: fpdm fphm: fphm kprq: kprq yzm: yzm submit_url_suffix: /doVerify ocr: engine: ddddocr # 或 paddleocr preprocess: true request: timeout: 10 retry_times: 3 delay_range: [1, 3]主程序读取配置使得更换目标网站或调整参数时无需修改代码。5.3 性能与并发考虑如果需要查验的发票量极大成千上万单线程顺序处理会非常慢。可以考虑使用并发但必须谨慎因为过高的并发请求会立刻被目标网站封禁。一个折中的方案是使用有限并发例如用concurrent.futures的ThreadPoolExecutor控制同时运行的线程数在3-5个并且每个线程内必须有足够的随机延迟。from concurrent.futures import ThreadPoolExecutor, as_completed def verify_single(invoice_info): # 封装单张发票的查验逻辑 verifier InvoiceVerifier(config[base_url]) # ... 具体查验步骤 return result def batch_verify(invoice_list, max_workers3): results [] with ThreadPoolExecutor(max_workersmax_workers) as executor: future_to_invoice {executor.submit(verify_single, inv): inv for inv in invoice_list} for future in as_completed(future_to_invoice): inv future_to_invoice[future] try: result future.result() results.append(result) except Exception as e: print(f发票 {inv} 查验失败: {e}) results.append({info: inv, error: str(e)}) return results5.4 道德、法律与合规性提醒最后也是最重要的一点我们必须清醒地认识到自动化工具的边界。遵守Robots协议检查目标网站的robots.txt文件尊重网站所有者设置的爬虫规则。控制访问频率你的脚本不应给目标网站服务器造成显著负担。添加延迟、限制并发是基本素养。明确使用目的本项目思路仅适用于个人学习、测试及在合法授权范围内的自动化操作例如处理自己公司拥有权限的发票数据。严禁用于攻击、干扰他人系统或未经授权批量获取敏感数据。数据隐私处理发票数据时注意其中包含的敏感信息如公司名称、税号、金额务必妥善保管防止泄露。自动化是一把双刃剑它能提升效率也可能带来风险。在享受技术便利的同时务必将其用在正途并在法律和道德框架内行事。
返回列表