文件分类程序的编写与原理

@伊吹澪  2017/02/01 14:12


很多时候遇到下载文件夹里有很多杂七杂八的文件,但如此多的文件里,整理起来又不方便,这时候就需要用一个脚本或程序对这些文件进行分类整理,那么,这种东西也是有的,在《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);

就不会报错了



添加新评论