资讯详情

资讯详情

从零构建BSCAN:链上地址扫描与画像生成实战

1. 项目缘起为什么折腾一个链上地址扫描工具大概两个月前我在做链上数据分析的时候发现了一件非常头疼的事遇到一个不认识的地址想快速搞清楚它到底活跃在哪条链、跟哪些地址打交道多、交易习惯是什么常规手段要么打开商业浏览器一个一个点要么写一次性脚本去节点同步数据前者费手后者费时间。来回折腾几次后我决定自己写一个轻量级的扫描工具代号就叫BSCAN全称是Blockchain Scan目标是做一个能通过地址反查交易记录、归纳交互行为、生成基础画像的命令行工具。这个系列的第一篇我先把项目的地基讲清楚为什么做、技术选型怎么定的、目录结构怎么规划、核心扫描流程怎么一步步实现以及在开发初期踩过的一些坑。适合正在入门链上数据分析、想自己搭一套本地地址扫描方案的开发者参考也适合对区块链数据底层结构好奇的产品和技术朋友围观。先说结论BSCAN不是一个想要替代商业浏览器的重工程它的定位更接近一个个人工具箱——你给它一个地址它从本地节点同步数据把该地址的交易记录、交互对手方、代币流动情况整理成结构化结果输出到SQLite数据库里供后续分析。整个项目用Python实现代码总量控制在两千行内单机就能跑。为什么不做成Web服务因为在项目初期核心矛盾是“快速验证扫描逻辑”而不是“对外提供服务”。Web化意味着要处理接口鉴权、并发请求、前端展示这些都会挤占核心开发时间。先把CLI版本跑通后面有需要再加API层这个节奏我试下来比较舒服。还有一个考虑是数据自主权。依赖第三方API虽然省事但请求配额、响应延迟、字段口径都不受控遇到深度扫描动不动要拉几万条记录时体验会很差。本地同步节点数据虽然前期搭建成本高一些但一旦跑起来查询和分析都随你折腾没有配额烦恼。2. 整体架构与技术选型为什么用这套组合2.1 语言为什么选Python而不是Node或者Go选语言这件事我前后纠结了两天。Go在处理并发和二进制分发上有天然优势Node在异步IO上也不差但最终我还是选了Python理由非常朴素项目里数据处理和格式化的活占了大头Python的交互式开发体验和数据生态最适合这种快速迭代的场景。链上地址扫描的核心工作其实是“清洗数据”从原始区块数据里把转账记录抠出来把事件日志解码成可读字段再聚合统计。这些操作在Python里写起来非常顺手尤其是配合类型注解和pydantic做数据模型验证能省掉大量手工检查的麻烦。另外后续如果要做统计分析pandas、numpy直接就能接上不用跨语言传递数据。有人可能担心Python性能不够用。我的实测结论是在处理单地址扫描的场景下瓶颈几乎都在节点同步和磁盘IO上Python本身的解析开销占比不高。只有当扫描地址量达到上万个、需要全量重建索引时才有必要考虑性能优化那时候再引入并行或多进程也不迟。2.2 数据获取节点RPC直连不折腾第三方聚合API数据获取是BSCAN最底层的模块。我一开始想过用现成的区块浏览器API这样可以跳过同步节点的过程。但实际调研后发现几个主流方案都有绕不开的问题有的限流严格单IP每秒只能请求几次深度扫描一个热门地址可能要跑几小时有的字段格式不统一同一个合约调用在不同浏览器上返回的解析结果差异很大增加了兼容成本。最终我选择直接对接链上节点的JSON-RPC接口。项目初期我接了一个本地同步节点用官方客户端跑在测试网上区块数据实时同步到本地磁盘。之所以用本地节点而不是公共RPC服务是因为扫描过程需要反复回溯历史区块——公共节点通常只允许查最近几百个区块的高度范围这满足不了深度回溯的需求。本地节点一次同步完成后后续所有扫描都走本地RPC速度非常稳定。代码层面用httpx库发POST请求调用eth_getBlockByNumber、eth_getTransactionReceipt等标准接口接口地址配置在环境变量里方便切换主网和测试网。2.3 存储设计SQLite起步别急着上重型数据库很多朋友一上来就规划PostgreSQL加Redis我觉得对于这种个人项目属于过度设计。BSCAN的第一版存储直接用SQLite理由有三一是单文件部署方便整个数据库几十兆拷贝到哪都能用二是事务能力够用扫描时批量写入、一致性要求不高三是后续如果要迁移到PostgreSQLSQL语句基本不用改SQLAlchemy的ORM层已经把差异屏蔽了。当然SQLite也有它的边界。如果后续地址扫描量达到百万级并发读写增多SQLite的单写多读限制就会成为瓶颈。我的规划是当单表记录超过五百万条时再考虑迁移到PostgreSQL同时把历史归档和热数据分离。这个切换动作在设计表结构时就预留了接口字段类型尽量不用SQLite私有特性确保迁移顺畅。数据库表结构我设计了四张核心表addresses存地址的基础标注信息transactions存交易哈希、区块高度和时间戳interactions存地址间的转账关系token_transfers存代币转移事件。每张表都建了必要的索引特别是地址和区块高度的组合索引查询速度直接影响扫描效率。2.4 项目目录结构与模块划分目录结构我用一个比较常规的分层方式核心目标是让新人拿到代码后能快速定位每个功能的位置bscan/ ├── cli.py # 命令行入口参数解析 ├── config.py # 全局配置读环境变量 ├── models.py # 数据模型定义 ├── db.py # 数据库连接与初始化 ├── scanner/ │ ├── __init__.py │ ├── block.py # 区块同步与高度管理 │ ├── transaction.py # 交易解析与转账关系提取 │ └── token.py # 代币事件解析 ├── analyzer/ │ ├── __init__.py │ ├── profile.py # 地址画像生成 │ └── report.py # 分析结果格式化输出 └── utils/ ├── __init__.py └── rpc.py # RPC请求封装与重试逻辑这样拆分的思路是scanner负责从链上取数和基础解析analyzer负责在已经入库的数据上做统计utils提供底层网络能力。上下层之间的数据交互全部通过models.py里定义的数据模型完成避免彼此内部字段的耦合。CLI部分用argparse做参数入口支持三个子命令init用于初始化数据库scan用于执行地址扫描report用于生成分析报告。整个过程不依赖任何Web框架跑起来非常清爽。3. 核心功能实操从零搭出第一个可用的扫描流程3.1 环境准备与关键依赖开发环境我用Python 3.11操作系统是常规的Linux服务器配置了4核8G内存和500G磁盘其中大约一半用于节点数据。依赖库非常克制一共四个pip install httpx web3 sqlalchemy python-dotenv这里说明一下选型原因httpx负责RPC请求支持异步和连接池重试web3库其实是可选的用它主要是方便做事件日志解码如果用原生RPC解析会比较繁琐sqlalchemy作为ORM层管理数据库操作python-dotenv管理环境变量。我不建议在这个阶段引入celery、redis这类重量级组件。地址扫描的耗时主要花在网络请求和数据写入上用一个简单的任务队列——甚至直接同步for循环——就足够了。前期多做减法后面才有空间做加法。3.2 连接本地节点搞定区块高度同步区块高度同步是扫描的地基。首先要确认本地节点已经完成了初始同步这个状态可以通过RPC调用web3_clientVersion接口来判断返回版本信息说明节点服务正常。然后再调用eth_blockNumber拿到当前最新高度。我封装了一个BlockManager类核心逻辑是启动时读取数据库里记录的最新已处理区块高度如果数据库为空则从配置指定的起始高度开始之后周期性轮询节点最新高度发现新高后按顺序逐块处理。这里有个重要的点节点同步是异步的本地磁盘的区块数据可能落后于网络实际状态所以扫描绝对高度要以本地节点返回的最新高度为准不要额外依赖外部API。import httpx import asyncio from time import sleep class BlockManager: def __init__(self, rpc_url, db_session): self.rpc_url rpc_url self.db db_session self.current_height self._load_processed_height() def _rpc_call(self, method, params): payload { jsonrpc: 2.0, method: method, params: params, id: 1 } resp httpx.post(self.rpc_url, jsonpayload, timeout10) resp.raise_for_status() return resp.json()[result] def latest_height(self) - int: return int(self._rpc_call(eth_blockNumber, []), 16) def sync_once(self, batch_size100) - int: target self.latest_height() processed 0 while self.current_height target: end min(self.current_height batch_size, target) for height in range(self.current_height 1, end 1): self._process_block(height) self.current_height end processed end - self.current_height sleep(0.2) return processed def _load_processed_height(self) - int: # 从数据库读取逻辑略 return 0这里batch_size我默认设成100单位是区块高度。需要注意不同链的单块交易量差异很大——有的链平均每块只有几十笔交易有的热门链高峰期单块能到几百笔。如果发现同步速度过慢可以动态调低batch_size让每一轮的RPC请求数和内存占用更平滑。3.3 解析转账事件建立地址映射关系区块链地址扫描的核心不是拿到交易哈希列表而是从交易里解析出“谁给谁转了多少钱”。这一步需要拆开区块数据看每一笔交易内部的Transfer记录。对于主链币的转账需从区块内的transaction对象里读取from、to和value字段。value是以Wei为单位的大整数显示时需要除以10的18次方。对于代币转账要解码交易receipt的logs根据事件签名的topic定位Transfer事件再解析其中的from、to和value。我在transaction.py里写了两个解析函数一个处理主链币一个处理代币。主链币的处理比较直接代币的逻辑需要额外做ABI解码这里用了web3库的decode_event_log方法简化操作def parse_native_transfer(tx) - NativeTransfer: return NativeTransfer( tx_hashtx[hash], block_heightint(tx[blockNumber], 16), from_addrtx[from], to_addrtx[to], valueint(tx[value], 16), timestamptx.get(timestamp) ) def parse_token_transfer(receipt, token_address) - TokenTransfer: for log in receipt[logs]: if log[address].lower() ! token_address.lower(): continue topics log[topics] if len(topics) 3: continue from_addr 0x topics[1][-40:] to_addr 0x topics[2][-40:] value int(log[data], 16) return TokenTransfer( tx_hashreceipt[transactionHash], token_addresstoken_address, from_addrfrom_addr, to_addrto_addr, valuevalue ) return None这里有一个新手容易忽略的细节topics数组里的第一个元素是事件签名哈希后面才是索引参数。Transfer事件的topic0固定是0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef。判断一个日志是否为转账事件时除了检查topics长度还要确认topic0匹配否则会把其他事件当转账解析进来。解析出来的结构体统一落库到interactions表。每次写入前用交易哈希做去重以tx_hash加from_addr加to_addr为唯一键防止同一笔交易在区块回滚后被重复处理。3.4 生成一张基础地址画像当扫描完成一批交易后analyzer模块会根据interactions表的数据为指定地址生成画像。画像包含五个维度累计转账次数、累计转账金额、活跃日期跨度、交互对手方Top名单、活跃时间段的粗略分布。画像的目的是快速回答三个问题这个地址是不是高频交易者它的资金主要流向哪里它的操作习惯是否符合某个模式。这三个问题的答案能帮助我初步判断地址属性——是普通用户、交易所归集地址还是自动化机器人。生成画像的SQL用GROUP BY和ORDER BY组合逻辑集中在profile.py里下面是核心查询逻辑的示意def build_profile(session, address): # 统计转账次数与总额 stats session.execute( text( SELECT COUNT(*) AS cnt, SUM(value) AS total_value FROM interactions WHERE from_addr :addr OR to_addr :addr ), {addr: address.lower()} ).fetchone() # 统计对手方Top10 counterparties session.execute( text( SELECT counterparty, SUM(cnt) AS total FROM ( SELECT to_addr AS counterparty, COUNT(*) AS cnt FROM interactions WHERE from_addr :addr UNION ALL SELECT from_addr AS counterparty, COUNT(*) AS cnt FROM interactions WHERE to_addr :addr ) GROUP BY counterparty ORDER BY total DESC LIMIT 10 ), {addr: address.lower()} ).fetchall() return Profile(addressaddress, statsstats, counterpartiescounterparties)实测下来一个中等活跃度的地址扫描5000个区块的数据从同步到生成画像大约需要两分钟。时间主要花在RPC请求上本地节点响应速度在5毫秒以内但几千个请求累积起来还是有可见的延迟。3.5 参数调优与性能取舍初版跑通后我做了几个关键参数的调整一是RPC请求的超时从默认的30秒改为10秒配合自动重试避免某个请求卡住拖死整个任务二是批量写入从单条insert改成bulk_save_objects每次攒够200条再提交三是同步轮询间隔从0.5秒改成0.2秒测试网响应快可以适当激进一点。这几个调整下来扫描同样规模的区块耗时从原来的三分钟缩短到两分以内。我不是特别追求极致的性能毕竟目标是个人工具稳定和可读性优先级更高。后续如果要大幅提速方向是把RPC请求改成异步并发但异步带来的复杂度提升会让这个项目的入门成本变高我暂时不碰。4. 开发中踩过的坑以及排查实录4.1 RPC限流和数据一致性开发中最先遇到的问题就是RPC限流。本地节点虽然没有严格的IP限制但大流量下的响应延迟会明显变高偶尔直接返回空结果。我最开始没有处理这种情况结果扫描任务跑到一半就停了下来数据库里留下了一半数据。解决办法是给RPC请求加上超时重试和退避机制。具体实现是接收侧如果连续三次无响应任务主动暂停15秒再继续避免把本地节点拖崩。同时在请求头里加了一个自定义标记字段方便在节点日志里追踪。数据一致性方面初始设计里我没有考虑区块回滚的情况。某次测试网操作后我刷新了数据库发现同一笔交易出现在了两个地址的交互记录里排查后确认是节点在同步时先给了一个未完全确认的区块后续回滚后又重新广播了同一笔交易。解决方式是在扫描前做一个区块确认检查只有当区块高度落后于最新高度超过6个确认数时才处理同时为interactions表加唯一索引兜底。4.2 大区块数据处理异常的修复过程有一次扫描时任务停在某个区块高度不动了日志里疯狂打印超时异常。我手动用RPC调了一下那个区块的高度发现该区块里有超过300笔交易单块的数据量是平均水平的五倍以上。我的同步逻辑是逐笔解析入库300笔交易串行处理每笔交易还要查询一次receipt累积下来耗时接近一分钟。优化思路是加缓存同一区块内的交易receipt一次性批量获取避免逐笔请求。不过现实的限制是RPC接口不支持批量返回receipt我只能在应用层做并发用asyncio.gather并发请求多个receipt限制最大并发数为10。修改后该区块的处理时间降到十秒以内整个扫描过程又顺畅了。这里我学到一个经验链上数据的分布极不均匀单个区块的数据量可能有数量级的波动代码设计时不能只看平均值要给极端情况留缓冲。4.3 地址大小写与校验问题地址处理也踩了一个不大不小的坑。链上地址在原始数据里是带大小写的EIP-55格式时同一地址的大小写变体会被视觉识别为不同地址。我在做地址关联统计时没有先做lower()处理导致同一地址被拆成了两个实体画像数据直接失真。修复方案是所有地址入库前统一转小写对外展示时再按EIP-55格式校验还原。这个改动涉及模型层和查询层的所有字段虽然改动面大但一次性解决后就没有再出现过类似问题。4.4 常见问题速查表我整理了一份高频问题的排查对照表给遇到类似问题的朋友做参考现象可能原因处理方式RPC请求超时本地节点负载过高加退避重试降低batch_size同一交易入库两次区块回滚后重复广播唯一索引约束检查确认高度转账金额显示为超大数value未从最小单位换算除以对应精度主链币除1e18地址关联统计失真大小写未统一统一lower后再入库代币事件解析为空topic0不匹配核对事件签名哈希数据库体积膨胀过快全量存了原始日志只存解析后的结构化记录4.5 关于RPC使用的一个额外提醒这里要特别提醒一点扫描过程中如果直接使用公共RPC服务需要在代码里体面地控制请求频率避免给对方服务造成压力。我之前测试过某些公共节点请求频率稍高就会被临时封禁封禁时间从几分钟到几小时不等。自建本地节点虽然前期成本高但从长期使用体验来说更可控也不用担心被封禁影响任务进度。5. 写在BSCAN系列的开头目前进展与后续方向目前BSCAN第一版已经能完成单地址的基础扫描和画像生成代码量在1800行左右核心功能稳定可用。整个开发过程大概用了两周左右——前三天在做技术选型和数据结构设计中间一周集中写完采集和解析逻辑最后三天用来调性能和处理边界情况。我自己在测试中比较满意的一个场景是给一个看起来平平无奇的测试网地址扫描它最近三万条交易记录BSCAN能快速告诉我它的交互对手集中在哪些地址主要活跃时间段分布在几点是否出现了零转账的异常模式。这些信息对于判断地址是否自动化操作很有帮助。不过必须说明我目前的分析只是针对公开链上数据的技术性观察不涉及对任何地址背后主体的身份判断。后续的系列规划大致是这样第二篇重点写代币事件解析和合约调用追踪第三篇写地址聚类和标签体系的建设第四篇如果时间允许会整理一个批量扫描多个地址并输出对比报告的功能。到时候也会把代码完整整理成开源形态发布方便有同样需求的朋友直接拿去用。BSCAN目前还需要持续打磨的短板是底层的数据清洗逻辑对不同链的兼容性——现在主要支持的是EVM系列的公链和测试网非EVM链的数据格式差异较大只能后续单独适配。我个人的计划是等EVM侧的功能稳定后再考虑扩展支持其他类型的链。这个项目从想法到落地最大的体会是链上数据分析的门槛比想象中低主要工作在于定义清楚自己的分析目标以及构建一套可靠的本地数据管道。扫描工具本身不是目的能够持续产出有价值的链上行为洞察才是做这个项目的真正动力。下一篇再见。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →