文章482
标签257
分类63

给女朋友写小工具的总结之-正则表达式

女朋友最近要用到电力系统分析相关的一些数据, 但是用到的BPA软件生成的文件有好几个, 八万多行的文件都有. 每次处理起来都是自己手动输入, 很是麻烦. 我今天一看, 不就是基本的文件I/O加上正则表达嘛, 这又什么难得, Python走起!

一. 需求分析

多个类似于:

# File name: 33bpa.lis

Wall clock : Sun Sep 08 09:52:36 2019

<MULTI_NODE_EQUAL_RESULT>
.#      ???             ??????            ??????           ??????             ??????
.#                     G0+jB0(p.u.)        G1+jB1(p.u.)        G2+jB2(p.u.)       Im(kA)  Ia(deg)
.#----------------  ------------------  ------------------  ------------------  ------------------
B "BUS-30  " 100.0    0.0000    0.0000    3.4355  -51.9441    3.2023  -52.7944   30.0554  -86.2160
B "BUS-31  " 100.0    0.0000    0.0000    3.9110  -30.8303    3.5087  -33.5190   17.9425  -82.7702
B "BUS-32  " 100.0    0.0000    0.0000    3.6146  -36.5561    3.2913  -38.7305   21.2086  -84.3531
........................
SWING CASE: 39测试     POWERFLOW CASE: 039bpa             * * *  输 出 信 息  * * *                            Date: 2019-09-06

 * 发电机"BUS-30   100.0 "输出数据列表

     功角        (度)              最大值 =   -51.3867/   74.0          最小值 =   -51.3869/  186.0          参考发电机    BUS-31   100.0  
     0.0  -51.3868       0.0  -51.3868       1.0  -51.3868       2.0  -51.3868       3.0  -51.3868       4.0  -51.3867       5.0  -51.3867       6.0  -51.3867
     7.0  -51.3867       8.0  -51.3867       9.0  -51.3867      10.0  -51.3867      11.0  -51.3868      12.0  -51.3868      13.0  -51.3868      14.0  -51.3868
    15.0  -51.3868      16.0  -51.3868      17.0  -51.3868      18.0  -51.3868      19.0  -51.3868      20.0  -51.3868      21.0  -51.3868      22.0  -51.3868
    23.0  -51.3868      24.0  -51.3868      25.0  -51.3868      26.0  -51.3868      27.0  -51.3868      28.0  -51.3868      29.0  -51.3868      30.0  -51.3868
    31.0  -51.3868      32.0  -51.3868      33.0  -51.3868      34.0  -51.3868      35.0  -51.3868      36.0  -51.3868      37.0  -51.3868      38.0  -51.3868
    39.0  -51.3868      40.0  -51.3868      41.0  -51.3868      42.0  -51.3868      43.0  -51.3868      44.0  -51.3868      45.0  -51.3868      46.0  -51.3868
    47.0  -51.3868      48.0  -51.3868      49.0  -51.3868      50.0  -51.3868      51.0  -51.3868      52.0  -51.3868      53.0  -51.3868      54.0  -51.3868
    55.0  -51.3868      56.0  -51.3868      57.0  -51.3868      58.0  -51.3868      59.0  -51.3868      60.0  -51.3868      61.0  -51.3868      62.0  -51.3868
    ......................

这样的文件中提取某些参数, 并构建对应的矩阵, 包括对称矩阵等.

二. 实现分析

1. 构建配置文件:

首先创建config.json配置文件, 今后从此文件中读取配置, 而不用在源码上进行修改!

{
    "data_folder": "E:\\BPA_generator",
    "out_file": "ieee90.OUT",
    "swi_file": "IEEE90.SWI",
    "lis_file": "ieee90.lis",

    "motor_number": [1,2,3],
    "fault_time": 1,

    "save_to": ".\\generate.txt"
}

在python中读取配置:

# 读入配置文件
with open("config.json") as f:
    config = json.load(f)
    data_folder = config["data_folder"]
    out_file = config["out_file"]
    swi_file = config["swi_file"]
    lis_file = config["lis_file"]

    motor_number = config["motor_number"]
    fault_time = float(config["fault_time"])

    motor_dict = dict()
    for i in range(len(motor_number)):
        motor_dict[motor_number[i]] = i

    save_to = config["save_to"]

