关于批量查看账户地址持有USDT的余额
比如要查询0x55d398326f99059ff775485246999027b3197955这个地址在BSC链上的地址。
币安的 API 不提供 EVM 地址(如 0x 开头地址)直接查询余额的接口,因为这是链上资产,属于链上钱包的范畴。你要查某个地址是否持有 USDT,需要调用的是 区块链节点服务(如 BSC 上的 RPC)或使用区块链数据 API 服务提供商(如 BscScan、Ankr、Infura 等。
excel读取和存储类
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.stream.StreamSupport;
import java.util.stream.Collectors;
import java.text.DecimalFormat;
public class ExcelFirstColumnReader {
/**
* 读取Excel文件第一列数据
* @param filePath Excel文件路径
* @param sheetIndex 工作表索引(从0开始,默认第一个工作表)
* @param skipHeader 是否跳过表头(第一行)
* @return 第一列数据列表
*/
public static List<String> readFirstColumn(String filePath, int sheetIndex, boolean skipHeader) {
List<String> firstColumnData = new LinkedList<>();
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = createWorkbook(fis, filePath)) {
Sheet sheet = workbook.getSheetAt(sheetIndex);
// 使用Java 8 Stream处理行数据
firstColumnData = StreamSupport.stream(sheet.spliterator(), false)
.skip(skipHeader ? 1 : 0) // 根据参数决定是否跳过表头
.filter(row -> row != null && row.getCell(0) != null) // 过滤空行和空单元格
.map(row -> getCellValueAsString(row.getCell(0))) // 获取第一列单元格值
.filter(value -> value != null && !value.trim().isEmpty()) // 过滤空值
.collect(Collectors.toList());
} catch (IOException e) {
System.err.println("读取Excel文件时发生错误: " + e.getMessage());
e.printStackTrace();
}
return firstColumnData;
}
/**
* 读取Excel文件第一列数据(默认读取第一个工作表,跳过表头)
* @param filePath Excel文件路径
* @return 第一列数据列表
*/
public static List<String> readFirstColumn(String filePath) {
return readFirstColumn(filePath, 0, true);
}
/**
* 读取Excel文件第一列数据(指定是否跳过表头)
* @param filePath Excel文件路径
* @param skipHeader 是否跳过表头
* @return 第一列数据列表
*/
public static List<String> readFirstColumn(String filePath, boolean skipHeader) {
return readFirstColumn(filePath, 0, skipHeader);
}
/**
* 根据文件扩展名创建对应的Workbook
*/
private static Workbook createWorkbook(FileInputStream fis, String filePath) throws IOException {
if (filePath.toLowerCase().endsWith(".xlsx")) {
return new XSSFWorkbook(fis);
} else if (filePath.toLowerCase().endsWith(".xls")) {
return new HSSFWorkbook(fis);
} else {
throw new IllegalArgumentException("不支持的文件格式,请使用.xls或.xlsx文件");
}
}
/**
* 将单元格值转换为字符串
*/
private static String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
// 处理数字,避免科学计数法
DecimalFormat df = new DecimalFormat("#.##########");
return df.format(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try {
// 尝试获取公式计算结果
return getCellValueAsString(cell);
} catch (Exception e) {
return cell.getCellFormula();
}
case BLANK:
case _NONE:
default:
return "";
}
}
/**
* 打印第一列数据(用于调试)
*/
public static void printFirstColumn(List<String> data) {
System.out.println("=== Excel第一列数据 ===");
data.stream()
.forEach(System.out::println);
System.out.println("=== 共" + data.size() + "条数据 ===");
}
// 使用示例
public static void main(String[] args) {
String filePath = "D:\\candy\\无标题.xls"; // 替换为你的Excel文件路径
try {
// 方式1:默认读取(跳过表头)
List<String> data1 = readFirstColumn(filePath);
System.out.println("方式1 - 默认读取(跳过表头):");
// printFirstColumn(data1);
// // 方式2:包含表头
// List<String> data2 = readFirstColumn(filePath, false);
// System.out.println("\n方式2 - 包含表头:");
// printFirstColumn(data2);
//
// // 方式3:指定工作表和是否跳过表头
// List<String> data3 = readFirstColumn(filePath, 0, true);
// System.out.println("\n方式3 - 指定参数:");
// printFirstColumn(data3);
//
// // Java 8 Stream操作示例
// System.out.println("\n=== Stream操作示例 ===");
// List<String> filteredData = data1.stream()
// .filter(item -> item.length() > 3) // 过滤长度大于3的数据
// .map(String::toUpperCase) // 转为大写
// .distinct() // 去重
// .sorted() // 排序
// .collect(Collectors.toList());
//
// System.out.println("过滤后的数据:");
// filteredData.forEach(System.out::println);
} catch (Exception e) {
System.err.println("处理Excel文件时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 支持追加写入的Excel工具方法
*/
public static boolean writeMapToExcel(Map<String, String> data, String filePath, String sheetName) {
return writeMapToExcel(data, filePath, sheetName, false);
}
/**
* 写入Map数据到Excel文件,支持追加模式
* @param data 要写入的数据
* @param filePath 文件路径
* @param sheetName 工作表名称
* @param appendMode 是否为追加模式
* @return 写入是否成功
*/
public static boolean writeMapToExcel(Map<String, String> data, String filePath, String sheetName, boolean appendMode) {
if (data == null || data.isEmpty()) {
System.err.println("数据为空,无法写入Excel文件");
return false;
}
// 确保文件扩展名为.xls
if (!filePath.toLowerCase().endsWith(".xls")) {
filePath = filePath.replaceAll("\\.[^.]*$", "") + ".xls";
}
File file = new File(filePath);
Workbook workbook = null;
Sheet sheet = null;
int startRowIndex = 1; // 默认从第二行开始写入数据(第一行是表头)
try {
// 根据追加模式决定是创建新文件还是读取已存在的文件
if (appendMode && file.exists()) {
// 追加模式:读取已存在的文件
try (FileInputStream fis = new FileInputStream(file)) {
workbook = new HSSFWorkbook(fis);
// 查找或创建指定的工作表
sheet = workbook.getSheet(sheetName != null ? sheetName : "Sheet1");
if (sheet == null) {
sheet = workbook.createSheet(sheetName != null ? sheetName : "Sheet1");
// 新创建的sheet需要添加表头
createHeaders(workbook, sheet);
startRowIndex = 1;
} else {
// 已存在的sheet,找到最后一行的位置
startRowIndex = sheet.getLastRowNum() + 1;
// 如果文件存在但没有数据行(只有表头或空文件),从第二行开始
if (startRowIndex <= 1) {
// 检查是否有表头,如果没有则创建
Row firstRow = sheet.getRow(0);
if (firstRow == null || firstRow.getLastCellNum() < 2) {
createHeaders(workbook, sheet);
}
startRowIndex = 1;
}
}
}
} else {
// 覆盖模式:创建新文件
workbook = new HSSFWorkbook();
sheet = workbook.createSheet(sheetName != null ? sheetName : "Sheet1");
createHeaders(workbook, sheet);
startRowIndex = 1;
}
// 写入数据
List<Map.Entry<String, String>> entries = data.entrySet()
.stream()
.collect(Collectors.toList());
for (int i = 0; i < entries.size(); i++) {
Map.Entry<String, String> entry = entries.get(i);
Row dataRow = sheet.createRow(startRowIndex + i);
// 写入address列
Cell addressCell = dataRow.createCell(0);
addressCell.setCellValue(entry.getKey() != null ? entry.getKey() : "");
// 写入balance列
Cell balanceCell = dataRow.createCell(1);
balanceCell.setCellValue(entry.getValue() != null ? entry.getValue() : "");
}
// 自动调整列宽(只在数据较少时执行,避免性能问题)
if (data.size() < 1000) {
sheet.autoSizeColumn(0);
sheet.autoSizeColumn(1);
}
// 写入文件
try (FileOutputStream fos = new FileOutputStream(file)) {
workbook.write(fos);
}
// 计算总行数(不包括表头)
int totalDataRows = sheet.getLastRowNum();
System.out.println(String.format("%s %d 条数据到文件: %s (当前文件共有 %d 条数据)",
appendMode ? "成功追加" : "成功写入",
data.size(),
filePath,
totalDataRows));
return true;
} catch (IOException e) {
System.err.println("操作Excel文件时发生错误: " + e.getMessage());
e.printStackTrace();
return false;
} finally {
// 关闭workbook
if (workbook != null) {
try {
workbook.close();
} catch (IOException e) {
System.err.println("关闭workbook时发生错误: " + e.getMessage());
}
}
}
}
/**
* 创建Excel表头
* @param workbook 工作簿
* @param sheet 工作表
*/
private static void createHeaders(Workbook workbook, Sheet sheet) {
// 创建表头样式
CellStyle headerStyle = createHeaderStyle(workbook);
// 创建表头行
Row headerRow = sheet.createRow(0);
Cell addressHeader = headerRow.createCell(0);
addressHeader.setCellValue("address");
addressHeader.setCellStyle(headerStyle);
Cell balanceHeader = headerRow.createCell(1);
balanceHeader.setCellValue("balance");
balanceHeader.setCellStyle(headerStyle);
}
/**
* 创建表头样式
* @param workbook 工作簿
* @return 表头样式
*/
private static CellStyle createHeaderStyle(Workbook workbook) {
CellStyle headerStyle = workbook.createCellStyle();
// 设置字体
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerFont.setFontHeightInPoints((short) 12);
headerStyle.setFont(headerFont);
// 设置背景色
headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 设置边框
headerStyle.setBorderTop(BorderStyle.THIN);
headerStyle.setBorderBottom(BorderStyle.THIN);
headerStyle.setBorderLeft(BorderStyle.THIN);
headerStyle.setBorderRight(BorderStyle.THIN);
// 设置对齐方式
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
return headerStyle;
}
/**
* 便捷方法:追加写入数据到Excel
* @param data 要追加的数据
* @param filePath 文件路径
* @param sheetName 工作表名称
* @return 追加是否成功
*/
public static boolean appendMapToExcel(Map<String, String> data, String filePath, String sheetName) {
return writeMapToExcel(data, filePath, sheetName, true);
}
/**
* 便捷方法:覆盖写入数据到Excel
* @param data 要写入的数据
* @param filePath 文件路径
* @param sheetName 工作表名称
* @return 写入是否成功
*/
public static boolean overwriteMapToExcel(Map<String, String> data, String filePath, String sheetName) {
return writeMapToExcel(data, filePath, sheetName, false);
}
}
调用BSC api的类,API_KEY需要在https://bscscan.com/login?cmd=last地址,先注册,再生成API_KEY
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class BscScanUsdtBalanceChecker {
private static final String API_KEY = "you api key"; // 替换为你自己的 API KEY
// private static final String ADDRESS = "0xc6b5189a01eb5812b2adf41dcfaf0c1cc6b79514";
private static final String USDT_CONTRACT = "0x55d398326f99059ff775485246999027b3197955";
static String resultPath = "D:\\candy\\余额0618.xls";
// 重试配置
private static final int MAX_RETRY_COUNT = 3; // 最大重试次数
private static final int CONNECT_TIMEOUT = 10; // 连接超时(秒)
private static final int READ_TIMEOUT = 30; // 读取超时(秒)
private static final int WRITE_TIMEOUT = 10; // 写入超时(秒)
private static final long RETRY_DELAY_MS = 1000; // 重试间隔(毫秒)
public static void main(String[] args) throws Exception {
Map<String, String> result = new LinkedHashMap<>();
// 配置OkHttpClient,添加超时设置
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) // 连接失败时自动重试
.build();
List<String> addressList = ExcelFirstColumnReader.readFirstColumn("D:\\candy\\无标题.xls");
System.out.println("开始查询 " + addressList.size() + " 个地址的USDT余额...");
int successCount = 0;
int failCount = 0;
for (int i = 0; i < addressList.size(); i++) {
String address = addressList.get(i);
System.out.printf("处理进度: %d/%d (%s)\n", i + 1, addressList.size(), address);
String url = String.format(
"https://api.bscscan.com/api?module=account&action=tokenbalance&contractaddress=%s&address=%s&tag=latest&apikey=%s",
USDT_CONTRACT, address, API_KEY
);
// 随机延迟,避免请求过于频繁
int randomDelay = ThreadLocalRandom.current().nextInt(1, 3);
Thread.sleep(randomDelay * 250);
// 执行带重试的HTTP请求
String balance = executeRequestWithRetry(client, url, address);
if (balance != null) {
result.put(address, balance);
successCount++;
System.out.println("✓ " + address + " -- USDT余额: " + balance);
if(result.size()>=100){
boolean writeSuccess = ExcelFirstColumnReader.writeMapToExcel(result, resultPath, "USDT余额",true);
if (writeSuccess) {
System.out.println("结果已保存到: " + resultPath);
} else {
System.out.println("保存文件失败!");
}
result.clear();
}
} else {
failCount++;
System.out.println("✗ " + address + " -- 查询失败");
// 失败的地址也可以记录,余额设为"查询失败"
result.put(address, "查询失败");
}
}
// 输出统计信息
System.out.println("\n=== 查询完成 ===");
System.out.println("总地址数: " + addressList.size());
System.out.println("成功查询: " + successCount);
System.out.println("查询失败: " + failCount);
// 写入Excel文件
boolean writeSuccess = ExcelFirstColumnReader.writeMapToExcel(result, resultPath, "USDT余额",true);
if (writeSuccess) {
System.out.println("结果已保存到: " + resultPath);
} else {
System.out.println("保存文件失败!");
}
// 关闭HTTP客户端
client.dispatcher().executorService().shutdown();
client.connectionPool().evictAll();
}
/**
* 执行带重试机制的HTTP请求
* @param client OkHttpClient实例
* @param url 请求URL
* @param address 地址(用于日志)
* @return 余额字符串,失败返回null
*/
private static String executeRequestWithRetry(OkHttpClient client, String url, String address) {
Request request = new Request.Builder()
.url(url)
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build();
Exception lastException = null;
for (int attempt = 1; attempt <= MAX_RETRY_COUNT; attempt++) {
try {
return executeRequest(client, request, address, attempt);
} catch (Exception e) {
lastException = e;
System.out.printf(" 第%d次尝试失败: %s\n", attempt, e.getMessage());
// 如果不是最后一次尝试,则等待后重试
if (attempt < MAX_RETRY_COUNT) {
try {
long delayMs = RETRY_DELAY_MS * attempt; // 递增延迟
System.out.printf(" 等待%dms后重试...\n", delayMs);
Thread.sleep(delayMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
System.out.println(" 重试被中断");
break;
}
}
}
}
System.out.printf(" 地址 %s 重试%d次后仍然失败: %s\n",
address, MAX_RETRY_COUNT,
lastException != null ? lastException.getMessage() : "未知错误");
return null;
}
/**
* 执行单次HTTP请求
* @param client OkHttpClient实例
* @param request HTTP请求
* @param address 地址(用于日志)
* @param attempt 当前尝试次数
* @return 余额字符串
* @throws Exception 请求异常
*/
private static String executeRequest(OkHttpClient client, Request request, String address, int attempt) throws Exception {
try (Response response = client.newCall(request).execute()) {
// 检查HTTP状态码
if (!response.isSuccessful()) {
throw new RuntimeException("HTTP错误: " + response.code() + " " + response.message());
}
// 检查响应体
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new RuntimeException("响应体为空");
}
String responseString = responseBody.string();
if (responseString == null || responseString.trim().isEmpty()) {
throw new RuntimeException("响应内容为空");
}
// 解析JSON响应
JsonObject json;
try {
json = JsonParser.parseString(responseString).getAsJsonObject();
} catch (Exception e) {
throw new RuntimeException("JSON解析失败: " + e.getMessage());
}
// 检查API响应状态
if (!json.has("status")) {
throw new RuntimeException("API响应缺少status字段");
}
String status = json.get("status").getAsString();
if (!"1".equals(status)) {
String message = json.has("message") ? json.get("message").getAsString() : "未知错误";
String result = json.has("result") ? json.get("result").getAsString() : "";
throw new RuntimeException("API返回错误: " + message + " " + result);
}
// 解析余额
if (!json.has("result")) {
throw new RuntimeException("API响应缺少result字段");
}
String balanceRaw = json.get("result").getAsString();
if (balanceRaw == null || balanceRaw.trim().isEmpty()) {
throw new RuntimeException("余额数据为空");
}
try {
BigDecimal balance = new BigDecimal(balanceRaw).divide(BigDecimal.TEN.pow(18));
return balance.toPlainString();
} catch (Exception e) {
throw new RuntimeException("余额数据格式错误: " + balanceRaw);
}
} catch (java.net.SocketTimeoutException e) {
throw new Exception("请求超时: " + e.getMessage());
} catch (java.net.ConnectException e) {
throw new Exception("连接失败: " + e.getMessage());
} catch (java.io.IOException e) {
throw new Exception("网络IO错误: " + e.getMessage());
}
}
}
代码中的USDT_CONTRACT是合约地址,因为0x开头的链上地址(如你的钱包地址),本身是不存储USDT的余额,USDT是BEP-20(或ERC-20)代币,它的余额是记录在合约中,不存储在主账户的余额中。
举个例子说明
以太坊 / BSC 等 EVM 链上的账户地址只记录两类资产
native token 比如 ETH、BNB 的余额可以直接查账户余额(如 eth_getBalance)
token(如 USDT) 是合约发行的资产,只能通过对应合约的 balanceOf(address) 方法查询
0x55d398326f99059ff775485246999027b3197955
就是告诉API:“请在这个合约(即 USDT 合约)中,查这个地址的余额”。
如果你查询的是其他代币,比如 BUSD、DAI、APE,就要传入对应代币的合约地址

浙公网安备 33010602011771号