PAYATHON 2026

支付宝下载的 CSV 对账单无法打开且显示乱码,如何解决?

支付老李

结论

下载对账单接口通常返回一个账单压缩包的下载 URL,并不直接返回可阅读的 CSV 内容。通过该 URL 下载文件后,需要先按 ZIP 格式解压,再用正确的字符编码读取其中的 CSV。

如果将 ZIP 二进制数据直接保存为 .csv,或者用文本编辑器打开压缩包,通常会看到乱码,甚至无法打开文件。解压后中文仍然异常,可以尝试使用 GBK 或兼容范围更大的 GB18030 读取,不要直接按 UTF-8 打开。

先确认实际下载到了什么

文件扩展名不一定可靠,还要检查响应状态、响应头和文件头。

ZIP 文件的开头通常是十六进制 50 4B,对应字符 PK。可以执行:

file alipay_bill
xxd -l 16 alipay_bill

如果文件头类似:

00000000: 504b 0304 ...

下载到的就是 ZIP 压缩包。将其保存为 .zip 后解压:

mv alipay_bill alipay_bill.zip
unzip alipay_bill.zip -d alipay_bill

如果下载内容以 {、< 等字符开头,拿到的可能是 JSON 错误信息、HTML 错误页面或网关提示,而非账单文件。此时需要检查:

  • HTTP 状态码是否为 200
  • 是否正确处理了 HTTP 重定向
  • 下载 URL 是否已经失效
  • 响应正文中是否包含错误码或错误信息
  • 程序是否遗漏了鉴权、代理或网络配置

账单下载 URL 的有效期应以当前支付宝开放平台文档和接口实际返回为准。获取 URL 后应尽快下载,不要长期保存后反复使用。

正确的处理流程

完整流程如下:

  1. 调用 alipay.data.dataservice.bill.downloadurl.query 获取账单下载 URL。
  2. 使用 HTTP 客户端访问该 URL。
  3. 以二进制方式保存响应,不能先将其转换为字符串。
  4. 判断下载结果是 ZIP、CSV 还是错误响应。
  5. 如果是 ZIP,先解压。
  6. 使用合适的字符编码读取解压后的 CSV。
  7. 使用 CSV 解析器处理字段,不要直接按逗号切割。

Java 下载和解压示例

下面的示例假设已经取得有效的账单下载 URL。下载时直接处理字节流,避免错误解码二进制数据。

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class AlipayBillDownloader {

    public static void download(String downloadUrl, Path targetFile) throws IOException {
        HttpURLConnection connection =
                (HttpURLConnection) URI.create(downloadUrl).toURL().openConnection();

        connection.setRequestMethod("GET");
        connection.setConnectTimeout(10_000);
        connection.setReadTimeout(60_000);
        connection.setInstanceFollowRedirects(true);

        int status = connection.getResponseCode();
        if (status < 200 || status >= 300) {
            InputStream errorStream = connection.getErrorStream();
            String errorBody = errorStream == null
                    ? ""
                    : new String(errorStream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);

            throw new IOException(
                    "Download failed, HTTP status=" + status + ", response=" + errorBody
            );
        }

        try (InputStream input = new BufferedInputStream(connection.getInputStream())) {
            Files.copy(input, targetFile, StandardCopyOption.REPLACE_EXISTING);
        } finally {
            connection.disconnect();
        }
    }

    public static void unzip(Path zipFile, Path outputDirectory) throws IOException {
        Files.createDirectories(outputDirectory);
        Path normalizedOutput = outputDirectory.toAbsolutePath().normalize();

        try (ZipInputStream zipInput =
                     new ZipInputStream(new BufferedInputStream(Files.newInputStream(zipFile)))) {

            ZipEntry entry;
            while ((entry = zipInput.getNextEntry()) != null) {
                Path output = normalizedOutput.resolve(entry.getName()).normalize();

                // 防止 ZIP Slip:禁止压缩包将文件写到目标目录之外
                if (!output.startsWith(normalizedOutput)) {
                    throw new IOException("Illegal ZIP entry: " + entry.getName());
                }

                if (entry.isDirectory()) {
                    Files.createDirectories(output);
                } else {
                    Files.createDirectories(output.getParent());

                    try (BufferedOutputStream out =
                                 new BufferedOutputStream(Files.newOutputStream(output))) {
                        zipInput.transferTo(out);
                    }
                }

                zipInput.closeEntry();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        String downloadUrl = "替换为接口返回的账单下载URL";

        Path zipFile = Path.of("alipay-bill.zip");
        Path outputDirectory = Path.of("alipay-bill");

        download(downloadUrl, zipFile);
        unzip(zipFile, outputDirectory);
    }
}

不要用下面这种方式下载 ZIP:

String content = new String(inputStream.readAllBytes());
Files.writeString(Path.of("bill.csv"), content);

这段代码会先用默认字符编码解释 ZIP 二进制数据,导致文件内容损坏。

解压后的 CSV 仍然乱码

如果已经找到解压后的 CSV,但中文显示异常,通常是读取编码与文件编码不一致。可以依次尝试:

  • GB18030
  • GBK
  • UTF-8

Java 读取示例:

import java.io.BufferedReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

Path csvFile = Path.of("alipay-bill/账单文件.csv");
Charset charset = Charset.forName("GB18030");

try (BufferedReader reader = Files.newBufferedReader(csvFile, charset)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

GB18030 和 GBK 的兼容范围较广,但不能仅凭经验认定所有支付宝账单文件都固定使用某一种编码。最终仍要结合实际文件内容和当前官方接口说明进行确认。

在 macOS 或 Linux 上,也可以先转换编码:

iconv -f GB18030 -t UTF-8 source.csv > bill-utf8.csv

如果已经确认源文件使用 GBK,则执行:

iconv -f GBK -t UTF-8 source.csv > bill-utf8.csv

转换前建议保留原文件。源编码判断错误时,转换后的内容仍会乱码。

使用 Excel 打开时的处理方法

直接双击 CSV 时,Excel 可能使用系统默认编码打开文件,造成中文乱码。可以改用导入方式:

  1. 打开 Excel。
  2. 在“数据”中选择“从文本/CSV”。
  3. 选择解压后的 CSV 文件。
  4. 在编码或文件原始格式中尝试 GB18030、GBK 或 UTF-8。
  5. 确认预览中的中文正常后,再加载数据。

不要为了让 Excel 识别编码,直接在原始二进制下载结果前添加 UTF-8 BOM。只有确认文件已经转换成 UTF-8 文本后,才能按需添加 BOM。

还需要注意的几个问题

不要依赖下载文件名

下载 URL 可能没有明显的 .zip 后缀,响应头中的文件名也可能经过 URL 编码。程序应综合以下信息判断文件类型:

  • HTTP Content-Type
  • HTTP Content-Disposition
  • 文件头特征
  • 响应正文是否为错误信息

CSV 不能简单按逗号切割

账单字段中可能包含引号、逗号或换行,下面这种处理方式并不可靠:

String[] fields = line.split(",");

正式处理账单时,应使用支持 RFC 4180 常见规则的 CSV 解析库,例如 Apache Commons CSV 或 OpenCSV,并按照实际账单格式配置分隔符、引号和空行处理规则。

保留原始下载文件

排查问题时,建议同时保留:

  • 接口返回的下载 URL 信息,注意脱敏
  • HTTP 状态码和关键响应头
  • 原始下载文件
  • 解压后的 CSV
  • 实际使用的字符编码

不要将完整下载 URL、应用私钥、签名参数或含有交易信息的账单上传到公开平台。

如果原始文件头是 PK,说明文件还没有解压。如果解压后的 CSV 中文仍然异常,通常需要检查字符编码。如果文件内容是 JSON 或 HTML,则需要回到下载 URL、HTTP 状态和错误响应这一层继续排查。

备注:内容仅供参考。