Java正则系列: (1)入门教程

本文简要介绍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] 范围表示法: 匹配 ad 之间的单个字符, 或者 17之间的单个字符, 整体只匹配单个字符, 而不是 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.Patternjava.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[]]]> 和前面的空格, 以及如何去除这些空格。

Java正则系列: (1)入门教程

在结果对话框中可以查看有哪些地方会被替换, 可以去掉不想替换的元素。没问题的话, 点击 OK 按钮, 就会进行替换。

Java正则系列: (1)入门教程

8. 相关链接

原文链接: http://www.vogella.com/tutorials/JavaRegularExpressions/article.html

原文日期: 2016.06.24

翻译日期: 2017-12-28

翻译人员: 铁锚 http://blog.csdn.net/renfufei/

上一篇:无线路由器的WDS功能如何配置?


下一篇:解读 Knative Eventing v0.11.0 新特性