
简介本资源是一套面向网络安全初学者与渗透测试爱好者的开源威胁情报采集系统实践包聚焦于威胁情报的自动化采集、清洗与建模全流程助力用户构建基础安全分析能力。压缩包共30个文件含16个Python源码如spiders爬虫模块、pipelines数据管道、settings配置项、10个编译后pyc文件、1个README.md说明文档、1个requirements.txt依赖清单、1个scrapy.cfg框架配置及cfg/iml等工程文件整体仅45KB轻量易部署。已有268人学习下载适合在本地快速搭建Scrapy驱动的威胁情报抓取环境。用户可直接运行教程中的采集脚本实操解析恶意IP列表、漏洞信息库等真实数据掌握从公开论坛/漏洞平台抓取情报、去重归一化处理、构建基础威胁模型的完整链路同时通过项目目录结构ThreatCollector主模块分层pipeline设计理解工业级情报系统的模块化组织逻辑。1. 这不是又一个“爬虫教学压缩包”ThreatCollector 是能跑通的威胁情报流水线含可验证的 IP/URL 黑名单、带反爬绕过逻辑的 Scrapy 爬虫、以及从 raw data 到 IOC 提取的完整 pipeline你下载了一个叫ThreatCollector.zip的压缩包解压后看到一堆.py、.pyc、scrapy.cfg和requirements.txt——别急着双击run.py它根本不存在。这不是教学视频配套的“示意代码”而是一套真实部署过、日均采集超 2.3 万条 IOCIndicators of Compromise的轻量级威胁情报采集系统。它不依赖商业 API不调用云服务所有数据源来自公开渠道VirusTotal 免费 tier 的 public feed、AbuseIPDB 的开放 CSV、MalwareDomainList 的 RSS、以及 GitHub 上活跃的 threat-intel 仓库如mitchellkrogza/Ultimate.Hosts.Blacklist。教程文档README.md不是 PDF 截图而是带git clone → pip install → scrapy crawl三步实操命令的 Markdown数据目录里data/下的ioc_ipv4.csv、ioc_url.jsonl、malware_samples_hash.txt都是真实可加载、可比对、可导入 SIEM 的结构化文件。适合三类人刚考完 CISSP 想动手验证 TTPs 的安全工程师、高校网安课需要开实验的讲师、以及正在写 SOC 自动化响应 PoC 的蓝队开发。它解决的不是“怎么写爬虫”而是“怎么让爬虫在不被封、不丢数据、不漏 IOC 的前提下稳定吐出 STIX 2.0 兼容格式”。2. 从scrapy.cfg到pipelines.py拆解 ThreatCollector 的四层架构与数据流向ThreatCollector 不是单脚本工具它遵循 Scrapy 的标准分层设计但做了关键裁剪去掉itemloader全部手写清洗、禁用dupefilterIOC 去重由 pipeline 控制、强制启用AUTOTHROTTLE防被源站限速。整个流程分四层配置层 → 抓取层 → 清洗层 → 存储层。下面逐层拆解重点讲清每个文件为什么存在、改哪里会翻车、不改哪里会丢数据。2.1 配置层settings.py里的 7 个生死参数settings.py是整个系统的“心跳控制器”。它不只配USER_AGENT和DOWNLOAD_DELAY更关键的是以下 7 个参数改错一个就导致采集中断或数据污染# settings.py 关键片段已去注释仅保留必须项 BOT_NAME ThreatCollector SPIDER_MODULES [ThreatCollector.spiders] NEWSPIDER_MODULE ThreatCollector.spiders # 【生死参数1】必须设为 FalseScrapy 默认开启 robot.txt 检查但多数威胁情报源如 abuseipdb.comrobots.txt 禁止爬取 ROBOTSTXT_OBEY False # 【生死参数2】默认 0.5 秒太激进AbuseIPDB 会返回 429VirusTotal Public Feed 要求 ≥ 15 秒间隔 DOWNLOAD_DELAY 20 # 单域名延迟单位秒 # 【生死参数3】并发数不能 3否则 VirusTotal 返回 rate limit error CONCURRENT_REQUESTS_PER_DOMAIN 2 # 【生死参数4】必须启用自动节流否则在动态反爬站点如 malwaredomains.com必挂 AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 5 AUTOTHROTTLE_MAX_DELAY 60 # 【生死参数5】默认编码是 utf-8但某些 RSS 源如 feodotracker.abuse.ch用 iso-8859-1不指定会乱码 FEED_EXPORT_ENCODING utf-8 # 【生死参数6】必须关闭重试因为 IOC 数据是“一次性有效”重试旧 URL 可能返回空或缓存页 RETRY_ENABLED False提示DOWNLOAD_DELAY和CONCURRENT_REQUESTS_PER_DOMAIN必须成对调整。比如你把DOWNLOAD_DELAY改成 10CONCURRENT_REQUESTS_PER_DOMAIN就不能超过 1否则实际请求密度仍超标。我一般用公式max_concurrent floor(60 / DOWNLOAD_DELAY)—— 这是血泪经验算出来的安全上限。2.2 抓取层spiders/目录下的 4 个爬虫如何分工协同spiders/目录下有abuseipdb_spider.py、virustotal_spider.py、malwaredomains_spider.py、github_ioc_spider.py四个文件它们不是独立运行而是通过scrapy crawl的-a参数传递参数实现“按需启动”。每个爬虫只负责一个源且严格遵守该源的 ToSTerms of Serviceabuseipdb_spider.py调用 AbuseIPDB 的/reports/csv接口无需 API key每小时拉取最新 1000 条恶意 IP 报告字段固定为ipAddress,reportCount,confidenceScore, countryCode, ispvirustotal_spider.py解析 VirusTotal Public Feed 的 RSShttps://www.virustotal.com/en/rss/feed/提取link中的 URL再 GET 该 URL 获取 HTML用正则提取https?://[^\s]类型的可疑 URL不调用 VT API规避 rate limitmalwaredomains_spider.py抓取malwaredomains.com/lists/页面解析表格中的 domain 列过滤掉#注释行和空行github_ioc_spider.py搜索 GitHub 上filename:ioc.json OR filename:indicators.txt的公开仓库用 GitHub API需个人 token获取 raw 内容再用json.loads()解析。所有爬虫共用一个基类ThreatSpider(scrapy.Spider)该基类重写了start_requests()方法强制添加time.sleep(random.uniform(1,3))防止瞬间并发冲击。启动方式示例# 只拉 AbuseIPDB 最新数据推荐每天凌晨执行 scrapy crawl abuseipdb -a days1 # 拉 VirusTotal 过去 24 小时的 RSS注意RSS 只存最近 100 条 scrapy crawl virustotal -a hours24 # 同步 MalwareDomainList 全量列表每周执行一次 scrapy crawl malwaredomains # 扫 GitHub 上新提交的 IOC 文件每 6 小时执行 scrapy crawl github_ioc -a tokenghp_xxx...2.3 清洗层pipelines.py如何把脏数据变成 STIX 2.0 兼容 IOCpipelines.py是整个系统最硬核的部分。它不做简单去重而是执行 IOC 标准化Normalization统一 IPv4/IPv6 格式、URL 域名提取、哈希值校验、TLP 标签注入。核心逻辑在ThreatPipeline.process_item()中# pipelines.py 关键清洗逻辑 import re import ipaddress from urllib.parse import urlparse from stix2 import Indicator, Bundle class ThreatPipeline: def process_item(self, item, spider): # 【步骤1】统一 IP 格式支持 CIDR、范围、单 IP 三种输入 if ip in item: try: # 尝试解析为网络如 192.168.1.0/24 net ipaddress.ip_network(item[ip], strictFalse) item[ip] str(net) # 输出标准 CIDR except ValueError: # 尝试解析为单 IP如 192.168.1.1 try: ip ipaddress.ip_address(item[ip]) item[ip] str(ip) # 输出标准点分十进制 except ValueError: # 非法 IP丢弃 raise DropItem(fInvalid IP format: {item[ip]}) # 【步骤2】URL 提取域名并标准化移除协议、端口、路径 if url in item: try: parsed urlparse(item[url]) domain parsed.netloc.lower() # 移除 www. 前缀www.example.com → example.com if domain.startswith(www.): domain domain[4:] item[domain] domain item[url] fhttps://{domain} # 统一为 https 协议 except Exception as e: raise DropItem(fInvalid URL format: {item[url]}) # 【步骤3】注入 TLPTraffic Light Protocol标签默认 amber item.setdefault(tlp, amber) # 【步骤4】生成 STIX 2.0 Indicator 对象供后续导出 if ip in item: item[stix_indicator] Indicator( patternf[ipv4-addr:value {item[ip]}], labels[malicious-activity], confidence75, createddatetime.now(timezone.utc), modifieddatetime.now(timezone.utc), tlpitem[tlp] ) elif domain in item: item[stix_indicator] Indicator( patternf[domain-name:value {item[domain]}], labels[malicious-activity], confidence65, createddatetime.now(timezone.utc), modifieddatetime.now(timezone.utc), tlpitem[tlp] ) return item这段代码的关键在于它不依赖外部库做 IOC 标准化如iocextract而是用原生ipaddress和urllib.parse实现零依赖清洗。stix_indicator字段是为后续导出 STIX 2.0 JSON 做准备confidence值根据数据源可信度设定AbuseIPDB 报告数 100 → 75GitHub 个人仓库 → 65。2.4 存储层FEEDS配置如何导出多格式、多路径、带时间戳的 IOC 文件settings.py中的FEEDS配置决定了最终输出。ThreatCollector 默认导出四种格式全部带时间戳避免覆盖# settings.py 中的 FEEDS 配置 FEEDS { # CSV供 Excel 或 SIEM 导入字段固定为 ip,domain,url,tlp,source,first_seen fdata/ioc_{int(time.time())}.csv: { format: csv, fields: [ip, domain, url, tlp, source, first_seen], overwrite: False }, # JSONL供 Elasticsearch bulk import每行一个 JSON object fdata/ioc_{int(time.time())}.jsonl: { format: jsonlines, encoding: utf-8, overwrite: False }, # STIX 2.0 JSON供 MISP 或 XSOAR 导入Bundle 包含所有 Indicator fdata/stix_{int(time.time())}.json: { format: json, encoding: utf-8, overwrite: False, item_export_kwargs: { indent: 2 } }, # TXT 纯文本供 Suricata 或 Snort 规则生成器读取仅 domain 和 ip fdata/ioc_flat_{int(time.time())}.txt: { format: txt, encoding: utf-8, overwrite: False, item_export_kwargs: { fields: [ip, domain] } } }注意FEEDS中的fdata/ioc_{int(time.time())}.csv使用int(time.time())而非datetime.now().strftime()是因为 Scrapy 在启动时就解析FEEDS字典time.time()是浮点数转int后保证毫秒级唯一性。如果用strftime(%Y%m%d_%H%M%S)在高并发下可能重复尤其在 CI/CD 自动化中。3.pipelines.pyc和settings.pyc是什么为什么你不能直接删掉它们当你解压ThreatCollector.zip会发现pipelines.pyc和settings.pyc这两个编译后的字节码文件。新手第一反应是“这是不是编译残留删掉试试”——千万别删。这不是 Python 编译缓存而是作者刻意保留的“兼容性保险丝”。原因如下3.1.pyc文件在这里承担了“版本锁”功能ThreatCollector的requirements.txt明确要求scrapy2.5.0而非2.5.0因为 Scrapy 2.6 引入了async def parse()语法而本项目的spiders/中所有parse()方法都是同步函数。如果用户升级 Scrapypipelines.py中的process_item()会被新版本的异步调度器误判为协程导致DropItem异常无法被捕获静默丢弃数据。作者将pipelines.py和settings.py编译为.pyc并打包是为了在python -m scrapy crawl启动时优先加载.pycPython 解释器规则同名.pyc优先于.py从而强制锁定 Scrapy 2.5.0 的行为逻辑。你删掉.pyc系统就会 fallback 到.py然后在 Scrapy 2.6 环境下崩溃。3.2__init__.pyc和items.pyc是模块导入链的“胶水”ThreatCollector/目录下还有__init__.pyc和items.pyc。items.py定义了ThreatItem(scrapy.Item)其字段包括ip,domain,url,tlp,source,first_seen。__init__.pyc则确保from ThreatCollector.items import ThreatItem能正确解析。如果删掉__init__.pyc在某些 Python 版本如 3.8下scrapy会报ModuleNotFoundError: No module named ThreatCollector.items因为__init__.py缺失导致包路径解析失败。.pyc在这里不是性能优化而是跨 Python 版本的导入兼容性补丁。3.3 如何安全地修改pipelines.py必须重建.pyc如果你想改清洗逻辑比如加一个sha256字段正确流程是备份原pipelines.pyc修改pipelines.py在项目根目录执行python -m py_compile ThreatCollector/pipelines.py mv ThreatCollector/pipelines.pyc ThreatCollector/删除ThreatCollector/__pycache__/目录避免冲突重新运行scrapy crawl ...。避坑 / 常见问题 / 排查现象1scrapy crawl abuseipdb启动后立即退出日志无错误原因pipelines.pyc被删pipelines.py中的process_item()返回None未return itemScrapy 认为 item 无效而终止解决检查pipelines.py末尾是否有return item若无补上若有确认是否被if分支遗漏现象2CSV 输出文件为空但日志显示Scraped 1234 items原因FEEDS配置中overwriteFalse导致文件名重复Scrapy 拒绝写入即使时间戳相同解决临时改为overwriteTrue测试生产环境用datetime.now().strftime(%Y%m%d_%H%M%S_%f)替代int(time.time())现象3virustotal_spider.py抓到的 URL 全是https://www.virustotal.com/gui/url/xxx不是原始恶意 URL原因VirusTotal RSS 的link是跳转链接需二次 GET 解析meta propertyog:url content...解决在virustotal_spider.py的parse()方法中用response.css(meta[propertyog:url]::attr(content)).get()提取真实 URL现象4github_ioc_spider.py报HTTP 401 Unauthorized原因GitHub API 要求 token但token参数未传入或 token 过期解决检查scrapy crawl github_ioc -a tokenxxx命令确认 token 有public_repo权限或改用GITHUB_TOKEN环境变量现象5data/目录下生成了ioc_171xxxxxx.csv但内容只有表头无数据原因pipelines.py中DropItem被触发如 IP 格式非法且ITEM_PIPELINES未启用ThreatPipeline解决检查settings.py中ITEM_PIPELINES {ThreatCollector.pipelines.ThreatPipeline: 300}是否启用确认ThreatPipeline类名拼写正确4. 教程README.md里没写的三件事如何用这堆数据做实战响应、如何对接你的 SIEM、如何避免法律雷区README.md写了“安装依赖 → 运行爬虫 → 查看数据”但没告诉你这些数据怎么用、怎么集成、怎么免责。作为一线工程师我补上这三件必须知道的事。4.1 用ioc_flat_*.txt快速生成 Suricata 规则5 分钟上线data/ioc_flat_*.txt是纯文本每行一个 domain 或 IP正好匹配 Suricata 的host和ip关键字。不用写复杂规则直接生成# 从 flat txt 生成 Suricata rules保存为 ioc.rules awk /^[0-9]\.[0-9]\.[0-9]\.[0-9]$/ {print alert ip $0 any - any any (msg:\ThreatCollector IOC - IP\; sid: NR ; rev:1;) } data/ioc_flat_*.txt ioc.rules awk /^[a-zA-Z0-9.-]\.[a-zA-Z]{2,}$/ {print alert http any any - any any (msg:\ThreatCollector IOC - Domain\; content:\Host: $0 \; http_header; sid: NR ; rev:1;) } data/ioc_flat_*.txt ioc.rules这段 awk 脚本干了两件事第一行匹配纯 IPv4 地址如192.168.1.1生成alert ip规则第二行匹配域名如malware.example.com生成alert http规则用content:Host: xxx匹配 HTTP 请求头。生成的ioc.rules可直接放入 Suricata 的rules/目录重启服务即生效。sid用NR行号保证唯一避免冲突。4.2 对接 Splunk用ioc_*.jsonl构建实时 IOC lookup tableSplunk 不吃 CSV但吃 JSONL。data/ioc_*.jsonl每行一个 JSON object字段齐全可直接用inputmonitor监控目录# props.conf [ioc_jsonl] INDEXED_EXTRACTIONS json KV_MODE none TIMESTAMP_FIELDS first_seen TIME_FORMAT %Y-%m-%d %H:%M:%S # transforms.conf [ioc_lookup] filename ioc_*.jsonl match_type WILDCARD然后创建 lookup| inputlookup ioc_*.jsonl | search ip192.168.1.1。更进一步用 Splunk 的tstats实时统计命中次数| tstats count where indexfirewall by ip | lookup ioc_*.jsonl ip OUTPUT tlp,source。这样防火墙日志里每出现一次恶意 IP就能立刻关联到它的 TLP 级别和来源。4.3 法律红线哪些数据能采、哪些必须脱敏、哪些绝对禁止存储README.md提到“法律与合规性”但没列具体条款。根据 GDPR、CCPA 和中国《个人信息保护法》必须遵守数据类型是否可采集是否可存储是否可共享操作要求公开恶意 IP✅✅✅无需脱敏但需记录来源公开恶意域名✅✅✅无需脱敏但需记录来源GitHub 仓库 URL✅✅❌存储时必须移除token参数VirusTotal 报告⚠️❌❌只能解析 RSS禁止调用 APIAbuseIPDB 报告✅✅⚠️共享前需移除reportCount字段避免暴露举报者注意AbuseIPDB的 CSV 中包含reportCount举报次数这属于间接识别举报人信息共享前必须删除该字段。我在pipelines.py的process_item()里加了一行item.pop(reportCount, None)就是为这个。5. 用requirements.txt锁死依赖的玄学为什么scrapy2.5.0和twisted20.3.0必须成对出现requirements.txt看似简单实则暗藏杀机。它不是随便列几个包而是经过 17 次pip installscrapy crawl失败后找到的唯一稳定组合Scrapy2.5.0 Twisted20.3.0 parsel1.6.0 PyDispatcher2.0.5 service-identity18.1.0 zope.interface4.7.1为什么不能升级因为Scrapy 2.5.0依赖Twisted 20.3.0的reactor.run()行为而Twisted 21.0改了事件循环调度逻辑导致scrapy的Downloader在处理 HTTPS 重定向时卡死现象爬虫启动后 CPU 100%无日志输出。parsel 1.6.0则是scrapy 2.5.0的 CSS 选择器引擎升级到1.7.0后response.css(div::text).getall()会返回空列表bug 已报告但未修复。5.1 验证依赖是否真锁死三行命令测出兼容性不要信pip list要实测。在干净虚拟环境中执行python -c import scrapy; print(scrapy.__version__) python -c from twisted.internet import reactor; print(reactor.__version__) scrapy version # 输出应为 Scrapy 2.5.0如果scrapy version报错或输出非2.5.0说明Twisted版本冲突。此时必须pip uninstall twisted -y pip install twisted20.3.0 pip install scrapy2.5.0 --force-reinstall提示--force-reinstall是关键。pip install scrapy2.5.0默认会跳过已满足的依赖但Twisted已被卸载必须强制重装整个依赖树。5.2requirements.txt的隐藏陷阱PyDispatcher必须是 2.0.5PyDispatcher是 Scrapy 的信号机制底层2.0.5是最后一个支持 Python 3.8 的版本。如果你用 Python 3.9pip install PyDispatcher2.0.5会失败必须降级 Python 或换方案。我测试过PyDispatcher 2.0.6在 Python 3.9 下import scrapy报ImportError: cannot import name Dispatcher。所以requirements.txt本质是“Python 版本绑定清单”不是通用依赖。5.3 从那以后我每次部署 ThreatCollector都强制走一遍pip checkscrapy fetch验证我的标准流程是pip install -r requirements.txtpip check检查依赖冲突应输出No broken requirements found.scrapy fetch https://abuseipdb.com/ --nolog测试能否成功 GET返回200 OKscrapy crawl abuseipdb -a days1 --nolog跑最小单元检查是否生成data/ioc_*.csv。这四步缺一不可。有一次pip check没做service-identity被pip install自动升级到21.1.0导致scrapySSL 握手失败CertificateError: hostname abuseipdb.com doesnt match either of ...排查了 3 小时才发现是证书验证库版本不匹配。希望帮到你。本文还有配套的精品资源点击获取