其中motor_dict用于构建将给定的发电机序号与数组下标进行一一对应的反向索引! 这样可以在寻找到对应的发电机数据后, 写入对应的结果数组!

2. 分析

1): *.OUT文件
SWING CASE: 39测试     POWERFLOW CASE: 039bpa             * * *  输 出 信 息  * * *                            Date: 2019-09-06

 * 发电机"BUS-30   100.0 "输出数据列表

     功角        (度)              最大值 =   -51.3867/   74.0          最小值 =   -51.3869/  186.0          参考发电机    BUS-31   100.0  
     0.0  -51.3868       0.0  -51.3868       1.0  -51.3868       2.0  -51.3868       3.0  -51.3868       4.0  -51.3867       5.0  -51.3867       6.0  -51.3867
     7.0  -51.3867       8.0  -51.3867       9.0  -51.3867      10.0  -51.3867      11.0  -51.3868      12.0  -51.3868      13.0  -51.3868      14.0  -51.3868
    15.0  -51.3868      16.0  -51.3868      17.0  -51.3868      18.0  -51.3868      19.0  -51.3868      20.0  -51.3868      21.0  -51.3868      22.0  -51.3868
    23.0  -51.3868      24.0  -51.3868      25.0  -51.3868      26.0  -51.3868      27.0  -51.3868      28.0  -51.3868      29.0  -51.3868      30.0  -51.3868
    31.0  -51.3868      32.0  -51.3868      33.0  -51.3868      34.0  -51.3868      35.0  -51.3868      36.0  -51.3868      37.0  -51.3868      38.0  -51.3868
    39.0  -51.3868      40.0  -51.3868      41.0  -51.3868      42.0  -51.3868      43.0  -51.3868      44.0  -51.3868      45.0  -51.3868      46.0  -51.3868
    47.0  -51.3868      48.0  -51.3868      49.0  -51.3868      50.0  -51.3868      51.0  -51.3868      52.0  -51.3868      53.0  -51.3868      54.0  -51.3868
    55.0  -51.3868      56.0  -51.3868      57.0  -51.3868      58.0  -51.3868      59.0  -51.3868      60.0  -51.3868      61.0  -51.3868      62.0  -51.3868

       速度偏差      (Hz)              最大值 =    14.8872/  295.0          最小值 =     0.0000/    0.0
     0.0    0.0000       0.0    0.0000       1.0    0.1273       2.0    0.2546       3.0    0.3820       4.0    0.5094       5.0    0.6368       6.0    0.7643
     7.0    0.8919       8.0    1.0196       9.0    1.1474      10.0    1.2755      10.0    1.2755      11.0    1.2734      12.0    1.2599      13.0    1.2395
    14.0    1.2154      15.0    1.1896      16.0    1.1639      17.0    1.1392      18.0    1.1166      19.0    1.0967      20.0    1.0802      21.0    1.0675
    22.0    1.0590      23.0    1.0548      24.0    1.0553      25.0    1.0606      26.0    1.0708      27.0    1.0860      28.0    1.1062      29.0    1.1314
    30.0    1.1617      31.0    1.1971      32.0    1.2377      33.0    1.2838      34.0    1.3355      35.0    1.3932      36.0    1.4575      37.0    1.5291
    38.0    1.6088      39.0    1.6979      40.0    1.7977      41.0    1.9098      42.0    2.0361      43.0    2.1784      44.0    2.3388      45.0    2.5186
    46.0    2.7182      47.0    2.9362      48.0    3.1684      49.0    3.4068      50.0    3.6398      51.0    3.8528      52.0    4.0308      53.0    4.1620
    ......................

*.OUT类型文件如上所示, 要求找出功角数据下面的与配置文件中给定的初始故障数据相同的数据, 如: 1: -51.3868.

