一、序言
在软件构造Lab 3中,要求实现一个基于语法读入的功能,对文本文件中的数据进行解析,判断其中的信息是否符合规定的格式要求。若符合,则按照格式要求从文件中获取信息;若不能,则说明格式错误,需要输出一个错误提示给用户,要求用户输入格式正确的文本。
二、正则表达式
在Java程序设计中,有一个很强大的工具——正则表达式。它可以按照自定义的格式对文本进行解析、匹配,根据文本的特征提取出我们需要的信息。下面是Java中常见的一些正则表达式符号:
符号 | 含义 |
[abc] | a、b或c |
[^abc] | 除a、b、c之外的任何字符 |
[a-zA-Z] | a~z、A~Z |
[a-d[m-p]] | a~d或m~p |
[a-z&&[def]] | d、e或f |
[a-z&&[^bc]] | a~z中除了b、c以外的字母 |
[a-z&&[^m-p]] | a~z中除了m~p以外的字母 |
. | 任意字符 |
\d | 数字0~9 |
\D | 非数字 |
\s | 空白符( \t\n\x0B\f\r) |
\S | 非空白符 |
\w | 英文字母和数字 |
\W | 非英文字母和数字 |
在使用正则表达式进行匹配时,还需要使用到一些类:
- Pattern 类:
pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。
- Matcher 类:
Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。
代码实例:
public static void main(String args[]) {
String str = "123abc123";
String regex = "\\d+([a-z]+)\\d+";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(str);
if (m.find()) {
System.out.println(m.group(0));
System.out.println(m.group(1));
}
}
假设有一个待匹配的字符串“123abc123”,我们规定的正则表达式是两段连续的数字中间夹着一段连续的小写英文字母,即“\\d+([a-z]+)\\d+”,然后要对字符串进行匹配。
首先,我们需要将正则表达式变为编译后的格式,需要用到前面介绍的Pattern类。然后,对字符串进行匹配,又要用到Matcher类。Matcher类的find方法会表明是否在字符串中匹配到了正则表达式规定格式的信息。若匹配到了,则返回true,否则返回false。当有多个匹配结果时,可以将代码中的if替换为while,每次循环对其中的一个匹配结果进行处理。
代码的运行结果为:
123abc123
abc
可以发现,Matcher的匹配结果中有多个group,其中的内容不太一样。这样的group叫做“捕获组”,可以将正则表达式内括号中的部分进行单独分组并给出。正则表达式中的每个括号对应着一个组,其序号按照括号出现的先后顺序从1到n进行排序,而0对应的组是整个表达式。并且,这些括号是允许嵌套的。
在我们的例子中,正则表达式中的“[a-z]+”被单独括了起来,在处理各捕获组时便可以直接获取到这部分的内容,即中间的小写字母。
三、实验中的应用
在实验中,要求匹配如下格式的文本内容(“//”后面是注释,不包含在格式要求内):
Employee{ //多个员工信息ZhangSan{Manger,139-0451-0000} //{}前是员工姓名,大小写字母构成//不同的行内的员工名字是唯一的,不能重复LiSi{Secretary,151-0101-0000} //{}内第一个分量是职务,大小写字母和空格WangWu{Associate Dean,177-2021-0301}//{}内第二个分量是中国境内合法的手机号码//共 11 位,分为三段(3-4-4),用“-”相连}Period{2021-01-10,2021-03-06} //表示排班的开始日期和结束日期Roster{ //表示排班计划ZhangSan{2021-01-10,2021-01-31} //分别为姓名、开始日期、结束日期LiSi{2021-02-01,2021-02-28} //上述所有日期的格式为 yyyy-MM-ddWangWu{2021-03-01,2021-03-10}//出现在 Roster 内的员工,必须在 Employee 中已有定义,否则不合法} //文件里描述的 Employee、 Period 、 Roster 的次序是不确定的,违反该次序不能被看作非法。
可以注意到,整个文本中包含了三块信息,即Employee、Period和Roster,且它们之间的顺序可以是任意的。我们的第一个任务就是先分别将三个块中的信息提取出来。但是,此处不能调用String类的split方法使用大括号进行分割,因为其中有多层嵌套的大括号,并且可能会出现一些非法格式的干扰项。于是,我们考虑使用正则表达式。
注意到三部分的大括号嵌套层数是固定的,Employee和Roster是两层,而Period是一层,并且大括号前面都有唯一确定的字符串标识。我们可以利用这个特性来进行匹配。对于单层的大括号,其正则表达式是:
\\{[^\\{\\}]+\\}
这里的“{”和“}”均使用“\\”进行了转义,因为大括号在正则表达式中有特殊的意义。而在两个大括号中间,是一些非大括号的字符,可以用“[^\\{\\}]”来表示。最后在这个正则表达式的前面加上标签“Period”,就可以匹配到Period块中的数据了。以下是代码和运行结果:
Pattern p = Pattern.compile("Period\\{([^\\{\\}]+)\\}");
Matcher m = p.matcher(text);
if (m.find()) {
System.out.println(m.group(0));
System.out.println(m.group(1));
}
Period{2021-01-10,2021-03-06}
2021-01-10,2021-03-06
可以看到,我们在正则表达式的适当位置插入小括号,可以在匹配的同时直接提取出我们感兴趣的信息(如输出的第二行所示)。
而对于Employee和Roster的双层嵌套大括号,其正则表达式会更复杂一些:
\\{[^\\{\\}]*(\\{[^\\{\\}]*\\}[^\\{\\}]*)*\\}
其含义其实很简单,就是在最外层的两个大括号中间,又有一对大括号,而在这四个大括号之间的位置,会有若干非大括号符号组成的字符串。在书写时我们只需要牢记,“\\{”代表前半个大括号,“\\}”代表后半个大括号,而“[^\\{\\}]”代表其他的一些非大括号字符。根据双层括号的特点{非大括号{非大括号}非大括号}即可写出对应的正则表达式。
代码和匹配结果如下:
p = Pattern.compile("Employee\\{[^\\{\\}]*(\\{[^\\{\\}]*\\}[^\\{\\}]*)*\\}");
m = p.matcher(text);
String employeeText = "";
if (m.find()) {
System.out.println(m.group(0));
}
Employee{ ZhangSan{Manger,139-0451-0000} LiSi{Secretary,151-0101-0000} WangWu{Associate Dean,177-2021-0301}}
p = Pattern.compile("[a-zA-Z]+\\{[^\\{\\}]*\\}");
m = p.matcher(rosterString);
List<String> rosterList = new ArrayList<>();
if (m.find()) {
System.out.println(m.group(0));
}
Roster{ ZhangSan{2021-01-10,2021-01-31} LiSi{2021-02-01,2021-02-28} WangWu{2021-03-01,2021-03-10}}
现在,三个模块中的信息以及分别被我们提取出来了,剩下的工作就是对每个块中的信息进行进一步解析,提取出我们可以使用的数据。
对于Employee和Roster,外层大括号内又包含着若干个单层的大括号。我们可以使用同刚才一样的方法,对每个单层大括号进行匹配。由于匹配结果不止一个,我们将if改成while,处理所有的匹配结果。以Roster为例:
p = Pattern.compile("[a-zA-Z]+\\{[^\\{\\}]*\\}");
m = p.matcher(rosterString);
List<String> rosterList = new ArrayList<>();
while (m.find()) {
System.out.println(m.group(0));
}
匹配结果为:
ZhangSan{2021-01-10,2021-01-31}
LiSi{2021-02-01,2021-02-28}
WangWu{2021-03-01,2021-03-10}
由此可见,我们又将外层括号中的每个条目单独提取出来了。Employee模块中也可以采用相同的策略获取每个条目的信息。接下来的工作,就是逐一处理这些条目的信息,判断是否符合格式要求,若符合则对这些信息进行提取。还是以Roster为例,我们可以对刚刚提取出的每个条目,调用String类的split方法,分别获取姓名和两个日期字符串,然后再次使用正则表达式,判断每个字符串是否符合要求:
for (String r : rosterList) {
String[] tmp = r.split("\\{|\\}|,");
if (tmp.length != 3)
System.out.println("Wrong format!");
if (!tmp[0].matches("[a-zA-Z]+") || !tmp[1].matches("\\d\\d\\d\\d-\\d\\d-\\d\\d") || !tmp[2].matches("\\d\\d\\d\\d-\\d\\d-\\d\\d"))
System.out.println("Wrong format!");
System.out.println(tmp[0]);
System.out.println(tmp[1]);
System.out.println(tmp[2]);
}
输出结果为:
ZhangSan
2021-01-10
2021-01-31
LiSi
2021-02-01
2021-02-28
WangWu
2021-03-01
2021-03-10
可见,我们已经成功检查了文本的格式,并且将整块信息分割成一个个我们可以直接使用的单独的信息。对于其他模块的信息,可以采用相同的方法,先分割,再逐一匹配,最后获得需要的信息。唯一的区别,只有在进行匹配时的正则表达式不同。如,在匹配“yyyy-mm-dd”格式的日期时,正则表达式应为:
\\d\\d\\d\\d-\\d\\d-\\d\\d
当完成所有工作后,基于语法的文本数据读入功能也就实现了。