前几天在网上下载了一堆教程,但是名字是用中文数字命名的,在windows文件管理下无法按数字顺序进行排序,这让强迫症的我很不爽,所以就想写个程序批量修改一下。
作为C++小白的我,花了好长时间,终于大致搞明白了。因为路径名和文件名都涉及到中文字符,所以花了很长时间,走了很多弯路,于是想写篇博客记录一下心路历程。
以后可能会经常写博客来记录一些学习心得(希望能坚持下来),也方便以后查阅。
本篇文章主要介绍用C++读取和修改文件名的方法,提供将中文数字修改为两位阿拉伯数字的算法,并介绍C++中对于中文字符(串)的处理。
第一次写,如有错误不当之处,敬请批评指正。
C++中使用_findfirst、_findnext、_findclose这几个函数来读取文件名,并将文件名等信息存储在_finddata_t结构体中。而修改文件名使用的是rename函数。
但是,由于路径名和文件名都涉及到中文字符,所以单纯使用这几个函数会出现乱码等问题,为了方便,统一使用宽字符wchar_t、宽字符串wstring,以及宽字符下的_wfindfirst、_wfindnext、_wfindclose函数。
这些数据类型和普通类型功能都是一样的,唯一不同的是底层的存储。为了方便,在读取和修改文件名部分使用普通类型来说明。
先声明一个结构体_finddata_t,用来存储文件信息,但无需初始化。然后可以使用三个函数来读取文件名。注意需要包含头文件< io.h >
结构体_finddata_t定义如下:
struct _finddata64i32_t {
unsigned attrib;
__time64_t time_create; /* -1 for FAT file systems */
__time64_t time_access; /* -1 for FAT file systems */
__time64_t time_write;
_fsize_t size;
char name[260];
};
其中attrib指的是文件属性(attribute),有以下六种:
_A_ARCH(存档)
_A_HIDDEN(隐藏)
_A_NORMAL(正常)
_A_RDONLY(只读)
_A_SUBDIR(文件夹)
_A_SYSTEM(系统)
三个函数的使用方法如下:
intptr_t _findfirst( char *filespec, struct _finddata_t *fileinfo );
返回值:
如果查找成功的话,将返回一个intptr_t型的唯一的查找用的句柄,实际上相当于int。这个句柄将在_findnext函数中被使用。若失败,则返回-1。
参数:
filespec:标明文件的字符串,可支持通配符。比如:*.c,则表示当前文件夹下的所有后缀为C的文件。 这个字符串要包括完整的路径名。
fileinfo :这里就是用来存放文件信息的结构体的指针。已经说过,这个结构体必须在调用此函数前声明。
函数成功后,函数会把找到的文件的信息放入这个结构体中。
注意:如果将返回值赋给long型变量,有时可能会出现编译不通过的问题,因为这个函数的返回值实际上是intptr_t而不是long,可能会造成类型不兼容。这时候把定义的long型变量改为intptr_t即可。
int _findnext( intptr_t handle, struct _finddata_t *fileinfo );
返回值:
若成功返回0,否则返回-1。
参数:
handle:即由_findfirst函数返回回来的句柄。
fileinfo:文件信息结构体的指针。找到文件后,函数将该文件信息放入此结构体中。
int _findclose( long handle );
返回值:成功返回0,失败返回-1。
参数: handle :_findfirst函数返回回来的句柄。
读取完毕后,用该函数关闭文件结束查找。
为了解决中文路径的问题,实际使用的是兼容中文字符的_wfinddata_t结构体,以及_wfindfirst、_wfindnext、_wfindclose三个函数,与不带w的几个函数功能完全相同,只是路径名参数也必须使用宽字符以兼容。
而修改文件名,使用的是rename函数,使用方法如下:
int rename( char const* OldFileName, char const* NewFileName );
返回值:
成功返回0;失败(如遇到同一文件夹下文件重名)返回-1。
参数:
OldFileName是原文件名字符串的指针,NewFileName是新文件名字符串的指针。
注意:此函数不支持宽字符类型的重载,因此中文在使用时还要转换为普通字符串。
一开始在网上找过一份批量改名的源码,虽然是用C++写的但是C风格特别浓重(分配内存用的是malloc/free且大量使用C风格字符串),这让我一个没有系统学习过C的人看着比较难受,所以又大改了一下,基本全部改为了使用string类(实际上是wstring类)的方法。
注意使用string类、wstring类需要包含头文件< string >。
C/C++中对于字符串处理的常用函数有:
char *strcat( char *str1, const char *str2 );
功能:函数将字符串str2 连接到str1的末端,并返回指针str1。
char *strchr( const char *str, int ch );
功能:函数返回一个指向str 中ch 首次出现的位置,当没有在str 中找ch到返回NULL。
char *strcpy( char *to, const char *from );
功能:复制字符串from 中的字符到字符串to,包括空值结束符。返回值为指针to。
size_t strlen( char *str );
功能:函数返回字符串str 的长度( 即空值结束符之前字符数目)。
int strcmp( const char *str1, const char *str2 );
功能:比较字符串str1 and str2, 返回负值说明str1比str2短,返回正值说明str1比str2长,返回0说明str1与str2一样长。
而在C++的string类中,可以用重载的+、=实现字符串连接和复制的功能,且有常用的几个成员函数:
函数名称 | 功能 |
---|---|
append() | 在字符串的末尾添加文本 |
at() | 按给定索引值返回字符 |
c_str() | 将字符串以C字符数组的形式返回 |
substr() | 返回某个子字符串 |
insert() | 替换字符 |
erase() | 删除字符 |
replace() | 替换字符 |
length() | 返回字符串的长度 |
size() | 返回字符串中字符的数量 |
这里只列出了修改文件名可能用到的一些函数,string类包含的成员函数远不止这么多,具体可以查阅手册。
对于中文字符的处理其实十分简单,只需要把char改为wchar_t类型,string改为wstring类型即可,二者的功能几乎是完全一样的,只需要注意以下不同:
1、C语言是不支持宽字符类型的,C风格字符串的strcat、strcpy等函数在使用宽字符时,只需把str改为wcs,即函数名写成wcscat、wcscpy等即可。
2、若要在控制台输出宽字符/宽字符串,必须使用宽字节流对象wcin、wcout,并且要绑定为中文地区语言。
在使用前加入以下代码即可(一次即可):
//使用宽字节流对象,绑定为中文
locale china("chs");//use china character
wcin.imbue(china);//use locale object
wcout.imbue(china);
3、对于string、wstring类各自来说,成员函数length()、size()以及C风格的strlen()功能是完全相同的,都是返回该字符串除结束符外的字符数量。但是对于string类(每个字符是char)来说,英文和数字是占1个字节,算作一个字符,汉字是占两个字节,算作两个字符。而对于wstring类(每个字符是wchar_t),无论中文、英文、数字,都是占两个字节,算作一个字符。
如以下代码:
string stra = "CPPstring";
cout << stra << endl;
cout << "测试string.size: " << stra.size() << endl;
cout << "测试string.length: " << stra.length() << endl;
cout << "测试strlen(string.c_str()): " << strlen(stra.c_str()) << endl;
cout << endl;
string strb = "我是一个字符串abc123"; //string中英文和数字视为一个字符,汉字视为两个字符
cout << strb << endl;
cout << "测试string.size: " << strb.size() << endl;
cout << "测试string.length: " << strb.length() << endl;
cout << "测试strlen(string.c_str()): " << strlen(strb.c_str()) << endl;
cout << endl;
wstring strc = L"我是一个宽字符串abc123"; //wstring中无论中英文,一个字视为一个字符
wcout << strc << endl;
cout << "测试wstring.size: " << strc.size() << endl;
cout << "测试wstring.length: " << strc.length() << endl;
cout << "测试wcslen(wstring.c_str()): " << wcslen(strc.c_str()) << endl;
cout << endl;
这里顺带一提sizeof操作符。相比较strlen()等,sizeof更像一种特殊的编译预处理而非函数,因为它的值是在编译阶段就确定的。如果对一个字符串str使用sizeof,是计算其指针所占的字节数,而字符串本身所占的空间是在堆内存分配的。在vs2017下编译,sizeof(string)和sizeof(wstring)的值都是28(这一点对于不同的库可能有所不同),而sizeof(string.c_str())和sizeof(wstring.c_str())的值都是4。
更多关于char、wchar_t的不同,如二者在底层的编码方式,以及关于ASCII、Unicode等问题,可以参考这篇文章:
c++汉字字符处理
由于rename函数不支持宽字节作为参数,所以还要考虑二者相互转换的问题。关于这部分这篇文章讲的很详细:
C/C++多字节与宽字符串的相互转换
此处限于篇幅不再赘述。
最后提供将中文转换为阿拉伯数字的思路。由于我下载的文件全都是一百以内的编号,所以写的时候是全部转换为两位数字,而一位数如5命名为05,算法比较简单。具体的思路就是,先找到字符十,如果没有就依次寻找一~九,找到了看前后有无数字,然后决定十字是改为1还是直接删除。如果都没有,函数返回0。
#include "stdafx.h"
//"stdafx.h"中包含的头文件有
using namespace std;
bool ModifyNumber(wstring & wstr); //中文数字替换为两位的阿拉伯数字字符串,成功返回1,未找到数字返回0
string ws2s(const wstring & ws); //宽字符串转换为普通字符串
wchar_t ChsNum[11] = { L'零', L'一', L'二', L'三', L'四', L'五', L'六', L'七', L'八', L'九', L'十' };
wchar_t ArbNum[10] = { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9' };
int main()
{
//使用宽字节流对象,绑定为中文
locale china("chs");//use china character
wcin.imbue(china);//use locale object
wcout.imbue(china);
wstring dirpath = L"F:\\测试\\"; //注意宽字符或宽字符串在初始化时要加前缀L
_wfinddata_t file; //使用宽字节的_wfinddata_t对象而非_finddata_t
long lf; //是否遍历完毕的标志位
wchar_t suffixs[] = L"*.txt"; //要寻找的文件类型后缀,也统一使用宽字符串
vector fileNameList; //文件夹下该类型文件的名字向量表
wchar_t *p;
int psize = dirpath.size() + 6; //后面要把后缀加上,为了防止数组越界需要多开一点空间,6个正好
p = new wchar_t[psize];
wcscpy(p, dirpath.c_str());
//获取文件名,存入向量表
if ((lf = _wfindfirst(wcscat(p, suffixs), &file)) == -1l)
{
cout << "文件没有找到!\n";
}
else
{
cout << "\n文件列表:\n";
do {
wcout << file.name << endl;
wstring str(file.name);
fileNameList.push_back(str);
cout << endl;
} while (_wfindnext(lf, &file) == 0);
}
_findclose(lf); //使用完毕后要关闭文件
delete[] p;
//遍历文件名向量表,并进行修改
cout << "\n开始修改文件名:" << endl;
for (vector ::iterator iter = fileNameList.begin(); iter != fileNameList.end(); ++iter)
{
wstring oldName = dirpath + *iter;
wstring newName = oldName;
//找到需要修改处并修改
bool foundNum = ModifyNumber(newName);
cout << "foundNum=" << foundNum << endl;
wcout << "oldName:" << oldName << endl;
wcout << "newName:" << newName << endl;
wcout << "oldName size = " << oldName.size() << endl;
wcout << "newName size = " << newName.size() << endl;
//为了使用rename函数还要先转换回普通字符串
string str_oldName = ws2s(oldName);
string str_newName = ws2s(newName);
//进行重命名
if (foundNum)
{
rename(str_oldName.c_str(), str_newName.c_str());
}
cout << endl;
}
system("pause");
return 0;
}
/*中文数字替换为两位的阿拉伯数字字符串,成功返回1,未找到数字返回0*/
bool ModifyNumber(wstring & wstr)
{
unsigned int locTen = wstr.find(ChsNum[10]);
if (locTen == wstring::npos) //找不到字符十,1~9
{
int i = 1;
unsigned int locUnit;
for (i = 1; i <= 9; ++i)
{
locUnit = wstr.find(ChsNum[i]);
if (locUnit != wstring::npos)
{
wstr.replace(locUnit, 1, 1, ArbNum[i]);
wstr.insert(locUnit, 1, ArbNum[0]);
break;
}
}
if (locUnit == wstring::npos && i == 10) //未找到数字
return 0;
}
else //能找到字符十,组合前后的数
{
wchar_t beforeten = L'零';
int _isfrom10to20 = 1;
if (locTen > 0) //考虑到可能字符串开头就是十
{
beforeten = wstr.at(locTen - 1);
for (int count = 1; count <= 9; count++)
{
if (beforeten == ChsNum[count])
{
_isfrom10to20 = 0;
break;
}
}
}
if (locTen == 0 || _isfrom10to20 == 1)
{
int i = 1;
wstr.replace(locTen, 1, 1, ArbNum[1]);
wchar_t afterten = wstr.at(locTen + 1);
for (i = 1; i <= 9; ++i)
{
if (afterten == ChsNum[i]) //11~19
{
wstr.replace(locTen + 1, 1, 1, ArbNum[i]);
break;
}
}
if (i == 10) //10
wstr.insert(locTen + 1, 1, ArbNum[0]);
}
else //21-99
{
int i = 1, j = 1;
wchar_t afterten = wstr.at(locTen + 1);
for (i = 1; i <= 9; ++i)
{
if (beforeten == ChsNum[i])
{
wstr.replace(locTen - 1, 1, 1, ArbNum[i]);
break;
}
}
for (j = 1; j <= 9; ++j)
{
if (afterten == ChsNum[j]) //非整十
{
wstr.replace(locTen + 1, 1, 1, ArbNum[j]);
wstr.erase(locTen, 1);
break;
}
}
if (j == 10) //整十
{
wstr.replace(locTen, 1, 1, ArbNum[0]);
}
}
}
return 1;
}
/*宽字符串转换为普通字符串*/
string ws2s(const wstring & ws)
{
string curLocale = setlocale(LC_ALL, NULL); //curLocale="C"
setlocale(LC_ALL, "chs");
const wchar_t* wcs = ws.c_str();
size_t dByteNum = sizeof(wchar_t)*ws.size() + 1;
cout << "ws.size():" << ws.size() << endl;
char* dest = new char[dByteNum];
wcstombs_s(NULL, dest, dByteNum, wcs, _TRUNCATE);
string result = dest;
delete[] dest;
setlocale(LC_ALL, curLocale.c_str());
return result;
}
关于更完善的数字转换算法,可以参考这个代码,不过是用Java写的:
java实现中文数字与阿拉伯数字互相转换
https://blog.csdn.net/xiexu911/article/details/79990774
https://blog.csdn.net/orz_3399/article/details/53415987
https://blog.csdn.net/k346k346/article/details/50082705
https://blog.csdn.net/rentian1/article/details/78498975