分析: 由于有多台发电机都有这样的数据, 且有可能出现功角, 转速等多个条目, 但是功角一定是首个出现的条目. 所以可以先通过正则表达式寻找到: 发电机”BUS-30 100.0 “输出数据列表 字段, 然后再按行找到对应的时间, 加入结果集即可.(这里用到了正则分隔.)

代码:

############# 1. 故障时各台电机功角 #################
fault_power_angle = list()

# 读取发电机功角行向量
with open(data_folder + "\\" + out_file, 'r', encoding='gbk') as f:
    find_flag = False
    for line in f:
        line = line.strip()
        if line.find('发电机') >= 0 and line.find('输出数据列表') >= 0:
            motor_index = int(re.findall(r"\d+\.?\d*", line)[0])
            if (motor_number.__contains__(motor_index)):
                find_flag = True
                continue

        if find_flag:
            line_data = [x.strip() for x in re.split(r" +", line)]
            if len(line_data) < 8 or not is_all_float(line_data): 
                continue
            line_data = [float(x) for x in line_data]
            # 寻找故障停止时间的功角
            for i in range(0, len(line_data), 2):
                time = line_data[i]
                power_angle = line_data[i + 1]

                if time == fault_time:
                    fault_power_angle.append(power_angle)
                    find_flag = False
                    break

# print(fault_power_angle)
  1. 按行读入文件(注: 在win下生成的文件为gbk编码, 所以需要添加encoding), 并判断是否包含相应的字符串使用str.find()方法;

  2. 一旦发现了数据标题标志, 将标题中的电机序号提出:

    # 原字符串为: *发电机"BUS-30   100.0 "输出数据列表*
    motor_index = int(re.findall(r"\d+\.?\d*", line)[0])

    使用[\d+.?\d*]可以提取类似于: [数字.数字****]的效果(其中小数点: .可要可不要!)

  3. 如果提取出的电机序号在给定的电机需求之内list.\__contains__(e), 则将find_flag置为True, 此时表示, 要在接下来的数据中寻找所需的数据!

  4. find_flag被置为True后, 表示接下来的数据可以在下文中找到!

  5. 分析每一行的数据, 将数据按照多个空格为间隔拆分为多个数据.

  6. 如果处理过的数据长度小于8(正常的数据应当每行为8个), 或者不全为float类型, 则这一行数据不合法! 判断数据是否为浮点数的函数:

    # 字符串小数判断
    def is_float(str):
        if str.count('.') == 1: #小数有且仅有一个小数点
            left = str.split('.')[0]  #小数点左边(整数位,可为正或负)
            right = str.split('.')[1]  #小数点右边(小数位,一定为正)
            lright = '' #取整数位的绝对值(排除掉负号)
            if str.count('-') == 1 and str[0] == '-': #如果整数位为负,则第一个元素一定是负号
                lright = left.split('-')[1]
            elif str.count('-') == 0:
                lright = left
            else:
                return False
            if right.isdigit() and lright.isdigit(): #判断整数位的绝对值和小数位是否全部为数字
                return True
            else:
                return False
        else:
            return False
    
    # 判断一整行的数据是否都为浮点数
    def is_all_float(arr):
        for s in arr:
            if not is_float(s):
                return False
        return True
    
  7. 如果这一簇数据符合条件, 则将数据全部浮点数化:

    line_data = [float(x) for x in line_data]
  8. 之后寻找故障停止时间的功角数据, 由于数据为两个一组, 所以构建(0, len, 2)步长为2的index索引, 并寻找

  9. 一旦找到, 就将数据结果加入结果集中, 并将find_flag标志位清空.

2): *.lis文件
# File name: 33bpa.lis

Wall clock : Sun Sep 08 09:52:36 2019

<MULTI_NODE_EQUAL_RESULT>
.#      ???             ??????            ??????           ??????             ??????
.#                     G0+jB0(p.u.)        G1+jB1(p.u.)        G2+jB2(p.u.)       Im(kA)  Ia(deg)
.#----------------  ------------------  ------------------  ------------------  ------------------
B "BUS-30  " 100.0    0.0000    0.0000    3.4355  -51.9441    3.2023  -52.7944   30.0554  -86.2160
B "BUS-31  " 100.0    0.0000    0.0000    3.9110  -30.8303    3.5087  -33.5190   17.9425  -82.7702
B "BUS-32  " 100.0    0.0000    0.0000    3.6146  -36.5561    3.2913  -38.7305   21.2086  -84.3531
........................

