Python自动化测试脚本开发:从信息收集到Web扫描实战指南
1. 项目概述:为什么选择Python作为网安自动化测试的起点
如果你刚踏入网络安全这个领域,面对海量的漏洞、复杂的系统和日复一日的重复性测试任务,可能会感到无从下手。我刚开始做渗透测试的时候,也经历过这个阶段,每天手动点来点去,效率低不说,还容易遗漏关键点。后来,我意识到,真正的效率提升和技能进阶,是从“会用手”到“会用脑”,再到“会让工具替你干活”的过程。而“让工具替你干活”的核心,就是自动化。在众多编程语言中, Python 以其简洁的语法、丰富的库生态和强大的社区支持,成为了网络安全领域自动化脚本开发的“瑞士军刀”。
这个“基于Python的自动化测试脚本开发”项目,就是为你量身打造的网安实战入门路径。它不是一个空中楼阁的理论课程,而是一个从零开始,手把手教你如何用Python解决实际安全测试问题的实战指南。无论是想自动化你的信息收集流程、批量验证漏洞,还是构建一个简单的漏洞扫描器雏形,Python都能让你事半功倍。这个项目适合所有对网络安全感兴趣,具备基本计算机操作知识,但可能编程经验为零的朋友。你不用害怕代码,我会用最直白的方式,带你理解每一个步骤背后的“为什么”,而不仅仅是“怎么做”。
2. 项目核心思路与工具选型解析
2.1 为什么是“自动化测试”而非“自动化攻击”
首先必须明确一个核心概念:我们开发的是 自动化测试脚本 ,其根本目的是为了提升安全评估的效率、覆盖率和准确性,辅助安全人员更全面地发现潜在风险。它模拟的是安全工程师的测试思维和操作流程,而不是攻击者的恶意行为。所有脚本都应在获得明确授权的测试环境中运行。这个定位决定了我们脚本的设计原则: 可控、可审计、结果明确、对目标影响最小化 。例如,一个端口扫描脚本应该有速率限制,一个目录爆破脚本应该能识别并跳过可能造成服务异常的特定路径。
2.2 Python生态中的“神兵利器”选型
Python在网络安全自动化中如此流行,离不开其背后强大的第三方库。对于入门项目,我们主要聚焦于几个核心领域的库:
-
网络请求与Web交互:Requests & Selenium
- Requests :这是处理HTTP请求的绝对主力。它比Python自带的
urllib库简洁直观太多。无论是向API接口发送数据,还是获取网页内容进行分析,Requests都是首选。在自动化测试中,我们常用它来构造特定的HTTP请求以测试参数注入、越权访问等漏洞。 - Selenium :当测试对象是带有复杂JavaScript交互的现代Web应用时,单纯的HTTP请求就不够用了。
Selenium可以自动化浏览器(如Chrome、Firefox),模拟真实用户点击、输入、提交表单等操作。这对于测试需要登录态、或依赖前端渲染逻辑的安全功能(如CSRF Token验证、前端输入过滤绕过)至关重要。
- Requests :这是处理HTTP请求的绝对主力。它比Python自带的
-
数据解析与处理:BeautifulSoup & lxml
- 从网页或API返回的往往是HTML或XML格式的数据。我们需要从中提取有用的信息,比如链接、表单、特定的文本内容。
BeautifulSoup提供了非常友好的API来解析和遍历HTML/XML文档,即使页面结构混乱,它也能帮你轻松定位元素。对于追求极致性能的场景,lxml是更好的选择。
- 从网页或API返回的往往是HTML或XML格式的数据。我们需要从中提取有用的信息,比如链接、表单、特定的文本内容。
-
网络底层操作:Scapy & Socket
- Scapy :这是一个功能极其强大的交互式数据包处理程序。你可以用它来构造、发送、捕获和解析网络层、传输层的各种数据包(ARP, ICMP, TCP, UDP等)。对于学习网络协议、制作自定义的扫描器或协议模糊测试工具,Scapy是绝佳的学习和实践平台。
- Socket :Python标准库中的
socket模块提供了底层的网络接口。理解socket编程有助于你更深入地理解网络通信的本质,虽然在实际自动化脚本中我们可能更多使用高级库,但掌握基础对排查复杂网络问题很有帮助。
-
并发与效率提升:Threading/Asyncio & Queue
- 自动化测试经常涉及对大量目标(如IP列表、URL列表)执行相同操作。串行执行会非常缓慢。利用
threading(多线程)或asyncio(异步I/O)可以实现并发,大幅提升效率。同时,使用queue(队列)来管理任务分发,可以优雅地协调多个工作线程,避免资源竞争和混乱。
- 自动化测试经常涉及对大量目标(如IP列表、URL列表)执行相同操作。串行执行会非常缓慢。利用
注意 :工具选型不是一成不变的。对于简单的HTTP接口测试,
Requests足矣;对于需要浏览器渲染的复杂Web应用测试,再引入Selenium。入门阶段建议从Requests和BeautifulSoup开始,它们能解决80%的Web信息收集和简单漏洞验证需求。
2.3 开发环境搭建:告别配置噩梦
很多新手卡在第一步:环境配置。这里提供一个稳定、隔离的方案。
- 安装Python :直接从官网下载最新稳定版(如Python 3.10+)。安装时务必勾选“Add Python to PATH”,这样可以在命令行任何位置直接使用
python和pip命令。 - 使用虚拟环境(Virtual Environment) :这是专业开发的必备习惯。它为每个项目创建独立的Python包安装空间,避免不同项目间的依赖冲突。
激活后,命令行提示符前会出现# 在项目目录下,创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS/Linux) source venv/bin/activate(venv)标识。 - 安装核心库 :在激活的虚拟环境中,使用
pip安装。pip install requests beautifulsoup4 selenium scapySelenium需要对应的浏览器驱动(如ChromeDriver),需根据你的浏览器版本单独下载并放入系统PATH,或指定路径。 - 选择代码编辑器 : VS Code 是绝佳选择。轻量、免费、插件生态丰富。安装Python扩展后,能获得代码高亮、智能提示、调试等强大功能。PyCharm是更专业的IDE,功能全面但更重。新手从VS Code开始更友好。
实操心得 :我强烈建议将你的项目目录结构标准化。例如:
web_security_scanner/
├── venv/ # 虚拟环境目录(.gitignore忽略)
├── src/ # 源代码
│ ├── modules/ # 自定义模块,如扫描器、解析器
│ ├── utils/ # 工具函数,如日志、HTTP请求封装
│ └── main.py # 主程序入口
├── config/ # 配置文件
├── logs/ # 日志文件
├── data/ # 数据文件(如字典、目标列表)
├── requirements.txt # 项目依赖列表
└── README.md # 项目说明
这样的结构清晰明了,便于协作和维护。使用 pip freeze > requirements.txt 可以生成依赖清单,别人拿到项目后,一键 pip install -r requirements.txt 就能复现环境。
3. 第一个实战脚本:从信息收集开始
我们从一个最经典、最实用的场景开始: 自动化子域名枚举 。这是渗透测试前期信息收集的关键一步,目标是发现目标主站关联的更多资产,扩大攻击面。
3.1 设计思路拆解
手动子域名枚举费时费力。自动化脚本的核心思路是:
- 数据源 :从多个公开渠道(如搜索引擎接口、证书透明度日志、DNS数据集)获取潜在子域名。
- 解析与去重 :从返回的数据中提取出干净的域名。
- 有效性验证 :对提取出的域名进行DNS解析,确认其是否真实存在(有A记录或CNAME记录)。
- 结果输出 :将有效的子域名列表保存下来,供后续使用。
对于入门项目,我们从最简单的“字典爆破”和“利用公开API”两种方式实现。
3.2 基于字典爆破的子域名枚举
这种方法假设目标使用了常见的子域名命名规则(如 www , mail , dev , test )。我们需要一个子域名字典文件。
核心代码实现与解析 :
import requests
import concurrent.futures
from urllib.parse import urlparse
def read_subdomain_dict(file_path):
"""读取子域名字典文件"""
with open(file_path, 'r', encoding='utf-8') as f:
# 去除每行两端的空白字符,并过滤掉空行
subdomains = [line.strip() for line in f if line.strip()]
return subdomains
def check_subdomain_exists(subdomain, main_domain):
"""检查一个子域名是否存在(通过DNS解析)"""
full_domain = f"{subdomain}.{main_domain}"
try:
# 使用socket.gethostbyname进行A记录查询,简单快速
import socket
socket.gethostbyname(full_domain)
return True, full_domain
except socket.gaierror: # 解析失败
return False, full_domain
def brute_force_subdomains(main_domain, dict_path, max_workers=50):
"""
主函数:并发爆破子域名
:param main_domain: 主域名,如 'example.com'
:param dict_path: 子域名字典文件路径
:param max_workers: 并发线程数
"""
print(f"[*] 开始对 {main_domain} 进行子域名爆破...")
subdomains_list = read_subdomain_dict(dict_path)
found_domains = []
# 使用线程池进行并发查询,极大提升速度
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交所有任务
future_to_sub = {executor.submit(check_subdomain_exists, sub, main_domain): sub for sub in subdomains_list}
# 异步处理完成的任务
for future in concurrent.futures.as_completed(future_to_sub):
sub = future_to_sub[future]
try:
exists, full_domain = future.result()
if exists:
print(f"[+] 发现子域名: {full_domain}")
found_domains.append(full_domain)
# else: 可以静默失败,或记录日志
except Exception as exc:
print(f"[-] 检查 {sub} 时发生异常: {exc}")
print(f"[*] 爆破完成。共发现 {len(found_domains)} 个有效子域名。")
# 将结果保存到文件
with open(f'found_subdomains_{main_domain}.txt', 'w') as f:
for domain in found_domains:
f.write(domain + '\n')
return found_domains
if __name__ == "__main__":
# 使用示例
target_domain = "example.com" # 替换为你的测试目标(必须在授权范围内!)
dictionary_file = "subdomains_top500.txt" # 一个常见的子域名字典
brute_force_subdomains(target_domain, dictionary_file)
代码要点解析与避坑指南 :
- 并发控制(
ThreadPoolExecutor) :这是脚本效率的关键。DNS查询是I/O密集型操作,CPU大部分时间在等待网络响应,非常适合多线程。max_workers不宜设置过高(如超过200),否则可能被目标DNS服务器拒绝或对本地网络造成压力。50-100是个合理的起步值。 - 异常处理 :
socket.gethostbyname在域名无法解析时会抛出socket.gaierror异常。我们用try...except捕获它,并返回False。这是程序健壮性的体现。 - 资源管理 :使用
with语句管理文件打开和线程池,确保资源在使用后被正确关闭,即使中间发生异常。 - 字典质量 :爆破的效果严重依赖于字典。可以从开源项目(如
SecLists)中获取高质量的专用子域名字典。初始阶段,一个包含几百个常见子域名的字典就足够练习。
3.3 利用证书透明度(CT)日志枚举子域名
这是一种更高效、更被动的方式。证书透明度项目要求CA公开其颁发的SSL/TLS证书,这些证书里就包含了域名信息。我们可以从CT日志的公开API中获取数据。
核心代码实现 :
import requests
import json
def get_subdomains_from_crt_sh(domain):
"""从 crt.sh 证书透明度日志查询子域名"""
url = f"https://crt.sh/json?q=%.{domain}&exclude=expired"
subdomains = set() # 使用集合自动去重
try:
response = requests.get(url, timeout=30)
response.raise_for_status() # 如果状态码不是200,抛出HTTPError异常
data = response.json()
for item in data:
# crt.sh 返回的域名可能在‘common_name’或‘name_value’字段
name = item.get('common_name', '')
if name and domain in name:
# 清理数据,可能包含通配符*和换行符
name = name.replace('*.', '').strip()
if name.endswith(domain):
subdomains.add(name)
alt_names = item.get('name_value', '')
if alt_names:
# name_value 可能是一个字符串,用换行符分隔多个域名
for alt_name in alt_names.split('\n'):
alt_name = alt_name.replace('*.', '').strip()
if alt_name and domain in alt_name and alt_name.endswith(domain):
subdomains.add(alt_name)
except requests.exceptions.RequestException as e:
print(f"[-] 请求 crt.sh API 失败: {e}")
return []
except json.JSONDecodeError as e:
print(f"[-] 解析 crt.sh 返回的JSON数据失败: {e}")
return []
return list(subdomains)
# 使用示例
if __name__ == "__main__":
target = "example.com"
found = get_subdomains_from_crt_sh(target)
print(f"[*] 从 crt.sh 发现 {len(found)} 个子域名:")
for sd in sorted(found):
print(f" {sd}")
技术细节与注意事项 :
- API使用 :
crt.sh提供了友好的JSON接口。q=%.{domain}表示查询所有以该域名结尾的证书记录。exclude=expired过滤掉过期证书。 - 数据清洗 :证书中的域名可能包含通配符(
*.)、多余空格或换行。必须进行清洗和规范化,才能得到干净的域名列表。 - 去重 :使用Python的
set集合类型存储结果,因为它自动保证元素唯一性,比用列表手动判断if x not in list高效得多。 - 错误处理 :网络请求可能超时或失败,API返回的也可能不是合法JSON。完善的异常处理能让脚本在部分失败时仍能提供有用信息,而不是直接崩溃。
实操心得 :将这两种方法(字典爆破和CT日志查询)结合起来,效果会更好。可以先从CT日志获取一批高质量的、真实使用过的子域名,再用字典爆破去尝试一些可能未公开或新上线的服务。记得将结果保存到文件,并标注来源,便于后续分析。
4. 进阶实战:构建一个简单的Web目录/文件扫描器
发现子域名后,下一步就是探测这些Web服务上存在哪些敏感目录或文件(如后台登录页、配置文件、备份文件等)。手动访问效率极低,自动化扫描器应运而生。
4.1 扫描器核心架构设计
一个健壮的目录扫描器需要考虑以下几点:
- 并发引擎 :高速发送HTTP请求。
- 字典管理 :加载和遍历目录/文件路径字典。
- 请求定制 :支持自定义请求头(如User-Agent)、HTTP方法(GET/HEAD)、超时和延迟。
- 响应分析 :根据状态码、响应体大小、标题或特定关键词判断路径是否存在。
- 结果过滤与展示 :高亮显示存在的路径,并能够过滤掉常见的404页面。
4.2 实现一个基础并发扫描器
我们使用 requests 和 concurrent.futures 来实现。
import requests
import concurrent.futures
from threading import Lock
import time
import sys
class SimpleDirScanner:
def __init__(self, base_url, wordlist_path, max_threads=30, delay=0):
"""
初始化扫描器
:param base_url: 目标基础URL,如 http://example.com
:param wordlist_path: 目录字典文件路径
:param max_threads: 最大并发线程数
:param delay: 每次请求之间的延迟(秒),用于避免触发WAF
"""
self.base_url = base_url.rstrip('/')
self.wordlist_path = wordlist_path
self.max_threads = max_threads
self.delay = delay
self.found_paths = []
self.lock = Lock() # 用于线程安全地打印和保存结果
self.session = requests.Session() # 使用Session保持连接,提升性能
# 设置一个通用的请求头,模拟浏览器
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
def load_wordlist(self):
"""加载字典文件"""
try:
with open(self.wordlist_path, 'r', encoding='utf-8', errors='ignore') as f:
# 去除空白,并确保路径以/开头(如果需要)
words = [line.strip() for line in f if line.strip()]
# 如果字典里不是以/开头,可以在这里统一加上
# words = ['/' + w if not w.startswith('/') else w for w in words]
return words
except FileNotFoundError:
print(f"[-] 字典文件未找到: {self.wordlist_path}")
sys.exit(1)
def check_path(self, path):
"""检查单个路径是否存在"""
url = f"{self.base_url}{path}"
try:
# 使用HEAD方法,只获取响应头,效率更高。如果目标不支持HEAD,回退到GET。
resp = self.session.head(url, timeout=10, allow_redirects=True)
# 如果HEAD返回405(方法不允许),则尝试GET
if resp.status_code == 405:
resp = self.session.get(url, timeout=10, allow_redirects=False)
except requests.exceptions.RequestException as e:
# 网络错误、超时等
with self.lock:
print(f"[-] 请求失败 {url}: {e}")
return None
# 根据状态码判断。2xx和3xx通常表示存在,403/401表示存在但需要权限,404不存在。
if resp.status_code in [200, 201, 202, 204, 301, 302, 307, 308, 401, 403]:
with self.lock:
print(f"[{resp.status_code}] {url} (Size: {len(resp.content) if resp.content else 'N/A'})")
self.found_paths.append((url, resp.status_code, len(resp.content or b'')))
return (url, resp.status_code)
# 可以添加更多逻辑,比如检查响应内容是否包含特定关键词来判断是否为自定义404页面
return None
def run(self):
"""启动扫描"""
print(f"[*] 开始扫描: {self.base_url}")
print(f"[*] 加载字典: {self.wordlist_path}")
words = self.load_wordlist()
print(f"[*] 字典条目数: {len(words)}")
print(f"[*] 并发线程: {self.max_threads}")
print("-" * 50)
start_time = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_threads) as executor:
# 将每个字典条目作为任务提交
future_to_word = {executor.submit(self.check_path, word): word for word in words}
for future in concurrent.futures.as_completed(future_to_word):
word = future_to_word[future]
# 这里主要处理任务完成的回调,结果已在check_path中打印和保存
if self.delay:
time.sleep(self.delay) # 请求间延迟
elapsed_time = time.time() - start_time
print("-" * 50)
print(f"[*] 扫描完成。耗时: {elapsed_time:.2f} 秒")
print(f"[*] 发现 {len(self.found_paths)} 个有效路径。")
# 将结果保存到文件
if self.found_paths:
output_file = f"scan_results_{self.base_url.replace('://', '_').replace('/', '_')}.txt"
with open(output_file, 'w') as f:
for url, status, size in self.found_paths:
f.write(f"[{status}] {url}\n")
print(f"[*] 结果已保存至: {output_file}")
return self.found_paths
# 使用示例
if __name__ == "__main__":
# !!! 重要:仅在获得明确授权的目标上测试 !!!
target_url = "http://testphp.vulnweb.com" # 这是一个故意设计存在漏洞的练习网站
wordlist = "common_dirs.txt" # 一个常见的目录字典文件
scanner = SimpleDirScanner(target_url, wordlist, max_threads=20, delay=0.1)
scanner.run()
4.3 扫描器优化与高级技巧
上面的基础版本可以工作,但很简陋。一个生产可用的扫描器需要更多考量:
-
智能状态码判断 :
- 有些网站所有不存在的路径都返回200,并显示一个自定义404页面。简单的状态码判断会误报。
- 优化方案 :先访问一个肯定不存在的随机路径(如
/this-path-should-not-exist-12345),获取其响应内容、大小和标题,作为“404样本”。在扫描其他路径时,不仅比较状态码,还比较响应体大小和标题的相似度。如果和“404样本”高度相似,即使状态码是200,也判定为不存在。
-
请求速率限制与随机延迟 :
- 高速连续的请求极易触发目标的防御机制(WAF/IPS),导致IP被封锁。
- 优化方案 :在
check_path函数中引入随机延迟(time.sleep(random.uniform(0.5, 2))),模拟人类操作。更精细的控制可以使用令牌桶等算法。
-
递归扫描与模糊测试 :
- 发现一个目录(如
/admin/)后,可以自动对该目录进行下一层扫描。 - 发现一个带参数的页面(如
/view.php?id=1),可以自动对参数进行模糊测试(SQL注入、XSS等)。 - 这需要更复杂的调度器和状态管理,是进阶方向。
- 发现一个目录(如
-
结果去重与排序 :
- 扫描结果可能包含大量重复或相似条目(如经过多次重定向的同一页面)。
- 优化方案 :对最终发现的URL进行规范化处理,并根据状态码、响应大小或潜在风险进行排序输出。
避坑指南 :
- 法律与授权 :这是红线。绝对不要在未获得书面授权的情况下对任何系统进行扫描。使用本地搭建的靶场(如DVWA、bWAPP)或专门用于安全测试的在线平台(如
testphp.vulnweb.com)进行练习。 - User-Agent轮换 :使用固定的User-Agent容易被识别。可以维护一个列表,在每次请求时随机选择。
- 处理SSL证书警告 :对自签名证书的网站,
requests会报错。可以通过verify=False参数忽略,但会降低安全性。仅在测试环境中使用。 - 超时设置 :务必设置合理的超时(如
timeout=10),避免因个别请求卡住而阻塞整个扫描队列。
5. 集成与扩展:打造你的自动化工作流
单一的脚本能力有限。真正的威力在于将多个小型、专注的脚本组合成一个自动化工作流。
5.1 使用Python调度任务
假设我们有一个信息收集工作流:先枚举子域名,然后对每个发现的子域名进行端口扫描(简易版),最后对开放的Web端口进行目录扫描。
我们可以编写一个主协调脚本:
import subprocess
import json
import time
from pathlib import Path
def run_subdomain_enum(target_domain):
"""调用子域名枚举脚本"""
print(f"[*] 阶段1: 子域名枚举 - {target_domain}")
# 假设我们有一个独立的子域名枚举脚本 sub_enum.py
result_file = f"subdomains_{target_domain}.json"
# 使用subprocess调用其他Python脚本
cmd = ["python", "src/modules/sub_enum.py", "-d", target_domain, "-o", result_file]
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f"[+] 子域名枚举完成,结果保存在 {result_file}")
# 读取结果
with open(result_file, 'r') as f:
return json.load(f) # 假设返回JSON列表
except subprocess.CalledProcessError as e:
print(f"[-] 子域名枚举失败: {e.stderr}")
return []
def simple_port_check(domain, ports=[80, 443, 8080, 8443]):
"""简易端口检查(实际应用建议使用nmap或更专业的库)"""
import socket
open_ports = []
for port in ports:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1) # 1秒超时
result = sock.connect_ex((domain, port))
sock.close()
if result == 0:
open_ports.append(port)
return open_ports
def run_dir_scan(url):
"""调用目录扫描脚本"""
print(f"[*] 对 {url} 进行目录扫描...")
# 假设我们的目录扫描器是一个类,可以导入
from src.modules.dir_scanner import SimpleDirScanner
scanner = SimpleDirScanner(url, "data/common_dirs.txt", max_threads=15, delay=0.2)
return scanner.run()
def main_workflow(target_domain):
"""主工作流"""
print(f"[*] 启动自动化信息收集工作流,目标: {target_domain}")
# 1. 子域名发现
subdomains = run_subdomain_enum(target_domain)
if not subdomains:
print("[-] 未发现子域名,工作流终止。")
return
print(f"[*] 共发现 {len(subdomains)} 个子域名。")
all_results = {}
# 2. 对每个子域名进行简易端口扫描和Web探测
for sub in subdomains[:5]: # 演示用,只处理前5个
print(f"\n[*] 处理子域名: {sub}")
open_ports = simple_port_check(sub)
print(f"[*] 开放端口: {open_ports}")
# 3. 对Web服务(80,443)进行目录扫描
web_results = {}
if 80 in open_ports:
web_results['http'] = run_dir_scan(f"http://{sub}")
if 443 in open_ports:
web_results['https'] = run_dir_scan(f"https://{sub}")
all_results[sub] = {
'open_ports': open_ports,
'web_scan_results': web_results
}
# 短暂暂停,避免请求过快
time.sleep(2)
# 4. 汇总报告
report_file = f"full_scan_report_{target_domain}.json"
with open(report_file, 'w') as f:
json.dump(all_results, f, indent=4)
print(f"\n[*] 工作流执行完毕。完整报告已保存至: {report_file}")
if __name__ == "__main__":
# 同样,仅在授权目标测试
main_workflow("example-test.com")
这个工作流脚本展示了如何将不同的功能模块(子域名枚举、端口检查、目录扫描)串联起来,形成一个完整的自动化侦察流程。在实际中,每个模块都可以更复杂,并且可以加入更多的决策逻辑(比如只对特定CMS进行深度扫描)。
5.2 日志记录与错误处理
一个健壮的自动化系统必须有完善的日志记录,方便回溯和调试。建议使用Python内置的 logging 模块。
import logging
from logging.handlers import RotatingFileHandler
def setup_logging(log_file='automation.log'):
"""配置日志"""
logger = logging.getLogger('WebSecAuto')
logger.setLevel(logging.DEBUG)
# 文件处理器,自动轮转,每个文件10MB,保留5个备份
file_handler = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5)
file_handler.setLevel(logging.INFO)
file_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_format)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING) # 控制台只显示警告及以上
console_format = logging.Formatter('%(levelname)s: %(message)s')
console_handler.setFormatter(console_format)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
# 在脚本中使用
logger = setup_logging()
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
logger.info(f"成功请求 {url}, 状态码: {response.status_code}")
except requests.exceptions.Timeout:
logger.error(f"请求 {url} 超时")
except requests.exceptions.HTTPError as e:
logger.warning(f"请求 {url} 失败,状态码: {e.response.status_code}")
良好的日志可以让你在脚本运行数小时后,依然能清晰知道每个步骤的执行情况,快速定位问题所在。
6. 常见问题与排查技巧实录
在实际开发和使用自动化脚本时,你会遇到各种各样的问题。这里记录一些典型场景和解决思路。
6.1 请求被屏蔽或返回异常结果
- 现象 :脚本突然收不到任何有效响应,或者返回大量验证码页面、错误页面。
- 可能原因 :
- IP被封锁 :请求频率过高,触发了目标的WAF或速率限制规则。
- User-Agent被识别 :使用了脚本默认的User-Agent(如
python-requests)。 - 缺少必要的Cookie或Header :目标网站需要特定的会话Cookie或反爬虫Header(如
Referer,X-Requested-With)。
- 排查与解决 :
- 降低频率 :在请求间增加随机延迟(
time.sleep(random.uniform(1, 3)))。使用更少的并发线程。 - 伪装浏览器 :使用常见的浏览器User-Agent字符串,并可以准备一个列表进行轮换。
- 模拟完整会话 :先用浏览器手动访问目标网站,通过开发者工具(F12)的Network面板,复制一次完整请求的所有Headers(特别是Cookie),在
requests中设置。 - 使用代理池 :如果单个IP被封锁,需要通过代理服务器轮换IP。可以从可靠的供应商获取代理IP列表,并在请求时通过
proxies参数设置。
- 降低频率 :在请求间增加随机延迟(
6.2 脚本运行速度慢或内存占用高
- 现象 :扫描大量目标时,脚本越来越慢,甚至卡死。
- 可能原因 :
- 同步阻塞 :使用了同步且未设置超时的请求,某个请求卡住会阻塞整个线程。
- 内存泄漏 :在循环中不断创建对象而未释放;或者将大量响应内容(如图片)完整加载到内存中。
- 字典过大 :一次性将超大的字典文件读入内存。
- 排查与解决 :
- 设置超时 :为所有网络请求设置合理的
timeout(连接超时和读取超时)。 - 流式处理响应 :对于可能返回大文件的请求,使用
stream=True参数,然后迭代读取响应内容。r = requests.get(url, stream=True) for chunk in r.iter_content(chunk_size=8192): # 处理每一块数据,而不是一次性加载到内存 process_chunk(chunk) - 分块读取字典 :对于非常大的字典文件,不要用
readlines()全部读入。可以使用生成器逐行读取。def read_wordlist_in_chunks(file_path, chunk_size=1000): with open(file_path, 'r') as f: chunk = [] for line in f: chunk.append(line.strip()) if len(chunk) >= chunk_size: yield chunk chunk = [] if chunk: yield chunk
- 设置超时 :为所有网络请求设置合理的
6.3 解析动态网页内容失败
- 现象 :用
requests+BeautifulSoup获取的页面HTML是空的,或者缺少关键数据(如通过AJAX加载的内容)。 - 可能原因 :目标页面严重依赖JavaScript动态渲染内容,初始HTML只是一个空壳。
- 排查与解决 :
- 检查页面源代码 :在浏览器中右键“查看网页源代码”,对比
requests获取的HTML。如果差异很大,说明是动态页面。 - 使用Selenium :切换到
Selenium,它可以驱动真实的浏览器,等待JS执行完毕后再获取完整的DOM。 - 寻找隐藏的API :很多时候,动态数据是通过后台API(通常是JSON格式)获取的。用浏览器的开发者工具,在Network面板的XHR/Fetch标签页中,查找页面加载时发起的API请求,直接模拟这些请求往往更高效。
- 检查页面源代码 :在浏览器中右键“查看网页源代码”,对比
6.4 代码在别人环境无法运行
- 现象 :你的脚本在自己电脑上运行良好,但同事或朋友运行时报错,通常是“ModuleNotFoundError”。
- 原因 :缺少项目依赖库,或者Python版本不兼容。
- 解决 :
- 使用
requirements.txt:这是Python项目的标配。在项目根目录运行pip freeze > requirements.txt生成依赖清单。别人拿到代码后,先pip install -r requirements.txt。 - 明确Python版本 :在
README.md中说明项目所需的Python版本(如Python 3.8+)。 - 考虑使用虚拟环境 :鼓励协作者也使用虚拟环境,避免全局包污染。
- 使用
6.5 如何让脚本更“智能”和“友好”
- 添加进度条 :对于长时间运行的任务,一个进度条能极大提升用户体验。可以使用
tqdm库。from tqdm import tqdm import time wordlist = ["path1", "path2", ...] # 很大的列表 for path in tqdm(wordlist, desc="扫描进度"): check_path(path) time.sleep(0.1) - 支持命令行参数 :使用
argparse库让脚本可以通过命令行接收目标、线程数、字典文件等参数,使其更灵活。import argparse parser = argparse.ArgumentParser(description='简易目录扫描器') parser.add_argument('-u', '--url', required=True, help='目标URL') parser.add_argument('-w', '--wordlist', default='common.txt', help='字典文件路径') parser.add_argument('-t', '--threads', type=int, default=20, help='并发线程数') args = parser.parse_args() # 使用 args.url, args.wordlist, args.threads - 生成结构化的报告 :不要只输出文本。将结果保存为JSON、CSV或HTML格式,便于后续导入其他工具进行分析。
开发自动化测试脚本是一个持续迭代的过程。从解决一个具体的小问题开始,写出第一个能跑的脚本,然后不断遇到问题、解决问题、优化代码、增加功能。在这个过程中,你不仅会熟练掌握Python,更会深刻理解网络协议、Web应用架构和安全测试的思维方式。记住,工具是思维的延伸,清晰的思路永远比复杂的代码更重要。先从模仿和改造现有的简单脚本开始,逐步加入自己的逻辑,最终你将能设计出贴合自己工作流的强大自动化工具。
更多推荐
所有评论(0)