『前端学正则 』想轻松的处理各种HTML?试试分组引用与替换

103 阅读7分钟

正则除查找之外,另一实用场景就是替换,通过它可以灵活修改匹配到的字符串。

什么是分组

解释什么是分组前, 先来分析一个真实的场景,提取下文中所有的生日

姓名	  / 籍贯		  体重		生日
于小彤 / 中国辽宁 / 63 KG / 1994-05-27
张惠妹 / 台湾台东县卑南乡 / 46 KG / 1972-08-09
沈佳妮 / 中国上海 / 51 KG / 1983-05-22
沈丹萍 / 中国南京 / 65 KG / 1960-02-19
王紫逸 / 中国香港 / 65 KG / 1986-12-15
吴健 / 中国淄博 / 68 KG / 1978-01-03
金荷娜 / 韩国 / 48 KG / 1978-02-21
张曼玉 / 中国 / 40 KG / 1964-09-20
薛佳凝 / 哈尔滨 / 45 KG / 1978-08-13

基于前面所学,相信你能快速写出匹配正则:\d{4}-\d{2}-\d{2} 。然后我需要获取每个结果中的月份,大部分人做法是在程序中使用yyyy-MM-dd 对日期进行格式化,然后在提取月份。

理解正则分组之后,大可不必这么麻烦,稍微改一下正则: (\d{4})-(\d{2})-(\d{2}) ,给日期年月日部分都加上括号,人为把结果划分成了三组。每组都有一个组号分别是1,2,3。然后在程序中可通过组号获取指定组的内容,比如获取月份就是第2组。

如何在程序中提取分组,请查阅各语言的API,以下列举了java与javascript提取组方式.

// java 
public static void main(String[] args) {
      Pattern pattern=Pattern.compile("(\d{4})-(\d{2})-(\d{2})");
        Matcher matcher = pattern.matcher("小丽生日是2011-11-23");
        if (matcher.find()) {
            String month = matcher.group(2);// 获取第2组中的月份
            System.out.println("月份是:"+month);
        }
    }
// 输出如下结果:
"月份是:11"
// javascript
"小丽生日是2011-11-23".match(/(\d{4})-(\d{2})-(\d{2})/);
// 得出结果如下:
['2011-11-23', '2011', '11', '23']

定义

分组指将匹配的内容,使用( )划分成多个组块,分好的组可用于提取反向引用以及替换操作。

你可能会有疑问,()不是用作子表达式么?子表达与分组是共存的,但侧重点不一样:子表达式针对正则的匹配逻辑,分组针对匹配的内容。

接下来学习分组中的反向引用与替换,很实用的功能,必须要掌握。

\1反向引用

在表达式中引用之前的分组,即反向引用。来看一个真实的场景:要提取以下html 中所有合法标题,

<h1>一级标题</h1>
<h2>二级标题</h2>
<h2>三级标题</h3>
<h4>四级标题</h4>
<h5>五级标题</h5>
<h6>六级标题</h6>

html中共有6级标题,标签是h[1-6],得出匹配正则<h[1-6]>.*?</h[1-6]>,正常情况下它能匹配所有标题,问题在于文中的标题三是错误的,

三级标题

,它是h2开头,结尾确是h3。如何解决呢?比较笨的方法可以这么写:

<h1>.*?</h1>|
<h2>.*?</h2>|
<h3>.*?</h3>|
<h4>.*?</h4>|
<h5>.*?</h5>|
<h6>.*?</h6>
#注:实际匹配不存在换行,这么写是方便观看

使用反向引用可以更轻松解决这个问题: <(h[1-6])>.*?</\1> 步骤拆解如下:

  • <(h[1-6])> 匹配开始标签,并把标签名加入分组1
  • .*? 标签中间可以是任意内容。
  • </\1> 匹配结束标签,标签内容通过\1引用分组1(意思是与分组1的内容一致)

练习

请匹配正确的html标题

<h1>一级标题</h1>
<h2>二级标题</h2>
<h2>三级标题</h3>
<h4>四级标题</h4>
<h5>五级标题</h5>
<h6>六级标题</ha>
/<(h[1-6])>.*?</\1>/gm

定义

反向引用指通过\组号引用之前的分组,可以把分组理解成一个变量,在通过变量名(组号)引用。它不能引用之后的内容,固作称作反向引用,比如这样正则是错误的:<\1>.*?</(h[1-6])>

引用替换

我们经常使正则然后进行替换操作,比如匹配文中所有空行,然后替换成空(删除它),又或者是找出错误的单词,替换成正确的。其实正则有更为强大的替换能力,比如把普通文中所有http链接,一键替换成标签,更厉害一点还可以把文本替换成 insert sql 语句。

但是在学习这些复杂替换操作前,先学习一些简单的。请找出下文中所有日期,并统一替换成yyyy-MM-dd格式:

姓名    / 籍贯      体重    生日
于小彤 / 中国辽宁 / 63 KG / 1994-05-27
张惠妹 / 台湾台东县卑南乡 / 46 KG / 1972/08/09
沈佳妮 / 中国上海 / 51 KG / 1983.05.22
沈丹萍 / 中国南京 / 65 KG / 1960/02/19
王紫逸 / 中国香港 / 65 KG / 1986/12/15
吴健 / 中国淄博 / 68 KG / 1978.01.03
金荷娜 / 韩国 / 48 KG / 1978/02/21
张曼玉 / 中国 / 40 KG / 1964/09/20
薛佳凝 / 哈尔滨 / 45 KG / 1978/08/13

