ARTICLE DETAIL

建站实战干货

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

Python+HTML轻量主机安全态势感知系统

2026/10/3 14:25:04 拓冰建站 浏览量
Python+HTML轻量主机安全态势感知系统 简介本资源是一个基于Python后端与HTML前端技术实现的主机安全态势感知系统项目面向网络安全初学者、运维开发人员及高校信息安全专业学生用于学习主机层安全监控、日志分析与可视化展示的完整实践方案。压缩包共20个文件含8个核心Python源码如app.py、WorldMapChart.py等、9个编译后的pyc文件支持快速部署、1个IP地理信息数据库mmdb支撑攻击源定位、以及.gitignore和说明txt整体20.04MB结构清晰模块覆盖数据采集、状态图表渲染、异常行为分析与Web界面交互。目前已有1204人学习下载读者可直接运行调试掌握psutil系统监控、Flask轻量服务搭建、动态ECharts图表集成、安全事件阈值告警配置等关键技术同时获得可扩展的安全态势仪表盘原型适合作为课程设计、毕设参考或企业级安全工具二次开发基础。1. 主机安全态势感知不是大屏炫技它是一套能自动发现异常进程、爆破尝试和配置漂移的PythonHTML轻量系统很多人一听到“安全态势感知”脑子里立刻浮现出满屏跳动的3D地球、红蓝对抗热力图、实时滚动的IP攻击链——那其实是SOC平台的展示层离一线运维人员的真实需求很远。真正需要它的是每天要巡检20台Linux服务器、手动查ps aux、翻/var/log/auth.log、比对/etc/passwd哈希值的中小团队工程师。这个标题里的“基于Python与HTML的主机安全态势感知系统”说白了就是用Python做采集分析引擎把结果用纯静态HTML页面不依赖Node、不跑Web服务渲染出来双击就能打开查看支持离线归档、支持定时自动生成、支持多主机聚合对比。它不替代SIEM但能让你在没部署ELK、没买商业EDR时5分钟内看清哪台机器正在被暴力SSH爆破、哪个服务意外监听了公网端口、哪个用户最近新增了sudo权限。适合DevOps、SRE、信安初筛岗也适合写毕设——代码全在本地不连外网不调API不碰敏感数据所有逻辑可审计、可调试、可删减。2. 用Python采集真实主机安全指标从进程树到登录日志的6类必采项主机安全态势的根基不是算法而是可信、及时、可比的原始数据。我们不抓包、不装Agent、不改内核模块只用Linux标准命令Python标准库完成采集。核心原则就一条所有采集脚本必须能在无root权限下运行80%的指标如进程列表、端口监听、用户列表仅在需要读取/var/log/或/etc/shadow时提示sudo且明确标注哪些项会失败。2.1 六类基础指标采集逻辑与Python实现我们定义6类不可省略的基础指标每类对应一个独立采集函数输出结构化字典最终统一序列化为JSON供前端消费指标类别采集方式权限要求关键字段示例为什么必采活跃进程ps -eo pid,ppid,user,%cpu,%mem,vsz,rss,tty,stat,time,comm,args --sort-%cpu普通用户pid,user,comm,args,stat含Z/S/R等状态进程异常僵尸、高CPU挖矿、隐藏参数第一线索监听端口ss -tulnlsof -i -P -n补全进程名普通用户ss/ sudolsoflocal_addr,local_port,state,pid,program公网暴露端口、非预期监听如Redis未绑定127.0.0.1用户账户getent passwdlastlog -t 30普通用户username,uid,gid,home_dir,shell,last_login新增高权限账户、长期未登录账号复活SSH爆破痕迹grep Failed password /var/log/auth.log* 2/dev/null | tail -n 100sudo必需ip,user,time,count_per_ip_24h需聚合最常见入侵入口需统计频次而非单条日志关键配置文件哈希sha256sum /etc/passwd /etc/group /etc/sudoers /etc/ssh/sshd_configsudo必需filepath,sha256,mtime配置漂移检测基线比对历史快照磁盘与内存使用率df -h --outputsource,pcent,target | grep -v Use%free -h普通用户filesystem,use_percent,mounted_on,total_mem,used_mem资源耗尽型攻击如日志刷爆磁盘前置信号提示所有采集函数都封装在collector.py中每个函数带超时控制subprocess.run(..., timeout15)避免卡死。失败时返回空列表并记录warning日志不影响其他指标采集。下面给出SSH爆破痕迹采集的核心Python代码它做了三件事解压gz日志、按IP聚合失败次数、过滤掉内网IP减少噪音# collector.py import subprocess import re import gzip import glob from collections import defaultdict from datetime import datetime, timedelta def collect_ssh_bruteforce(): 采集最近24小时SSH爆破失败记录按IP聚合频次 返回: [{ip: 192.168.1.100, count: 42, last_time: 2024-05-20 14:22:03}] # 1. 构建日志路径支持压缩日志 log_paths [/var/log/auth.log] log_paths.extend(glob.glob(/var/log/auth.log.*.gz)) # 2. 提取所有失败密码行 failed_lines [] for path in log_paths: try: if path.endswith(.gz): with gzip.open(path, rt, encodingutf-8) as f: lines f.readlines() else: with open(path, r, encodingutf-8) as f: lines f.readlines() # 匹配 Failed password for ... from 192.168.1.100 port ... pattern rFailed password for .*? from (\d\.\d\.\d\.\d) port for line in lines: if Failed password in line: ip_match re.search(pattern, line) if ip_match: # 只保留公网IP排除10/172.16-31/192.168 ip ip_match.group(1) if not ip.startswith((10., 172.16., 172.17., 172.18., 172.19., 172.20., 172.21., 172.22., 172.23., 172.24., 172.25., 172.26., 172.27., 172.28., 172.29., 172.30., 172.31., 192.168.)): # 提取时间戳auth.log格式May 20 14:22:03 time_str .join(line.split()[:3]) try: dt datetime.strptime(time_str, %b %d %H:%M:%S) # 补全年份假设为当前年 dt dt.replace(yeardatetime.now().year) # 只取24小时内 if dt datetime.now() - timedelta(hours24): failed_lines.append((ip, dt)) except ValueError: continue except (PermissionError, FileNotFoundError, UnicodeDecodeError): continue # 3. 按IP聚合 ip_count defaultdict(int) ip_last_time {} for ip, dt in failed_lines: ip_count[ip] 1 if ip not in ip_last_time or dt ip_last_time[ip]: ip_last_time[ip] dt # 4. 构建结果列表 result [] for ip, count in ip_count.items(): result.append({ ip: ip, count: count, last_time: ip_last_time[ip].strftime(%Y-%m-%d %H:%M:%S) }) return sorted(result, keylambda x: x[count], reverseTrue)这段代码的关键点在于时间处理auth.log没有年份需动态补全且必须校验是否在24小时内否则历史日志会污染结果IP过滤直接用字符串前缀排除私有网段比正则更高效且避免误杀172.10.x.x这类合法公网IP容错设计try/except覆盖PermissionError无sudo、FileNotFoundError日志轮转后不存在、UnicodeDecodeError日志编码异常任一环节失败都不中断整体采集。2.2 多主机批量采集用SSH免密rsync同步执行单台机器采集只是起点。生产环境至少3台起我们用最朴素的方式实现批量本地Python脚本通过SSH连接各主机在远程执行采集命令结果回传JSON。不依赖Ansible、不装额外Agent只要目标机开了SSH且配置了免密登录。# batch_collector.py import paramiko import json import os from concurrent.futures import ThreadPoolExecutor, as_completed def collect_from_host(host_config): host_config: {hostname: web01, ip: 10.0.1.10, user: admin, key_path: /home/user/.ssh/id_rsa} 返回: (hostname, data_dict) 或 (hostname, None) on error client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect( hostnamehost_config[ip], usernamehost_config[user], key_filenamehost_config[key_path], timeout30 ) # 在远程执行采集脚本提前上传collector.py stdin, stdout, stderr client.exec_command(cd /tmp python3 collector.py --json) output stdout.read().decode(utf-8) error stderr.read().decode(utf-8) if error: return host_config[hostname], {error: fRemote exec error: {error[:200]}} data json.loads(output) data[collected_at] datetime.now().isoformat() data[hostname] host_config[hostname] return host_config[hostname], data except Exception as e: return host_config[hostname], {error: str(e)} finally: client.close() # 配置多主机明文存config.json生产环境建议加密 hosts [ {hostname: db01, ip: 10.0.1.20, user: ops, key_path: ./id_rsa}, {hostname: cache01, ip: 10.0.1.30, user: ops, key_path: ./id_rsa}, ] # 并发采集 results {} with ThreadPoolExecutor(max_workers5) as executor: future_to_host {executor.submit(collect_from_host, h): h for h in hosts} for future in as_completed(future_to_host): hostname, data future.result() results[hostname] data # 保存为all_hosts_data.json with open(all_hosts_data.json, w, encodingutf-8) as f: json.dump(results, f, indent2, ensure_asciiFalse)注意collector.py需提前用scp上传到每台目标机的/tmp/目录并确保目标机已安装Python3。--json参数是我们在collector.py中添加的命令行选项用于触发JSON输出模式而非默认的print调试模式。3. 用纯HTMLCSSJS构建零依赖态势看板不跑服务、不连CDN、双击即开很多所谓“Web界面”的安全工具实际依赖Flask启动本地服务、或强制联网加载Bootstrap CDN——这在离线审计、客户内网交付、甚至某些军工涉密场景下根本不可行。本系统坚持纯静态HTML方案所有样式、交互、图表全部打包进单个HTML文件无外部HTTP请求无JavaScript框架用原生ES6Chart.js离线版已下载到本地。3.1 HTML骨架与安全元信息声明我们采用最严格的HTML5标准声明显式指定中文编码、禁止缓存、禁用不安全特性!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 !-- 禁止浏览器自动识别电话/邮箱防止安全信息泄露 -- meta nameformat-detection contenttelephoneno, emailno !-- 强制HTTPS即使本地file://协议也生效 -- meta http-equivContent-Security-Policy contentdefault-src self; script-src self; style-src self; img-src self data:; !-- 禁用缓存确保每次打开都是最新数据 -- meta http-equivCache-Control contentno-cache, no-store, must-revalidate meta http-equivPragma contentno-cache meta http-equivExpires content0 title主机安全态势感知报告 - 2024-05-20/title !-- 内联CSS所有样式写在这里不引用外部.css -- style :root { --primary: #2c3e50; --warn: #e67e22; --danger: #e74c3c; --success: #27ae60; } body { font-family: Segoe UI, Microsoft YaHei, sans-serif; margin: 0; padding: 0; background: #f8f9fa; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin: 16px; overflow: hidden; } .card-header { background: var(--primary); color: white; padding: 12px 16px; font-weight: 600; } .card-body { padding: 16px; } /style /head body header classcard div classcard-header 主机安全态势总览2024-05-20 14:30:00/div div classcard-body pstrong数据来源/strongdb01, cache01, web01共3台/p pstrong最后更新/strongspan idlast_update2024-05-20 14:30:00/span/p pstrong风险摘要/strongspan idrisk_summary发现2台主机存在SSH爆破行为1台监听公网Redis端口/span/p /div /header !-- 后续内容区域 -- div idcontent/div !-- Chart.js 离线版已下载 chart.min.js 到本地 ./js/ 目录 -- script src./js/chart.min.js/script !-- 核心业务JS解析JSON、渲染图表、生成表格 -- script // 所有JS逻辑写在这里不拆分外部文件 /script /body /html提示Content-Security-Policy头是安全底线它禁止任何外部资源加载确保HTML文件离线打开时不会偷偷请求CDN或上报数据format-detection防止手机浏览器将IP地址误识别为电话号码而添加点击拨号功能——这是真实踩过的坑。3.2 用Chart.js绘制三类核心态势图表我们只用三个图表讲清全局风险主机风险分布饼图、爆破IP地理热力图模拟、进程CPU占用TOP5柱状图。所有数据来自all_hosts_data.json通过fetch()读取注意file://协议下Chrome会拒绝fetch本地文件解决方案见避坑章节。!-- 在 body 中插入图表容器 -- div classcard div classcard-header 主机风险等级分布/div div classcard-body canvas idriskChart height200/canvas /div /div script // 1. 加载JSON数据兼容file://协议 function loadJsonData() { // 方案A如果部署在Web服务器上直接fetch if (window.location.protocol ! file:) { return fetch(all_hosts_data.json).then(r r.json()); } // 方案Bfile://协议下用XMLHttpRequest绕过CORS仅Chrome 93支持 return new Promise((resolve, reject) { const xhr new XMLHttpRequest(); xhr.open(GET, all_hosts_data.json, true); xhr.responseType text; xhr.onload function() { if (xhr.status 200) { try { resolve(JSON.parse(xhr.responseText)); } catch (e) { reject(e); } } else { reject(new Error(Load JSON failed: xhr.status)); } }; xhr.onerror reject; xhr.send(); }); } // 2. 渲染风险分布饼图 async function renderRiskChart() { const data await loadJsonData(); // 计算每台主机的风险分数规则爆破IP数×10 异常进程数×5 公网端口数×20 const riskScores Object.entries(data).map(([hostname, hostData]) { const bruteCount (hostData.ssh_bruteforce || []).reduce((sum, item) sum item.count, 0); const abnormalProcs (hostData.processes || []).filter(p p.stat.includes(Z) || // 僵尸进程 (p.user ! root p.comm minerd) || // 常见挖矿进程名 p.args.toLowerCase().includes(xmrig) // 挖矿参数 ).length; const publicPorts (hostData.listening_ports || []).filter(p p.local_addr * || p.local_addr 0.0.0.0 ).length; const score bruteCount * 10 abnormalProcs * 5 publicPorts * 20; return { hostname, score }; }); // 分级0-10低危11-50中危51高危 const levels { low: 0, medium: 0, high: 0 }; riskScores.forEach(({score}) { if (score 10) levels.low; else if (score 50) levels.medium; else levels.high; }); const ctx document.getElementById(riskChart).getContext(2d); new Chart(ctx, { type: pie, data: { labels: [低危, 中危, 高危], datasets: [{ data: [levels.low, levels.medium, levels.high], backgroundColor: [#27ae60, #e67e22, #e74c3c] }] }, options: { responsive: true, plugins: { legend: { position: bottom } } } }); } renderRiskChart(); /script这段代码的关键设计双协议兼容自动检测file://协议并切换XMLHttpRequest加载避免Chrome报net::ERR_FILE_NOT_FOUND风险评分规则透明不黑盒分数计算逻辑完全开放运维可自行调整权重如某客户认为爆破比挖矿更紧急就把bruteCount * 10改成* 20进程异常判定明确不仅看statZ还结合进程名minerd和参数xmrig避免误报zombie.sh这类正常脚本。3.3 表格渲染用原生DOM操作生成可排序、可搜索的安全详情表态势看板必须支持快速定位问题主机。我们放弃jQuery用原生document.createElement动态生成表格并添加排序和搜索功能div classcard div classcard-header SSH爆破高危IP按频次降序/div div classcard-body input typetext idsearchIp placeholder输入IP搜索... stylewidth: 200px; padding: 6px; margin-bottom: 12px; table idbruteTable classtable table-striped thead tr th onclicksortTable(0)IP span idsortIcon0↕/span/th th onclicksortTable(1)频次/th th onclicksortTable(2)最后出现时间/th th关联主机/th /tr /thead tbody idbruteTbody !-- 数据由JS填充 -- /tbody /table /div /div// 表格排序函数支持多列 let sortDirection {}; function sortTable(colIndex) { const tbody document.getElementById(bruteTbody); const rows Array.from(tbody.querySelectorAll(tr)); // 切换升/降序 sortDirection[colIndex] sortDirection[colIndex] asc ? desc : asc; const dir sortDirection[colIndex]; // 更新图标 document.querySelectorAll(#sortIcon0, #sortIcon1, #sortIcon2).forEach(el el.textContent ↕); document.getElementById(sortIcon${colIndex}).textContent dir asc ? ↑ : ↓; rows.sort((a, b) { const aVal a.cells[colIndex].textContent.trim(); const bVal b.cells[colIndex].textContent.trim(); let diff 0; if (colIndex 1) { // 频次列按数字排序 diff parseInt(aVal) - parseInt(bVal); } else { // 其他列按字符串排序 diff aVal.localeCompare(bVal); } return dir asc ? diff : -diff; }); rows.forEach(row tbody.appendChild(row)); // 重排DOM } // 搜索功能 document.getElementById(searchIp).addEventListener(input, function() { const filter this.value.toLowerCase(); const rows document.querySelectorAll(#bruteTbody tr); rows.forEach(row { const ipCell row.cells[0]; row.style.display ipCell ipCell.textContent.toLowerCase().includes(filter) ? : none; }); }); // 渲染表格数据在loadJsonData后调用 function renderBruteTable(data) { const tbody document.getElementById(bruteTbody); tbody.innerHTML ; // 清空 // 收集所有爆破IP跨主机去重 const allBrute []; Object.entries(data).forEach(([hostname, hostData]) { if (hostData.ssh_bruteforce) { hostData.ssh_bruteforce.forEach(item { allBrute.push({...item, hostname}); }); } }); // 按频次降序 allBrute.sort((a, b) b.count - a.count); // 渲染前10条 allBrute.slice(0, 10).forEach(item { const tr document.createElement(tr); tr.innerHTML td${item.ip}/td td${item.count}/td td${item.last_time}/td td${item.hostname}/td ; tbody.appendChild(tr); }); }注意sortTable函数支持点击表头切换升/降序图标实时更新搜索框实时过滤无需回车。所有逻辑都在一个HTML文件内无外部依赖。4. 把Python采集结果注入HTML用Jinja2模板生成可离线部署的报告页纯手写HTMLJS虽然可控但维护成本高——每次加一个新指标就要改HTML、改JS、改CSS。我们引入Jinja2模板引擎用Python脚本将all_hosts_data.json注入预定义HTML模板生成最终的report.html。这样前端呈现逻辑和数据采集逻辑彻底分离且生成的HTML仍是100%静态、零依赖。4.1 设计可复用的Jinja2模板结构创建templates/report.html使用Jinja2语法占位关键设计点所有图表数据用{{ data }}注入JS中直接const data {{ data|tojson }};tojson过滤器自动转义防XSSCSS内联JS内联Chart.js离线路径硬编码动态生成主机卡片用{% for host in hosts %}循环渲染每台主机的详情区块风险摘要自动计算模板内用{% set risk_summary ... %}做简单逻辑避免JS里重复计算。!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 meta nameformat-detection contenttelephoneno, emailno meta http-equivContent-Security-Policy contentdefault-src self; script-src self; style-src self; img-src self data:; meta http-equivCache-Control contentno-cache, no-store, must-revalidate title主机安全态势感知报告 - {{ report_time }}/title style :root { --primary: #2c3e50; --warn: #e67e22; --danger: #e74c3c; --success: #27ae60; } body { font-family: Segoe UI, Microsoft YaHei, sans-serif; margin: 0; padding: 0; background: #f8f9fa; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin: 16px; overflow: hidden; } .card-header { background: var(--primary); color: white; padding: 12px 16px; font-weight: 600; } .card-body { padding: 16px; } /style /head body header classcard div classcard-header 主机安全态势总览{{ report_time }}/div div classcard-body pstrong数据来源/strong{% for host in hosts %}{{ host.hostname }}{% if not loop.last %}, {% endif %}{% endfor %}共{{ hosts|length }}台/p pstrong最后更新/strongspan idlast_update{{ report_time }}/span/p pstrong风险摘要/strongspan idrisk_summary{{ risk_summary }}/span/p /div /header !-- 风险分布饼图 -- div classcard div classcard-header 主机风险等级分布/div div classcard-body canvas idriskChart height200/canvas /div /div !-- 爆破IP表 -- div classcard div classcard-header SSH爆破高危IP按频次降序/div div classcard-body input typetext idsearchIp placeholder输入IP搜索... stylewidth: 200px; padding: 6px; margin-bottom: 12px; table classtable table-striped thead tr th onclicksortTable(0)IP span idsortIcon0↕/span/th th onclicksortTable(1)频次/th th onclicksortTable(2)最后出现时间/th th关联主机/th /tr /thead tbody idbruteTbody {% for item in all_brute %} tr td{{ item.ip }}/td td{{ item.count }}/td td{{ item.last_time }}/td td{{ item.hostname }}/td /tr {% endfor %} /tbody /table /div /div !-- 每台主机详情卡片 -- {% for host in hosts %} div classcard div classcard-header️ {{ host.hostname }}{{ host.ip }}/div div classcard-body h4⚠️ 风险项/h4 ul {% if host.ssh_bruteforce and host.ssh_bruteforce|length 0 %} li发现 {{ host.ssh_bruteforce|length }} 个爆破IP最高频次 {{ host.ssh_bruteforce|sort(attributecount, reversetrue)|first|default({count:0}).count }}/li {% endif %} {% if host.listening_ports %} {% set public_ports host.listening_ports|selectattr(local_addr, equalto, *)|list|length %} {% if public_ports 0 %} li监听 {{ public_ports }} 个公网端口如 {{ host.listening_ports|selectattr(local_addr, equalto, *)|first|default({program:}).program }}/li {% endif %} {% endif %} /ul h4 关键指标/h4 table classtable table-sm trtd磁盘使用率/tdtd{{ host.disk_usage|first|default({use_percent:N/A}).use_percent }}/td/tr trtd内存使用率/tdtd{{ host.memory_usage|first|default({used_mem:N/A}).used_mem }}/td/tr trtd活跃进程数/tdtd{{ host.processes|length }}/td/tr /table /div /div {% endfor %} script src./js/chart.min.js/script script // 图表数据注入 const data {{ data|tojson }}; // 渲染风险饼图 const riskCtx document.getElementById(riskChart).getContext(2d); const riskData { labels: [低危, 中危, 高危], datasets: [{ data: [{{ low_count }}, {{ medium_count }}, {{ high_count }}], backgroundColor: [#27ae60, #e67e22, #e74c3c] }] }; new Chart(riskCtx, { type: pie, data: riskData, options: { responsive: true, plugins: { legend: { position: bottom } } } }); // 排序函数同前 let sortDirection {}; function sortTable(colIndex) { // ...同3.3节代码此处省略 } // 搜索函数同前 document.getElementById(searchIp).addEventListener(input, function() { // ...同3.3节代码此处省略 }); /script /body /html4.2 Python脚本一键生成report.html创建generate_report.py读取all_hosts_data.json计算风险摘要调用Jinja2渲染# generate_report.py import json import os from datetime import datetime from jinja2 import Environment, FileSystemLoader def calculate_risk_summary(data): 计算全局风险摘要文本 total_brute 0 public_port_hosts [] for hostname, host_data in data.items(): if host_data.get(ssh_bruteforce): total_brute len(host_data[ssh_bruteforce]) if host_data.get(listening_ports): public_ports [p for p in host_data[listening_ports] if p.get(local_addr) in [*, 0.0.0.0]] if public_ports: public_port_hosts.append(hostname) parts [] if total_brute 0: parts.append(f发现{total_brute}个SSH爆破IP) if public_port_hosts: parts.append(f{len(public_port_hosts)}台主机监听公网端口{, .join(public_port_hosts)}) return .join(parts) if parts else 暂未发现高危风险 def main(): # 1. 加载数据 with open(all_hosts_data.json, r, encodingutf-8) as f: data json.load(f) # 2. 计算风险分数与分级 risk_scores [] for hostname, host_data in data.items(): brute_count len(host_data.get(ssh_bruteforce, [])) abnormal_procs len([p for p in host_data.get(processes, []) if p.get(stat, ).startswith(Z) or (minerd in p.get(comm, ).lower()) or (xmrig in p.get(args, ).lower())]) public_ports len([p for p in host_data.get(listening_ports, []) if p.get(local_addr) in [*, 0.0.0.0]]) score brute_count * 10 abnormal_procs * 5 public_ports * 20 risk_scores.append({hostname: hostname, score: score}) low_count sum(1 for s in risk_scores if s[score] 10) medium_count sum(1 for s in risk_scores if 11 s[score] 50) high_count sum(1 for s in risk_scores if s[score] 50) # 3. 收集所有爆破IP跨主机 all_brute [] for hostname, host_data in data.items(): if host_data.get(ssh_bruteforce): for item in host_data[ssh_bruteforce]: all_brute.append({**item, hostname: hostname}) all_brute p a hrefhttps://download.csdn.net/download/weixin_42848583/85111605 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p