*.lis文件格式如上所示, 要求提取指定行的RjX的数据. 并构建对称矩阵.

############# 2. 正序阻抗 R与X  #################
row = len(motor_number)
col = len(motor_number)

r_resistance_matrix = [[0] * row for c in range(col)]
x_resistance_matrix = [[0] * row for c in range(col)]

with open(data_folder + "\\" + lis_file, 'r', encoding='gbk') as f:
    for line in f:
        line = line.strip().replace("\"", "")
        line_data = [x.strip() for x in re.split(r" +", line)]
        if line_data[0] != "L":
            continue

        data_from = motor_dict[int(re.sub(r"\D", "", line_data[1]))]
        data_to = motor_dict[int(re.sub(r"\D", "", line_data[3]))]
        r = float(line_data[-4])
        x = float(line_data[-3])

        r_resistance_matrix[data_from][data_to] = r
        x_resistance_matrix[data_from][data_to] = x

    for i in range(row):
        for j in range(col):
            r_resistance_matrix[j][i] = r_resistance_matrix[i][j]
            x_resistance_matrix[j][i] = x_resistance_matrix[i][j]

# print(r_resistance_matrix)
# print(x_resistance_matrix)

代码如上所示.

  1. 首先构建输出的矩阵NxN矩阵

    r_resistance_matrix = [[0] * row for c in range(col)]
    x_resistance_matrix = [[0] * row for c in range(col)]
  2. 按行读入文件, 并进行数据预处理: 删去前后空格, 替换多余符号, 拆分数据等

  3. 判断分隔的行首个所需字符为: "L"

  4. 当满足条件时, 可以通过正则表达式提取数字:通过将非数字字符替换为空字符!

    data_from = motor_dict[int(re.sub(r"\D", "", line_data[1]))]
    data_to = motor_dict[int(re.sub(r"\D", "", line_data[3]))]

    其中: \D 表示非数字字符

  5. 最终提取所需数据, 并通过反向索引字典, 找到对应的结果index位置, 加入!

  6. 最后, 根据上三角矩阵, 生成对角矩阵

    for i in range(row):
        for j in range(col):
            r_resistance_matrix[j][i] = r_resistance_matrix[i][j]
            x_resistance_matrix[j][i] = x_resistance_matrix[i][j]
3): *.swi文件

有了以上处理方法, 处理最后一个文件显得很是简单!

############# 3. 发电机动能/平衡交流节点电压  #################
node_voltage = list()
motivate_energy = list()


with open(data_folder + "\\" + swi_file, 'r', encoding='gbk') as f:
    for line in f:
        line = line.strip().replace("\"", "")
        line_data = [x.strip() for x in re.split(r" +", line)]
        if line_data[0] != "MF":
            continue

        node_voltage.append(float(line_data[2]))
        motivate_energy.append(float(line_data[3]))

# print(node_voltage)
# print(motivate_energy)
4): 保存文件

将文件按照配置路径保存为*.txt格式即可!

def format_matrix(s):
    return s.replace("], [", ";")

############# 4. 保存 #################
with open(save_to, 'w') as f:
    f.write("############# 1. 故障时各台电机功角 #################\n")
    f.write(str(fault_power_angle) + "\n\n")
    f.write("############# 2. 正序阻抗 R与X  #################\n")
    f.write("# 1. 电阻\n")
    f.write(format_matrix(str(r_resistance_matrix))[1:-1] + "\n\n")
    f.write("# 2. 电抗\n")
    f.write(format_matrix(str(x_resistance_matrix))[1:-1] + "\n\n")
    f.write("############# 3. 发电机动能/平衡交流节点电压  #################\n")
    f.write("# 1. 节点电压\n")
    f.write(str(node_voltage) + "\n\n")
    f.write("# 2. 动能\n")
    f.write(str(motivate_energy) + "\n")

print("生成成功!")

此处由于保存时, 需要保存为MATLAB中的矩阵输入格式, 所以, 可以通过将Python中的数组字符串之间的], [替换为;并删除前后多余的[]即可!

三. 正则表达式总结

1. 基本语法

1): 元字符
代码 说明 举例
. 匹配除换行符外任意一个字符
[abc] 字符集合,只能表示一个字符位置。匹配所包含的任意一个字符
[^abc] 字符集合,只能表示一个字符位置。匹配除去集合内字符的任意一个字符
[a-z] 字符范围,一个集合,表示一个字符位置匹配所包含的任意一个字符
[^a-z] 字符范围,一个集合,表示一个字符位置匹配除去集合内字符的任意一个字符
\b 匹配单词的边界
\B 不匹配单词的边界
\d 等同[0-9] 匹配一位数字
\D 等同[^0-9]匹配一位非数字
\s 匹配1位空白字符,包括换行符、制表符、空格等同[\f\r\n\t\v]
\S 匹配1位非空白字符
\w 等同[a-zA-Z0-9_] 包含中文
\W 匹配\w之外的字符
2): 转义
  • 凡是在正则表达式中有特殊意义的符号,转义时使用\
3): 重复
代码 说明 举例
* 前面的正则表达式重复0次或多次
+ 前面的正则表达式重复至少一次
前面的正则表达式重复0次或1次
{n} 重复n次
{n,} 重复n次以上
{n,m} 重复n到m次
4): 分组(捕获)断言
代码 说明 举例
x y 匹配x或y
(pattern) 分组(捕获)后会自动分配组号从1开始可以改变优先级 \数字 匹配对应的分组(指的是前一个匹配上的分组的内容)
(?:pattern) 只改变优先级不分组
(?exp)(?nameexp) 分组捕获 给组命名Python句法为(?Pexp)
(?=exp) 零宽度正预测先行断言断言exp一定在匹配的右边出现
(?<=exp) 零宽度正回顾后发断言断言exp一定出现在匹配的左边出现
(?!exp) 零宽度负预测先行断言断言exp一定不会出现在右侧
(?<!exp) 零宽度负回顾后发断言断言exp一定不会出现在左侧
(?#comment) 注释

断言不会占用分组号

5): 贪婪与非贪婪
  • 默认贪婪模式,尽可能多的匹配字符串
代码 说明 举例
*? 匹配任意次,尽可能少重复
+? 匹配至少一次,尽可能少重复
?? 匹配0或1次,尽可能少重复
{n}? 匹配至少n次,尽可能少重复
{n,m}? 匹配至少n次,至多m次,尽可能少重复
6): 引擎选项
代码 说明 举例
IgnoreCase 匹配时忽略大小写 re.Ire.IGNORECASE
Singleline 单行模式,可穿透/n re.Sre.DOTALL
Multiline 多行模式 re.Mre.MULTILINE
IgnorePatternWhitespace 忽略表达式中空白字符,若要使用空白字符 re.Xre.VERBOSE

代码 说明 举例

IgnoreCase 匹配时忽略大小写 re.Ire.IGNORECASE

Singleline 单行模式,可穿透/n re.Sre.DOTALL

Multiline 多行模式 re.Mre.MULTILINE

IgnorePatternWhitespace 忽略表达式中空白字符,若要使用空白字符 re.Xre.VERBOSE

2. Java中使用正则表达式

Java 提供了功能强大的正则表达式API,在java.util.regex 包下.

1): 正则表达式实例

下面是一个简单的Java正则表达式的例子,用于在文本中搜索 http://

String text =  "This is the text to be searched " +
        "for occurrences of the http:// pattern.";
String pattern = ".*http://.*";
boolean matches = Pattern.matches(pattern, text);
System.out.println("matches = " + matches);ches = " + matches);

示例代码实际上没有检测找到的 http:// 是否是一个合法超链接的一部分,如包含域名和后缀(.com,.net 等等)。代码只是简单的查找字符串 http:// 是否出现

2): Java中关于正则表达式的API
Pattern: java.util.regex.Pattern