实现替换需要3步:

定义:

替换操作指将正则匹配到的内容,替换成指定字符串,该字符串可通过组号引用组进行拼装。通过组号引用组进行拼装。通过0可以引用整个匹配的内容。比如:日期“1960/02/19”被匹配之后 0表示整个日期,0表示整个日期,1、22、3 分别表示年月日。请注意反向引用与替换引用的语法区别,前者是使用\组号,而替换使用$组号。

0PythonJavascript中不支持,使用0在 Python与Javascript中不支持,使用&代替

这种替换方式在各工具以及各编程语言中都支持,非常方便。以下列举了在Java及Js中的例子:

//Java
String s = "1978/08/13".replaceAll("(\d{4})[-.\/](\d{2})[-.\/](\d{2})",
        "$1-$2-$3");
System.out.println(s);
// 输出结果
1978-08-13
//JavaScript
"1978/08/13 1991.28.11".replaceAll(/(\d{4})[-./](\d{2})[-./](\d{2})/g,"$1-$2-$3")
// 输出结果
1978-08-13 1991-28-11

大小写转换

操作符描述兼容性
\u 单个转大写转换一下个字符为
\U 全部转大写转换\U后所有字符转
\U...\E 区间转大写\U与\E区间的内容转
\l 单个转小写转换一下个字符为小写
\L 全部转小写转换\L后所有字符转小写
\L...\E 区间转小写\L与\U区间的内容转小写

具体使用方法是:在替换字符串中加入转换操作符。举例把单词首字母转成大写:

  • 编写匹配正则:\w+
  • 首字母转大写并替换:\u$0
  • "my love" 被替换后结果就是"My Love"

分组的其它应用

关于分组还存在一些特殊情况,需要提前了解一下:

  • (?<名称> )命名分组
  • (?: ) 移除分组
  • ( ( ) )嵌套分组
  • ()+分组使用量词

(?<名称> )命名分组

默认情况下通过组号来取值,此外也可以自定义命名组,语法是(?<名称> ),然后在程序中就可以通过<>中的名称来取值。如:<(?h[1-6])>.*?</\1> 该表达式就命名了一个title的组,在js的结果中就可通过title属性取值。

let ret = "<h1>一级标题</h1>".match(/<(?<title>h[1-6])>.*?</\1>/)
consloe.log(ret)

注:数组中还包含了groups,index,input等属性

注意:这种命名组只能用于在程序中提取操作,不能进行反向引用,也不能用在替换操作中。上例在替换中如果使用 $title或在反向引用中使用 \title 都是无效的。只能通过组号\1进行引用。这也说明命名组后,组号一样有效。也正因为这种局限性所以命名组使用的很少。

(?: )移除分组

()即用于子表达式,同时也是一个分组。如果只想用作子表达式,而不想用于分组就可以使用(?: )从分组列表中移除。比如(?:\d{4})-(\d{2})-(\d{2}) 该表达式只存在两个组,月1和日1和日2。

你可能在想这么做的意义是什么呢?在一些复杂场景中这是有用的,用让组号变得更清晰。以下正则用于匹配http 链接:

"https://www.baid.com".match(/((https?)://)?((\w+.)?\w+.\w+)/)
// 得出结果如下:
0: "https://www.baid.com"
1: "https://"
2: "https"
3: "www.baid.com"
4: "www."

总共会有4个组,非常的乱,如果只想获取协议和域名组,就可以用(?:)把其它从组去中掉,同时子表达式的逻辑依然存在

"https://www.baid.com".match(/(?:(https?)://)?((?:\w+.)?\w+.\w+)/)
// 得出结果如下:
0: "https://www.baid.com"
1: "https"
2: "www.baid.com"

( ( ) )嵌套分组

在嵌套分组中组号是如何命名的呢?比如:生日((\d{4})-(\d{2})(\d{2})) 其组号的命名顺序是以开括号出现顺序为准。

"生日2019-09-21".match(/生日((\d{4})-(\d{2})-(\d{2}))/)
// 得出结果如下:
0: "生日2019-09-21"
1: "2019-09-21"
2: "2019"
3: "09"
4: "21"

()+分组使用量词

同一个分组如果使用了量词,该分组会代表多个值,这时通过组号去提取值的时候会得到该组最后匹配的值。如(\d)+匹配12345,通过组号去提取值的时候会得到该组最后匹配的值。如(\d)+ 匹配12345,通过1将得到5

本章练习

Sql语句转换

在日常开发工作中,经常有需求将文本内容导入到数据库,常见的做法是,使用数据导入工具,并按照一定格式解析文本导入。如果你需过滤掉错误行,又或者统一日期格式等自定义操作,这时就可以使用正则的替换操作来完成。把文本替换成 Insert Sql 然后在导入。

请将文本替换成 Insert Sql语句

规则说明:

  • 表名:user
  • 列名:name,city,weight,birthday
  • 日期统一为:yyyy-MM-dd 格式

示例:insert into user (name,city,weight,birthday) values('张惠妹','台湾台东县卑南乡',46,'1972-08-09');

参考答案

  • 正则:(.+?) / (.+?) / (\d{2}) KG / (\d{4}).(\d{2}).(\d{2})
  • 替换:insert into user (name,city,weight,birthday) values('1,1','2',3,3,'4-55-6');