本文简要介绍Java的正则表达式及其实现方式,并通过实例讲解正则表达式的具体用法。
1. 正则表达式
1.1. 简介
正则表达式(Regular Expression), 简称 正则, 也翻译为 正规式, 用来表示文本搜索模式。英文缩写是 regex(reg-ex
).
搜索模式(search pattern)可能多种多样, 如, 单个字符(character), 特定字符串(fixed string), 包含特殊含义的复杂表达式等等. 对于给定的字符串, 正则表达式可能匹配一到多次, 也可能一次都不匹配。
正则表达式一般用来查找、编辑和替换文本(text), 本质上, text(文本) 和 string(字符串) 是一回事。
用正则表达式来分析/修改文本的过程, 称为: 应用于文本/字符串的正则表达式 。正则表达式扫描字符串的顺序是从左到右. 每个字符都只能被匹配成功一次, 下次匹配扫描就会从后面开始。例如, 正则表达式 aba
, 匹配字符串 ababababa 时, 只会扫描到两个匹配(aba_aba__
)。
1.2. 示例
最简单的例子是字母串。例如, 正则表达式 Hello World
能匹配的就是字符串 “Hello World”。 正则表达式中, 点号 .
(dot,英文句号)属于通配符, 点号匹配任意一个字符(character); 例如, “a” 或者 “1”; 当然, 默认情况下点号不能匹配换行 \n
, 需要特殊标识指定才行。
下表列举了一些简单的正则表达式,和对应的匹配模式。
正则表达式 | Matches |
---|---|
this is text | 完全匹配 “this is text” |
this\s+is\s+text | 匹配的内容为: “this”, 加上1到多个空白符(whitespace character, 如空格,tab,换行等), 加上 “is”, 加上1到多个空白符, 再加上 “text”. |
^\d+(\.\d+)? | 正则表达式以转义字符 ^ (小尖号)打头, 表示这行必须以小尖号后面的字符模式开始, 才会达成匹配. \d+ 匹配1到多个数字. 英文问号 ? 表示可以出现 0~1次. \. 匹配的是字符 “.”, 小括号(parentheses) 表示一个分组. 所以这个正则表达式可以匹配正整数或者小数,如: “5”, “66.6” 或者 “5.21” 等等. |
说明,中文的全角空格(
)字符不属于空白字符(whitespace characters), 可以认为其属于一个特殊的汉字。
1.3. 编程语言对正则表达式的支持
大多数编程语言都支持正则表达式, 如 Java、Perl, Groovy 等等。但各种语言的正则表达式写法略有一些不同。
2. 预备知识
本教程要求读者具备Java语言相关的基础知识。
下面的一些示例通过 JUnit 来验证执行结果。如果不想使用JUnit, 也可以改写相关代码。关于JUnit的知识请参考 JUnit教程: http://www.vogella.com/tutorials/JUnit/article.html。
3. 语法规则
本章介绍各种正则元素的范本, 我们会先介绍什么是元字符(meta character)。
3.1. 通用表达式简介
正则表达式 | 说明 |
---|---|
. | 点号(. ), 匹配任意一个字符 |
^regex | 小尖号(^ ), 起始标识, 前面不能出现其他字符. |
regex$ | 美元符号($ ,dollar,美刀), 结束标识,后面不能再出现其他字符. |
[abc] | 字符组(set), 匹配 a 或 b 或 c. |
[abc][vz] | 字符组(set), 匹配 a 或 b 或 c,紧接着是 v 或 z. |
[^abc] | 如果小尖号(^ , caret, 此处读作 非 ) 出现在中括号里面的第一位, 则表示否定(negate). 这里匹配: 除 a , b , c 之外的其他任意字符. |
[a-d1-7] | 范围表示法: 匹配 a 到 d 之间的单个字符, 或者 1 到 7 之间的单个字符, 整体只匹配单个字符, 而不是 d1 这种组合. |
X|Z | 匹配 X 或者 Z . |
XZ | 匹配XZ , X和Z必须按顺序全部出现. |
$ | 判断一行是否结束. |
3.2. 元字符
下面这些是预定义的元字符(Meta characters), 可用于提取通用模式, 如 \d
可以代替 [0-9]
, 或者[0123456789]
。
正则表达式 | 说明 |
---|---|
\d | 单个数字, 等价于 [0-9] 但更简洁 |
\D | 非数字, 等价于 [^0-9] 但更简洁 |
\s | 空白字符(whitespace), 等价于 [ \t\n\x0b\r\f] |
\S | 非空白字符, 等价于 [^\s] |
\w | 反斜线加上小写w, 表示单个标识符,即字母数字下划线, 等价于 [a-zA-Z_0-9] |
\W | 非单词字符, 等价于 [^\w] |
\S+ | 匹配1到多个非空白字符 |
\b | 匹配单词外边界(word boundary), 单词字符指的是 [a-zA-Z0-9_] |
这些元字符主要取自于对应单词的英文首字母, 例如: digit(数字), space(空白), word (单词), 以及 boundary(边界)。对应的大写字符则用来表示取反。
3.3. 量词
量词(Quantifier)用来指定某个元素可以出现的次数。?
, *
, +
和 {}
等符号定义了正则表达式的数量。
正则表达式 | 说明 | 示例 |
---|---|---|
* | 0到多次, 等价于 {0,} | X* 匹配0到多个连续的X, .* 则匹配任意字符串 |
+ | 1到多次, 等价于 {1,} | X+ 匹配1到多个连续的X |
? | 0到1次, 等价于 {0,1} | X? 匹配0个,后者1个X |
{n} | 精确匹配 n 次 {} 前面序列出现的次数 | \d{3} 匹配3位数字, .{10} 匹配任意10个字符. |
{m, n} | 出现 m 到 n 次, | \d{1,4} 匹配至少1位数字,至多4位数字. |
*? | 在量词后面加上 ? , 表示懒惰模式(reluctant quantifier). 从左到右慢慢扫描, 找到第一个满足正则表达式的地方就暂停搜索, 用来尝试匹配最少的字符串. |
3.4. 分组和引用
可以对正则表达式进行分组(Grouping), 用圆括号 ()
括起来。这样就可以对括号内的整体使用量词。
当然, 在进行替换的时候, 还可以对分组进行引用。也就是捕获组(captures the group)。向后引用(back reference) 指向匹配中该分组所对应的字符串。进行替换时可以通过 $ 来引用。
使用 $
来引用一个捕获组。例如 $1
表示第一组, $2
表示第二组, 以此类推, $0
则表示整个正则所匹配的部分。
例如, 想要去掉单词后面, 句号/逗号(point or comma)前面的空格。可以把句号/逗号写入正则中, 然后原样输出到结果中即可。
// 去除单词与 `.|,` 之间的空格
String pattern = "(\\w)(\\s+)([\\.,])";
System.out.println(EXAMPLE_TEST.replaceAll(pattern, "$1$3"));
提取 标签的内容:
// 提取 <title> 标签的内容
pattern = "(?i)(<title.*?>)(.+?)()";
String updated = EXAMPLE_TEST.replaceAll(pattern, "$2");
3.5. 环视
环视(lookaround), 分为顺序环视(Lookahead)与逆序环视(lookbehind), 属于零宽度断言(zero-length assertion)。 类似于行起始标识(^
)和结束标识($
); 或者单词边界(\b
)一类的位置标识。
顺序否定环视(Negative look ahead), 用于在匹配的同时, 排除掉某些情形。也就是说其后面不能是符合某种特征的字符串。
顺序否定环视(Negative look ahead) 使用 (?!pattern)
这种格式定义。例如, 下面的正则, 只匹配后面不是 b 字母的 “a” 字母。
a(?!b)
类似的, 顺序环视(look ahead), 也叫顺序肯定环视。 如,只匹配a字母, 但要求后面只能是 b 字母, 否则这个 a 就不符合需要:
a(?=b)
注意,环视 是一种向前/后查找的语法:
(?=exp)
, 会查找后面位置的 exp; 所环视的内容却不包含在正则表达式匹配中。环视(lookaround)是一种高级技巧, 环视的部分不会匹配到结果之中, 但却要求匹配的字符串前面/后面具备环视部分的特征。
如果将等号换成感叹号, 就是环视否定
(?!exp)
, 变成否定语义,也就是说查找的位置的后面不能是exp。逆序肯定环视,
(?<=exp)
, 表示所在位置左侧能够匹配 exp逆序否定环视,
(?<!exp)
, 表示所在位置左侧不能匹配 exp详情请参考: 正则应用之——逆序环视探索: http://blog.csdn.net/lxcnn/article/details/4954134
参考: 利用正则表达式排除特定字符串 http://www.cnblogs.com/wangqiguo/archive/2012/05/08/2486548.html
3.6. 正则表达式的模式
在正则表达式开头可以指定模式修饰符(mode modifier)。还可以组合多种模式, 如 (?is)pattern
。
(?i)
正则表达式匹配时不区分大小写。(?s)
单行模式(single line mode), 使点号(.
) 匹配所有字符, 包括换行(\n
)。(?m)
多行模式(multi-line mode), 使 小尖号(^
,caret) 和 美元符号($
, dollar) 匹配目标字符串中每一行的开始和结束。
3.7. Java中的反斜杠
在Java字符串中, 反斜杠(\
, backslash) 是转义字符, 有内置的含义。在源代码级别, 需要使用两个反斜杠\\
来表示一个反斜杠字符。如果想定义的正则表达式是 \w
, 在 .java
文件源码中就需要写成 \\w
。 如果想要匹配文本中的1个反斜杠, 则源码中需要写4个反斜杠 \\\\
。
4. String类正则相关的方法
4.1. String 类重新定义了正则相关的方法
Java中 String
类内置了4个支持正则的方法, 即: matches()
, split()
, replaceFirst()
和replaceAll()
方法。 需要注意, replace()
是纯字符串替换, 不支持正则表达式。
这些方法并没有对性能进行优化。稍后我们将讨论优化过的类。
方法 | 说明 |
---|---|
s.matches("regex") | 判断字符串 s 是否能匹配正则 "regex" . 只有整个字符串匹配正则才返回 true . |
s.split("regex") | 用正则表达式 "regex" 作为分隔符来拆分字符串, 返回结果是 String[] 数组. 注意 "regex" 对应的分隔符并不包含在返回结果中. |
s.replaceFirst("regex", "replacement" ) | 替换第一个匹配 "regex" 的内容为 "replacement . |
s.replaceAll("regex", "replacement") | 将所有匹配 "regex" 的内容替换为 "replacement . |
下面是对应的示例。
package de.vogella.regex.test;
public class RegexTestStrings {
public static final String EXAMPLE_TEST = "This is my small example "
+ "string which I'm going to " + "use for pattern matching.";
public static void main(String[] args) {
System.out.println(EXAMPLE_TEST.matches("\\w.*"));
String[] splitString = (EXAMPLE_TEST.split("\\s+"));
System.out.println(splitString.length);// should be 14
for (String string : splitString) {
System.out.println(string);
}
// 将所有空白符(whitespace) 替换为 tab
System.out.println(EXAMPLE_TEST.replaceAll("\\s+", "\t"));
}
}
4.2. 示例
下面给出一些正则表达式的使用示例。请参照注释信息。
If you want to test these examples, create for the Java project de.vogella.regex.string
.
如果想测试这些示例, 请将java文件放到一个Java包下, 如 de.vogella.regex.string
。
package de.vogella.regex.string;
public class StringMatcher {
// 如果字符串完全匹配 "`true`", 则返回 true
public boolean isTrue(String s){
return s.matches("true");
}
// 如果字符串完全匹配 "`true`" 或 "`True`", 则返回 true
public boolean isTrueVersion2(String s){
return s.matches("[tT]rue");
}
// 如果字符串完全匹配 "`true`" 或 "`True`"
// 或 "`yes`" 或 "`Yes`", 则返回 true
public boolean isTrueOrYes(String s){
return s.matches("[tT]rue|[yY]es");
}
// 如果包含字符串 "`true`", 则返回 true
public boolean containsTrue(String s){
return s.matches(".*true.*");
}
// 如果包含3个字母, 则返回 true
public boolean isThreeLetters(String s){
return s.matches("[a-zA-Z]{3}");
// 当然也等价于下面这种比较土的方式
// return s.matches("[a-Z][a-Z][a-Z]");
}
// 如果不以数字开头, 则返回 true
public boolean isNoNumberAtBeginning(String s){
// 可能 "^\\D.*" 更好一点
return s.matches("^[^\\d].*");
}
// 如果包含了 `b` 之外的字符, 则返回 true
public boolean isIntersection(String s){
return s.matches("([\\w&&[^b]])*");
}
// 如果包含的某串数字小于300, 则返回 true
public boolean isLessThenThreeHundred(String s){
return s.matches("[^0-9]*[12]?[0-9]{1,2}[^0-9]*");
}
}
And a small JUnit Test to validates the examples.
我们通过 JUnit 测试来验证。
package de.vogella.regex.string;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class StringMatcherTest {
private StringMatcher m;
@Before
public void setup(){
m = new StringMatcher();
}
@Test
public void testIsTrue() {
assertTrue(m.isTrue("true"));
assertFalse(m.isTrue("true2"));
assertFalse(m.isTrue("True"));
}
@Test
public void testIsTrueVersion2() {
assertTrue(m.isTrueVersion2("true"));
assertFalse(m.isTrueVersion2("true2"));
assertTrue(m.isTrueVersion2("True"));;
}
@Test
public void testIsTrueOrYes() {
assertTrue(m.isTrueOrYes("true"));
assertTrue(m.isTrueOrYes("yes"));
assertTrue(m.isTrueOrYes("Yes"));
assertFalse(m.isTrueOrYes("no"));
}
@Test
public void testContainsTrue() {
assertTrue(m.containsTrue("thetruewithin"));
}
@Test
public void testIsThreeLetters() {
assertTrue(m.isThreeLetters("abc"));
assertFalse(m.isThreeLetters("abcd"));
}
@Test
public void testisNoNumberAtBeginning() {
assertTrue(m.isNoNumberAtBeginning("abc"));
assertFalse(m.isNoNumberAtBeginning("1abcd"));
assertTrue(m.isNoNumberAtBeginning("a1bcd"));
assertTrue(m.isNoNumberAtBeginning("asdfdsf"));
}
@Test
public void testisIntersection() {
assertTrue(m.isIntersection("1"));
assertFalse(m.isIntersection("abcksdfkdskfsdfdsf"));
assertTrue(m.isIntersection("skdskfjsmcnxmvjwque484242"));
}
@Test
public void testLessThenThreeHundred() {
assertTrue(m.isLessThenThreeHundred("288"));
assertFalse(m.isLessThenThreeHundred("3288"));
assertFalse(m.isLessThenThreeHundred("328 8"));
assertTrue(m.isLessThenThreeHundred("1"));
assertTrue(m.isLessThenThreeHundred("99"));
assertFalse(m.isLessThenThreeHundred("300"));
}
}
5. Pattern与Matcher简介
For advanced regular expressions the java.util.regex.Pattern
and java.util.regex.Matcher
classes are used.
要支持正则表达式的高级特性, 需要借助 java.util.regex.Pattern
和 java.util.regex.Matcher
类。
首先创建/编译 Pattern
对象, 用来定义正则表达式。对 Pattern
对象, 给定一个字符串, 则产生一个对应的 Matcher
对象。通过 Matcher
对象就可以对 String
进行各种正则相关的操作。
package de.vogella.regex.test;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexTestPatternMatcher {
public static final String EXAMPLE_TEST = "This is my small example string which I'm going to use for pattern matching.";
public static void main(String[] args) {
Pattern pattern = Pattern.compile("\\w+");
// 如需忽略大小写, 可以使用:
// Pattern pattern = Pattern.compile("\\w+", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(EXAMPLE_TEST);
// 查找所有匹配的结果
while (matcher.find()) {
System.out.print("Start index: " + matcher.start());
System.out.print(" End index: " + matcher.end() + " ");
System.out.println(matcher.group());
}
// 将空格替换为 tabs
Pattern replace = Pattern.compile("\\s+");
Matcher matcher2 = replace.matcher(EXAMPLE_TEST);
System.out.println(matcher2.replaceAll("\t"));
}
}
6. 正则表达式示例
下面列出了常用的正则表达式使用情景。希望读者根据实际情况进行适当的调整。
6.1 或(Or)
任务: 编写正则表达式, 用来匹配包含单词 “Joe” 或者 “Jim” , 或者两者都包含的行。
创建 de.vogella.regex.eitheror
包和下面的类。
package de.vogella.regex.eitheror;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class EitherOrCheck {
@Test
public void testSimpleTrue() {
String s = "humbapumpa jim";
assertTrue(s.matches(".*(jim|joe).*"));
s = "humbapumpa jom";
assertFalse(s.matches(".*(jim|joe).*"));
s = "humbaPumpa joe";
assertTrue(s.matches(".*(jim|joe).*"));
s = "humbapumpa joe jim";
assertTrue(s.matches(".*(jim|joe).*"));
}
}
6.2. 匹配电话号码
任务: 编写正则表达式, 匹配各种电话号码。
假设电话号码(Phone number)的格式为 “7位连续的数字”; 或者是 “3位数字加空格/横线, 再加上4位数字”。
package de.vogella.regex.phonenumber;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CheckPhone {
@Test
public void testSimpleTrue() {
String pattern = "\\d\\d\\d([,\\s])?\\d\\d\\d\\d";
String s= "1233323322";
assertFalse(s.matches(pattern));
s = "1233323";
assertTrue(s.matches(pattern));
s = "123 3323";
assertTrue(s.matches(pattern));
}
}
6.3. 判断特定数字范围
以下示例用来判断文本中是否具有连续的3位数字。
创建 de.vogella.regex.numbermatch
包和下面的类。
package de.vogella.regex.numbermatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CheckNumber {
@Test
public void testSimpleTrue() {
String s= "1233";
assertTrue(test(s));
s= "0";
assertFalse(test(s));
s = "29 Kasdkf 2300 Kdsdf";
assertTrue(test(s));
s = "99900234";
assertTrue(test(s));
}
public static boolean test (String s){
Pattern pattern = Pattern.compile("\\d{3}");
Matcher matcher = pattern.matcher(s);
if (matcher.find()){
return true;
}
return false;
}
}
6.4. 校验超链接
假设需要从网页中找出所有的有效链接。当然,需要排除 “javascript:” 和 “mailto:” 开头的情况。
创建 de.vogella.regex.weblinks 包, 以及下面的类:
package de.vogella.regex.weblinks;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LinkGetter {
private Pattern htmltag;
private Pattern link;
public LinkGetter() {
htmltag = Pattern.compile("<a\\b[^>]*href=\"[^>]*>(.*?)</a>");
link = Pattern.compile("href=\"[^>]*\">");
}
public List<String> getLinks(String url) {
List<String> links = new ArrayList<String>();
try {
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(new URL(url).openStream()));
String s;
StringBuilder builder = new StringBuilder();
while ((s = bufferedReader.readLine()) != null) {
builder.append(s);
}
Matcher tagmatch = htmltag.matcher(builder.toString());
while (tagmatch.find()) {
Matcher matcher = link.matcher(tagmatch.group());
matcher.find();
String link = matcher.group().replaceFirst("href=\"", "")
.replaceFirst("\">", "")
.replaceFirst("\"[\\s]?target=\"[a-zA-Z_0-9]*", "");
if (valid(link)) {
links.add(makeAbsolute(url, link));
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return links;
}
private boolean valid(String s) {
if (s.matches("javascript:.*|mailto:.*")) {
return false;
}
return true;
}
private String makeAbsolute(String url, String link) {
if (link.matches("http://.*")) {
return link;
}
if (link.matches("/.*") && url.matches(".*$[^/]")) {
return url + "/" + link;
}
if (link.matches("[^/].*") && url.matches(".*[^/]")) {
return url + "/" + link;
}
if (link.matches("/.*") && url.matches(".*[/]")) {
return url + link;
}
if (link.matches("/.*") && url.matches(".*[^/]")) {
return url + link;
}
throw new RuntimeException("Cannot make the link absolute. Url: " + url
+ " Link " + link);
}
}
6.5. 查找重复的单词
下面的正则表达式用来匹配重复的单词。
\b(\w+)\s+\1\b
\b
是单词边界, \1
则引用第一个分组, 此处的第一个分组为前一个单词 (\w+)
。
(?!-in)\b(\w+) \1\b
通过环视否定, 来匹配前面不是 “-in
” 开始的重复单词。
提示: 可以在最前面加上 (?s)
标志来执行跨行搜索。
6.6. 查找每行起始位置的元素
下面的正则, 用来查找一行开头的单词 “title”, 前面允许有空格。
(\n\s*)title
6.7. 找到非Javadoc风格的语句
有时候, 在Java代码中会出现非Javadoc风格(Non-Javadoc)的语句; 如 Java 1.6 中的 @Override
注解, 用于告诉IDE该方法覆写了超类方法。这种是可以从源码中清除的。下面的正则用来找出这类注解。
(?s) /\* \(non-Javadoc\).*?\*/
6.7.1. 用 Asciidoc 替换 DocBook 声明
例如有下面这样的XML:
<programlisting language="java">
<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="./examples/statements/MyClass.java" />
</programlisting>
可以用下面的正则来匹配:
`\s+<programlisting language="java">\R.\s+<xi:include xmlns:xi="http://www\.w3\.org/2001/XInclude" parse="text" href="\./examples/(.*).\s+/>\R.\s+</programlisting>`
替换目标可以是下面这样的regex:
`\R[source,java]\R----\R include::res/$1[]\R----
7. 在Eclipse中使用正则表达式
在Eclipse或者其他编辑器中, 可以使用正则来执行查找和替换。一般使用快捷键 Ctrl+H
打开 搜索/Search 对话框。
选择 File Search 选项卡, 并勾选 Regular expression 标识, 则可以进行正则查找/替换。当然, 还可以指定文件类型, 以及查找/替换的目录范围。
下图展示了如何查找XML标签 <![CDATA[]]]>
和前面的空格, 以及如何去除这些空格。
在结果对话框中可以查看有哪些地方会被替换, 可以去掉不想替换的元素。没问题的话, 点击 OK
按钮, 就会进行替换。
8. 相关链接
- 示例代码下载: http://www.vogella.com/code/index.html
- Regular-Expressions.info on Using Regular Expressions in Java
- Regulare xpressions examples
- The Java Tutorials: Lesson: Regular Expressions
原文链接: http://www.vogella.com/tutorials/JavaRegularExpressions/article.html
原文日期: 2016.06.24
翻译日期: 2017-12-28