java.util.regex.Pattern 简称 Pattern, 是Java正则表达式API中的主要入口,无论何时,需要使用正则表达式,从Pattern 类开始!

  • Pattern.matches()

检查一个正则表达式的模式是否匹配一段文本的最直接方法是调用静态方法Pattern.matches(),示例如下:

String text    =
        "This is the text to be searched for occurrences of the pattern.";
String pattern = ".*is.*";
boolean matches = Pattern.matches(pattern, text);
System.out.println("matches = " + matches);
// true

上面代码在变量 text 中查找单词 “is” 是否出现,允许”is” 前后包含 0或多个字符(由 .* 指定).
Pattern.matches() 方法适用于检查一个模式在一个文本中出现一次的情况,或适用于Pattern类的默认设置。

如果需要匹配多次出现,甚至输出不同的匹配文本,或者只是需要非默认设置.需要通过Pattern.compile() 方法得到一个Pattern 实例。

  • Pattern.compile()

如果需要匹配一个正则表达式在文本中多次出现,需要通过Pattern.compile() 方法创建一个Pattern对象。示例如下

String text    =
        "This is the text to be searched " +
        "for occurrences of the http:// pattern.";
String patternString = ".*http://.*";
Pattern pattern = Pattern.compile(patternString);

可以在Compile 方法中,指定一个特殊标志:Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
Pattern 类包含多个标志(int 类型),这些标志可以控制Pattern 匹配模式的方式。上面代码中的标志使模式匹配是忽略大小写.

  • Pattern.matcher()

一旦获得了Pattern对象,接着可以获得Matcher对象。Matcher 示例用于匹配文本中的模式.示例如下
Matcher matcher = pattern.matcher(text);

Matcher类有一个matches()方法,可以检查文本是否匹配模式。以下是关于Matcher的一个完整例子.

String text    =
        "This is the text to be searched " +
        "for occurrences of the http:// pattern.";
String patternString = ".*http://.*";
Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
boolean matches = matcher.matches();
System.out.println("matches = " + matches);
  • Pattern.split()

Pattern 类的 split()方法,可以用正则表达式作为分隔符,把文本分割为String类型的数组。示例:

String text = "A sep Text sep With sep Many sep Separators";
String patternString = "sep";
Pattern pattern = Pattern.compile(patternString);
String[] split = pattern.split(text);
System.out.println("split.length = " + split.length);
for(String element : split){
    System.out.println("element = " + element);
}

上例中把text 文本分割为一个包含5个字符串的数组。

  • Pattern.pattern()

Pattern 类的 pattern 返回用于创建Pattern 对象的正则表达式,示例:

String patternString = "sep";
Pattern pattern = Pattern.compile(patternString);
String pattern2 = pattern.pattern();

上面代码中 pattern2 值为sep ,与patternString 变量相同。

Matcher: java.util.regex.Matcher

java.util.regex.Matcher 类用于匹配一段文本中多次出现一个正则表达式,Matcher 也适用于多文本中匹配同一个正则表达式。

Matcher 有很多有用的方法,详细请参考官方JavaDoc。这里只介绍核心方法。

以下代码演示如何使用Matcher

String text  =  "This is the text to be searched for occurrences of the http:// pattern.";
String patternString = ".*http://.*";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(text);
boolean matches = matcher.matches();

首先创建一个Pattern,然后得到Matcher ,调用matches() 方法,返回true 表示模式匹配,返回false表示不匹配。
可以用Matcher 做更多的事。

  • 创建Matcher

通过Pattern 的matcher() 方法创建一个Matcher。

String text    =
        "This is the text to be searched " +
        "for occurrences of the http:// pattern.";

String patternString = ".*http://.*";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(text);
  • matches()

Matcher 类的 matches() 方法用于在文本中匹配正则表达式

boolean matches = matcher.matches();

如果文本匹配正则表达式,matches() 方法返回true。否则返回false。

matches() 方法不能用于查找正则表达式多次出现。如果需要,请使用find(), start() 和 end() 方法。

  • lookingAt()

lookingAt()matches() 方法类似,最大的不同是,lookingAt()方法对文本的开头匹配正则表达式;

