BUAA-OO pre3-正则表达式

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*可以匹配空串(即没有任何字符的表达式),aaaaaa等等。
  • +,一次或多次匹配前面的字符或子表达式。比如我们上面的例子,匹配任意长度的无符号整数(长度为不小于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,要求Ggle中间的o个数大于等于2,小于等于20,那么可以写成Go{2,20}gle

|

类似于逻辑运算中的或,比如ClassA|ClassB,可以匹配ClassA或者ClassB

[]

再灵活一点!我们上面都是将一个固定的字符或者表达式片段重复一定的次数,现在,我们要让我们选择的那个字符或者表达式片段不再固定,而是可以由各种元素组成。

  • [xyz],字符集,用于匹配任意一个在字符集中的字符,比如上面Class的例子,可以改写成Class[AB]
  • [^xyz],反向字符集,与上面正好相反,用于匹配任意一个字符集之外的字符。
  • [a-z],字符范围,匹配任意一个属于这个范围的字符,上面Class的例子,如果想要匹配ClassAClassZ,可以写成Class[A-Z]
  • [^a-z],反向字范围,匹配任意一个不属于这个范围的字符。

()

  • 将字符串的内容捕获以用于提取。
运算符 说明
\ 转义符
(), (?:), (?=), [] 圆括号和方括号
*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, 任何元字符(即上述的\d\w等)、任何字符 定位点和序列
|

以上优先级从上到下由高到低。

转义字符

遇到转移字符时,需要用\\限制,例如要匹配字符.,就需要写成\\.

正则表达式生成思路

好的正则表达式也需要层次化划分,这样可以使正则表达式更明晰易读、出现错误更好定位,同时也避免了过度匹配带来的爆栈等风险。(当然因为自己第一遍写的时候毫无这个意识所以写出来的非常丑

  1. 分析字符串的层次结构
  2. 从底层开始写,不断使用前一层的“子表达式”,(即使用有意义的变量名代替单纯的表达式,类似于主函数调用子函数)
  3. 对于有捕获组的表达式,对捕获组进行命名

实例:

以pre3中的输入格式为例:


邮件信息是符合邮件信息格式的字符串。

根据时间精度的不同,可能出现以下四种认定为正确的邮件信息格式:

  1. username@domain-yyyy-mm-dd

    例:lethean@buaa.edu.cn-2020-12-02

  2. username@domain-yyyy-mm-dd-hh

    例:myname-lethean@buaa.edu.cn-2020-12-02-15

  3. username@domain-yyyy-mm-dd-hh:mimi

    例:Lethean@buaa.edu.cn-2020-12-02-15:01

  4. 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"

层次:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IB2VYOXf-1614600694232)(D:\QQ\DATA\2987805870\FileRecv\MobileFile\965C12CD166E98030B4BB01B0E96FB13.png)]

  • 第一层:
    • 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。

posted @ 2021-03-08 20:48  blurrrr  阅读(149)  评论(0)    收藏  举报