面向 AI 助手:本页的 Markdown 版本位于 https://ark.volcengine.com/region:cn-beijing/docs/usage-csv-charge-value-list-best-practice.md,全部文档索引见 https://ark.volcengine.com/llms.txt。

用量明细 CSV 解析最佳实践

本文介绍如何解析通过 查询用量明细导出任务状态 返回的 DownloadUrl 下载得到的 CSV 文件,重点说明切换时刻、生效规则、charge_value_list 字段结构、兼容策略和多语言示例代码。

切换时刻与生效规则#

GetRecordExportTask 每次调用按导出任务查询窗口的结束时间 EndTime 决定该任务的 CSV 使用哪种格式。切换时刻按调用用户分为两类:

  • 2026 年 9 月 2 日前已产生调用的用户:切换时刻为 2026 年 9 月 15 日。
  • 2026 年 9 月 2 日及以后新增调用的用户:切换时刻为 2026 年 9 月 2 日。

生效规则如下:

  • EndTime 早于或等于对应切换时刻:CSV 返回旧格式。
  • EndTime 晚于对应切换时刻:CSV 返回新格式。

任务提交后,该任务对应的 CSV 字段格式保持不变,不会在轮询期间发生切换。

CSV 格式变更说明#

GetRecordExportTask 接口的请求参数、响应参数以及 DownloadUrl 的生成方式保持不变。本次变更仅影响通过 DownloadUrl 下载的 CSV 文件内容格式。

  • 旧格式:18 列。
  • 新格式:16 列。
  • 删除字段:input_token_charge_item、output_token_charge_item、cache_hit_token_charge_item。
  • 新增字段:charge_value_list。该字段位于 volc_instance_id 之后、timestamp 之前,用于统一返回各计费项信息。
  • 其他字段:其余字段名称、顺序和含义保持不变。

新旧 CSV 对比#

text
旧格式(18 列)
... | volc_instance_id | input_token_charge_item | output_token_charge_item | cache_hit_token_charge_item | timestamp | pt_hour

新格式(16 列)
... | volc_instance_id | charge_value_list | timestamp | pt_hour

charge_value_list 字段说明#

当导出任务对应的 CSV 使用新格式时,charge_value_list 字段的单元格值为 JSON 数组字符串,数组中的每个元素表示一个计费项。

示例如下:

json
[
  {"code":"Doubao-lite-32k-inference-prompt_cn-beijing_realtime","value":54,"scale":1000},
  {"code":"Doubao-lite-32k-inference-completion_cn-beijing_realtime","value":5,"scale":1000},
  {"code":"Doubao-lite-32k-inference-KVcache-hit_cn-beijing_realtime","value":0,"scale":1000}
]

字段说明:

  • code:计费项名称,对应旧格式中的 *_charge_item 字段值。
  • value:整数计量值,例如 token 数或其他计费计量值。
  • scale:整数缩放因子。

真实计费值为 value / scale。

接入建议#

  • 如需同时兼容历史 CSV 与新 CSV:建议优先按列名判断;若读取链路依赖列位置,也可按列数 18 或 16 做分支处理。
  • 当读取到 charge_value_list 字段时:需先执行 JSON 解析,再遍历数组元素获取各计费项的真实计费值。
  • 若已有旧逻辑依赖 input_token_charge_item、output_token_charge_item、cache_hit_token_charge_item 三列:需迁移为读取 charge_value_list 后按 code 分发处理。

注意事项#

  • 该变更仅影响通过 DownloadUrl 下载的 CSV 文件内容,不影响 GetRecordExportTask 接口的请求参数、响应参数 schema 和 DownloadUrl 字段本身的返回逻辑。
  • 建议在任务完成并拿到 DownloadUrl 后尽快下载文件。下载链接有效期为 12 小时,过期后需重新提交任务以获取新链接。
  • 若业务需要长期兼容历史导出结果,建议同时保留旧格式和新格式的解析分支,不要只按固定列位置解析。

示例代码#

以下示例用于读取通过 DownloadUrl 下载得到的 CSV 文件,解析 charge_value_list 字段中的 JSON 数组字符串,并输出每条记录的请求 ID、各计费项的真实计费值,以及汇总后的 total_billing。你可以将其作为本地排查、校验 CSV 解析逻辑或接入业务系统前的验证脚本。

Python
import csv
import json
import sys


def main() -> None:
    if len(sys.argv) != 2:
        raise SystemExit("Usage: python parse_charge_value_list.py <csv_path>")

    with open(sys.argv[1], newline="", encoding="utf-8") as file:
        reader = csv.DictReader(file)
        for row in reader:
            items = json.loads(row["charge_value_list"])
            total_billing = 0.0

            print(f"req_id={row.get('req_id', '')}")
            for item in items:
                billing = item["value"] / item["scale"]
                total_billing += billing
                print(f"  - {item['code']}: {billing:.6f}")

            print(f"total_billing={total_billing:.6f}")


if __name__ == "__main__":
    main()
Go
package main

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"os"
)

type ChargeValue struct {
	Code  string `json:"code"`
	Value int64  `json:"value"`
	Scale int64  `json:"scale"`
}

