很多时候遇到下载文件夹里有很多杂七杂八的文件,但如此多的文件里,整理起来又不方便,这时候就需要用一个脚本或程序对这些文件进行分类整理,那么,这种东西也是有的,在《PcHome学电脑2007合订本》里有提供一个WSH脚本来归类桌面上的文件,那么写WSH脚本肯定没有IDE里写程序里方便,那么我们就来做一个这样的程序。
为了方便,并且当作是对C#的复习,我们就用C#写一个。
准备工作:
C#控制台程序默认的代码是这样的:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FileClass
{
class Program
{
static void Main(string[] args)
{
}
}
}
Main方法里默认提供了一个string数组类型的args参数,这是外部传入的参数,在命令行输入"fileclass.exe a",就会在程序中传入一个"a"的参数,参数args数组里的第一个成员就是这个"a"参数,在C#里,数组成员索引以0开始,现在我们明白了传入参数的意义,那么我们就来写个args参数传入的实例。
static void Main(string[] args)
{
foreach(string i in args)
{
Console.WriteLine(i);
}
Console.ReadKey();
}
这段代码的作用是将传入的参数显示在控制台中,那么我们在命令行试试"fileclass.exe a b c"会出现什么吧。
G:\FileClass\FileClass\bin\Debug>fileclass.exe a b c
a
b
c
G:\FileClass\FileClass\bin\Debug>
如上所示,在控制台中分别显示了"a" "b" "c"三个参数,那么对于文件拖放到本程序会有什么效果呢?
我们尝试将当前目录中的"FileClass.pdb"文件拖放到FileClass.exe上,控制台将会如下显示:
G:\FileClass\FileClass\bin\Debug\FileClass.pdb
可以得知,文件拖放到本程序,相当于将文件的路径作为参数传入本程序中,这样,得到了程序路径,算是对文件分类程序编写走出了第一步。
得到了文件路径,那现在就要对文件路径进行处理。既然要以扩展名为分类的依据,那么首先就得先获得扩展名,C#提供了很多类,这些包括我们即将用到的IO类,既然有这些类的帮助,那我们的工作量就小了很多。
我们尽量不要把所有的代码一股脑的塞进Main方法里,那么我们就新建一个类,来帮助我们处理这些东西。
在此之前,我们要确定一件事情,如何将扩展名识别为他们的类型呢?为了方便后续添加,我们采用INI配置的方式,来对这个分类做个配置。
[All]
Num=13
Folder=F:\DOWNLOADS\
[Type]
-1=文件夹
0=常规
1=应用程序
2=压缩包
3=图片
4=音乐
5=视频
6=种子
7=文档
8=文本文档
9=手机程序
10=镜像
11=脚本
12=WEB
13=字体
[Ext]
1=exe msi
2=zip rar r0* r1* arj gz sit sitx sea ace bz2 7z tar tbz
3=jpg jpeg bmp gif png tga tif dds
4=mp3 wav wma mpa ram ra aac aif m4a ape mid tta
5=avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg mkv ts swf
6=torrent
7=doc pdf ppt pps docx pptx wps
8=txt ini conf
9=apk ipa
10=iso wim img
11=bat sh py vbs js reg crx nvg bin sp
12=html php jsp
13=ttf ttc
我是为了对下载文件夹进行分类,那么我就定一个目录是下载文件夹的目录,在[All]节中写入一个Folder键的值,将他定为下载目录。[All]节中的[Num]为需要分类的扩展名的数量,这里的数量是和[Ext]节里的是一样的,而[Type]节的内容就是对这些扩展名的类型解释,因为在此之外的扩展名没有分类,所以统一到"常规"分类里,我们把他的键定为0,而文件夹非文件,没有扩展名,所以我们把他定为-1。现在区分的依据已经写好了,现在就该对程序本体进行编写了。
开始编写:
因为我们用到INI,那么就要对INI进行读写,在这我们就不自己写了,就贴一个INI类直接用吧。
///
/// INI操作类
///
public class INI
{
[System.Runtime.InteropServices.DllImport("kernel32")]
private static extern long WritePrivateProfileString(string section, string key, string val, string filePath);
[System.Runtime.InteropServices.DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section, string key, string def, System.Text.StringBuilder retVal, int size, string filePath);
private string sPath = null;
///
/// 设置INI地址
///
/// 地址
public INI(string path)
{
this.sPath = path;
}
///
/// 写入INI
///
/// 节点
/// 键
/// 值
public void WirteINI(string section, string key, string value)
{
WritePrivateProfileString(section, key, value, sPath);
}
///
/// 读取INI
///
/// 节点
/// 键
/// 默认值
///
public string ReadINI(string section, string key,string def="")
{
System.Text.StringBuilder temp = new System.Text.StringBuilder(255);
GetPrivateProfileString(section, key, def, temp, 255, sPath);
return temp.ToString();
}
}
该类提供了三个方法,一个是构造方法
INI Conf=new INI("C://test.ini");
这样就实例化了一个变量为Conf的INI配置,INI()里含一个string类型参数,是INI配置文件的地址
第二个方法是写入INI配置方法
Conf.WriteINI("Test","Test1","Test");
在刚刚实例化的INI中,对[Test]节里的"Test1"键写入"Test"的值,三个参数都是string类型的。
第三个方法是读取INI配置方法
string Value=Conf.ReadINI("Test","Test1");
前面对Conf配置文件的[Test]节里的"Test1"键进行了写入,现在就是对配置文件的[Test]节里的"Test1"键进行读取,得到的值赋值给Value变量,所以Value变量的值是"Test"。其中,该方法还有个可选参数,是用于读取配置后未得到值而默认返回的值,默认为为空,可以自行设置。
string Value=Conf.ReadINI("Test","Test2","Baka");
由于[Test]节内并没有"Test2"键,所以得到的返回值是默认返回值,这里设置成"Baka",所以Value的值为Baka。
扩展名的处理
说了这么多,还是没有对程序开始编写,但是呢,心急吃不了热豆腐,对INI的配置读取写入都会了,后面就没什么好怕的了。
刚刚说了,不要什么代码都一股脑的塞进Main方法里,所以这里我们新建一个类,我把它命名为Ext类。
///
/// 扩展名操作类
///
public class Ext
{
///
/// 配置文件
///
private INI Conf;
///
/// 初始化本类
///
/// INI配置路径
public Ext(string conf)
{
Conf = new INI(conf);
}
}
这里定义了一个INI类的私有字段Conf,用于存放实例化INI类后的内容。所以在下面,我们写个实例化Ext类的方法,方法接受一个string类型的参数,用于传入INI所在的位置。方法内,我们对INI类进行实例化。
现在我们做好了对Ext类实例化的准备,接着我们就要在Main方法里,对Ext类进行实例化,刚刚在Main方法里做了对参数传递的实例,现在我们不要那段代码,保留Main方法默认的样子,接着在Main方法里写入:
string Conf = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "\\FileClass.ini";
这里先定义一下INI所在的位置。Path类属于System命名空间的IO类,Process类属于Sytem命名空间的Diagnostics类,所以我们在开头的引用命名空间那里,加入
using System.IO;
using System.Diagnostics;
Path类里有个GetDirectoryName的方法,作用是返回指定路径字符串的目录信息,参数只有一个:文件或目录的路径。Process.GetCurrentProcess().MainModule.FileName是获得程序在进程中的绝对路径,这个和Path.GetCurrentDirectory不一样的是,前者获得的是程序的绝对路径,后者是获得程序的工作路径。
这下,就获得了本程序下的FileClass.ini配置文件的路径。
也有时候因为某种原因导致配置文件丢失,这个时候我们需要自动生成一个配置文件模版,所以我们接着在配置文件路径定义后面,加上初始化INI配置文件
while(File.Exists(Conf)==false)
{
File.WriteAllText(Conf, "[All]\n\n[Type]\n\n[Ext]\n", Encoding.ASCII);
INI tmpini = new INI(Conf);
tmpini.WirteINI("All", "Num", "1");
tmpini.WirteINI("All", "Folder", "");
tmpini.WirteINI("Type", "-1", "文件夹");
tmpini.WirteINI("Type", "0", "常规");
tmpini.WirteINI("Type", "1", "应用程序");
tmpini.WirteINI("Ext", "1", "exe msi");
}
初始化了一个INI模版,接下来程序就不会报错了,用户只需要对模版进行照猫画虎的修改就可以了。
在程序运行过程中,可能会因为一些异常导致整个程序崩溃,所以我们要进行捕获异常,而带配置的程序常见的异常就是找不到配置,所以在实例化Ext类前,我们采用try..catch..final语句进行捕获异常。因为有了catch,可无需final,所以我们就用try..catch语句。
try
{
}
catch(Exception M)
{
Console.WriteLine(M.Message);
}
Console.ReadKey();
这样我们就完成了对异常进行了捕捉并且提示出来。并且在代码执行的最后能停下来,让用户看到做了哪些操作,哪些操作执行失败。
接着我们在try块里进行实例化,try块会尝试进行操作,一旦发生异常则转移到catch块进行捕获异常。
Ext ExtType = new Ext(Conf);
实例化完Ext类,现在要对参数进行判断,当没有参数的时候要给予提示,有参数则继续工作,所以在实例化代码下面接着写上
if (args.Length > 0)
{
}
else
{
Console.WriteLine("没有文件被载入。");
}
初步判断完成。现在就是要完善这个有参数传入的部分。
Main方法的参数args是个数组,所以需要对args遍历一遍,可以使用foreach语句或for语句,这里我们用for语句:
for (int i = 0; i < args.Length; i++)
{
}
定义一下文件旧路径和新路径以及文件类型的变量
string OPath, NPath,Type;
OPath = args[i];
Type = ExtType.GetType(OPath);
NPath = ExtType.GetFolder() + Type + @"\";
ExtType是我们刚刚实例化的Ext类,GetType和GetFolder是我们自己定义的方法,OPath是文件的旧路径,NPath是文件的新路径,Type是文件类型,这三个变量都是string类型的变量。现在暂停对Main方法的编写,接下来对Ext类进行完善。
首先我们来获取INI配置中的目录,就在Ext类中定义一个公开获取目录的方法。
///
/// 取得归纳目录路径
///
///
public string GetFolder()
{
return Conf.ReadINI("All", "Folder");
}
以上的方法很简单,就是调用INI类的读取配置方法,将读到的值返回而已。
编写GetType方法需要做个准备,就是要明白,怎么处理这个配置文件。回顾下配置文件:
[Type]
-1=文件夹
2=压缩包
[Ext]
2=zip rar r0* r1* arj gz sit sitx sea ace bz2 7z tar tbz
这里截取了[Type]和[Ext]两个节点的数据,以文件夹和压缩包类型为例。现在我们先来看压缩包类型,它的键是2,之前也设定了在配置里归类的扩展名有13个,那么只需要用for语句进行遍历一遍就行。
可以看到,在[Ext]节点里,键为2的值有一大串,并且以空格为分隔符,那么,我们就要对这个数据进行分割,将其存为一个扩展名数组。这里在Ext类里定义一个CutExt方法,用于分割这个扩展名字符串。
///
/// 将扩展名配置存为数组
///
/// 扩展名配置
///
private string[] CutExt(string Ext)
{
string[] Exts = Ext.Split(' ');
return Exts;
}
分割完扩展名,并且通过该方法得到了一个扩展名数组,现在,要对扩展名进行匹配,我们来写个方法完成这项过程:
///
/// 判断扩展名是否符合
///
/// 判断的扩展名
/// 被判断的扩展名配置
///
private bool IsExt(string Ext, string Exts)
{
Ext = Ext.Substring(1);
string[] exts = CutExt(Exts);
for (int i = 0; i < exts.Length; i++)
{
string ext = exts[i];
if (ext.IndexOf("*") >= 0)
{
if (Regex.IsMatch(Ext, ConvertRegex(ext)))
{
return true;
}
}
else
{
if (Ext == ext)
{
return true;
}
}
}
return false;
}
因为在配置文件中出现了"r1*" "r2*"这样通配符的字符串,所以,我们借用了正则匹配,但是正则表达式中的"*"并不是通配符的意思,而是限定符,所以我们得做个改变,编写一个方法来完成这个转换。
///
/// 正则表达式转换
///
/// 待转换字符串
///
public string ConvertRegex(string Str)
{
return "^" + Regex.Escape(Str).Replace("\\*", ".*").Replace("\\?", ".") + "$";
}
注意,正则表达式需要使用System命名空间的Text类下的RegularExpressions类,所以在开头的引用命名空间中加入:
using System.Text.RegularExpressions;
现在都能使用了
稍微解释下前面判断扩展名是否符合的方法
对参数Ext使用Substring(1)是为了处理扩展名前的".",Substring的作用就是从某个起始位置开始截断字符串,如果再加一个参数,则是截断字符数,我们不需要截断那么精确,只是要处理掉扩展名的"."而已,处理结果就是这样的
处理前
.zip
处理后
zip
新建一个string数组变量exts存放被CutExt方法处理的扩展名数组,用for语句遍历扩展名,在循环体内,我们对扩展名进行判断,新建一个string变量ext存放exts数组成员,接着使用IndexOf方法对通配符进行查询,如果查询到,使用正则匹配,如果没有,则直接匹配。正则匹配的方法就不细说,百度上都有。
现在来编写获得文件类型的GetType方法
///
/// 获得指定路径类型
///
/// 文件路径
///
public string GetType(string Name)
{
if (File.Exists(Name))
{
string ext = Path.GetExtension(Name).ToLower();
for (int i = 1; i <= Convert.ToInt32(Conf.ReadINI("All", "Num")); i++)
{
string FileExt = Conf.ReadINI("Ext", Convert.ToString(i));
if (IsExt(ext, FileExt))
{
return Conf.ReadINI("Type", Convert.ToString(i));
}
}
return Conf.ReadINI("Type", "0");
}
else
{
return Conf.ReadINI("Type", "-1");
}
}
我们先对是文件还是文件夹进行判断。是文件,我们接着判断;是文件夹,直接从INI中读取[Type]节中的-1键,取得文件夹这个字符串值。
File类的Exists方法是判断文件是否存在,文件夹不是文件,当然判断不存在了,不存在它就是文件夹,当然这个不是很严谨,万一真的什么都不存在呢,可以按需完善它。
现在判断到文件的确存在,那就建个string类型的ext变量存放文件的扩展名。Path类的GetExtension方法能取得文件的扩展名,结果例如".zip",因为不确定是否会是大写还是小写,所以我们统一成小写,使用ToLower方法。一切完成后,开始遍历配置中的扩展名,读取INI配置中的扩展名数,并转换成int32类型。从1开始遍历,因为-1是文件夹,0是无法归类的常规文件,不是可以按扩展名归类的部分,所以从1开始遍历。
我们已经编写好了扩展名匹配的方法,为了美观,将从配置中得到的扩展名长字符串存为一个变量,接着用扩展名匹配的方法进行匹配,由于我们的工作都做好了,所以很简单的判断就能完成,如果匹配到了,就读取配置文件的[Type]节中指定内容并返回,没有匹配到就读取键0,返回为常规就行。
程序核心编写:
现在基本工作都完成了,可以对Main方法继续编写了
回到刚刚编写Main方法的部分,在之前定义路径的部分继续往下写。
可能在整理文件夹的时候,把用于归类的文件夹也放进去了,结果一层文件夹套着另一层文件夹,很是麻烦,所以判断下移动的文件夹是不是归档文件夹,不是那就继续执行操作。
if (ExtType.IsSystemFolder(OPath) == false)
{
}
else
{
Console.WriteLine("'{0}'是归纳文件夹,移动操作跳过!", OPath);
}
这里看到,我又在Ext类里定义了一个判断文件夹的方法,所以我们回到Ext类编写这个方法
///
/// 判断是否为归纳文件夹
///
/// 文件夹名
///
public bool IsSystemFolder(string FolderName)
{
for (int i = -1; i <= Convert.ToInt32(Conf.ReadINI("All", "Num")); i++)
{
string FolderType = GetFolder() + Conf.ReadINI("Type", Convert.ToString(i));
if (FolderName == FolderType)
{
return true;
}
}
return false;
}
遍历所有扩展名对应目录,并与待匹配的目录进行匹配,匹配成功就返回true,失败就false。
现在也判断了是不是归纳目录,现在文件路径传入了程序,也正确返回了它的类型,正在准备做移动工作,那么他们移动的目标文件夹存在不存在很重要,如果不存在,是会发生异常,所以我们判断下文件夹在不在,不在就创建一个。
while (Directory.Exists(NPath) == false)
{
Directory.CreateDirectory(NPath);
Console.WriteLine("'{0}'不存在,已创建该文件夹!", NPath);
}
这里用if也可以,但万一if执行过程中发生了点问题,没新建文件夹成功,那么后面就会将文件移动到一个不存在的文件夹。
接下来判断移动的文件与将要移动的位置是否相同,如果相同,就跳过操作。
if (OPath != NPath + Path.GetFileName(OPath))
{
}
else
{
Console.WriteLine("'{0}'与'{1}'一样,移动操作跳过!", Path.GetFileName(OPath), Path.GetFileName(NPath + Path.GetFileName(OPath)));
}
Path类的GetFileName方法是取得某路径的文件名或文件夹名。
接着缩小范围,以上发生的问题都不存在,要进行文件复制了,这个时候要对文件和文件夹进行区分,因为移动命令是不同的。
if (Type == "文件夹")
{
Directory.Move(OPath, NPath + Path.GetFileName(OPath));
}
else
{
File.Move(OPath, NPath + Path.GetFileName(OPath));
}
Console.WriteLine("'{0}'是'{1}',已被移动到'{2}'", Path.GetFileName(OPath), Type, NPath);
写到这里,整合一下代码,编译测试下:
可能会出现
startIndex cannot be larger than length of string.
Parameter name: startIndex
这样的错误,所以还需要改进下,到时候我会贴解决办法,如果你有解决办法,可以评论区帮助我
补充
之前的startindex错误是substring引发的,所以找到处理扩展名的那段,把
Ext=Ext.Substring(1);
改成
Ext=Ext.Substring(Ext.IndexOf(".")+1);
就不会报错了