matches() 对整个文本匹配正则表达式。换句话说,如果正则表达式匹配文本开头而不匹配整个文本,lookingAt() 返回true,而matches() 返回false。 示例:

String text    =
        "This is the text to be searched " +
        "for occurrences of the http:// pattern.";
String patternString = "This is the";
Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
System.out.println("lookingAt = " + matcher.lookingAt());
System.out.println("matches   = " + matcher.matches());

上面的例子分别对文本开头和整个文本匹配正则表达式 “this is the”. 匹配文本开头的方法lookingAt() 返回true。

对整个文本匹配正则表达式的方法 matches() 返回false因为整个文本包含多余的字符,而 正则表达式要求文本精确匹配"this is the",前后又不能有额外字符。

  • find() + start() + end()

find() 方法用于在文本中查找出现的正则表达式,文本是创建Matcher时,通过 Pattern.matcher(text) 方法传入的。如果在文本中多次匹配,find() 方法返回第一个,之后每次调用 find() 都会返回下一个。

start()end() 返回每次匹配的字串在整个文本中的开始和结束位置。实际上, end() 返回的是字符串末尾的后一位,这样,可以在把 start()end() 的返回值直接用在String.substring() 里。

String text    =
        "This is the text which is to be searched " +
        "for occurrences of the word 'is'.";
String patternString = "is";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(text);
int count = 0;
while(matcher.find()) {
    count++;
    System.out.println("found: " + count + " : "  + matcher.start() + " - " + matcher.end());
}

这个例子在文本中找到模式 “is” 4次,输出如下:

found: 1 : 2 - 4
found: 2 : 5 - 7
found: 3 : 23 - 25
found: 4 : 70 - 72
  • reset()

reset() 方法会重置Matcher 内部的 匹配状态。当find() 方法开始匹配时,Matcher 内部会记录截至当前查找的距离。调用 reset() 会重新从文本开头查找。

也可以调用 reset(CharSequence) 方法. 这个方法重置Matcher,同时把一个新的字符串作为参数传入,用于代替创建 Matcher 的原始字符串。

  • group()

假设想在一个文本中查找URL链接,并且想把找到的链接提取出来。当然可以通过 start()end()方法完成。但是用group()方法更容易些。

分组在正则表达式中用括号表示,例如: (John)

此正则表达式匹配John, 括号不属于要匹配的文本。括号定义了一个分组。当正则表达式匹配到文本后,可以访问分组内的部分。

使用group(int groupNo) 方法访问一个分组。一个正则表达式可以有多个分组。每个分组由一对括号标记。想要访问正则表达式中某分组匹配的文本,可以把分组编号传入 group(int groupNo)方法。

group(0) 表示整个正则表达式,要获得一个有括号标记的分组,分组编号应该从1开始计算。

String text    =  "John writes about this, and John writes about that," +
                        " and John writes about everything. "  ;
String patternString1 = "(John)";
Pattern pattern = Pattern.compile(patternString1);
Matcher matcher = pattern.matcher(text);
while(matcher.find()) {
    System.out.println("found: " + matcher.group(1));
}

以上代码在文本中搜索单词John从每个匹配文本中,提取分组1,就是由括号标记的部分。输出如下

found: John
found: John
found: John
  • 多分组

上面提到,一个正则表达式可以有多个分组,例如:(John) (.+?)
这个表达式匹配文本John 后跟一个空格,然后跟1个或多个字符,最后跟一个空格。你可能看不到最后的空格。

这个表达式包括一些字符有特别意义。字符点 . 表示任意字符。 字符 + 表示出现一个或多个,和. 在一起表示 任何字符,出现一次或多次。字符? 表示 匹配尽可能短的文本

完整代码如下

String text    =
          "John writes about this, and John Doe writes about that," +
                  " and John Wayne writes about everything."
        ;
String patternString1 = "(John) (.+?) ";
Pattern pattern = Pattern.compile(patternString1);
Matcher matcher = pattern.matcher(text);
while(matcher.find()) {
    System.out.println("found: " + matcher.group(1) +
                       " "       + matcher.group(2));
}

注意代码中引用分组的方式。代码输出如下

