本文介绍如何解析通过 查询用量明细导出任务状态 返回的 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 对比#
旧格式(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 数组字符串,数组中的每个元素表示一个计费项。
示例如下:
[
{"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 解析逻辑或接入业务系统前的验证脚本。
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()
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)
}
}
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;
}
}
#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