C# 文件的输入与输出
一个 文件 是一个存储在磁盘中带有指定名称和目录路径的数据集合。当打开文件进行读写时,它变成一个 流。
从根本上说,流是通过通信路径传递的字节序列。
有两个主要的流:输入流 和 输出流。
输入流用于从文件读取数据(读操作),输出流用于向文件写入数据(写操作)。
C# I/O 类
System.IO 命名空间有各种不同的类,用于执行各种文件操作,如创建和删除文件、读取或写入文件,关闭文件等。
下表列出了一些 System.IO 命名空间中常用的非抽象类:
I/O 类 | 描述 |
---|---|
BinaryReader | 从二进制流读取原始数据。 |
BinaryWriter | 以二进制格式写入原始数据。 |
BufferedStream | 字节流的临时存储。 |
Directory | 有助于操作目录结构。 |
DirectoryInfo | 用于对目录执行操作。 |
DriveInfo | 提供驱动器的信息。 |
File | 有助于处理文件。 |
FileInfo | 用于对文件执行操作。 |
FileStream | 用于文件中任何位置的读写。 |
MemoryStream | 用于随机访问存储在内存中的数据流。 |
Path | 对路径信息执行操作。 |
StreamReader | 用于从字节流中读取字符。 |
StreamWriter | 用于向一个流中写入字符。 |
StringReader | 用于读取字符串缓冲区。 |
StringWriter | 用于写入字符串缓冲区。 |
FileStream 类
System.IO 命名空间中的 FileStream 类有助于文件的读写与关闭。该类派生自抽象类 Stream。
您需要创建一个 FileStream 对象来创建一个新的文件,或打开一个已有的文件。创建 FileStream 对象的语法如下:
FileStream <object_name> = new FileStream( <file_name>, <FileMode Enumerator>, <FileAccess Enumerator>, <FileShare Enumerator>);
例如,创建一个 FileStream 对象 F 来读取名为 sample.txt 的文件:
FileStream F = new FileStream("sample.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
参数 | 描述 |
---|---|
FileMode | FileMode 枚举定义了各种打开文件的方法。FileMode 枚举的成员有:
|
FileAccess | FileAccess 枚举的成员有:Read、ReadWrite 和 Write。 |
FileShare | FileShare 枚举的成员有:
|
实例
下面的程序演示了 FileStream 类的用法:
代码如下:
// 注意是大写的
// 注意:C#里方法名首字母一般大写
using System;
using System.IO;
namespace FileOperationApplication{
public class APP{
static void Main(string[] args)
{
// FileMode.Truncate是打开一个已有的文件并清零
FileStream fileStream = new FileStream("abc.txt",FileMode.OpenOrCreate,FileAccess.ReadWrite);
for(int i = 0;i <= 7;i++){
fileStream.WriteByte((byte)i);
}
// 定位到文本头部,开始读,多读一个byte看看读不到东东是啥
fileStream.Position = 0;
for(int i = 0;i <= 10;i++){
if(i == 10){
Console.Write(fileStream.ReadByte());
}else{
Console.Write(fileStream.ReadByte()+",");
}
}
// 写完之后,关闭
fileStream.Close();
Console.ReadKey();
}
}
}
运行效果如下:
C# 高级文件操作
上面的实例演示了 C# 中简单的文件操作。但是,要充分利用 C# System.IO 类的强大功能,您需要知道这些类常用的属性和方法。
在下面的章节中,我们将讨论这些类和它们执行的操作。请单击链接详细了解各个部分的知识:
主题 | 描述 |
---|---|
文本文件的读写 | 它涉及到文本文件的读写。StreamReader 和 StreamWriter 类有助于完成文本文件的读写。 |
二进制文件的读写 | 它涉及到二进制文件的读写。BinaryReader 和 BinaryWriter 类有助于完成二进制文件的读写。 |
Windows 文件系统的操作 | 它让 C# 程序员能够浏览并定位 Windows 文件和目录。 |
C# 文本文件的读写
StreamReader 和 StreamWriter 类用于文本文件的数据读写。这些类从抽象基类 Stream 继承,Stream 支持文件流的字节读写。
StreamReader 类
StreamReader 类继承自抽象基类 TextReader,表示阅读器读取一系列字符。
下表列出了 StreamReader 类中一些常用的方法:
序号 | 方法 & 描述 |
---|---|
1 | public override void Close() 关闭 StreamReader 对象和基础流,并释放任何与读者相关的系统资源。 |
2 | public override int Peek() 返回下一个可用的字符,但不使用它。 |
3 | public override int Read() 从输入流中读取下一个字符,并把字符位置往前移一个字符。 |
实例
下面的实例演示了读取名为 abc.txt 的文件。文件如下:
我们会慢慢长大成人 随着季节的不停变换 路边盛开的鲜花也在不断变化 那个季节盛开的鲜花到底叫什么名字 轻轻摇曳着 一旦触摸它就会轻轻地被扎到 用鼻子靠近闻一闻 会有一股淡淡的青涩太阳的芳香 随着那股香味慢慢变淡 我们也在长大成人 但是 那朵花一定会在某处继续盛开
当您编译和执行上面的程序时,它会显示文件的内容。
代码如下:
// 注意是大写的
// 注意:C#里方法名首字母一般大写
using System;
using System.IO;
namespace FileOperationApplication{
public class APP{
static void Main(string[] args)
{
// using 语句,可以关闭StreamReader
using(StreamReader reader = new StreamReader("abc.txt")){
string line;
while((line = reader.ReadLine()) != null){
Console.WriteLine(line);
}
}
Console.ReadKey();
}
}
}
运行结果如下:
StreamWriter 类
StreamWriter 类继承自抽象类TextWriter,表示编写器写入一系列字符。
下表列出了 StreamWriter 类中一些常用的方法:
序号 | 方法 & 描述 |
---|---|
1 | public override void Close() 关闭当前的 StreamWriter 对象和基础流。 |
2 | public override voidFlush() 清理当前编写器的所有缓冲区,使得所有缓冲数据写入基础流。 |
3 | public virtual void Write(bool value) 把一个布尔值的文本表示形式写入到文本字符串或流。(继承自 TextWriter。) |
4 | public override void Write( char value ) 把一个字符写入到流。 |
5 | public virtual void Write( decimal value ) 把一个十进制值的文本表示形式写入到文本字符串或流。 |
6 | public virtual void Write( double value ) 把一个 8 字节浮点值的文本表示形式写入到文本字符串或流。 |
7 | public virtual void Write( int value ) 把一个 4 字节有符号整数的文本表示形式写入到文本字符串或流。 |
8 | public override void Write( string value ) 把一个字符串写入到流。 |
9 | public virtual void WriteLine() 把行结束符写入到文本字符串或流。 |
实例
下面的实例演示了使用 StreamWriter 类向文件写入文本数据:
代码如下:
// 注意是大写的
// 注意:C#里方法名首字母一般大写
using System;
using System.IO;
namespace FileOperationApplication{
public class APP{
static void Main(string[] args)
{
string[] nameArr = new string[]{"面码","逢坂大河"};
// using会自动关闭写流
using(StreamWriter writer = new StreamWriter("def.txt")){
foreach(string name in nameArr){
writer.WriteLine(name);
}
}
// 从文件中读取并显示每行
string line = ""; // 注意:这里是StreamReader是流!!!不是StringReader!!!
using(StreamReader reader = new StreamReader("def.txt")){
// 从文件读取显示
while((line = reader.ReadLine()) != null){
Console.WriteLine(line);
}
}
Console.ReadKey();
}
}
}
运行效果如下:
C# 二进制文件的读写
BinaryReader 和 BinaryWriter 类用于二进制文件的读写。
BinaryReader 类
BinaryReader 类用于从文件读取二进制数据。
一个 BinaryReader 对象通过向它的构造函数传递 FileStream 对象而被创建。
下表列出了 BinaryReader 类中一些常用的方法:
序号 | 方法 & 描述 |
---|---|
1 | public override void Close() 关闭 BinaryReader 对象和基础流。 |
2 | public virtual int Read() 从基础流中读取字符,并把流的当前位置往前移。 |
3 | public virtual bool ReadBoolean() 从当前流中读取一个布尔值,并把流的当前位置往前移一个字节。 |
4 | public virtual byte ReadByte() 从当前流中读取下一个字节,并把流的当前位置往前移一个字节。 |
5 | public virtual byte[] ReadBytes( int count ) 从当前流中读取指定数目的字节到一个字节数组中,并把流的当前位置往前移指定数目的字节。 |
6 | public virtual char ReadChar() 从当前流中读取下一个字节,并把流的当前位置按照所使用的编码和从流中读取的指定的字符往前移。 |
7 | public virtual char[] ReadChars( int count ) 从当前流中读取指定数目的字节,在一个字符数组中返回数组,并把流的当前位置按照所使用的编码和从流中读取的指定的字符往前移。 |
8 | public virtual double ReadDouble() 从当前流中读取一个 8 字节浮点值,并把流的当前位置往前移八个字节。 |
9 | public virtual int ReadInt32() 从当前流中读取一个 4 字节有符号整数,并把流的当前位置往前移四个字节。 |
10 | public virtual string ReadString() 从当前流中读取一个字符串。字符串以长度作为前缀,同时编码为一个七位的整数。???Excuse Me??? |
BinaryWriter 类
BinaryWriter 类用于向文件写入二进制数据。
一个 BinaryWriter 对象通过向它的构造函数传递 FileStream 对象而被创建。
下表列出了 BinaryWriter 类中一些常用的方法:
序号 | 方法 & 描述 |
---|---|
1 | public override void Close() 关闭 BinaryWriter 对象和基础流。 |
2 | public virtual voidFlush() 清理当前编写器的所有缓冲区,使得所有缓冲数据写入基础设备。 |
3 | public virtual long Seek( int offset,SeekOrigin origin ) 设置当前流内的位置。如:Seek(-3,Seek.current()) |
4 | public virtual void Write( bool value ) 把一个单字节的布尔值写入到当前流中,0 表示 false,1 表示 true。 |
5 | public virtual void Write( byte value ) 把一个无符号字节写入到当前流中,并把流的位置往前移一个字节。 |
6 | public virtual void Write( byte[] buffer ) 把一个字节数组写入到基础流中。 |
7 | public virtual void Write( char ch ) 把一个 Unicode 字符写入到当前流中,并把流的当前位置按照所使用的编码和要写入到流中的指定的字符往前移。 |
8 | public virtual void Write( char[] chars ) 把一个字符数组写入到当前流中,并把流的当前位置按照所使用的编码和要写入到流中的指定的字符往前移。 |
9 | public virtual void Write( double value ) 把一个 8 字节浮点值写入到当前流中,并把流位置往前移八个字节。 |
10 | public virtual void Write( int value ) 把一个 4 字节有符号整数写入到当前流中,并把流位置往前移四个字节。 |
11 | public virtual void Write( string value ) 把一个以长度为前缀的字符串写入到 BinaryWriter 的当前编码的流中,并把流的当前位置按照所使用的编码和要写入到流中的指定的字符往前移。???Excuse Me??? |
实例
下面的实例演示了读取和写入二进制数据:
代码如下:
// 注意是大写的
// 注意:C#里方法名首字母一般大写
using System;
using System.IO;
namespace FileOperationApplication{
public class APP{
static void Main(string[] args)
{
BinaryWriter binaryWriter = null;
BinaryReader binaryReader = null;
int i = 67;
double d = 3.1415926;
bool b = true;
string str1 = "未闻花名";
string str2 = "龙与虎";
try{
// 二进制输出流是通过包装一个FileStream对象
binaryWriter = new BinaryWriter(new FileStream("ghi.data",FileMode.Create));
}catch(IOException e){
Console.WriteLine("无法创建:"+e.Message);
return;
}
// 按顺序,写数据;到时候也要按顺序读
try{
binaryWriter.Write(i);
binaryWriter.Write(d);
binaryWriter.Write(b);
binaryWriter.Write(str1);
binaryWriter.Write(str2);
}catch(IOException e){
Console.WriteLine("无法写数据:"+e.Message);
}
// 关闭二进制输出流
binaryWriter.Close();
// 读取二进制流
try{
binaryReader = new BinaryReader(new FileStream("ghi.data",FileMode.Open));
}catch(IOException e){
Console.WriteLine("打开文件失败"+e.Message);
return;
}
// 按写入时的顺序,输出到屏幕
try{
// 读取后,会自动将流的位置顺移
int ii = binaryReader.ReadInt32();
Console.WriteLine("int :{0}",ii);
double dd = binaryReader.ReadDouble();
Console.WriteLine("double:{0}",dd);
bool bb = binaryReader.ReadBoolean();
Console.WriteLine("bool:{0}",bb);
// 怎么知道读多长??? 难道是以结尾符 \0判断???
string strstr1 = binaryReader.ReadString();
Console.WriteLine("string:{0}",strstr1);
string strstr2 = binaryReader.ReadString();
Console.WriteLine("string:{0}",strstr2);
}catch(IOException e){
Console.WriteLine("打开文件失败"+e.Message);
}
binaryReader.Close();
Console.ReadKey();
}
}
}
效果如下:
C# Windows 文件系统的操作
C# 允许您使用各种目录和文件相关的类来操作目录和文件,比如 DirectoryInfo 类和 FileInfo 类。
DirectoryInfo 类
DirectoryInfo 类派生自 FileSystemInfo 类。它提供了各种用于创建、移动、浏览目录和子目录的方法。该类不能被继承。
下表列出了 DirectoryInfo 类中一些常用的属性:
序号 | 属性 & 描述 |
---|---|
1 | Attributes 获取当前文件或目录的属性。 |
2 | CreationTime 获取当前文件或目录的创建时间。 |
3 | Exists 获取一个表示目录是否存在的布尔值。 |
4 | Extension 获取表示文件存在的字符串。 |
5 | FullName 获取目录或文件的完整路径。 |
6 | LastAccessTime 获取当前文件或目录最后被访问的时间。 |
7 | Name 获取该 DirectoryInfo 实例的名称。 |
下表列出了 DirectoryInfo 类中一些常用的方法:
序号 | 方法 & 描述 |
---|---|
1 | public void Create() 创建一个目录。 |
2 | public DirectoryInfo CreateSubdirectory( string path ) 在指定的路径上创建子目录。指定的路径可以是相对于 DirectoryInfo 类的实例的路径。 |
3 | public override void Delete() 如果为空的,则删除该 DirectoryInfo。 |
4 | public DirectoryInfo[] GetDirectories() 返回当前目录的子目录。 |
5 | public FileInfo[] GetFiles() 从当前目录返回文件列表。 |
FileInfo 类
FileInfo 类派生自 FileSystemInfo 类。它提供了用于创建、复制、删除、移动、打开文件的属性和方法,且有助于FileStream 对象的创建。该类不能被继承。
下表列出了 FileInfo 类中一些常用的属性:
序号 | 属性 & 描述 |
---|---|
1 | Attributes 获取当前文件的属性。 |
2 | CreationTime 获取当前文件的创建时间。 |
3 | Directory 获取文件所属目录的一个实例。 |
4 | Exists 获取一个表示文件是否存在的布尔值。 |
5 | Extension 获取表示文件存在的字符串。 |
6 | FullName 获取文件的完整路径。 |
7 | LastAccessTime 获取当前文件最后被访问的时间。 |
8 | LastWriteTime 获取文件最后被写入的时间。 |
9 | Length 获取当前文件的大小,以字节为单位。 |
10 | Name 获取文件的名称。 |
下表列出了 FileInfo 类中一些常用的方法:
序号 | 方法 & 描述 |
---|---|
1 | public StreamWriterAppendText() 创建一个 StreamWriter,追加文本到由 FileInfo 的实例表示的文件中。 |
2 | public FileStream Create() 创建一个文件。 |
3 | public override void Delete() 永久删除一个文件。 |
4 | public void MoveTo( string destFileName ) 移动一个指定的文件到一个新的位置,提供选项来指定新的文件名。 |
5 | public FileStream Open( FileMode mode ) 以指定的模式打开一个文件。 |
6 | public FileStream Open( FileMode mode, FileAccess access ) 以指定的模式,使用 read、write 或 read/write 访问,来打开一个文件。 |
7 | public FileStream Open( FileMode mode, FileAccess access, FileShare share ) 以指定的模式,使用 read、write 或 read/write 访问,以及指定的分享选项,来打开一个文件。 |
8 | public FileStreamOpenRead() 创建一个只读的 FileStream。 |
9 | public FileStreamOpenWrite() 创建一个只写的 FileStream。 |
实例
下面的实例演示了上面提到的类的用法:
当您编译和执行下面的程序时,它会显示文件的名称及它们在 Windows 目录中的大小。
代码如下:
// 注意是大写的
// 注意:C#里方法名首字母一般大写
using System;
using System.IO;
namespace FileOperationApplication{
public class APP{
static void Main(string[] args)
{
string currentFullPath = System.Environment.CurrentDirectory;
DirectoryInfo currentDir = new DirectoryInfo(currentFullPath);
// 获取当前路径下的所有文件及名称、大小
FileInfo[] fileInfoArr = currentDir.GetFiles();
foreach(FileInfo info in fileInfoArr){
// 长度要不要处理
Console.WriteLine("File:{0} Size:{1} byte",info.Name,info.Length);
}
Console.ReadKey();
}
}
}
运行效果如下:
C# 正则表达式
可以先阅读一下 正则_01_基础教程
正则表达式 是一种匹配输入文本的模式。.Net 框架提供了允许这种匹配的正则表达式引擎。
模式由一个或多个字符、运算符和结构组成。
定义正则表达式
下面列出了用于定义正则表达式的各种类别的字符、运算符和结构。
- 字符转义
- 字符类
- 定位点
- 分组构造
- 限定符
- 反向引用构造
- 备用构造
- 替换
- 杂项构造
字符转义
正则表达式中的反斜杠字符(\)指示其后跟的字符是特殊字符,或应按原义解释该字符。
\w 匹配任何字类字符,包括下划线。与“[A-Za-z0-9_]”等效。
下表列出了转义字符:
转义字符 | 描述 | 模式 | 匹配示例 |
---|---|---|---|
\a | 与报警 (bell) 符 \u0007 匹配。 | \a | "Warning!" + '\u0007' 中的 "\u0007" |
\b | 在字符类中,与退格键 \u0008 匹配。 | [\b]{3,} | "\b\b\b\b" 中的 "\b\b\b\b" |
\t | 与制表符 \u0009 匹配。 | (\w+)\t | "Name\tAddr\t" 中的 "Name\t" 和 "Addr\t" |
\r | 与回车符 \u000D 匹配。(\r 与换行符 \n 不是等效的。) | \r\n(\w+) | "\r\nHello World." 中的 "\r\nHello" |
\v | 与垂直制表符 \u000B 匹配。 | [\v]{2,} | "\v\v\v" 中的 "\v\v\v" |
\f | 与换页符 \u000C 匹配。 | [\f]{2,} | "\f\f\f" 中的 "\f\f\f" |
\n | 与换行符 \u000A 匹配。 | \r\n(\w+) | "\r\Hello\nWorld." 中的 "\r\nHello" |
\e | 与转义符 \u001B 匹配。 | \e | "\x001B" 中的 "\x001B" |
\ nnn ???Excuse Me??? | 使用八进制表示形式指定一个字符(nnn 由二到三位数字组成)。 | \w\040\w | "a bc d" 中的 "a b" 和 "c d" |
\x nn ???Excuse Me??? | 使用十六进制表示形式指定字符(nn 恰好由两位数字组成)。 | \w\x20\w | "a bc d" 中的 "a b" 和 "c d" |
\c X \c x ???Excuse Me??? | 匹配 X 或 x 指定的 ASCII 控件字符,其中 X 或 x 是控件字符的字母。 | \cC | "\x0003" 中的 "\x0003" (Ctrl-C) |
\u nnnn ???Excuse Me??? | 使用十六进制表示形式匹配一个 Unicode 字符(由 nnnn 表示的四位数)。 | \w\u0020\w | "a bc d" 中的 "a b" 和 "c d" |
\ ???Excuse Me??? | 在后面带有不识别的转义字符时,与该字符匹配。 | \d+[\+-x\*]\d+\d+[\+-x\*\d+ | "(2+2) * 3*9" 中的 "2+2" 和 "3*9" |
字符类
字符类与一组字符中的任何一个字符匹配。
下表列出了字符类:
字符类 | 描述 | 模式 | 匹配 |
---|---|---|---|
[character_group] | 匹配 character_group 中的任何单个字符。 默认情况下,匹配区分大小写。 | [mn] | "mat" 中的 "m","moon" 中的 "m" 和 "n" |
[^character_group] | 非:与不在 character_group 中的任何单个字符匹配。 默认情况下,character_group 中的字符区分大小写。 | [^aei] | "avail" 中的 "v" 和 "l" |
[ first - last ] | 字符范围:与从 first 到 last 的范围中的任何单个字符匹配。 | (\w+)\t | "Name\tAddress\t" 中的 "Name\t" 和 "Address\t" |
. | 通配符:与除 \n 之外的任何单个字符匹配。 若要匹配原意句点字符(. 或 \u002E),您必须在该字符前面加上转义符 (\.)。 | a.e | "have" 中的 "ave", "mate" 中的 "ate" |
\p{ name } ???Excuse Me??? | 与 name 指定的 Unicode 通用类别或命名块中的任何单个字符匹配。 | \p{Lu} | "City Lights" 中的 "C" 和 "L" |
\P{ name } ???Excuse Me??? | 与不在 name 指定的 Unicode 通用类别或命名块中的任何单个字符匹配。 | \P{Lu} | "City" 中的 "i"、 "t" 和 "y" |
\w | 与任何单词字母和下划线字符匹配。 | \w | "Room#1" 中的 "R"、 "o"、 "m" 和 "1" |
\W | 与任何非单词字母和下划线字符匹配。 | \W | "Room#1" 中的 "#" |
\s | 与任何空白字符匹配。 | \w\s | "ID A1.3" 中的 "D " |
\S | 与任何非空白字符匹配。 | \s\S | "int __ctr" 中的 " _" |
\d | 与任何十进制数字匹配。 | \d | "4 = IV" 中的 "4" |
\D | 匹配不是十进制数的任意字符。 | \D | "4 = IV" 中的 " "、 "="、 " "、 "I" 和 "V" |
定位点
定位点 或 原子零宽度 断言会使匹配成功或失败,具体取决于字符串中的当前位置,
但它们不会使引擎在字符串中前进或使用字符。仅仅是匹配位置
下表列出了定位点:
断言 | 描述 | 模式 | 匹配 |
---|---|---|---|
^ | 匹配必须从字符串或一行的开头开始。 | ^\d{3} | "567-777-" 中的 "567" |
$ | 匹配必须出现在字符串的末尾或出现在行或字符串末尾的 \n 之前。 | -\d{4}$ | "8-12-2012" 中的 "-2012" |
\A | 匹配必须出现在字符串的开头。 | \A\w{4} | "Code-007-" 中的 "Code" |
\Z | 匹配必须出现在字符串的末尾或出现在字符串末尾的 \n 之前。 | -\d{3}\Z | "Bond-901-007" 中的 "-007" |
\z | 匹配必须出现在字符串的末尾。 | -\d{3}\z | "-901-333" 中的 "-333" |
\G | 匹配必须出现在上一个匹配结束的地方。是不是要挨在一起?????? | \\G\(\d\) ???Excuse Me??? | "(1)(3)(5)[7](9)" 中的 "(1)"、 "(3)" 和 "(5)" |
\b | 匹配必须出现在 \w(字母数字)和 \W(非字母数字)字符之间的边界上。 | \w | "Room#1" 中的 "R"、 "o"、 "m" 和 "1" |
\B | 匹配不得出现在 \b 边界上。 | \Bend\w*\b | "end sends endure lender" 中的 "ends" 和 "ender" |
分组的引入:
对于要重复单个字符,非常简单,直接在字符后卖弄加上限定符即可,
例如:
a+ 表示匹配1个或一个以上的a
a?表示匹配0个或1个a (有没有,有的话,就是一个)
这些限定符如下所示:
X ? | X ,一次或一次也没有 (即有或没有) |
X * | X ,零次或多次 (任意次数) |
X + | X ,一次或多次 (至少一次或以上) |
X { n } | X ,恰好 n 次 |
X { n ,} | X ,至少 n 次 |
X { n , m } | X ,至少 n 次,但是不超过 m 次 |
但是我们如果要对多个字符进行重复怎么办呢?此时我们就要用到分组,
我们可以使用小括号"()"来指定要重复的子表达式,然后对这个子表达式进行重复,
例如:
(abc)? 表示0个或1个abc
这里一 个括号的表达式就表示一个分组
分组可以分为两种形式,捕获组和非捕获组。
捕获组
捕获组可以通过从左到右计算其开括号来编号 。例如,在表达式 (A)(B(C)) 中,存在四个这样的组:
0 | (A)(B(C)) |
1 | (A) |
2 | (B(C)) |
3 | (C) |
组零 始终代表整个表达式
之所以这样命名捕获组是因为在匹配中,保存了与这些组匹配的输入序列的每个子序列。
捕获的子序列稍后可以通过 Back 引用(反向引用) 在表达式中使用,也可以在匹配操作完成后从匹配器检索。
Back 引用
是说在后面的表达式中我们可以使用组的编号来引用前面的表达式所捕获到的文本序列。
注意:反向引用:引用的是前面捕获组中的文本而不是正则,也就是说反向引用处匹配的文本应和前面捕获组中的文本相同
【例】
(["']).*\1
其中使用了分组,\1就是对小括号这个分组的引用,
整个表达式,它匹配 包含在两个引号或者两个单引号中的所有字符串,如:"abc" 或 " ' " 或 ' " ' ,
但是请注意,它并不会对" a'或者 'a"匹配。
原因上面已经说明,Back引用 只是引用文本而不是表达式。
非捕获组
以 (?) 开头的组是纯的非捕获组,它不捕获文本 ,也不针对组合计进行计数。
就是说,如果小括号中以?号开头,那么这个分组就不会捕获文本,当然也不会有组的编号,因此也不存在Back 引用。
我们通过捕获组就能够得到我们想要匹配的内容了,那为什么还要有非捕获组呢?
答案就是: 位置
原因是捕获组捕获的内容是被存储在内存中,可供以后使用,
比如反向引用就是引用的内存中 存储的捕获组中 捕获的内容。
而非捕获组则不会捕获文本,也不会将它匹配到的内容单独分组来放到内存中。
所以,使用非捕获组较使用捕获组更节省内存。在实际情况中我们要酌情选用。
1、非捕获组(?:Pattern)
它的作用就是匹配Pattern字符,好处就是不捕获文本,不将匹配到的字符存储到内存中,从而节省内存。
【例】匹配indestry或者indestries
我们可以使用indestr(y|ies)或者indestr(?:y|ies)
【例】(?:a|A)123(?:b)可以匹配a123b或者A123b
非捕获组有很多种形式,其中包括:零宽度断言和模式修正符
使用小括号的时候,还有很多特定用途的语法。
分类 | 代码/语法 | 常见分组说明 |
---|---|---|
捕获 | (exp) | 匹配exp,并捕获文本到自动命名的组里,组0为整个,组1,组2等等 |
(?<name>exp) | 匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp) | |
(?:exp) | 匹配exp,不捕获匹配的文本,也不给此分组分配组号 | |
零宽断言 | (?=exp) | 匹配exp前面的位置 要匹配的文本的后缀是exp |
(?<=exp) | 匹配exp后面的位置 要匹配的文本的前缀是exp | |
(?!exp) | 匹配后面跟的不是exp的位置 要匹配的文本的后面不是exp | |
(?<!exp) | 匹配前面不是exp的位置 要匹配的文本的前面不是exp | |
注释 | (?#comment) | 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读 |
2、零宽度断言
零宽度: 就是没有宽度, 0个宽度, 不匹配任何字符,仅仅是匹配一个位置,如行首,行尾,词首,词尾
断言: 就是 X is Y, 或者 X is not Y
正:
负:
(?= X ) | X ,零宽的正 lookahead,要匹配的文本的后缀是X |
(?! X ) | X ,零宽的负 lookahead,要匹配的文本的后缀不是X |
(?<= X ) | X ,零宽的正 lookbehind,要匹配的文本的前缀是X |
(?<! X ) | X ,零宽的负 lookbehind,要匹配的文本的前缀不是X |
这四个非捕获组用于匹配表达式X,但是不包含表达式的文本,仅仅是匹配一个位置(零宽度)
(?=X )
零宽度 正 先行断言。仅当子表达式 X在 此位置的右侧匹配时才继续匹配。
也就是说:要匹配的文本的后缀是 X的话,才成立
也就是说:必须把这个非捕获组放在整个表达式的右侧。
例如,/w+(?=/d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。
(?!X)
零宽度 负 先行断言。仅当子表达式 X 不在此位置的右侧匹配时才继续匹配。
也就是说:要匹配的文本的后缀 不是X的话,才成立
例如,例如,/w+(?!/d) 与后面不跟数字的单词匹配,而不与该数字匹配 。
(?<=X)
零宽度 正 后发断言。仅当子表达式 X 在此位置的左侧 匹配时才继续匹配。
也就是说:要匹配的文本的前缀是X的话,才成立
例如,(?<=19)99 与跟在 19 后面的 99 的实例匹配。此构造不会回溯。
(?<!X)
零宽度 负 后发断言。仅当子表达式 X 不在此位置的左侧匹配时才继续匹配。
也就是说:要匹配的文本的前缀 不是X的话,才成立
例如,(?<!19)99 与不跟在 19 后面的 99 的实例匹配
【例1】正则表达式 (?<!4)56(?=9)
含义:匹配后面的文本56前面不能是4,后面必须是9组成。
因此,可以匹配如下文本 5569 ,与4569不匹配。
【例2】提取字符串 da12bka3434bdca4343bdca234bm 中包含在字符a和b之间的数字,
但是这个a之前的字符不能是c;
b后面的字符必须是d
显然,这里就只有3434这个数字满足要求。那么我们怎么提取呢?
首先,我们写出含有捕获组的正则表达式:[^c]a\d*bd
然后我们再将其变为非捕获组的正则表达式:(?<=[^c]a)\d*(?=bd)
3、模式修正符
以(?)开头的非捕获组除了零宽断言之外,还有模式修正符。
正则表达式中常用的模式修正符有m、i、g、s、e、x等。它们之间可以组合搭配使用。
m:
i:忽略大小写
g:全局匹配
s: 单行模式
e:
x:
(?imnsx-imnsx: ) 横杠的前面是打开或启用, 横杠的后面是关闭或禁用 子表达式中指定的选项。
例如,(?i-s: ) 将打开不区分大小写并禁用单行模式。
(?-i) 将关闭不区分大小写的开关
【例1】(?i)ab
表示对(?i)后的所有字符都 开启不区分大小写的开关。 故它可以匹配ab、aB、Ab、AB
【例2】(?i:a)b
它表示只对a开启不区分大小写的开关。故它可以匹配ab和Ab。却不能匹配aB和AB。
4、(?>Pattern)等同于侵占模式
匹配成功不进行回溯,这个比较复杂,与侵占量词“+”可以通用,比如:\d++ 可以写为 (?>\d+)。
【例】将一些多位的小数截短到三位小数:\d+\.\d\d[1-9]?\d+
在这种条件下 6.625 能进行匹配,这样做没有必要,因为它本身就是三位小数。最后一个“5”本来是给 [1-9] 匹配的,但是后面还有一个 \d+ 所以,[1-9] 由于是“?”可以不匹配所以只能放弃当前的匹配,将这个“5”送给 \d+ 去匹配,如果改为:
\d+\.\d\d[1-9]?+\d+
的侵占形式,在“5”匹配到 [1-9] 时,由于是侵占式的,所以不会进行回溯,后面的 \d+ 就匹配不到任东西了,所以导致 6.625 匹配失败。
这种情况,在替换时就有效了,比如把数字截短到小数点后三位,如果正好是三位小数的,就可以不用替换了,可以提高效率,侵占量词基本上就是用来提高匹配效率的。
把 \d+\.\d\d[1-9]?+\d+ 改为 \d+\.\d\d(?>[1-9]?)\d+ 这样是一样的。
分组构造
分组构造描述了正则表达式的子表达式,通常用于捕获输入字符串的子字符串。
下表列出了分组构造:
分组构造 | 描述 | 模式 | 匹配 |
---|---|---|---|
( subexpression ) | 捕获匹配的子表达式并将其分配到一个从零开始的序号中。 | (\w)\1 | "deep" 中的 "ee" |
(?< name >subexpression) | 将匹配的子表达式捕获到一个命名组中。 | (?< double>\w)\k< double> | "deep" 中的 "ee" |
(?< name1 -name2 >subexpression) | 定义平衡组定义。 | (((?'Open'\()[^\(\)]*)+((?'Close-Open'\))[^\(\)]*)+)*(?(Open)(?!))$ | "3+2^((1-3)*(3-1))" 中的 "((1-3)*(3-1))" |
(?: subexpression) | 定义非捕获组。 | Write(?:Line)? | "Console.WriteLine()" 中的 "WriteLine" |
(?imnsx-imnsx:subexpression) | 应用或禁用 subexpression 中指定的选项。 | A\d{2}(?i:\w+)\b | "A12xl A12XL a12xl" 中的 "A12xl" 和 "A12XL" |
(?= subexpression) | 零宽度正预测先行断言。 | \w+(?=\.) | "He is. The dog ran. The sun is out." 中的 "is"、 "ran" 和 "out" |
(?! subexpression) | 零宽度负预测先行断言。 | \b(?!un)\w+\b | "unsure sure unity used" 中的 "sure" 和 "used" |
(?< =subexpression) | 零宽度正回顾后发断言。 | (?A+B+) | "1ABB 3ABBC 5AB 5AC" 中的 "1ABB"、 "3ABB" 和 "5AB" |
实例
using System; using System.Text.RegularExpressions; public class Example { public static void Main() { string input = "1851 1999 1950 1905 2003"; string pattern = @"(?<=19)\d{2}\b"; foreach (Match match in Regex.Matches(input, pattern)) Console.WriteLine(match.Value); } }
限定符
限定符指定在输入字符串中必须存在上一个元素(可以是字符、组或字符类)的多少个实例才能出现匹配项。 限定符包括下表中列出的语言元素。
下表列出了限定符:
限定符 | 描述 | 模式 | 匹配 |
---|---|---|---|
* | 匹配上一个元素零次或多次。 | \d*\.\d | ".0"、 "19.9"、 "219.9" |
+ | 匹配上一个元素一次或多次。 | "be+" | "been" 中的 "bee", "bent" 中的 "be" |
? | 匹配上一个元素零次或一次。 | "rai?n" | "ran"、 "rain" |
{ n } | 匹配上一个元素恰好 n 次。 | ",\d{3}" | "1,043.6" 中的 ",043", "9,876,543,210" 中的 ",876"、 ",543" 和 ",210" |
{ n ,} | 匹配上一个元素至少 n 次。 | "\d{2,}" | "166"、 "29"、 "1930" |
{ n , m } | 匹配上一个元素至少 n 次,但不多于 m 次。 | "\d{3,5}" | "166", "17668", "193024" 中的 "19302" |
*? | 匹配上一个元素零次或多次,但次数尽可能少。 | \d*?\.\d | ".0"、 "19.9"、 "219.9" |
+? | 匹配上一个元素一次或多次,但次数尽可能少。 | "be+?" | "been" 中的 "be", "bent" 中的 "be" |
?? | 匹配上一个元素零次或一次,但次数尽可能少。 | "rai??n" | "ran"、 "rain" |
{ n }? | 匹配前导元素恰好 n 次。 | ",\d{3}?" | "1,043.6" 中的 ",043", "9,876,543,210" 中的 ",876"、 ",543" 和 ",210" |
{ n ,}? | 匹配上一个元素至少 n 次,但次数尽可能少。 | "\d{2,}?" | "166"、 "29" 和 "1930" |
{ n , m }? | 匹配上一个元素的次数介于 n 和 m 之间,但次数尽可能少。 | "\d{3,5}?" | "166", "17668", "193024" 中的 "193" 和 "024" |
反向引用构造
反向引用允许在同一正则表达式中随后标识以前匹配的子表达式。
下表列出了反向引用构造:
反向引用构造 | 描述 | 模式 | 匹配 |
---|---|---|---|
\ number | 反向引用。 匹配编号子表达式的值。 | (\w)\1 | "seek" 中的 "ee" |
\k< name > | 命名反向引用。 匹配命名表达式的值。 | (?< char>\w)\k< char> | "seek" 中的 "ee" |
备用构造
备用构造用于修改正则表达式以启用 either/or 匹配。
下表列出了备用构造:
备用构造 | 描述 | 模式 | 匹配 |
---|---|---|---|
| | 匹配以竖线 (|) 字符分隔的任何一个元素。 | th(e|is|at) | "this is the day. " 中的 "the" 和 "this" |
(?( expression )yes | no ) | 如果正则表达式模式由 expression 匹配指定,则匹配 yes;否则匹配可选的 no 部分。 expression 被解释为零宽度断言。 | (?(A)A\d{2}\b|\b\d{3}\b) | "A10 C103 910" 中的 "A10" 和 "910" |
(?( name )yes | no ) | 如果 name 或已命名或已编号的捕获组具有匹配,则匹配 yes;否则匹配可选的 no。 | (?< quoted>")?(?(quoted).+?"|\S+\s) | "Dogs.jpg "Yiska playing.jpg"" 中的 Dogs.jpg 和 "Yiska playing.jpg" |
替换
替换是替换模式中使用的正则表达式。
下表列出了用于替换的字符:
字符 | 描述 | 模式 | 替换模式 | 输入字符串 | 结果字符串 |
---|---|---|---|---|---|
$number | 替换按组 number 匹配的子字符串。 | \b(\w+)(\s)(\w+)\b | $3$2$1 | "one two" | "two one" |
${name} | 替换按命名组 name 匹配的子字符串。 | \b(?< word1>\w+)(\s)(?< word2>\w+)\b | ${word2} ${word1} | "one two" | "two one" |
$$ | 替换字符"$"。 | \b(\d+)\s?USD | $$$1 | "103 USD" | "$103" |
$& | 替换整个匹配项的一个副本。 | (\$*(\d*(\.+\d+)?){1}) | **$& | "$1.30" | "**$1.30**" |
$` | 替换匹配前的输入字符串的所有文本。 | B+ | $` | "AABBCC" | "AAAACC" |
$' | 替换匹配后的输入字符串的所有文本。 | B+ | $' | "AABBCC" | "AACCCC" |
$+ | 替换最后捕获的组。 | B+(C+) | $+ | "AABBCCDD" | AACCDD |
$_ | 替换整个输入字符串。 | B+ | $_ | "AABBCC" | "AAAABBCCCC" |
杂项构造
下表列出了各种杂项构造:
构造 | 描述 | 实例 |
---|---|---|
(?imnsx-imnsx) | 在模式中间对诸如不区分大小写这样的选项进行设置或禁用。 | \bA(?i)b\w+\b 匹配 "ABA Able Act" 中的 "ABA" 和 "Able" |
(?#注释) | 内联注释。该注释在第一个右括号处终止。 | \bA(?#匹配以A开头的单词)\w+\b |
# [行尾] | 该注释以非转义的 # 开头,并继续到行的结尾。 | (?x)\bA\w+\b#匹配以 A 开头的单词 |
Regex 类
Regex 类用于表示一个正则表达式。
下表列出了 Regex 类中一些常用的方法:
序号 | 方法 & 描述 |
---|---|
1 | public bool IsMatch( string input ) 指示 Regex 构造函数中指定的正则表达式是否在指定的输入字符串中找到匹配项。 |
2 | public bool IsMatch( string input, int startat ) 指示 Regex 构造函数中指定的正则表达式是否在指定的输入字符串中找到匹配项,从字符串中指定的开始位置开始。 |
3 | public static bool IsMatch( string input, string pattern ) 指示指定的正则表达式是否在指定的输入字符串中找到匹配项。 |
4 | public MatchCollection Matches( string input ) 在指定的输入字符串中搜索正则表达式的所有匹配项。 |
5 | public string Replace( string input, string replacement ) 在指定的输入字符串中,把所有匹配正则表达式模式的所有匹配的字符串替换为指定的替换字符串。 |
6 | public string[] Split( string input ) 把输入字符串分割为子字符串数组,根据在 Regex 构造函数中指定的正则表达式模式定义的位置进行分割。 |
实例 1
下面的实例匹配了以 'S' 开头的单词:
using System; using System.Text.RegularExpressions; namespace RegExApplication { class Program { private static void showMatch(string text, string expr) { Console.WriteLine("The Expression: " + expr); MatchCollection mc = Regex.Matches(text, expr); foreach (Match m in mc) { Console.WriteLine(m); } } static void Main(string[] args) { string str = "A Thousand Splendid Suns"; Console.WriteLine("Matching words that start with 'S': "); showMatch(str, @"\bS\S*"); Console.ReadKey(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
Matching words that start with 'S': The Expression: \bS\S* Splendid Suns
实例 2
下面的实例匹配了以 'm' 开头以 'e' 结尾的单词:
using System; using System.Text.RegularExpressions; namespace RegExApplication { class Program { private static void showMatch(string text, string expr) { Console.WriteLine("The Expression: " + expr); MatchCollection mc = Regex.Matches(text, expr); foreach (Match m in mc) { Console.WriteLine(m); } } static void Main(string[] args) { string str = "make maze and manage to measure it"; Console.WriteLine("Matching words start with 'm' and ends with 'e':"); showMatch(str, @"\bm\S*e\b"); Console.ReadKey(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
Matching words start with 'm' and ends with 'e': The Expression: \bm\S*e\b make maze manage measure
实例 3
下面的实例替换掉多余的空格:
using System; using System.Text.RegularExpressions; namespace RegExApplication { class Program { static void Main(string[] args) { string input = "Hello World "; string pattern = "\\s+"; string replacement = " "; Regex rgx = new Regex(pattern); string result = rgx.Replace(input, replacement); Console.WriteLine("Original String: {0}", input); Console.WriteLine("Replacement String: {0}", result); Console.ReadKey(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
Original String: Hello World Replacement String: Hello World
未完待续,下一章节,つづく