found: John writes
found: John Doe
found: John Wayne
  • 嵌套分组

在正则表达式中分组可以嵌套分组,例如((John) (.+?))
这是之前的例子,现在放在一个大分组里. (表达式末尾有一个空格)。

当遇到嵌套分组时, 分组编号是由左括号的顺序确定的。上例中,分组1 是那个大分组。分组2 是包括John的分组,分组3 是包括 .+? 的分组。当需要通过groups(int groupNo) 引用分组时,了解这些非常重要。

以下代码演示如何使用嵌套分组

String text    =
          "John writes about this, and John Doe writes about that," +
                  " and John Wayne writes about everything."
        ;
String patternString1 = "((John) (.+?)) ";
Pattern pattern = Pattern.compile(patternString1);
Matcher matcher = pattern.matcher(text);
while(matcher.find()) {
    System.out.println("found:   ");
}

输出如下

found:
found:
found: 
  • replaceAll() + replaceFirst()

replaceAll()replaceFirst() 方法可以用于替换Matcher搜索字符串中的一部分replaceAll() 方法替换全部匹配的正则表达式,replaceFirst() 只替换第一个匹配的。

在处理之前,Matcher 会先重置。所以这里的匹配表达式从文本开头开始计算。

示例如下

String text    =
          "John writes about this, and John Doe writes about that," +
                  " and John Wayne writes about everything."
        ;
String patternString1 = "((John) (.+?)) ";
Pattern pattern = Pattern.compile(patternString1);
Matcher matcher = pattern.matcher(text);

String replaceAll = matcher.replaceAll("Joe Blocks ");
System.out.println("replaceAll   = " + replaceAll);

String replaceFirst = matcher.replaceFirst("Joe Blocks ");
System.out.println("replaceFirst = " + replaceFirst);

输出如下

replaceAll = Joe Blocks about this, and Joe Blocks writes about that,
and Joe Blocks writes about everything.
replaceFirst = Joe Blocks about this, and John Doe writes about that,
and John Wayne writes about everything.

输出中的换行和缩进是为了可读而增加的。

注意第1个字符串中所有出现 John 后跟一个单词 的地方,都被替换为 Joe Blocks 。第2个字符串中,只有第一个出现的被替换。

  • appendReplacement() + appendTail()

appendReplacement()appendTail() 方法用于替换输入文本中的字符串短语,同时把替换后的字符串附加到一个 StringBuffer

find() 方法找到一个匹配项时,可以调用 appendReplacement() 方法,这会导致输入字符串被增加到StringBuffer 中,而且匹配文本被替换。 从上一个匹配文本结尾处开始,直到本次匹配文本会被拷贝。

appendReplacement() 会记录拷贝StringBuffer 中的内容,可以持续调用find(),直到没有匹配项。

直到最后一个匹配项目,输入文本中剩余一部分没有拷贝到 StringBuffer. 这部分文本是从最后一个匹配项结尾,到文本末尾部分。通过调用 appendTail() 方法,可以把这部分内容拷贝到 StringBuffer 中.

String text    =
          "John writes about this, and John Doe writes about that," +
                  " and John Wayne writes about everything."
        ;

String patternString1 = "((John) (.+?)) ";
Pattern      pattern      = Pattern.compile(patternString1);
Matcher      matcher      = pattern.matcher(text);
StringBuffer stringBuffer = new StringBuffer();

while(matcher.find()){
    matcher.appendReplacement(stringBuffer, "Joe Blocks ");
    System.out.println(stringBuffer.toString());
}
matcher.appendTail(stringBuffer);
System.out.println(stringBuffer.toString());

注意我们在while循环中调用appendReplacement() 方法。在循环完毕后调用appendTail()
代码输出如下:

Joe Blocks
Joe Blocks about this, and Joe Blocks
Joe Blocks about this, and Joe Blocks writes about that, and Joe Blocks
Joe Blocks about this, and Joe Blocks writes about that, and Joe Blocks
writes about everything.

四. 附录

正则表达式在线测试: https://c.runoob.com/front-end/854


本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/2019/09/08/给女朋友写小工具的总结之-正则表达式/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可