CSV格式文件处理算法

    csv(Comma-Separated Values)是一种用逗号分隔来存储表数据的格式。用回车来表示换行。例如有下表:

1997FordE350ac,abs,moon3000.00
1999ChevyVenture "Extended Edition" 4900.00
1999chevyVenture"Extended Edition, Very Large" 5000.00
1996JeepGrand Cherokee

MUST SELL!

air moon roof,loaded

4799.00

则储存为:

1997,Ford,E350,"ac,abs,moon",3000.00
1999,Chevy,Venture?"Extended Edition",,4900.00 
1999,chevy,"Venture""Extended Edition, Very Large""",,5000.00 
1996,Jeep,Grand Cherokee,"MUST SELL!
air moon roof,loaded",4799.00 

 csv的规则如下:
  1. 开头是不留空,以行为单位。
  2.  可含或不含列名,含列名则居文件第一行。
  3.  一行数据不跨行,无空行。若是有换行的数据,需要包含在双引号内。如上面的MUST SELL
  4.  以半角逗号(即,)作分隔符,列为空也要表达其存在。
  5. 列内容如存在半角引号(即"),替换成半角双引号("")转义,即用半角引号(即"")将该字段值包含起来。
  6. 文件读写时引号,逗号操作规则互逆。
  7. 内码格式不限,可为 ASCII、Unicode 或者其他。
  8. 不支持数字(这条规则我也不是很理解)
  9. 不支持特殊字符

基于上述的规则有如下的解析代码:

	std::string tempCsvCell;			//临时单元,存入读到的单元格数据
	TYPE_CSV_ROW tempCsvRow;			//代表一行数据
	bool bFirstDoubleQuotes		= false;//标志单元的第一个字符是否为双引号  
	bool bBeforeIsDoubleQuotes	= false;//当前字符的前一个字符是否为双引号
	bool bBeforeIsX0D			= false;//当前字符的前一个是否为回车符
	int iMaxRowSize(0);					//记录最长那一行的长度。最后面的时候,所有行都按该长度对齐。
	//先清空数据
	m_data.clear();						//m_data是储存最终结果的变量
	for (int i(0); i < fileContent.size(); i++)	//开始逐个字符解析,fileContent是整个文件的缓冲区
	{
		//读取一个字符ch
		char &ch = fileContent.at(i);
		if (bFirstDoubleQuotes) {//单元是以双引号开头,后面可能有特殊字符
			if (ch == '"') {
				bBeforeIsX0D = false;
				if (bBeforeIsDoubleQuotes) {	//按转义双引号处理
					tempCsvCell.append(1, (char)(ch));
					bBeforeIsDoubleQuotes = false;
				}
				else {							//表示这是一个单独的双引号
					bBeforeIsDoubleQuotes = true;
				}
			}
			else {
				if (bBeforeIsDoubleQuotes) {	//表示遇到了单独的双引号,那取消bFirstDoubleQueotes标志。遇到逗号或者回车时,就可以入“行”了
					bFirstDoubleQuotes = false;
				}
				bBeforeIsDoubleQuotes = false;
				if ('\r' == ch || '\n' == ch){
					if (bFirstDoubleQuotes) {			//遇到回车换行,但开头是双引号,只能当普通字符处理
						tempCsvCell.append(1, (char)(ch));
					}
					else if (false == bBeforeIsX0D) {	//遇到回车换行,开头不是双引号,可以入“行”了,“行”入“表”
						tempCsvRow.push_back(tempCsvCell);
						m_data.push_back(tempCsvRow);
						iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
						tempCsvRow.clear();
						tempCsvCell.clear();
						bFirstDoubleQuotes = false;
					}
					bBeforeIsX0D = (0x0d == ch);
				}
				else if (',' == ch) {
					bBeforeIsX0D = false;
					if (bFirstDoubleQuotes) {	//遇到逗号,但因为开头是双引号,它只能当普通逗号处理
						tempCsvCell.append(1, (char)(ch));
					}
					else {						//遇到逗号,开头不是双引号,可以入“行”
						bBeforeIsX0D = false;
						tempCsvRow.push_back(tempCsvCell);
						tempCsvCell.clear();
					}
				}
				else {							//遇到普通字符,直接入“行”
					bBeforeIsX0D = false;
					tempCsvCell.append(1, (char)(ch));
				}
			}
		}
		else{
			if (ch == '"') {
				bBeforeIsX0D = false;
				if (tempCsvCell.empty()) {
					// 空串,第一个是"
					bFirstDoubleQuotes = true;
					bBeforeIsDoubleQuotes = false;
				}
				else {
					tempCsvCell.append(1, (char)(ch));
					continue;
				}
			}
			else {
				bBeforeIsDoubleQuotes = false;
				if ('\r' == ch || '\n' == ch){
					if (false == bBeforeIsX0D) {
						tempCsvRow.push_back(tempCsvCell);
						m_data.push_back(tempCsvRow);
						iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
						tempCsvRow.clear();
						tempCsvCell.clear();
						bFirstDoubleQuotes = false;
						bBeforeIsDoubleQuotes = false;
					}
					else {
						// 连续\r\n不考虑设置为新的行  
					}
					bBeforeIsX0D = (ch == 0x0d);
				}
				else if (',' == ch) {
					bBeforeIsX0D = false;
					tempCsvRow.push_back(tempCsvCell);
					tempCsvCell.clear();
				}
				else {
					bBeforeIsX0D = false;
					tempCsvCell.append(1, (char)(ch));
				}
			}
		}
	}

	//将最后一部分放入
	if (false == tempCsvCell.empty()) {
		tempCsvRow.push_back(tempCsvCell);
		m_data.push_back(tempCsvRow);
		iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
		tempCsvRow.clear();
		tempCsvCell.clear();
	}

    当整个表被读入后,基于第4条规则(列为空也要保留,表达其存在)。它可以看成一个m*n的矩阵。不存在有参差不齐的情况。如果有,在读入时也会处理为m*n的对齐格式。后续的操作,也遵循这个规则。

    在读操作方面,有读列,读行,读单元数据,读列数,读行数的操作。

    在写方面,有添加行,添加列,删除行,删除列,修改单元数据的操作。

头文件《CSVReader.h》定义如下:

#pragma once
#include <string>
#include <vector>
using namespace std;

//定义CSV文件的最大限制,单位字节
#define MAX_FILE_BUFFER_SIZE 10*1024*1024

/*定义一个异常:越界*/
typedef struct  __EXCEPTION_CSV_OUT_OF_RANGE: public std::logic_error
{
	__EXCEPTION_CSV_OUT_OF_RANGE(const string &errMsgDescript)
		:logic_error(errMsgDescript){}
}EXCEPTION_CSV_OUT_OF_RANGE;

/*定义行和列数据类型*/
typedef vector<string> TYPE_CSV_ROW;
typedef vector<string> TYPE_CSV_COLUMN;

class CCSVReader
{
public:
	/*构造一个空的csv对象*/
	CCSVReader();
	~CCSVReader();

	/*读取一个csv文件
		调用该函数,若读取成功,会覆盖当前数据
	*/
	bool ReadFile(string sFilePath);

	/*保存文件
		sFilePath:要保存的文件名,可以是绝对路径或相对路径。
				  如果不填,则以ReadFile时的路径来保存。但如果没调用过ReadFile,则保存失败
	*/
	bool SaveFile(const string &sFilePath = "");

	/*返回表格的总行数*/
	size_t RowSize();

	/*返回表格的总列数*/
	size_t ColumnSize();

	/*获取指定位置的数据
	   iRow:行数,从0开始算起
	   iCol:列数,从0开始算起
	   若下标越界,抛出异常
	成功时返回读取到的数据
	*/
	string GetData(const int &iRow, const int &iCol) throw(EXCEPTION_CSV_OUT_OF_RANGE);

	/*设置或修改指定位置的数据
		iRow:行数,从0开始算起
		iCol:列数,从0开始算起
	若下标越界,返回失败false
	*/
	bool SetData(const int &iRow, const int &iCol, string &data);

	/*获取一行的数据
		iPos要获取的行数据位置,从0开始取
	 返回:如果参数超有效范围,抛出异常
	*/
	TYPE_CSV_ROW GetRow(const int &iPos) throw(EXCEPTION_CSV_OUT_OF_RANGE);

	/*获取一列的数据
		iPos要获取的行数据位置,从0开始取
	 返回:如果参数超有效范围,抛出异常*/
	TYPE_CSV_COLUMN GetColumn(const int &iPos) throw(EXCEPTION_CSV_OUT_OF_RANGE);

	/*追加一行数据*/
	void AppendRow(TYPE_CSV_ROW &newRow);

	/*插入一行数据
		iRowPos:要插入的位置,取值从0到row.size
		若越界,则抛出异常
	*/
	void InsertRow(const size_t &iRowPos, TYPE_CSV_ROW &newRow) throw(EXCEPTION_CSV_OUT_OF_RANGE);

	/*追加一列数据*/
	void AppendColumn(TYPE_CSV_COLUMN &newCol);

	/*插入一列数据
		iColPos:要插入的位置,取值从0到column.size
		若越界,则抛出异常
	*/
	void InsertColumn(const size_t &iColPos, TYPE_CSV_COLUMN &newCol) throw(EXCEPTION_CSV_OUT_OF_RANGE);

	/*移除一行数据
		iPos:要移除的那一行数据的位置,从0开始取值
	*/
	void RemoveRow(const int &iPos);

	/*移除一列数据
		iPos:要移除的那一列数据的位置,从0开始取值
	*/
	void RemoveColumn(const int &iPos);
private:
	string m_sFilePath;

	vector< TYPE_CSV_ROW > m_data;

private:
	//将一个单元数据转换为CSV存储格式
	string forCSV(const string &src){
		string ret;
		for (auto &c : src)
		{
			if (c == '"')
			{
				ret += "\"\"";
			}
			else
			{
				ret += c;
			}
		}
		return "\"" + ret + "\"";
	}

	//格式化输出一段文字。缓冲区上限为1024字节,注意不要超出。
	string Format(LPSTR szFormat, ...)
	{
		int iBufLen(1024);
		char *bufTmp(nullptr);

		//格式化参数
		va_list vl;
		va_start(vl, szFormat);

		bufTmp = new char[iBufLen];
		ZeroMemory(bufTmp, iBufLen);
		_vsnprintf(bufTmp, iBufLen, szFormat, vl);
		va_end(vl);

		string ret(bufTmp);
		delete[] bufTmp;
		return ret;
	}
};

实现的《CSVReader.cpp》文件如下:

#include "stdafx.h"
#include "CSVReader.h"
#include <fstream>


CCSVReader::CCSVReader()
{
}

CCSVReader::~CCSVReader()
{
}


bool CCSVReader::ReadFile(string sFilePath)
{
	//检查文件名长度
	if (sFilePath.size() == 0)
	{
		return false;
	}
	//去掉文件名两端的双引号
	if (sFilePath.at(0) == '"')
	{
		sFilePath.erase(0, 1);
	}
	if (sFilePath.at(sFilePath.size() - 1) == '"')
	{
		sFilePath.erase(sFilePath.size() - 1, 1);
	}

	//打开文件
	std::ifstream file;
	file.open(sFilePath, std::ios::in | ios::binary);
	if (file.fail())
	{
		file.close();
		return false;
	}

	//获取文件长度
	streampos begin = file.tellg();           //获得开头的位置
	file.seekg(0, ios::end);                  //定位到文件尾部
	streampos end = file.tellg();             //获得尾部的位置
	long fileLength = (long)(end - begin);    //利用streampos的"-"操作得到文件的字节长度
	if (fileLength > MAX_FILE_BUFFER_SIZE)
	{
		file.close();
		return false;
	}

	//读取文件内容
	vector<char> fileContent(fileLength);
	file.seekg(0);
	file.read(&(fileContent[0]), fileContent.size());

	/*解析算法的思路如下:
		首先明确“单元”的概念,对应于excel的单元格。
		一个单元格中的内容如果是普通字符,则在CSV文件中是直接储存的。
		一个单元格中的内容如果包含有逗号,双引号,回车这三种特殊字符,则必须用双引号围起来。并且双引号要转义为两个双引号
		解析的时候,是以单位格为目标进行的。逻辑思路如下:
		1.逐字符读取,保存到ch中,并对ch和相关状态进行判断处理
		2.如果ch是第一个字符,且不为双引号,可按步骤3来处理.否则到步骤4
		3.连续读取字符到临时单元中,直到:
		遇到逗号就将临时单元存入“行”中。回到步骤1。
		遇到的是回车换行,则将临时单元存入“行”,再将“行”放入“表”中。回到步骤1
		4.连续读取,遇到任何字符都存入临时单元中,连续出现的一对双引号通过转义为一个双引号也存入单元中。
		直到遇到单独存在的双引号。视为单元读取结束。读取下一个字符:
		若为逗号,则单元存入行中。回到步骤1
		若为回车换行,则单元入行,行入表,回到步骤1
		若都不是,可报错。或兼容性算法是继续纳入单元中,直到遇逗号或回车换行。然后回到步骤1
		*/
	std::string tempCsvCell;			//临时单元,存入读到的单元格数据
	TYPE_CSV_ROW tempCsvRow;			//代表一行数据
	bool bFirstDoubleQuotes = false;//标志单元的第一个字符是否为双引号  
	bool bBeforeIsDoubleQuotes = false;//当前字符的前一个字符是否为双引号
	bool bBeforeIsX0D = false;//当前字符的前一个是否为回车符
	int iMaxRowSize(0);					//记录最长那一行的长度。最后面的时候,所有行都按该长度对齐。
	//先清空数据
	m_data.clear();						//m_data就是上面所说的表
	for (int i(0); i < fileContent.size(); i++)	//开始逐个字符解析
	{
		//读取一个字符ch
		char &ch = fileContent.at(i);
		if (bFirstDoubleQuotes) {//单元是以双引号开头,后面可能有特殊字符
			if (ch == '"') {
				bBeforeIsX0D = false;
				if (bBeforeIsDoubleQuotes) {	//按转义双引号处理
					tempCsvCell.append(1, (char)(ch));
					bBeforeIsDoubleQuotes = false;
				}
				else {							//表示这是一个单独的双引号
					bBeforeIsDoubleQuotes = true;
				}
			}
			else {
				if (bBeforeIsDoubleQuotes) {	//表示遇到了单独的双引号,那取消bFirstDoubleQueotes标志。遇到逗号或者回车时,就可以入“行”了
					bFirstDoubleQuotes = false;
				}
				bBeforeIsDoubleQuotes = false;
				if ('\r' == ch || '\n' == ch){
					if (bFirstDoubleQuotes) {			//遇到回车换行,但开头是双引号,只能当普通字符处理
						tempCsvCell.append(1, (char)(ch));
					}
					else if (false == bBeforeIsX0D) {	//遇到回车换行,开头不是双引号,可以入“行”了,“行”入“表”
						tempCsvRow.push_back(tempCsvCell);
						m_data.push_back(tempCsvRow);
						iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
						tempCsvRow.clear();
						tempCsvCell.clear();
						bFirstDoubleQuotes = false;
					}
					bBeforeIsX0D = (0x0d == ch);
				}
				else if (',' == ch) {
					bBeforeIsX0D = false;
					if (bFirstDoubleQuotes) {	//遇到逗号,但因为开头是双引号,它只能当普通逗号处理
						tempCsvCell.append(1, (char)(ch));
					}
					else {						//遇到逗号,开头不是双引号,可以入“行”
						bBeforeIsX0D = false;
						tempCsvRow.push_back(tempCsvCell);
						tempCsvCell.clear();
					}
				}
				else {							//遇到普通字符,直接入“行”
					bBeforeIsX0D = false;
					tempCsvCell.append(1, (char)(ch));
				}
			}
		}
		else{
			if (ch == '"') {
				bBeforeIsX0D = false;
				if (tempCsvCell.empty()) {
					// 空串,第一个是"
					bFirstDoubleQuotes = true;
					bBeforeIsDoubleQuotes = false;
				}
				else {
					tempCsvCell.append(1, (char)(ch));
					continue;
				}
			}
			else {
				bBeforeIsDoubleQuotes = false;
				if ('\r' == ch || '\n' == ch){
					if (false == bBeforeIsX0D) {
						tempCsvRow.push_back(tempCsvCell);
						m_data.push_back(tempCsvRow);
						iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
						tempCsvRow.clear();
						tempCsvCell.clear();
						bFirstDoubleQuotes = false;
						bBeforeIsDoubleQuotes = false;
					}
					else {
						// 连续\r\n不考虑设置为新的行  
					}
					bBeforeIsX0D = (ch == 0x0d);
				}
				else if (',' == ch) {
					bBeforeIsX0D = false;
					tempCsvRow.push_back(tempCsvCell);
					tempCsvCell.clear();
				}
				else {
					bBeforeIsX0D = false;
					tempCsvCell.append(1, (char)(ch));
				}
			}
		}
	}

	//将最后一部分放入
	if (false == tempCsvCell.empty()) {
		tempCsvRow.push_back(tempCsvCell);
		m_data.push_back(tempCsvRow);
		iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
		tempCsvRow.clear();
		tempCsvCell.clear();
	}
	file.close();

	for (int i(0); i < m_data.size(); i++)	//对齐所有的行
	{
		if (m_data[i].size() < iMaxRowSize)
		{
			m_data[i].resize(iMaxRowSize);
		}
	}
	return true;
}


bool CCSVReader::SaveFile(const string &sFilePath/* = ""*/)
{
	string tmpFilePath = sFilePath.size() ? sFilePath : m_sFilePath;
	if (tmpFilePath.size() == 0)
	{
		return false;
	}

	//写入文件
	std::ofstream fileOut;
	fileOut.open(tmpFilePath, ios::out | ios::trunc);
	if (fileOut.bad())
	{
		return false;
	}


	for (auto &row : m_data)
	{
		for (int i(0); i < row.size(); i++)
		{
			//转换为csv格式的内容
			fileOut << forCSV(row[i]) << (i == row.size() - 1 ? "" : ",");
		}
		fileOut << endl;
	}
	fileOut.close();

	return true;
}

/*返回表格的总行数*/
size_t CCSVReader::RowSize()
{
	return m_data.size();
}

/*返回表格的总列数*/
size_t CCSVReader::ColumnSize()
{
	if (m_data.size() == 0)
	{
		return 0;
	}
	return m_data[0].size();
}

/*获取指定位置的数据
	iRow:行数,从0开始算起
	iCol:列数,从0开始算起
	若索引溢出,抛出异常
	*/
string CCSVReader::GetData(const int &iRow, const int &iCol)
{
	if (iRow >= m_data.size() || iCol >= m_data[iRow].size())
	{
		throw EXCEPTION_CSV_OUT_OF_RANGE(Format("下标[%d, %d]越界", iRow, iCol));
	}
	else
	{
		return m_data[iRow][iCol];
	}
}

/*设置指定位置的数据
	iRow:行数,从0开始算起
	iCol:列数,从0开始算起
	若索引溢出,返回false
	*/
bool CCSVReader::SetData(const int &iRow, const int &iCol, string &data)
{
	if (iRow < 0 || iCol < 0 || m_data.size() <= iRow || m_data[iRow].size() <= iCol)
	{
		return false;
	}
	m_data[iRow][iCol] = data;
	return true;
}

/*获取一行的数据
	iPos要获取的行数据位置,从0开始取
	返回:如果参数超有效范围,抛出异常
	*/
TYPE_CSV_ROW CCSVReader::GetRow(const int &iPos){
	if (iPos < 0 || iPos >= m_data.size())
	{
		throw EXCEPTION_CSV_OUT_OF_RANGE(Format("行下标[%d]越界", iPos));
	}
	return m_data[iPos];
}

/*获取一列的数据
	iPos要获取的行数据位置,从0开始取
	返回:如果参数超有效范围,抛出异常*/
TYPE_CSV_COLUMN CCSVReader::GetColumn(const int &iPos)
{
	if (iPos < 0 || m_data.size() == 0 || iPos >= m_data[0].size())
	{
		throw EXCEPTION_CSV_OUT_OF_RANGE(Format("列索引[%d]越界", iPos));
	}

	TYPE_CSV_COLUMN col;
	for (auto &row : m_data)
	{
		col.push_back(row[iPos]);
	}
	return col;
}

/*追加一行数据*/
void CCSVReader::AppendRow(TYPE_CSV_ROW &newRow)
{
	InsertRow(m_data.size(), newRow);
}

void CCSVReader::InsertRow(const size_t &iRowPos, TYPE_CSV_ROW &newRow)
{
	if (iRowPos < 0 || iRowPos >= m_data.size())
	{
		throw EXCEPTION_CSV_OUT_OF_RANGE(Format("行下标[%d]越界", iRowPos));
	}

	//要把每一行的数据长度同步为 max(newRow.size(), mdata[0].size())
	if (m_data.size() != 0 && m_data[0].size() > newRow.size())	//扩展newRow的长度
	{
		newRow.resize(m_data[0].size());
	}
	else if (m_data.size() != 0 && m_data[0].size() < newRow.size())	//扩展现有行的长度
	{
		for (int i(0); i < m_data.size(); i++)
		{
			m_data[i].resize(newRow.size());
		}
	}

	//如果一行都没有则插在开头位置
	//如果溢出则在最后,否则计算指定位置
	auto iter = (m_data.size() == 0) ? m_data.begin() :
		(iRowPos == m_data.size() ? m_data.end() : m_data.begin() + iRowPos);
	m_data.insert(iter, newRow);
}

/*追加一列数据*/
void CCSVReader::AppendColumn(TYPE_CSV_COLUMN &newCol)
{
	InsertColumn(m_data[0].size(), newCol);
}

/*插入一列数据
	iColPos:要插入数据的位置
	插入数据会影响到现有的行数。函数会将行数同步为max(newCol.size(), mdata.size())
	*/
void CCSVReader::InsertColumn(const size_t &iColPos, TYPE_CSV_COLUMN &newCol)
{
	if (iColPos < 0
		||
		(m_data.size() == 0 && iColPos != 0)
		||
		(m_data.size() != 0 && iColPos > m_data[0].size())
		)
	{
		throw EXCEPTION_CSV_OUT_OF_RANGE(Format("列索引[%d]越界", iColPos));
	}

	int iCurRowLength = m_data.size() == 0 ? 0 : m_data[0].size();//追加新行的初始长度
	for (int i(0); i < max(newCol.size(), m_data.size()); i++)
	{
		//获取要插入本行的数据,如果已经超出newCol范围,则插入空字符串
		string value = i < newCol.size() ? newCol[i] : "";

		//检查现有的行数,不足要追加空行
		if (m_data.size() <= i)
		{
			TYPE_CSV_ROW newRow(iCurRowLength);
			m_data.push_back(newRow);
		}

		auto iter = iColPos < m_data[i].size() ? (m_data[i].begin() + iColPos) : m_data[i].end();
		m_data[i].insert(iter, value);
	}
}

/*移除一行数据
	iPos:要移除的那一行数据的位置,从0开始取值
	若索引溢出,则执行结果无效
	*/
void CCSVReader::RemoveRow(const int &iPos)
{
	if (iPos >= 0 && iPos < m_data.size())
	{
		m_data.erase(m_data.begin() + iPos);
	}
}

/*移除一列数据
	iPos:要移除的那一列数据的位置,从0开始取值
*/
void CCSVReader::RemoveColumn(const int &iPos)
{
	for (int i(0); i < m_data.size(); i++)
	{
		if (iPos >= 0 && iPos < m_data[i].size()){
			m_data[i].erase(m_data[i].begin() + iPos);
		}
	}
}

使用时,将这两个文件放入工程中,然后引用头文件就可以了。如果VS报C4996警告,请在工程中忽略该警告即可。

或者可以从这个地方下载:https://download.csdn.net/download/learner_/10516197

PS:forCSV的实现其实是偷懒了,将所有单元都加双引号处理。




  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值