文章目录
Pre3-正则表达式
基础知识
正则表达式
正则表达式定义了字符串的模式,可以用来搜索、编辑或处理文本。
语法基础
字符
-
\d \D
\d
匹配一个数字字符。例如,使用\d
可以匹配0
或者1
等其他单个数字字符。等效于[0-9]
。\D
是\d
的补集,匹配一个非数字字符。等效于[^0-9]
。
-
\s \S
\s
匹配一个空白字符,包括空格、制表符、换页符等。与[\f\n\r\t\v]
等效。\S
是\s
的补集,匹配一个非空白字符,与[^\f\n\r\t\v]
等效。
-
\w \W
\w
匹配英文字母、数字、下划线组成的字符集中的字符。与[A-Za-z0-9_]
等效。\W
是\w
的补集,匹配英文字母、数字、下划线组成的字符集之外的字符。与[^A-Za-z0-9_]
等效。
-
\f \n \r \t \v
这些字符与大家在C语言课程中学习的转义字符具有相同的含义,分别表示匹配换页符、换行符、回车符、水平制表符、垂直制表符。
格式
^ $
这两个字符用于指定匹配位置。
^
匹配字符串的起始位置,常用于匹配字符串的前缀。比如,想要匹配以一个数字开头的字符串,可以在正则表达式开头写^\d
。$
正好相反,匹配字符串的结尾位置,以一个数字结尾的字符串,可以写成\d$
,常用于匹配后缀。
? * +
这三个字符用于对其前面的表达式内容的出现次数进行描述。
?
,零次或一次匹配前面的字符或子表达式。比如要匹配一个单词apple
的单数和复数形式,可以写成apple(s)?
。*
,零次或多次匹配前面的字符或子表达式。例如,a*
可以匹配空串(即没有任何字符的表达式),a
,aa
,aaa
等等。+
,一次或多次匹配前面的字符或子表达式。比如我们上面的例子,匹配任意长度的无符号整数(长度为不小于1的整数,且允许前导0),就可以写成\d+
。
{}
对于上面的? * +
三个符号的进一步扩充,对其前面的表达式内容的出现次数进行描述。
{n}
,正好匹配n次,还是之前那个例子,匹配四位无符号整数(允许有前导0),这时就不用写\d\d\d\d
了,\d{4}
即可。{n,}
,至少匹配n次。之前的a*
可以写作a{0,}
,而之前的\d+
可以写成\d{1,}
。{n,m}
,匹配至少n次,至多m次,要求n<=m。还记得使用Google进行搜索时,页面底部的Gooooooooooogle
么,我们现在匹配Google
,要求G
与gle
中间的o个数大于等于2,小于等于20,那么可以写成Go{2,20}gle
。
|
类似于逻辑运算中的或,比如ClassA|ClassB
,可以匹配ClassA
或者ClassB
。
[]
再灵活一点!我们上面都是将一个固定的字符或者表达式片段重复一定的次数,现在,我们要让我们选择的那个字符或者表达式片段不再固定,而是可以由各种元素组成。
[xyz]
,字符集,用于匹配任意一个在字符集中的字符,比如上面Class的例子,可以改写成Class[AB]
。[^xyz]
,反向字符集,与上面正好相反,用于匹配任意一个字符集之外的字符。[a-z]
,字符范围,匹配任意一个属于这个范围的字符,上面Class的例子,如果想要匹配ClassA
到ClassZ
,可以写成Class[A-Z]
。[^a-z]
,反向字范围,匹配任意一个不属于这个范围的字符。
()
- 将字符串的内容捕获以用于提取。
运算符 | 说明 |
---|---|
\ | 转义符 |
(), (?:), (?=), [] | 圆括号和方括号 |
*, +, ?, {n}, {n,}, {n,m} | 限定符 |
^, $ , 任何元字符(即上述的\d\w等)、任何字符 | 定位点和序列 |
\| | 或 |
以上优先级从上到下由高到低。
转义字符
遇到转移字符时,需要用\\
限制,例如要匹配字符.
,就需要写成\\.
。
正则表达式生成思路
好的正则表达式也需要层次化划分,这样可以使正则表达式更明晰易读、出现错误更好定位,同时也避免了过度匹配带来的爆栈等风险。(当然因为自己第一遍写的时候毫无这个意识所以写出来的非常丑)
- 分析字符串的层次结构
- 从底层开始写,不断使用前一层的“子表达式”,(即使用有意义的变量名代替单纯的表达式,类似于主函数调用子函数)
- 对于有捕获组的表达式,对捕获组进行命名
实例:
以pre3中的输入格式为例:
邮件信息是符合邮件信息格式的字符串。
根据时间精度的不同,可能出现以下四种认定为正确的邮件信息格式:
-
username@domain-yyyy-mm-dd
例:
lethean@buaa.edu.cn-2020-12-02
-
username@domain-yyyy-mm-dd-hh
例:
myname-lethean@buaa.edu.cn-2020-12-02-15
-
username@domain-yyyy-mm-dd-hh:mimi
例:
Lethean@buaa.edu.cn-2020-12-02-15:01
-
username@domain-yyyy-mm-dd-hh:mimi:ss
例:
myname--lethean@buaa.edu.cn-2020-12-20-15:01:20
其中
-
username@domain 为
邮件发送者的邮箱地址
- username 为用户名,domain 为域名
-
yyyy-mm-dd / yyyy-mm-dd-hh / yyyy-mm-dd-hh:mimi / yyyy-mm-dd-hh:mimi:ss 为
发送时间
- ‘y’ 代表一位年份数字,‘m’ 代表一位月份数字,‘d’ 代表一位日期数字,‘h’ 代表一位小时数字,‘mi’ 代表一位分钟数字,‘s’ 代表一位秒数数字
-
username 为只包含大小写字母、
-
的长度不为零的字符串,对于大小写不敏感。 -
domain 为只包含大小写字母、数字、
.
的长度不为零的字符串,对大小写敏感。
域名类别
域名中第一个 ‘.’ 字符(不含)之前的字符串(保证域名含有 ‘.’ 字符且域名类别不为空)
例如:
- @126.com 域名类别: “126” (不含引号)
- @buaa.edu.cn 域名类别: “buaa”
层次:
- 第一层:
String email = username+"@"+domain+"-"+time
;
- 第二层:
String username = [A-Za-z-]+
String domain =[A-Za-z0-9.]+
String time = date + exacttime
;
- 第三层:
String date =[0-9]{4}[-][0-9]{2}[-][0-9]{2}
;String exacttime =([-][0-9]{2})?([:][0-9]{2})?([:][0-9]{2})?
;
Pattern类
Pattern.compile()
由于Pattern类没有public的构造方法,所以要想生成pattern对象实例,使用compile,返回值为pattern对象实例,参数即正则表达式。Pattern.match()
故名思义,使用match来进- 行匹配,第一个参数是我们上面生成的pattern,第二个参数是一个待匹配的字符串,返回值为
boolean
类型,表示是否成功匹配。
Matcher 类
索引方法
索引方法提供了有用的索引值,精确表明输入字符串中在哪能找到匹配:
序号 | 方法及说明 |
---|---|
1 | public int start() 返回以前匹配的初始索引。 |
2 | public int start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引 |
3 | public int end() 返回最后匹配字符之后的偏移量。 |
4 | public int end(int group) 返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。 |
Start 方法返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引,end 方法最后一个匹配字符的索引加 1。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexMatches
{
private static final String REGEX = "\\bcat\\b";
private static final String INPUT =
"cat cat cat cattie cat";
public static void main( String[] args ){
Pattern p = Pattern.compile(REGEX);
Matcher m = p.matcher(INPUT); // 获取 matcher 对象
int count = 0;
while(m.find()) {
count++;
System.out.println("Match number "+count);
System.out.println("start(): "+m.start());
System.out.println("end(): "+m.end());
}
}
}
以上实例编译运行结果如下:
Match number 1
start(): 0
end(): 3
Match number 2
start(): 4
end(): 7
Match number 3
start(): 8
end(): 11
Match number 4
start(): 19
end(): 22
查找方法
查找方法用来检查输入字符串并返回一个布尔值,表示是否找到该模式:
序号 | 方法及说明 |
---|---|
1 | public boolean lookingAt() 尝试将从区域开头开始的输入序列与该模式匹配。 |
2 | public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列。 |
3 | public boolean find(int start) 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。 |
4 | public boolean matches() 尝试将整个区域与模式匹配。 |
matches 和 lookingAt 方法都用来尝试匹配一个输入序列模式。它们的不同是 matches 要求整个序列都匹配,而lookingAt 不要求。lookingAt 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。这两个方法经常在输入字符串的开始使用。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexMatches
{
private static final String REGEX = "foo";
private static final String INPUT = "fooooooooooooooooo";
private static final String INPUT2 = "ooooofoooooooooooo";
private static Pattern pattern;
private static Matcher matcher;
private static Matcher matcher2;
public static void main( String[] args ){
pattern = Pattern.compile(REGEX);
matcher = pattern.matcher(INPUT);
matcher2 = pattern.matcher(INPUT2);
System.out.println("Current REGEX is: "+REGEX);
System.out.println("Current INPUT is: "+INPUT);
System.out.println("Current INPUT2 is: "+INPUT2);
System.out.println("lookingAt(): "+matcher.lookingAt());
System.out.println("matches(): "+matcher.matches());
System.out.println("lookingAt(): "+matcher2.lookingAt());
}
}
Current REGEX is: foo
Current INPUT is: fooooooooooooooooo
Current INPUT2 is: ooooofoooooooooooo
lookingAt(): true
matches(): false
lookingAt(): false
替换方法
替换方法是替换输入字符串里文本的方法:
序号 | 方法及说明 |
---|---|
1 | public Matcher appendReplacement(StringBuffer sb, String replacement) 实现非终端添加和替换步骤。 |
2 | public StringBuffer appendTail(StringBuffer sb) 实现终端添加和替换步骤。 |
3 | public String replaceAll(String replacement) 替换模式与给定替换字符串相匹配的输入序列的每个子序列。 |
4 | public String replaceFirst(String replacement) 替换模式与给定替换字符串匹配的输入序列的第一个子序列。 |
5 | public static String quoteReplacement(String s) 返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。 |
实用技巧
-
捕获组的编号:0是一个特殊的捕获组,代表整个正则表达式,从1往后,按照左括号的出现顺序依次标号,举个例子:
A ( ( B C ) ( D ( E ) ) ) 0(all) 1(BCDE) 2(BC) 3(DE) 4(E)
-
命名捕获组:
- 将
(content)
改写为(?<name>content)
即可,其中name不需要写成加双引号的字符串。 - 然后在使用group时,使用
m.group("exponent")
即可(这里要加双引号)。 - 实例:比如上面的
username
就可以改写为String username = "(?<username>[A-Za-z-]+)";
- 将
HashMap
key-value
键值对组成的散列表,可以实现快速查找。
无序的,即不会记录插入的顺序。当出现同个key时会自动覆盖之前的数据。
Java HashMap 常用方法列表如下:
方法 | 描述 |
---|---|
clear() | 删除 hashMap 中的所有键/值对 |
clone() | 复制一份 hashMap |
isEmpty() | 判断 hashMap 是否为空 |
size() | 计算 hashMap 中键/值对的数量 |
put() | 将键/值对添加到 hashMap 中 |
putAll() | 将所有键/值对添加到 hashMap 中 |
putIfAbsent() | 如果 hashMap 中不存在指定的键,则将指定的键/值对插入到 hashMap 中。 |
remove() | 删除 hashMap 中指定键 key 的映射关系 |
containsKey() | 检查 hashMap 中是否存在指定的 key 对应的映射关系。 |
containsValue() | 检查 hashMap 中是否存在指定的 value 对应的映射关系。 |
replace() | 替换 hashMap 中是指定的 key 对应的 value。 |
replaceAll() | 将 hashMap 中的所有映射关系替换成给定的函数所执行的结果。 |
get() | 获取指定 key 对应对 value |
getOrDefault() | 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值 |
forEach() | 对 hashMap 中的每个映射执行指定的操作。 |
entrySet() | 返回 hashMap 中所有映射项的集合集合视图。 |
keySet() | 返回 hashMap 中所有 key 组成的集合视图。 |
values() | 返回 hashMap 中存在的所有 value 值。 |
merge() | 添加键值对到 hashMap 中 |
compute() | 对 hashMap 中指定 key 的值进行重新计算 |
computeIfAbsent() | 对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hasMap 中 |
computeIfPresent() | 对 hashMap 中指定 key 的值进行重新计算,前提是该 key 存在于 hashMap 中。 |
作业简介
task1
明明只需要几行的一个task困扰我好久orz
在网上找了很久都没有找到用多个分隔符进行分割的方法(如果有dl知道了可以告我一下),所以!此处我们应该先将空格替换为逗号,再利用正则表达式进行分割。
import java.util.Scanner;
public class Main {
public static void main(String[] argv) {
Scanner in = new Scanner(System.in);
String str = in.nextLine();
str = str.replace(","," ");//将空格替换为逗号
String[] inputList = str.split("\\s+");//\\s是空格类字符,+表示有一个或者很多个这样的表达式
System.out.println(inputList.length);
}
}
task2
新建一个Email
类,类中数据域包含username,domain,time等,利用邮件原有的字符串和正则表达式的匹配即可得到上述数据;
再新建一个ArrayList<Email>
,将生成的Email
对象依次加入ArrayList中。
for (Email s : out) {
String front = "\\b*[A-Za-z-]+@[A-Za-z0-9.]+";
Pattern rfront = Pattern.compile(front);
Matcher mfront = rfront.matcher(s.getStr());
if (mfront.find()) {
System.out.print(mfront.group(0) + " ");
}
String rear;
rear = "[0-9]{4}[-][0-9]{2}[-][0-9]{2}([-][0-9]{2})?([:][0-9]{2})?([:][0-9]{2})?\\b*";
Pattern rrear = Pattern.compile(rear);
Matcher mrear = rrear.matcher(s.getStr());
if (mrear.find()) {
System.out.println(mrear.group(0));
}
}
坑点:
-
username需要小写
-
根据username来排序
-
因此需要自己写一个比较函数,再调用
Collections.sort()
进行排序。 -
//main中: public static void sortEmail(ArrayList<Email> list) { Collections.sort(list); } //Email中: public class Email implements Comparable<Email> { private String str; private String name; public int compareTo(Email one) { return this.getName().compareTo(one.getName()); } }
-
task3
就,再给出指定信息的正则表达式并且匹配就好了:(此处我使用了static
类型防止正则表达式信息被误改)
private static String regexdomain = "@[A-Za-z0-9.]+";
private static String regextime1 = "[-][0-9]{4}[-][0-9]{2}[-][0-9]{2}";
private static String regextime4 = "[0-9]{2}[:][0-9]{2}[:][0-9]{2}";
private static String regextime3 = "[0-9]{2}[:][0-9]{2}";
private static String regextime2 = "[-][0-9]{4}[-][0-9]{2}[-][0-9]{2}[-][0-9]{2}";
//不同的正则表达式对应不同的匹配方法,采用上述方法需要注意匹配顺序,如下:
pattern = Pattern.compile(regextime4);
m = pattern.matcher(str);
if (m.find()) {
this.hour = m.group(0).split(":")[0];
this.minute = m.group(0).split(":")[1];
this.second = m.group(0).split(":")[2];
} else {
this.second = "";
pattern = Pattern.compile(regextime3);
m = pattern.matcher(str);
if (m.find()) {
this.hour = m.group(0).split(":")[0];
this.minute = m.group(0).split(":")[1];
} else {
this.minute = "";
pattern = Pattern.compile(regextime2);
m = pattern.matcher(str);
if (m.find()) {
this.hour = m.group(0).split("-")[4];
} else {
this.hour = "";
}
}
}
task4
根据给定信息查询就好:
private static String regexA = "a{2,3}b{2,4}a{2,4}c{2,3}";
private static String regexB = "a{2,3}(ba){0,100}(bc){2,4}";
private static String regexC = "a{2,3}(ba){0,100}(bc){2,4}";
private static String regexDprefix = "^(a){0,3}b{1,100}c{2,3}";//前缀,以^开头
private static String regexDsuffix = "b{1,2}a{1,2}c{0,3}$"; //后缀,以&结束
private static String regexE = "a(.*)b(.*)b(.*)c(.*)b(.*)c(.*)c(.*)";
坑点:
- 字符串需要加(),比如是
(ba){2}
而不是ba{2}
- 前缀后缀的写法
- C类是
lower(s) = a(x) + ba(y) + bc(z)
,但是自己判断是时候用的是str.toLowerCase方法 - D类后缀也是小写
- E类是子序列不是子串,所以用贪心匹配,分别列出每个字符至少出现的次数。
task5
在task4的基础上,利用ArrayList中的Email生成<Email.getname(),Email>
的键值对,删除和查找操作按照Hashmap的操作进行即可。
public class Mailbox {
private HashMap<String, Email> sites = new HashMap<String, Email>();
//生成HashMap
public Mailbox(ArrayList<Email> list) {
for (Email mail : list) {
sites.put(mail.getName(), mail);
}
}
//对HashMap的keyset进行排序
public Object[] sort() {
Set set = sites.keySet();
Object[] arr = set.toArray();
return arr;
}
//删除和查找的方法
}
坑点:
- 此处要求输出方式和task2一样,所以要对HashMap进行排序操作(本质即是对keyset进行排序操作)
task6
输入形式变了就变一下正则表达式的顺序呗。。。别的都是一样的吧。。。
(写这个task的时候我只想快点写完所以偷懒了)就在task5的基础上直接写了,新增的数据place多进行一次判断就好了,本质不变。
//以按名字删除为例
public void deletebyName(String name, String place) {
if (sites.containsKey(name)) {
if (sites.get(name).getPlace().equals(place)) {
sites.remove(name);
}
}
}
注意不能直接重新生成一个以<Email.getPlace(),Email>
为键值对的HashMap,因为存在不同的书在同一个place,所以新的书会覆盖原有图书。
可以考虑双重key,即将name和place同时作为key进行生成,但是我懒了所以也没有去查,有兴趣的同学可以查来看看hhh。