ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

安全自动化从零起步:requests、正则与POC扫描器实战指南

2026/9/15 5:24:12 拓冰建站 浏览量
安全自动化从零起步:requests、正则与POC扫描器实战指南 写安全脚本这件事绕不开的三个基本功requests库、正则表达式、脚本组织能力。很多想往安全方向走的朋友一上来就盯着各种扫描器和现成POC看结果真正动手写的时候才发现连一个稳定可靠的HTTP请求都发不明白连一段HTML里想要的内容都提取不出来后面全是空中楼阁。这篇文章想聊的就是这三件事怎么落地。requests管网络通信正则管信息提取脚本结构管整个验证流程怎么组织。POC可以理解成对某个目标做一次精确验证的脚本扫描器则是把这种验证能力批量化和自动化。我把从零写POC到搭一个可扩展扫描器框架的完整思路拆开讲附带我实际踩过的坑和调优经验适合刚接触安全自动化、打算用Python写自己检测工具的同学参考。1. 安全自动化脚本的基础认知与整体思路1.1 为什么requests库和正则表达式是入门核心在安全测试里绝大多数检测逻辑都可以简化成一句话向某个目标发起请求然后判断返回的内容是否符合某种特征。这句话拆开就是两个动作第一是发出请求第二是分析响应而requests库和正则表达式恰好分别对应了这两个动作。requests库是Python生态里处理HTTP请求的事实标准它封装了TCP连接、TLS握手、Cookie管理、重定向跟随这些底层细节让你用几行代码就能完成一个完整的HTTP交互。在实际安全场景里这意味着你可以快速模拟浏览器行为、构造不同方法的数据包、维持会话状态、控制请求频率这些都是后面所有自动化工作的地基。正则表达式则是分析响应时最顺手的工具。有人可能会问现在解析HTML不是有BeautifulSoup这类库吗确实有但安全脚本有一个特点你面对的目标响应未必是规范的HTML可能是接口返回的JSON片段、可能是报错页面、可能是日志文本、甚至可能是二进制流里夹带的可读字符串。这种情况下正则反而更通用、更轻量它不关心文档结构是否合法只关心你要找的字符串模式是否存在。所以它和requests的搭配组合才是安全自动化脚本里最核心的一对基本功。1.2 POC与扫描器从验证到规模化POC很多人叫它漏洞验证脚本但我觉得更准确的理解是一次针对性的判断。它做的事情通常很聚焦确认某个目标上是否存在某个可被验证的特征然后给出一个结论。这个结论可以是存在不存在无法判断三种状态对应的输出也就是三个字母Pass、Fail、Error。扫描器本质上就是POC的规模化形式。单个POC针对一个目标扫描器则要在几十个、几百个甚至上千个目标上重复执行同一种验证逻辑并且把结果汇总、去重、存储、输出报告。这听起来不复杂但当目标数量上升之后超时控制、并发限制、请求频率、网络异常处理这些问题会一个接一个地冒出来。你会发现能写POC和能写扫描器中间差的就是工程化能力。在我个人看来最好的学习路径不是一上来就研究某个大型扫描器的源码而是先把自己手头的一个POC脚本打磨到小但完整的程度再逐步给它加上批量、并发、存储、日志这些能力。这样每一步的改动都发生在自己熟悉的代码上遇到问题排查起来也快得多。1.3 安全边界与合规意识这一点我必须放在最前面说清楚。写POC和扫描器本身是安全研究、风险评估、漏洞管理等工作中的常规技术能力但任何检测行为都必须建立在合法合规的前提下。简单说就是你要测的目标得有明确授权无论是你自己维护的系统还是签订了测试授权书的项目都不能拿这些技术手段去探测没有授权的第三方目标。从技术层面讲脚本里也应该有意识地区分验证和攻击两种行为。安全验证类脚本的核心特征是请求数量可控、行为可解释、影响可评估。我会在后面的所有示例中都遵循这个原则——代码都只是做配置检查、特征识别、响应头验证这类无害化检测。如果你把它用到未经授权的目标上那就完全偏离了这篇文章的初衷后果也要自己承担。2. requests库实战从基础请求到稳健的请求策略2.1 基础请求与会话管理先看最基础的用法一个GET请求只需要两行核心代码import requests resp requests.get(https://example.com, timeout10) print(resp.status_code) print(resp.text[:500])resp.status_code拿到HTTP状态码resp.text拿到响应文本。这是所有检测脚本的起点。但实际写安全脚本时我几乎不会这样裸调requests.get而是会先创建一个Session对象像这样import requests s requests.Session() s.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) resp s.get(https://example.com, timeout10)为什么坚持用Session两个原因。第一Session会自动管理Cookie这对那些需要登录后才能访问的检测目标很关键你可以先请求登录接口后续所有请求自动携带会话凭证。第二Session对象可以统一设置请求头、代理、证书验证方式几十个请求都复用同一套配置代码会清爽很多。在安全检测场景里User-Agent非常重要。很多目标系统会对空UA或默认Python UA做拦截返回403或者一个反爬提示页。调整成一个常见浏览器的UA是脚本健壮性的第一步。如果你要测的目标有WAF甚至还需要在请求头、请求频率、请求路径等多个维度上都做仿真但这就属于对抗层面的内容了这里不展开。2.2 超时与重试机制写自动化脚本时最怕的就是一个请求卡住不动。默认情况下requests没有超时限制一旦目标服务器响应很慢或者连接被黑名单丢弃脚本就会一直挂着整个扫描流程被单点拖死。所以timeout参数绝对不是可选项是必须写的。timeout有讲究它可以接收一个元组比如timeout(3.05, 10)。第一个数字是连接阶段超时第二个是读取阶段超时。连接超时设短一点能快速判断目标不可达读取超时设长一点给慢速服务器足够的响应时间。我这里通常用(3.05, 15)这个值在多数内外网场景下都比较均衡。比基础超时更进一步的是重试机制。网络是脆弱的偶尔一次连接重置、证书抖动、临时限流并不代表目标真的有问题。给脚本加上重试逻辑能显著降低误报率。requests配合urllib3的Retry类用起来非常顺手from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry_strategy Retry( total3, backoff_factor0.5, status_forcelist[429, 500, 502, 503, 504], allowed_methods[GET, POST, HEAD], ) adapter HTTPAdapter(max_retriesretry_strategy) s requests.Session() s.mount(https://, adapter) s.mount(http://, adapter)这里total3指总共重试3次backoff_factor0.5是退避系数重试间隔按0.5 * 2^n秒递增也就是1秒、2秒、4秒这样退避避免重试请求瞬间打爆目标。status_forcelist里我放了429和5xx的常见状态码这些代表目标暂时没准备好或者被限流了值得再试一次。4xx的状态码一律不重试因为那是请求本身出了问题重试多少次结果都一样。2.3 处理429限流与请求频率控制最近看到很多朋友在问exceeded retry limit, last status: 429 too many requests这类报错这其实是请求太频繁被服务端限流了。在安全检测场景里这个问题几乎无法避免你发请求太快目标又不希望被频繁探测就会用429状态码告诉你太急了缓一缓。处理429的思路有两个层面。第一个层面就是上面写的重试机制在status_forcelist里把429加进去配合退避策略自动等待。第二个层面是主动控制请求频率从源头减少429出现的概率这里我用的是一段很简单的节流代码import time class RateLimiter: def __init__(self, interval0.5): self.interval interval self.last_request_time 0 def wait(self): now time.time() elapsed now - self.last_request_time if elapsed self.interval: time.sleep(self.interval - elapsed) self.last_request_time time.time() limiter RateLimiter(interval0.5) for url in target_list: limiter.wait() try: resp s.get(url, timeout(3.05, 15)) # 处理响应 except requests.RequestException as e: # 记录错误 continueRateLimiter的思路很朴素它记录了上一次请求的时间如果两次请求间隔小于设定的阈值就主动sleep补齐。interval0.5对应每秒最多2个请求这个速率对多数小型检测任务足够温和。如果你观察到目标还会返回429就把interval调大到1甚至2宁可慢一点也要保证数据质量。还有一点别忽略429的响应头里通常带有Retry-After字段服务端明确告诉你要等多少秒。如果后续做更复杂的功能可以解析这个字段动态决定等待时间比固定sleep更聪明if resp.status_code 429: retry_after resp.headers.get(Retry-After, 5) retry_after int(retry_after) if retry_after.isdigit() else 5 time.sleep(retry_after)3. 正则表达式在安全脚本中的核心应用3.1 正则构建思路与实际匹配正则表达式是安全脚本里提取和分析信息的核心手段它不是编程语言本身的语法更像一种独立的字符串匹配语言。匹配思路可以简单归纳为三步明确你要找的字符串长什么样把它翻译成正则模式再用re模块去执行。Python的re模块提供了几个实用函数。re.search在字符串里搜索第一个匹配位置re.findall返回所有匹配项的列表re.sub做替换re.match从字符串开头匹配用的场景少一些。安全脚本里最常用的是re.search和re.findall。先看一个最基础的版本号提取案例。假设你在检测一个Web应用响应HTML里有一行meta namegenerator contentWordPress 6.2.1 /你想把版本号6.2.1提取出来正则可以这样写import re content meta namegenerator contentWordPress 6.2.1 / match re.search(rnamegenerator content([^]), content, re.IGNORECASE) if match: print(match.group(1)) # 输出 WordPress 6.2.1这里我用了圆括号()来表示捕获组。([^])的意思是匹配一个或多个不是双引号的字符并把它们单独捕获出来。group(1)取的就是这段捕获内容。为什么要用这种写法而不是直接把整个字符串截出来因为版本号是不确定的你不知道它具体是6.2.1还是6.3用正则描述双引号之间的内容这个特征比死板地找固定字符串要通用得多。3.2 从响应中提取关键信息的模式归纳我在写检测脚本时把常用的正则提取场景归纳成了几类遇到类似需求直接套模式。提取链接是最常见的一类。从一个HTML页面里提取所有URL正则写法分两种简单版是把带引号的href值全部提取出来links re.findall(rhref[](.*?)[], html, re.IGNORECASE)注意到这里用了.*?这是非贪婪匹配。如果把?去掉变成.*贪婪模式会从第一个href一直匹配到最后一个引号结果会吞掉中间所有内容这往往是新手最容易踩的坑。非贪婪的意思是匹配尽可能少的字符遇到第一个结束引号就停下来正好符合提取单个链接的需求。服务端指纹识别也是一大类。很多Web框架会在响应头、页面源码或者特定路径上暴露自己的特征比如某些框架的Cookie字段名、特定的Meta标签、JS文件的路径特征。这类检测通常是两层匹配先用请求拿到特征区域再用正则做精确识别。比如server_header resp.headers.get(Server, ) if re.search(rnginx/[0-9.], server_header, re.IGNORECASE): print(版本指纹: , server_header)再说一个容易被忽视的场景从报错信息或日志里提取IP、邮箱、路径这类结构化数据。IP地址的正则比较长但要记清楚思路就是按四段数字、每段1到3位、点号分隔来描述ip_pattern r\b(?:\d{1,3}\.){3}\d{1,3}\b\b是单词边界防止它从一个更长的字符串中间匹配出来。这个正则不会判断IP是否合法比如999.999.999.999也会匹配上但对于日志分析和信息提取够用。如果要做严格判断还得把每段数字的范围限制在0到255那表达式会复杂很多实际脚本里反而不常用。3.3 正则容易踩的坑与新手的常见误区正则这块我有几个实际踩过的坑值得单独说一下。第一个坑是换行匹配问题。HTML源码通常是多行的而正则里的点号默认不会匹配换行符。如果你想匹配跨多行的内容就必须用re.DOTALL标志让点号连换行也匹配match re.search(rdiv classresult(.*?)/div, html, re.DOTALL)不加这个标志的话即便页面上确实有你要的内容只要它和前缀跨了行就会匹配失败排查起来还很隐蔽。第二个坑是HTML标签的嵌套问题。正则处理简单的成对标签还可以但遇到嵌套结构就会出错。比如匹配div到/div之间的内容如果内部还有嵌套的div非贪婪匹配会错误地在第一个内层div结束处停止。这种场景下正则不是好工具正确做法是改用BeautifulSoup或者lxml。安全脚本里遇到复杂HTML我的建议是能解析就解析正则只用来提取那些边界明确、格式固定的信息。第三个坑是编码问题。这一点和正则关系也很大如果你的正则里写了中文字符但目标页面是GBK编码直接匹配会失败。通常我会在拿到响应文本后先做个编码声明用resp.encoding resp.apparent_encoding让requests自动猜测编码或者直接直接resp.content.decode(utf-8, errorsignore)保证后面正则匹配的是正常字符而不是一堆乱码。4. POC脚本编写实战4.1 一个POC的目标设定与实现代码现在把前两章的东西串起来写一个完整可用的POC。我不打算拿某个具体的漏洞案例来演示那样容易诱导滥用我更想展示一个典型的安全配置检测POC检查目标网站是否设置了常用的安全响应头。这类POC在风险评估、等保测评、上线前检查里都会用到它是修改不了一行业务代码的轻量探测。思路是这样向目标URL发起一个GET请求拿到响应头然后依次检查X-Frame-Options、X-Content-Type-Options、Content-Security-Policy、Strict-Transport-Security、Referrer-Policy这几个安全头是否存在。如果存在再看它的值是否符合预期如果缺失则标记为风险项。完整代码import sys import requests REQUIRED_HEADERS [ X-Frame-Options, X-Content-Type-Options, Content-Security-Policy, Strict-Transport-Security, Referrer-Policy, ] def check_security_headers(url, timeout10): results {} try: resp requests.get(url, timeouttimeout, verifyFalse) except requests.RequestException as e: return {error: str(e)} for header in REQUIRED_HEADERS: value resp.headers.get(header) if value is None: results[header] 缺失 elif header X-Frame-Options and value not in (DENY, SAMEORIGIN): results[header] f配置值不够严格: {value} else: results[header] value results[status_code] resp.status_code return results if __name__ __main__: target sys.argv[1] if len(sys.argv) 1 else https://example.com result check_security_headers(target) if error in result: print(f[Error] {target} - {result[error]}) sys.exit(1) print(f[*] 检测目标: {target}) for header in REQUIRED_HEADERS: print(f {header}: {result[header]}) print(f 响应状态码: {result[status_code]}) print(f[*] 生成报告完毕)这段代码在命令行执行的方式是python poc_headers.py https://example.com。它会产生一个初步判断但还只是个单次探测的POC雏形。注意这里我写了verifyFalse意思是跳过TLS证书验证这是因为很多内网测试目标用的是自签名证书不做这个处理请求会直接被证书错误阻断。它会带来一个安全风险中间人攻击时无法判断目标身份所以这个开关只应该在授权测试环境使用不是让它在公网裸奔。4.2 判断逻辑与结果输出的设计思路POC的输出质量决定了它能不能被规模化使用。我设计判断逻辑的时候遵循了一个原则宁可保守不要误报。所谓保守是指当请求异常、页面内容不确定、响应结构不清晰的时候不要强行给结论。你看上面代码里如果请求抛异常我返回的是一个带error键的字典而不是假装它通过了或者失败了。在扫描场景里无法判断是一个独立的、重要的状态它会被汇总为Error提醒人工复核而不是混入Pass或Fail里污染数据。结果输出层我也建议遵循机器可读、人工可懂两条腿走路的原则。命令行打印是给人看的但后续做数据分析、写报告还需要一个结构化的输出格式。最简单的做法是把单个目标的结果转成JSON批量扫描结束再把所有JSON汇总成CSV或者Excel甚至存入SQLite。这一步的代码不复杂但能不能做好直接区分了能用的脚本和演示用的脚本。一个小技巧在POC初始阶段就给每个检查项分配一个唯一的标识符比如SEC-HDR-001对应X-Frame-Options缺失。这样在批量结果里按标识符聚合就能很快看出哪一类问题在目标群体里最集中这也是我从写报表需求倒推出来的结构设计越早做越省事。4.3 让POC更好用的几个细节一个POC写完之后值得花几分钟做几件小事。第一是支持从文件读取目标列表而不是只能一个参数一个参数地传。可以用argparse做命令行解析-u指定单个URL-f指定目标文件这样脚本单目标批量两种模式都能跑。第二是输出格式增加-o参数支持把结果写进CSV文件。CSV用Python自带的csv模块就好别引第三方库。第三是给脚本加上一个合理的退出码。全Pass退出0有Fail退出1有无法判断的退出2。这个习惯在后续接入CI/CD或者自动化调度系统时特别重要因为外部系统就是根据退出码来判断脚本执行结果的。这三个小改动加起来代码量不到三十行但是能让一个只在自己终端里手动跑的脚本升级成可以被其他系统调度的检测工具。从POC到工程化往往就是从这些不起眼的细节开始的。5. 扫描器基础架构5.1 多目标批量检测的并发设计单个POC写成熟之后下一步就是把它扩展到多目标。最基础的做法是一个for循环挨个跑但实际目标一多就会发现效率不够一个目标如果响应慢要等10秒10个目标就要100秒。这时候需要引入并发。Python的并发方案有threading、multiprocessing和asyncio三种。在requests这个场景里因为requests本身是同步阻塞的库asyncio直接用会很别扭threading反而是最简单直接的选择。为什么不用多进程因为多进程开销大而且这里的主要瓶颈是网络IO等待不是CPU计算线程完全够用。用concurrent.futures.ThreadPoolExecutor是线程方案里最舒服的写法它把线程池的创建、任务提交、结果回收封装得明明白白from concurrent.futures import ThreadPoolExecutor def scan_target(url): result check_security_headers(url) return url, result targets [https://example.com/a, https://example.com/b, https://example.com/c] with ThreadPoolExecutor(max_workers10) as executor: futures [executor.submit(scan_target, url) for url in targets] for future in futures: url, result future.result() print(f{url}: {result})max_workers10我一般这么定如果是内网目标网络延迟低可以调到20如果是公网目标为了避免触发目标限流我建议从5开始观察结果再往上调。这个参数不是越大越好调大了容易触发429反而是得不偿失。5.2 结果存储与去重实战批量扫描会产生大量重复数据比如同一份报告里同一个检测项反复出现比如因为302重定向导致同一个页面被不同的URL重复访问。更常见的情况是扫描过程中某个请求超时了你重试了一次结果这次成功了两个结果就被同时记了下来。去重最简单的策略是在内存里维护一个结果集合。写结果之前先判断是否存在相同签名签名可以用(url, header_name, result_value)这个元组来表示存在就跳过不存在就记录。代码大概是seen set() rows [] for url, result in results: for header, value in result.items(): signature (url, header, value) if signature not in seen: seen.add(signature) rows.append((url, header, value))不过内存去重只适合单次运行的场景如果扫描器要分批跑或者支持断点续跑就得把已处理过的目标持久化下来。最简单的做法是把已扫描的URL追加到一个文本文件里每次开始扫描前先把文件读进内存已经扫过的目标跳过。这个方案去重效果可靠而且文件本身就是后续排查的记录。存储方面我推荐先用CSV到数据量大了再考虑SQLite。CSV的好处是任何工具都能打开Excel、WPS、pandas都能直接处理分析。写入的时候注意统一用utf-8编码别用默认编码不然在Windows上打开CSV中文会乱码。我在代码里固定写一句open(output.csv, w, encodingutf-8-sig)utf-8-sig带BOM前缀Excel打开时就能正确识别编码这是一个很实用的细节。5.3 可扩展的插件化思路扫描器最怕的是每种检测逻辑都写进一个巨大无比的主文件几百行上千行挤在一起改一个地方不小心就影响了另外几个功能。我的做法是设计一个简单的检测项注册机制让每个检测逻辑独立成一个函数主程序只负责调度和汇总结果。思路大概是这样的定义一个PLUGINS列表每个元素是一个函数引用主程序遍历所有插件对每个目标执行所有插件然后汇总输出。新增一个检测项的时候只需要新写一个函数并把函数名追加进列表主程序不用做任何修改。def check_cors(url): # 模拟CORS检测逻辑 return {cors: detected} def check_cookie_secure(url): # 模拟Cookie安全属性检测逻辑 return {cookie_secure: ok} PLUGINS [check_security_headers, check_cors, check_cookie_secure] def run_plugins(url): all_results {} for plugin in PLUGINS: try: result plugin(url) all_results.update(result) except Exception as e: all_results[f{plugin.__name__}_error] str(e) return all_results插件化的好处不只是代码整洁。当你给新员工或者伙伴分配任务时可以让他们在不碰主程序的前提下独立开发检测函数测试好了再注册进来。这个模式在团队协作里非常实用也是我见过很多安全工具都用类似结构的原因。当然具体实现不一定要用类或者复杂的设计模式一个列表一个循环已经能解决大部分问题了。6. 常见问题与实战避坑6.1 网络请求层的典型问题处理网络请求层的故障在写扫描脚本时最常遇到我把它们归类整理成了一张表方便你排查时对照。现象常见原因处理办法请求卡住不返回未设置timeout所有请求都写timeout(3.05, 15)报SSLError目标证书无效或自签名授权测试环境下用verifyFalse并加警告报429 Too Many Requests请求频率过高被限流加退避重试解析Retry-After头调大请求间隔报ConnectionResetError目标主动断开连接捕获后重试一次不行就放弃该目标报Too many concurrent requests并发数过高调低max_workers把并发控制在5到10这里有一个我特别想强调的点像前面热词里提到的exceeded retry limit这类错误很多人会往代码逻辑方向排查但真正原因就是请求太密集了。解决方案不是在重试次数上较劲而是老老实实退避、降低并发、控制频率这三板和网络请求过载问题基本是配套的。SSL证书错误是另一个高频问题。有些自签名证书的测试目标直接用verifyTrue会立刻报错但如果你图省事全局verifyFalse又会在公网测试时带来安全隐患。我建议的做法是按环境区分内网测试项目在配置里统一禁用证书验证公网环境的脚本始终开启验证。不要让图省事把整个安全检测过程本身变得不安全。6.2 正则与文本处理的常见问题正则匹配失败很多时候不是正则本身写错了而是数据源的质量问题。编码不匹配是最常见的坑。服务器返回的是GBK编码的页面requests默认按照HTTP头里的charset去解码但如果页面头部写得不规范解码结果就是一堆乱码你的正则自然匹配不上。处理方式前面提到过resp.encoding resp.apparent_encoding或者用resp.content.decode()手动按字节解码这一类问题都能解决。换行符的问题也很隐蔽。Windows的换行符是\r\nLinux是\n如果你拿一个Linux环境写的正则去匹配Windows换行格式的响应某些情况下会因为\r导致匹配失败。稳妥的做法是拿响应文本后先做一次归一化text resp.text.replace(\r\n, \n)后续所有正则都在这个统一格式的文本上执行。还有一类问题是配置管理工具场景里的比如用正则做配置项替换的时候需要注意保留源格式别把缩进和注释搞乱了。这种场景下我更推荐用专业的配置解析库而不是正则硬上。正则适合做检测和提取做深度修改时要谨慎。6.3 脚本工程的健壮性设计最后聊几个让脚本在长期运行中不崩溃的设计点。第一个是日志系统。别只用print打输出因为print既没有时间戳也不能分级。我用Python自带的logging模块把不同级别的输出分开INFO记录正常的检测进度WARNING记录目标不可达ERROR记录异常堆栈。日志文件和数据文件分开存放排查问题的时候一目了然。这个习惯一开始没养成后面遇到一次跑了三个小时的扫描崩溃、还找不到原因才知道日志的重要性。第二个是异常隔离。在一个批量扫描循环里绝对不能因为一个目标的异常导致整个程序崩溃。每个目标的任务函数都要包一层try...except把异常捕获后记录到日志然后继续处理下一个目标。还要注意requests库的异常体系很丰富RequestException可以接住大部分网络异常但要更细粒度地处理超时、连接错误、代理错误可以分别捕获Timeout、ConnectionError和ProxyError。第三个是优雅退出。扫描跑到一半你想临时终止又不丢已扫描结果这个需求很实际。可以通过捕获KeyboardInterrupt信号来实现当用户按CtrlC的时候记录一个退出标记主循环检查到标记后停止接收新任务等当前正在执行的任务跑完再把已收集的结果写入文件。这样就不会出现跑了一半结果全丢的惨剧。个人实操体验与补充建议写到这里再聊几句我从实际项目中攒下来的体会。最开始我写这类脚本也没想过什么架构设计就是一个requests加一个正则跑通一个目标就觉得很了不起。真正让我改变的是第一次跑批量扫描五十个目标脚本跑了十几分钟结果文件输出了一片乱七八糟的重复项和假错误当时才意识到脚本能跑通和能好用之间还有很长的路。所以如果让我给刚接触POC和扫描器基础的同学一个建议我会说别急着追求花哨的并发框架和扫描器架构先把单目标POC写到自己愿意反复用的程度再逐步加上批量、并发、存储、日志这些能力。每一次改动都要保证前面的功能不出回归性问题这样你的脚本会像滚雪球一样越滚越扎实。我把这个流程走完一遍之后再回头看其他扫描器源码思路突然就通了——很多看起来高深的设计底层其实就是这一章讲的这些基础能力在不同场景下的排列组合。