关于批量查看账户地址持有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,就要传入对应代币的合约地址

posted @ 2025-06-18 11:30  Charlie-Pang  阅读(118)  评论(0)    收藏  举报