func main() {
	if len(os.Args) != 2 {
		panic("usage: go run main.go <csv_path>")
	}

	file, err := os.Open(os.Args[1])
	if err != nil {
		panic(err)
	}
	defer file.Close()

	reader := csv.NewReader(file)
	header, err := reader.Read()
	if err != nil {
		panic(err)
	}

	index := map[string]int{}
	for i, name := range header {
		index[name] = i
	}

	for {
		row, err := reader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			panic(err)
		}

		var items []ChargeValue
		if err := json.Unmarshal([]byte(row[index["charge_value_list"]]), &items); err != nil {
			panic(err)
		}

		totalBilling := 0.0
		fmt.Printf("req_id=%s\n", row[index["req_id"]])
		for _, item := range items {
			billing := float64(item.Value) / float64(item.Scale)
			totalBilling += billing
			fmt.Printf("  - %s: %.6f\n", item.Code, billing)
		}
		fmt.Printf("total_billing=%.6f\n", totalBilling)
	}
}
Java
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class ParseChargeValueList {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static class ChargeValue {
        public String code;
        public double value;
        public double scale;
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            throw new IllegalArgumentException("usage: java ParseChargeValueList <csv_path>");
        }

        try (BufferedReader reader = new BufferedReader(new FileReader(args[0]))) {
            List<String> header = parseCsvLine(reader.readLine());
            Map<String, Integer> index = new HashMap<>();

            for (int i = 0; i < header.size(); i++) {
                index.put(header.get(i), i);
            }

            String line;
            while ((line = reader.readLine()) != null) {
                List<String> row = parseCsvLine(line);
                List<ChargeValue> items = OBJECT_MAPPER.readValue(
                        row.get(index.get("charge_value_list")),
                        new TypeReference<List<ChargeValue>>() {}
                );
                double totalBilling = 0.0;

                System.out.println("req_id=" + row.get(index.get("req_id")));
                for (ChargeValue item : items) {
                    double billing = item.value / item.scale;
                    totalBilling += billing;
                    System.out.printf("  - %s: %.6f%n", item.code, billing);
                }
                System.out.printf("total_billing=%.6f%n", totalBilling);
            }
        }
    }

    private static List<String> parseCsvLine(String line) {
        List<String> result = new ArrayList<>();
        StringBuilder current = new StringBuilder();
        boolean inQuotes = false;

        for (int i = 0; i < line.length(); i++) {
            char ch = line.charAt(i);
            if (inQuotes) {
                if (ch == '"') {
                    if (i + 1 < line.length() && line.charAt(i + 1) == '"') {
                        current.append('"');
                        i++;
                    } else {
                        inQuotes = false;
                    }
                } else {
                    current.append(ch);
                }
            } else {
                if (ch == '"') {
                    inQuotes = true;
                } else if (ch == ',') {
                    result.add(current.toString());
                    current.setLength(0);
                } else {
                    current.append(ch);
                }
            }
        }

        result.add(current.toString());
        return result;
    }
}
C++
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

#include <nlohmann/json.hpp>

struct ChargeValue {
    std::string code;
    double value;
    double scale;
};

std::vector<std::string> ParseCsvLine(const std::string& line) {
    std::vector<std::string> result;
    std::string current;
    bool in_quotes = false;

    for (size_t i = 0; i < line.size(); ++i) {
        char ch = line[i];
        if (in_quotes) {
            if (ch == '"') {
                if (i + 1 < line.size() && line[i + 1] == '"') {
                    current.push_back('"');
                    ++i;
                } else {
                    in_quotes = false;
                }
            } else {
                current.push_back(ch);
            }
        } else {
            if (ch == '"') {
                in_quotes = true;
            } else if (ch == ',') {
                result.push_back(current);
                current.clear();
            } else {
                current.push_back(ch);
            }
        }
    }

    result.push_back(current);
    return result;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        throw std::runtime_error("Usage: ./parse_charge_value_list <csv_path>");
    }

    std::ifstream file(argv[1]);
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open CSV file.");
    }

    std::string line;
    if (!std::getline(file, line)) {
        throw std::runtime_error("CSV file is empty.");
    }

    const std::vector<std::string> header = ParseCsvLine(line);
    std::unordered_map<std::string, size_t> index;
    for (size_t i = 0; i < header.size(); ++i) {
        index[header[i]] = i;
    }

    while (std::getline(file, line)) {
        const std::vector<std::string> row = ParseCsvLine(line);
        const auto items = nlohmann::json::parse(row.at(index.at("charge_value_list")));
        double total_billing = 0.0;

        std::cout << "req_id=" << row.at(index.at("req_id")) << '\n';
        for (const auto& item : items) {
            const double billing = item.at("value").get<double>() / item.at("scale").get<double>();
            total_billing += billing;
            std::cout << "  - " << item.at("code").get<std::string>() << ": "
                      << std::fixed << std::setprecision(6) << billing << '\n';
        }
        std::cout << "total_billing=" << std::fixed << std::setprecision(6) << total_billing << '\n';
    }

    return 0;
}

返回示例如下:

req_id=021738b4-2db3-4c4d-9ec1-6e4d8f3b1a2c
  - Doubao-lite-32k-inference-prompt_cn-beijing_realtime: 0.054000
  - Doubao-lite-32k-inference-completion_cn-beijing_realtime: 0.005000
  - Doubao-lite-32k-inference-KVcache-hit_cn-beijing_realtime: 0.000000
total_billing=0.059000