5.JAVA NIO正则表达式

第五章 正则表达式

本章我们将讨论新的程序包java.util.regex(见图 5-1)中类的API(译注 10)。JSR51,即 Java规范请求(Java Specification Request),定义了新的I/O权能,它还明确了添加到Java平台 上的正则表达式处理技术。尽管严格说来正则表达式并不是I/O,但是它们最常用于浏览从文 件或数据流(stream)中读取的文本数据。

5.JAVA NIO正则表达式_第1张图片

Perl、egrep和其它文本处理工具有着功能强大的模式匹配(pattern matching),本章中你 将学会如何使用新的Java API达到相同的模式匹配效果。关于正则表达式的详细探究不在本书 的范围内,但是本书假设您熟悉正则表达式的实际作用。假若你不熟悉正则表达式,想要提升 自己的技能,又或者对本章内容感到困惑,我建议你挑选一本好的参考书。O’Reilly出版了的 一本正则表达式权威书籍(甚至JDK文档都引用了它):Mastering Regular Expressions,作者 Jeffrey E.F. Friedl(译注11 )。

1.正则表达式基础

正则表达式(regular expression)是描述或表达在目标字符序列内匹配一定字符模式的字符序列(译注 12 )。正则表达式在Unix世界已存在多年并凭借sed、grep(查找正则表达式)、 awk等文本编辑工具而广为使用。在Unix平台上使用的正则表达式因其长久的历史已经成了大 部分正则表达式处理器的基础。开放组(The Open Group)是Unix标准团体,它将正则表达式 句法作为Unix规范的一部分作了详述(译注 13 )。

(http://www.opengroup.org/onlinepubs/7908799/xbd/re.html)

广受欢迎的Perl (注 1)脚本语言将正则表达式处理技术直接纳入其语言句法的麾下。随 着Perl的演变及流行,它在添加更多复杂功能的同时也扩展了正则表达式句法。Perl因其能力 及灵活性而几乎无所不在,并且随后也建立了正则表达式处理技术的实际标准。

java.util.regex 的类提供的正则表达式权能(capability)效仿了 Perl 5 提供的权能。细微差别主要囿于一般用户极少接触的神秘领域。关于 java.util.regex.Pattern 类的细节,详见 5.4 节, 表 5-7 及 Javadoc API 文档。

在Perl脚本中,正则表达式被作为内联的(inline,译注 14 )子过程匹配变量值。将正则表 达式赋值(evaluation)集成到语言中使得Perl在文本处理的脚本应用中广受欢迎。直到现在, Java已经难以企及。因为在文本文件中处理非平凡模式(notrivial pattern)缺乏内置工作,所 以用Java处理文本文件相当累赘麻烦。

5.JAVA NIO正则表达式_第2张图片

Perl享有较高水平的正则表达式集成,正当Java从未触及这一高度之际,java.util.regex包为 Java带来了福音,它可以提供与Perl同等水平的表达能力。当然,Perl与Java的使用模型是不同的:Perl是过程式脚本语言,而Java是编译的、面向对象的语言。但是Java正则表达式API易于使用, 并且现在允许Java轻松实现文本处理任务, 而该任务在以往都是“外包”给Perl的。

2.Java正则表达式

java.util.regex 程序包只包含用于实现 Java 正则表达式处理技术的两个类,分别名 为 Pattern 和 Matcher 。 自 然 而 然 你 会 想 到 正 则 表 达 式 由 模 式 匹 配 ( pattern matching)而成.java.lang 还定义了一个新接口,它支持这些新的类。在研究 Patternt 和 Matcher 之前,我们先快速浏览一下 CharSequence 这一新概念。

另外,为方便起见 String 类为运行正则表达式匹配提供了一些新程序作为捷径。这些将 5.3 节中讨论。

1)CharSequence接口

正则表达式是根据字符序列进行模式匹配的。虽然 String 对象封装了字符序列,但是它们并不能够这样做的唯一对象。

JDK 1.4 定义了一个名为 CharSequence 的新接口,可描述特定不变的字符序列。该新接口是一个抽象(abstraction),它把字符序列从包含这些字符的具体实现(specific implementation)中 分离出来。 JDK 1.4 对“年高德勋”的 String 和 StringBuffer 类进行了改进, 用于实现 CharSequence 接口。 新的 CharBuffer 类(在第二章中介绍过)也实现了 CharSequence。 CharSequence 接口也在字符集映射中投入了使用(参见第六章)。

CharSequence 定义的 API 十分简单。毕竟它没有花太多“笔墨”描述字符序列。

package java.lang;

public interface CharSequence {

    int length();

    char charAt(int index);

    CharSequence subSequence(int start, int end);

    public String toString();
}

 

CharSequence 描述的每个字符序列通过 length( )方法会返回某个长度值。 通过调用 charAt( )可以得到序列的各个字符,其中索引是期望的字符位置(desired character position)。 字符位置从零到字符序列的长度之间,与我们熟悉的 String.charAt( )基本一样。

toString( )方法返回的 String 对象包括所描述的字符序列。这可能很有用,如打印字符序 列。正如之前提过的,String 现在实现了 CharSequence。String 和 CharSequence 同为不变的, 因此如果 CharSequence 描述一个完整的 String,那么 CharSequence 的 toString( )方法返回的是 潜在的 String 对象而不是副本。如果备份对象是 StringBuffer 或 CharBuffer,系统将创建一个 新的 String 保存字符序列的副本。

最 后 通 过 调 用 subSequence( ) 方 法 会 创 建 一 个 新 的 CharSequence 描 述 子 范 围 (subrange)。start 和 end 的指定方式与 String.substring( )的方式相同:start 必须是序列的有效 索引(valid index);end 必须比 start 大,标志的是最末字符的索引加一。换句话说,start 是 起始索引(计算在内),end 是结束索引(不计算在内)。

CharSequence 接口因为没有赋值方法(mutator method)看上去似乎是不变的,但是基本 的实现对象可能不是不变的。 CharSequence 方法反映了基本对象的现状。 如果状态改变, CharSequence 方法返回的信息同样会发生变化(见例 5-1)。如何你依赖 CharSequence 保持 稳定且不确认基础的实现, 你可以调用 toString( )方法, 对字符序列拍个真实不变的快照。

例 5-1 CharSequence 接口实例

 

package org.example;

import java.nio.CharBuffer;

/**
 * Demonstrate behavior of java.lang.CharSequence as implemented
 * by String, StringBuffer and CharBuffer.
 *
 * @author Ron Hitchens ([email protected])
 */
public class CharSeq {

    public static void main(String[] argv) {
        StringBuffer stringBuffer = new StringBuffer("Hello World");
        CharBuffer charBuffer = CharBuffer.allocate(20);
        CharSequence charSequence = "Hello World";
        //直接来源于String
        printCharSequence(charSequence);
        //来源于StringBuffer
        charSequence = stringBuffer;
        printCharSequence(charSequence);
        //更改StringBuffer
        stringBuffer.setLength(0);
        stringBuffer.append("Goodbye cruel world");
        //相同、“不变的”CharSequence产生了不同的结果
        printCharSequence(charSequence);
        //从CharBuffer中导出CharSequence
        charSequence = charBuffer;
        charBuffer.put("xxxxxxxxxxxxxxxxxxxx");
        charBuffer.clear();
        charBuffer.put("Hello World");
        charBuffer.flip();
        printCharSequence(charSequence);
        charBuffer.mark();
        charBuffer.put("Seeya");
        charBuffer.reset();
        printCharSequence(charSequence);
        charBuffer.clear();
        printCharSequence(charSequence);
        //更改基础CharBuffer会反映在只读的CharSequence接口上
    }

    private static void printCharSequence(CharSequence cs) {
        System.out.println("length=" + cs.length()
                + ", content='" + cs.toString() + "'");
    }
}

以下是执行 CharSequence 的结果:

length=11, content='Hello World' 
length=11, content='Hello World' 
length=19, content='Goodbye cruel world' 
length=11, content='Hello World' 
length=11, content='Seeya World' 
length=20, content='Seeya Worldxxxxxxxxx'

2)Pattern类

Pattern 类封装了正则表达式,它是你希望在目标字符序列中检索的模式。匹配正则表 达式的代价可能非常高昂,因为可能排列数量巨大,尤其是模式反复应用的情况。大部分正则表达式处理器(包括 Perl 在内,在封装中)首先会编译表达式,然后利用编译好的表达式在 输入中进行模式检测。

在这一点上 Java 正则表达式程序包别无两样。Pattern 类的实例是将一个编译好的正 则表达式封装起来。让我们看看完整的 Pattern API,看看它是如何使用的。记住,这并不 是一个句法完整的类文件,它只中去掉了类主体的方法签名。

package java.util.regex;

import java.util.regex.Matcher;

public final class Pattern implements java.io.Serializable {

    public static final int UNIX_LINES = 0x01;
    public static final int CASE_INSENSITIVE = 0x02;
    public static final int COMMENTS = 0x04;
    public static final int MULTILINE = 0x08;
    public static final int LITERAL = 0x10;
    public static final int DOTALL = 0x20;
    public static final int UNICODE_CASE = 0x40;
    public static final int CANON_EQ = 0x80;
    public static final int UNICODE_CHARACTER_CLASS = 0x100;

    public static boolean matches(String regex, CharSequence input)

    public static Pattern compile(String regex)

    public static Pattern compile(String regex, int flags)

    public String pattern()

    public int flags()

    public String[] split(CharSequence input, int limit)

    public String[] split(CharSequence input)

    public Matcher matcher(CharSequence input)
}

上面所列的第一个方法 matches( )是个公用程序。它可以进行完整的匹配操作,并根据正 则表达式是否匹配整个的(entire)输入序列返回一个布尔值。这种方法很容易上手,因为 你无须追踪任何对象;你要做的仅是调用一个简单的静态方法并测试结果。

    public boolean goodAnswer(String answer) {
        return (Pattern.matches("[Yy]es|[Yy]|[Tt]rue", answer));
    }

这种方法适用于默认设置尚可接受并且只需进行一次测试的情况。假如你要重复检查同一 模式,假如你要找的模式是输入的子序列,又假如你要设置非默认选项,那么你应当创建一个 新的 Pattern 对象并使用新对象的 API 方法。

需要注意的是 Pattern 类并没有公用建构函数。只有通过调用静态工厂方法才可以创建 新的实例。compile( )的两个形式采用的都是正则表达式的 String 参数。返回的 Pattern 对 象包含被转换成已编译内部形式的正则表达式。 如果你提供的正则表达式形态异常, 那么 compile( )工厂方法会抛出 java.util.regex.PatternSyntaxException(模式句法异 常)。这是未经检查的异常,因此如果你对自己使用的正则表达式是否可行存在疑虑(例如它 传递给你是一个变量),那么你可以把对 compile( )的调用放到 try/catch 块中进行检测。

compile( )的第二种形式接受标志有一个位掩码,这影响了正则表达式的默认编译。这些标 志启用了可选的编译模式行为,例如如何处理边界或不区分大小写等。(除 CANOB_EQ 外) 这些标志(flag)同样可由嵌入表达式内的子表达式启用。标志可以与布尔或(OR)表达式 结合使用,如下所示:

Pattern pattern = Pattern.compile("[A-Z][a-zA-Z]*", Pattern.CASE_INSENSITIVE | Pattern.UNIX_LINES);

所有标志默认为关闭状态。表 5-1 总结了各个编译时间选项的意义。

5.JAVA NIO正则表达式_第3张图片

Pattern 类的实例是不变的,各个实例与对应的正则表达式绑定,无法修改。Pattern 对象也 是线程安全的,可被多个线程同时使用。

因此,一旦你拿到一个 Pattern,你能对它做什么呢?

package java.util.regex;

import java.util.regex.Matcher;

public final class Pattern implements java.io.Serializable {

    // 这是部分的API列表
    public String pattern()

    public int flags()

    public String[] split(CharSequence input, int limit)

    public String[] split(CharSequence input)

    public Matcher matcher(CharSequence input)
}

下面是两个方法,作用是返回 Pattern 类 API 关于封装表达式的信息。pattern( )返回的 String 被用于初建 Pattern 实例(建立对象时字符串被传递给 compile( ))。另一个是 flags( ), 返回的是在编译模式时提供的标志位掩码。如果 Pattern 对象由无参数的 compile( )创建,则 flags( )返回的值是 0。返回的值反映的只是提供给 compile( )的明确标志值;它不包括由正则表 达式模式中的嵌入表达式设置的任何等效标志,等效标志如表 5-1 第二列所示。

实例方法 split( )是公用程序,它使用模式作为定界符(delimiter)来标记字符序列。这会 令人联想到 StringTokenizer,但是它的功能更强,因为定界符可以是匹配正则表达式的多字符 序列。另外,split( )方法是不监控状态的,它返回的是字符串标志的阵列而不是要求多个调用 程序并循环执行它们:

Pattern spacePat = Pattern.compile("\\s+");
String[] tokens = spacePat.split(input);

调用仅有一个参数的 split( )与调用两个参数但第二参数为零的该方法是等效的。split( )第 二个参数指示了输入序列被正则表达式拆分的限制次数。限制参数的意义在于防止超负荷。非 正的值有特殊的意义。

如果传递给 split( )的限值为负(任意负数),则字符序列将无限制地拆分直至输入穷尽。 返回的阵列可能是任何长度。如果给定的限值为零,则输入将被无限拆分,但是结尾的空字符 串将不在结果阵列内。如果限值为正,它设置的是返回的 String 阵列的最大值。假设限制为 n,那么正则表达式最大被用到 n-1 次。表 5-2 对这些组合进行了总结,生成该表格的代码见例 5-2。

5.JAVA NIO正则表达式_第4张图片

最后,matcher( )是为已编译的模式创建 Matcher 对象的工厂方法。匹配器(matcher)是 监控状态的匹配引擎(stateful matching engine),它知道如何对目标字符序列的模式(Pattern 对象从何而来)进行匹配。在创建 Matcher 时你必须提供原始的输入目标,但是随后可提供不 同的输入(5.2.3 节将对此进行讨论)。

利用模式类拆分字符串

根据若干不同的正则表达式模式和不同的限值对相同的输入字符串进行拆分,其结果如例 5-2 生成的矩阵所示。

例 5-2. 根据模式拆分字符串

package org.example;

import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Demonstrate behavior of splitting strings. The XML output created
 * here can be styled into HTML or some other useful form.
 * See poodle.xsl.
 *
 * @author Ron Hitchens ([email protected])
 */
public class Poodle {

    /**
     * Generate a matrix table of how Pattern.split( ) behaves with * various regex patterns and limit values.
     */
    public static void main(String[] argv) throws Exception {
        String input = "poodle zoo";
        Pattern space = Pattern.compile(" ");
        Pattern d = Pattern.compile("d");
        Pattern o = Pattern.compile("o");
        Pattern[] patterns = {space, d, o};
        int limits[] = {1, 2, 5, -2, 0};
        // 如果有参数就使用提供的参数。假设参数合理。
        // 用法:输入模式[pattern ...]
        // 别忘了引用参数。
        if (argv.length != 0) {
            input = argv[0];
            patterns = collectPatterns(argv);
        }
        generateTable(input, patterns, limits);
    }

    /**
     * Output a simple XML document with the results of applying
     * the list of regex patterns to the input with each of the
     * limit values provided. I should probably use the JAX APIs
     * to do this, but I want to keep the code simple.
     */
    private static void generateTable(String input,
                                      Pattern[] patterns, int[] limits) {
        System.out.println("");
        System.out.println("");
        System.out.println("\t");
        System.out.println("\t\tInput: " + input + "");
        for (int i = 0; i < patterns.length; i++) {
            Pattern pattern = patterns[i];
            System.out.println("\t\tRegex: " + pattern.pattern() + "");
        }
        System.out.println("\t");
        for (int i = 0; i < limits.length; i++) {
            int limit = limits[i];
            System.out.println("\t");
            System.out.println("\t\tLimit: "
                    + limit + "");
            for (int j = 0; j < patterns.length; j++) {
                Pattern pattern = patterns[j];
                String[] tokens = pattern.split(input, limit);
                System.out.print("\t\t");
                for (int k = 0; k < tokens.length; k++) {
                    System.out.print("" + tokens[k] + "");
                }
                System.out.println("");
            }
            System.out.println("\t");
        }
        System.out.println("
"); } /** * If command line args were given, compile all args after the * first as a Pattern. Return an array of Pattern objects. */ private static Pattern[] collectPatterns(String[] argv) { List list = new LinkedList(); for (int i = 1; i < argv.length; i++) { list.add(Pattern.compile(argv[i])); } Pattern[] patterns = new Pattern[list.size()]; list.toArray(patterns); return (patterns); } }

例 5-2 输出的是描述结果矩阵的 XML 文档,例 5-3 中的样式表将 XML 转换成 HTML 格 式便于显示在网页浏览器上。

例 5-3. 拆分矩阵样式表



    
    

    
        
            
                Poodle Zoo
            
            
                
            
        
    

    
        
        
, " "

3)Matcher类

Matcher 类为匹配字符序列的正则表达式模式提供了丰富的 API。Matcher 实例常常通 过对 Pattern 对象调用 matcher( )方法来创建的,它常常采用由该 Pattern 封装的正则表达 式:

package java.util.regex;

import java.util.regex.Pattern;

public final class Matcher {

    public Pattern pattern()

    public Matcher reset()

    public Matcher reset(CharSequence input)

    public boolean matches()

    public boolean lookingAt()

    public boolean find()

    public boolean find(int start)

    public int start()

    public int start(int group)

    public int end()

    public int end(int group)

    public int groupCount()

    public String group()

    public String group(int group)

    public String replaceFirst(String replacement)

    public String replaceAll(String replacement)

    public StringBuffer appendTail(StringBuffer sb)

    public Matcher appendReplacement(StringBuffer sb, String replacement)
}

Matcher 类的实例是监控状态型对象,它们封装了与特定输入字符序列匹配的具体正则 表达式。 Matcher 对象并不是线程安全的,因为它们在方法调用之间有保有内状态(hold internal state)。

一个 Matcher 实例来自一个 Pattern 实例,Matcher 对象的 pattern( )返回的是向后引 用(back reference),指向创建了 Matcher 的 Pattern 对象。

Matcher 对象可以重复使用,但是因其监控状态属性,为了开始新匹配操作它们必须处 于已知状态。这可通过调用 reset( )方法来实现,该方法在与匹配程序有关的 CharSequence 之 前为模式匹配备好了对象。无参数的 reset( )将使用上次为 Matcher 设置的 CharSequence。如 果你希望对新的字符序列进行匹配,那么你可以将一个新的 CharSequence 传递给 reset( ),随 后匹配将针对目标进行。例如,随着你读取各行的文件,你可以把它传递给 reset( )。参见例 5-4。

例 5-4. 简单的文件 grep

package org.example;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Simple implementation of the ubiquitous grep command.
 * First argument is the regular expression to search for (remember to
 * quote and/or escape as appropriate). All following arguments are
 * filenames to read and search for the regular expression.
 *
 * @author Ron Hitchens ([email protected])
 */
public class SimpleGrep {

    public static void main(String[] argv) throws Exception {
        if (argv.length < 2) {
            System.out.println("Usage: regex file [ ... ]");
            return;
        }
        Pattern pattern = Pattern.compile(argv[0]);
        Matcher matcher = pattern.matcher("");
        for (int i = 1; i < argv.length; i++) {
            String file = argv[i];
            BufferedReader br = null;
            String line;
            try {
                br = new BufferedReader(new FileReader(file));
            } catch (IOException e) {
                System.err.println("Cannot read '" + file + "': " + e.getMessage());
                continue;
            }
            while ((line = br.readLine()) != null) {
                matcher.reset(line);
                if (matcher.find()) {
                    System.out.println(file + ": " + line);
                }
            }
            br.close();
        }
    }
}

例 5-5 演示的是 reset( )较复杂的应用,它允许 Matcher 作用于若干不同的字符序列。

例 5-5. 提取匹配的表达式

package org.example;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Validates email addresses.
 * Regular expression found in the Regular Expression Library
 * at regxlib.com. Quoting from the site,
 * "Email validator that adheres directly to the specification
 * for email address naming. It allows for everything from
 * ipaddress and country-code domains, to very rare characters
 * in the username."
 *
 * @author Michael Daudel ([email protected]) (original)
 * @author Ron Hitchens ([email protected]) (hacked)
 */
public class EmailAddressFinder {

    public static void main(String[] argv) {
        if (argv.length < 1) {
            System.out.println("usage: emailaddress ...");
        }
        // 编译电子邮件地址检测器的模式
        Pattern pattern = Pattern.compile(
                "([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]" +
                        "{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))" +
                        "([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)", Pattern.MULTILINE);
        // 为模式制作一个Matcher对象
        Matcher matcher = pattern.matcher("");
        // 循环遍历参数并在每个寻找地址
        for (int i = 0; i < argv.length; i++) {
            boolean matched = false;
            System.out.println("");
            System.out.println("Looking at " + argv[i] + " ...");
            // 将匹配器复位,查看当前实参字符串
            matcher.reset(argv[i]);
            // 遇到匹配时循环
            while (matcher.find()) {
                // 找到一个匹配
                System.out.println("\t" + matcher.group());
                matched = true;
            }
            if (!matched) {
                System.out.println("\tNo email addresses found");
            }
        }
    }
}

以下 EmailAddressFinder 运行一些标准地址上的输出:

Looking at Ron Hitchens ,[email protected]., 
[email protected], 
[email protected],
Wilma  ...
[email protected] 
[email protected] 
[email protected] 
[email protected]

下一组方法返回的是正则表达式如何适用于目标字符串的布尔标志。首先是 matches( ), 如果整个(entire)字符序列匹配正则表达式的模式,则它返回 true。反之如果模式匹配的只是 子序列,方法将返回 false。在文件中,这种方法用于选取恰好满足一定模式的行是非常有用 的。这种行为(behavior)与作用于 Pattern 类的公用程序 matches( )相同。

lookingAt( )方法与 matches( )相似,但是它不要求整个序列的模式匹配。如果正则表达式 模式匹配字符序列的 beginning(开头),则 lookingAt( )返回 true。lookingAt( )方法往往从序列 的头部开始扫描。该方法的名字暗示了匹配程序正在“查看”目标是否以模式开头。如果返回 为 true,那么可以调用 start( )、end( )和 group( )方法匹配的子序列的范围(随后将给出更多关 于这些程序的内容)。

find( )方法运行的是与 lookingAt( )相同类型的匹配操作,但是它会记住前一个匹配的位置 并在之后重新开始扫描。从而允许了相继调用 find( )对输入进行逐句比对,寻找嵌入的匹配。 复位后第一次调用该方法,则扫描将从输入序列的首个字符开始。在随后调用中,它将从前一 个匹配的子序列后面的第一个字符重新开始扫描。如各个调用来说, 如果找到了模式将返回 true;反之将返回 false。通常你会使用 find( )循环访问一些文本来查找其中所有匹配的模式。

带位置参数的 find( )会在给定的索引位置进行隐式复位并从该位置开始扫描。然后如果需 要可以调用无参数的 find( )扫描输入序列剩余的部分。

一旦检查到匹配,你可以通过调用 start( )和 end( )确定匹配位于字符序列的什么位置。 Start( )方法返回的是匹配序列首个字符的索引;end( )方法返回的值等于匹配序列最末字符的 索引加一。这些返回值与 CharSequence.subsequence( )的返回值一致,可直接用于提取匹配的 子序列。

        CharSequence subseq;
        if (matcher.find()) {
            subseq = input.subSequence(matcher.start(), matcher.end());
        }

一些正则表达式可以匹配空字符串,这种情况下 start( )和 end( )将返回相同的值。只有当 匹配之前已经过 matches( )、lookingAt( )或检测 find( )的检测,start( )和 end( )返回的值才有意 义。如果没有检测到匹配或最后的匹配尝试返回的是 false,那么调用 start( )或 end( )将导致java.lang.IllegalStateException(Java 语言非法状态异常)。

为了了解带有 group 参数的 start( )和 end( ),我们首先需要知道表达式捕获组(expression capture group)。(见图 5-2)

5.JAVA NIO正则表达式_第5张图片

正则表达式可能包含称为捕获组(capture group)的子表达式,它们被小括号括了起来。 在正则表达式的求值期间将保存匹配这些捕获组表达式的输入子序列。一旦完全匹配操作完 成,这些保存的代码片断可通过确定相应的组号从 Matcher 对象上重新获取。

捕获组可以嵌套使用,数量可以通过从左到右计算左括弧(开括号)得到。无论整个表达 式是否有子组,它的捕获组总能记为组零(group zero)。例如,正则表达式 A((B)(C(D)))可能 有的捕获组编号如表 5-3 所示。

5.JAVA NIO正则表达式_第6张图片

这种分组句法存在异常事件。以(? 开头的组是个纯的(pure)或说是无法捕获的组。它的 值无法保存且它对无法计算捕获组编号。(句法细节参见表 5-7。)

让我们看看方法作用于捕获组的更多细节:

package java.util.regex;

public final class Matcher {

    // 这是API列表的一部分
    public int start()

    public int start(int group)

    public int end()

    public int end(int group)

    public int groupCount()

    public String group()

    public String group(int group)
}

捕获组在正则表达式模式中的编号由 groupCount( )方法返回。该值来自原始的 Pattern 对 象,是不可变的。组号必须为正且小于 groupCount( )返回的值。传递超出范围的组号将导致 java.lang.IndexOutOfBoundsException(java 语言索引出界异常)。

可以将捕获组号传递给 start( )和 end( )来确定子序列是否匹配已知的捕获组子表达式。有 可能出现这样一种情况,即整个表达式成功匹配但是有一个或多个的捕获组无法匹配。如果请 求的捕获组当前没有设置则 start( )和 end( )方法的返回值将为-1。

(正如之前看到的)你可以利用 start( )和 end( )返回的值从输入的 CharSequence 中提取出 匹配的子序列,但是 group( )方法为此提供了更简单的方式。调用带数字参数的 group( )将返回 一个字段,该字段是匹配特殊捕获组的子序列。如果你调用的 group( )不含参数,则返回将是 与整个正则表达式(组零)匹配的子序列。代码如下:

        String match0 = input.subSequence(matcher.start(), matcher.end()).toString();
        String match2 = input.subSequence(matcher.start(2), matcher.end(2)).toString();

上述代码与下列代码等效:

        String match0 = matcher.group();
        String match2 = matcher.group(2);

最后让我们看看 Matcher 对象解决修改字符序列的方法。正则表达式最常见的应用之一是 查找并替换(search-and-replace)。这种应用使用 replaceFirst( )和 replaceAll( )可以轻轻松松就 搞定。它们的行为方式是相同的,区别在于 replaceFirst( )在找到第一个匹配后就会停止,而 replaceAll( )将循环执行直到替换完所有的匹配。二者都带有 String 参数,String 参数是用于替 换输入字符序列中匹配模式的替换值(replacement value)。

package org.example;

public final class Matcher {

    // 这是API列表的一部分
    public String replaceFirst(String replacement)

    public String replaceAll(String replacement)
}

上文提过,捕获组在正则表达式内可以向后引用(back-reference)。它们也可以被你提供 组 replaceFirst( )或 replaceAll( )的替换字符串引用。捕获组号通过添加美元符号可嵌入替换字符串中。当替换字符串被替换成结果字符串时,每次出现的​g 将被 group( )返回的值代替。如果你想在替换字符串使用字面量(literal)美元符号, 那么你必须在它前面加个反斜杠符号 ($)。如果想要传递反斜杠符号,你必须多加一个反斜杠(\)。如果你想在捕获组引用后 面跟上字面量的数值型数字,那么你可以用反斜杠将它们与组号分开,像这样:123$2\456。 表 5-4 对此给出了一些例子。示例代码见例 5-6。

5.JAVA NIO正则表达式_第7张图片

例 5-6. 正则表达式替换

package java.util.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Exercise the replacement capabilities of the java.util.regex.Matcher
 * class. Run this code from the command line with three or more arguments.
 * 1) First argument is a regular expression
 * 2) Second argument is a replacement string, optionally with capture
 * group references ($1, $2, etc)
 * 3) Any remaining arguments are treated as input strings to which the
 * regular expression and replacement strings will be applied.
 * The effect of calling replaceFirst( ) and replaceAll( ) for each input
 * string will be listed.
 * 

* Be careful to quote the commandline arguments if they contain spaces * or special characters. *

* * @author Ron Hitchens ([email protected]) */ public class RegexReplace { public static void main(String[] argv) { // 完整性检查,至少需要三个参数 if (argv.length < 3) { System.out.println("usage: regex replacement input ..."); return; } // 用助词符号名保存正则及替换字符串 String regex = argv[0]; String replace = argv[1]; // 编译表达式;一次只能编译一个 Pattern pattern = Pattern.compile(regex); // 得到Matcher实例,暂时先使用虚设的输入字符串 Matcher matcher = pattern.matcher(""); // 打印输出用于参考 System.out.println(" regex: '" + regex + "'"); System.out.println(" replacement: '" + replace + "'"); // 对各个剩余的参数字符串应用正则/替换 for (int i = 2; i < argv.length; i++) { System.out.println("------------------------"); matcher.reset(argv[i]); System.out.println(" input: '" + argv[i] + "'"); System.out.println("replaceFirst( ): '" + matcher.replaceFirst(replace) + "'"); System.out.println(" replaceAll( ): '" + matcher.replaceAll(replace) + "'"); } } }

下列是运行 RegexReplace 后的输出结果:

regex: '([bB])yte' 
replacement: '$1ite' 
-----------------------
input: 'Bytes is bytes' 
replaceFirst( ): 'Bites is bytes' 
replaceAll( ): 'Bites is bites'

记住:正则表达式会在你提供的字符串中翻译反斜杠。另外在字面量的 String 中,Java 编译器要求各个反斜杠需要有两个反斜杠, 即如果你想在正则中转义(escape)一个反斜 杠),那么你在编译过的 String 需要中需要两反斜杠。如果在编译的正则字符串中需要两个 连续的反斜杠,那么在 Java 源代码中需要四个连接的反斜杠。

为了生成 a\b 的替换序列,replaceAll( )的 String 字面量参数必须是 a\\b(见例 57)。统计这些反斜杠时千万要小心啊!

例 5-7. 正则表达式中的反斜杠

package org.example;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Demonstrate behavior of backslashes in regex patterns.
 *
 * @author Ron Hitchens ([email protected])
 */
public class BackSlashes {

    public static void main(String[] argv) {
        // 在输入中把“a\b”替换成XYZ或ABC
        String rep = "a\\\\b";
        String input = "> XYZ <=> ABC <";
        Pattern pattern = Pattern.compile("ABC|XYZ");
        Matcher matcher = pattern.matcher(input);
        System.out.println(matcher.replaceFirst(rep));
        System.out.println(matcher.replaceAll(rep));
        // 在输入中更改所有的新行来转义, DOS-like CR/LF 
        rep = "\\\\r\\\\n";
        input = "line 1\nline 2\nline 3\n";
        pattern = Pattern.compile("\\n");
        matcher = pattern.matcher(input);
        System.out.println("");
        System.out.println("Before:");
        System.out.println(input);
        System.out.println("After (dos-ified, escaped):");
        System.out.println(matcher.replaceAll(rep));
    }
}

下列是运行BackSlashes的输出结果:

> a\b <=> ABC <
> a\b <=> a\b <

Before: 
line 1 
line 2 
line 3

After (dos-ified, escaped): 
line 1\r\nline 2\r\nline 3\r\n

Matcher API 列出了两个追加方法,它们在循环访问输入字符序列时很有用,它们重复 调用 find( )。

package java.util.regex;

public final class Matcher { // 这是部分的API列表

    public StringBuffer appendTail(StringBuffer sb)

    public Matcher appendReplacement(StringBuffer sb, String replacement)
}

追加方法不是返回已经运行过替换的新 String 而是添加到你提供的 StringBuffer 对 象上。这就允许你决定在找到匹配的各点是否进行替换或计算与多个输入字符串的匹配结果。 通过 appendReplacement( )和 appendTail( ),你可以全面控制查找和替换过程。

Matcher 对象记住的状态信息位(the bits of state information)之一是追加位置(append position)。追加位置是用于记住输入字符序列的量,这些字符序列已经通 过之前调用 appendReplacement( )复制了出来。当调用 appendReplacement( )时,将发生如下过程:

  1. 从 输 入 中 读 取 字 符 是 从 当 前 追 加 位 置 开 始 , 读 取 的 字 符 将 被 添 加 到 已 知 的 StringBuffer 中。最后复制的字符就在匹配模式的首个字符之前。这个字符位于 start( )返回的索引减一的位置。

  2. 如先前描述的, 替换字符串被添加给 StringBuffer 并替换任何嵌入的捕获组引 用。

  3. 追加位置更新成跟在匹配模式后面的字符的索引,这个索引是 end( )返回的值。

 

仅当前一个匹配操作成功(通常调用 find( ))appendReplacement( )方法才能正常工作。如 果前一个匹配返回的是 false 或在复位后立即调用该方法,你将得到一个“令人愉快的奖 励”:java.lang.IllegalStateException(java 语言非法状态异常)。

但是别忘了:在输入中除了最后的模式匹配外还有剩余的字符。你很可能不想失去它们, 但是 appendReplacement( )不会复制它们,并且在 find( )无法找到更多的匹配后 end( )将不会返 回有用的值。这种情况下 appendTail( )方法正好可以复制输入中余下的部分。它仅是复制了从 当前追加位置到输入结果的所有字符,并把它们追加到给定的 StringBuffer 中。下列代码 是 appendReplacement( )和 appendTail( )典型的使用情况。

        Pattern pattern = Pattern.compile("([Tt])hanks");
        Matcher matcher = pattern.matcher("Thanks, thanks very much");
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            if (matcher.group(1).equals("T")) {
                matcher.appendReplacement(sb, "Thank you");
            } else {
                matcher.appendReplacement(sb, "thank you");
            }
        }
        matcher.appendTail(sb);

5.JAVA NIO正则表达式_第8张图片

经过追加操作的该序列生成的 StringBuffer 对象 sb 包含字符串“Thank you, thank you very much”。例 5-8 是个完整的代码示例,它显示了这种替换类型及运行相同替换的代替方式。在这个简单例子里,可以使用捕获组的值,因为匹配模式的首个字母与替换的首个字母相同。在 更复杂的例子中, 输入与替换值间可能不存在重叠部分(overlap)。 利用 Matcher.find( )和 Matcher.appendReplacement( ),你可以通过编程方式调解每个替换,从而可能随时在各个点引 入不同的替换值。

例 5-8. 正则表达式追加/替换

package org.example;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Test the appendReplacement() and appendTail() methods of the
 * java.util.regex.Matcher class.
 *
 * @author Ron Hitchens ([email protected])
 */
public class RegexAppend {

    public static void main(String[] argv) {
        String input = "Thanks, thanks very much";
        String regex = "([Tt])hanks";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(input);
        StringBuffer sb = new StringBuffer();
        //循环直到遇到匹配
        while (matcher.find()) {
            if (matcher.group(1).equals("T")) {
                matcher.appendReplacement(sb, "Thank you");
            } else {
                matcher.appendReplacement(sb, "thank you");
            }
        }
        // 完成到StringBuffer的传送
        matcher.appendTail(sb);
        // 打印结果
        System.out.println(sb.toString());
        // 让我们再试试在替换中使用$n转义
        sb.setLength(0);
        matcher.reset();
        String replacement = "$1hank you";
        //循环直到遇到匹配
        while (matcher.find()) {
            matcher.appendReplacement(sb, replacement);
        }
        // 完成到StringBuffer传送
        matcher.appendTail(sb);
        // 打印结果
        System.out.println(sb.toString());
        // 再来一次,简单的方法(因为这个父子很简单)
        System.out.println(matcher.replaceAll(replacement));
        // 最后一次,只使用字符串 
        System.out.println(input.replaceAll(regex, replacement));
    }
}

3.String类的正则表达式方法

从前几个章节来看,字符串和正则表达式关联密切是显而易见的。 这是很自然,我们的 “老朋友”Sting 类已经添加了一些公用程序来执行常见的正则表达式操作:

package java.lang;

public final class String implements java.io.Serializable, Comparable, CharSequence {

    // 这是部分的API列表
    public boolean matches(String regex)

    public String[] split(String regex)

    public String[] split(String regex, int limit)

    public String replaceFirst(String regex, String replacement)

    public String replaceAll(String regex, String replacement)
}

所有新的 String 方法是 Pattern 或 Matcher 类的传递调用(pass-through call)。现在你知道 了 Pattern 和 Matcher 是如何使用的,并且利用这些 String 公用程序的交互操作(interoperate) 应当是傻瓜式的。表 5-6 对这些方法作了总结,不再对方法一一赘述。

5.JAVA NIO正则表达式_第9张图片

表 5-6 中,假定 String 对象名为 input,Pattern 对象名为 pat,而 Matcher 对象名为 match:

        String input = "Mary had a little lamb";
        String[] tokens = input.split("\\s+");// 在空白符上拆分

 

在 JDK 1.4 之前,没有一个正则表达式公用程序缓存任何表达式或进行其它优化。一些 JVM 实现可能选择缓存或重用模式对象,但是它们却不足以采信。如果你希望重复应用相同 的模式匹配操作,那么使用 java.util.regex 中的类更为高效。

4.Java正则表达式句法

下面是 java.util.regex 包支持的正则表达式句法的总结。Java 世界“瞬息万变”,因此你 需要时常检查一下你正在使用的 Java 实现提供的当前文档。这里提供的信息为你提供了迈出 第一步的快速参考。

java.util.regex 类可全面感知 Unicode(fully Unicode-aware),它们完全遵循《Unicode 技 术报告#18:Unicode 正则表达式指南》中的准则。文章参见:

http://www.unicode.org/unicode/reports/tr18。

之前提过,java.util.regex 的句法与 Perl 相似,却又不尽相同。java.util.regex 缺少的是能够 在表达式中嵌入 Perl 代码这一主要功能(这要求插入完整的 Perl 解译器)。Java 句法中添加 的 重 要 功 能 是 占 有 型 量 词 ( possessive quantifier ) , 它 们 比 常 规 的 贪 婪 量 词 ( greedy quantifier)还要“贪婪”。占有型量词会尽可能地多匹配目标,即便这意味着表达式剩余的部 分 无 法 匹 配 。 Java 正 则 表 达 式 也 支 持 一 些 Perl 不 支 持 的 Unicode 转 义 序 列 。 java.util.regex.Pattern 完整详情请参阅 Javadoc 页面。

表 5-7 是正则表达式一览表。它复制的是《Java In A Nutshell》第四版(O’Reilly)。

5.JAVA NIO正则表达式_第10张图片

5.JAVA NIO正则表达式_第11张图片

5.JAVA NIO正则表达式_第12张图片

5.JAVA NIO正则表达式_第13张图片

5.面向对象的文件Grep

例 5-9 实现了一个面向熟悉的 grep 命令的对象。Grep 类的实例包含正则表达式,可用于 浏览相同模式的不同文件。Grep.grep( )程序的结果是类型安全的 Grep.MatchedLine 对象阵列。 MatchedLine 类包含在 Grep 中的类。你必须将它作为 Grep.MatchedLine 引用或将它单独导入。

package org.example;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A file searching class, similar to grep, which returns information
 * about lines matched in the specified files. Instances of this class
 * are tied to a specific regular expression pattern and may be applied
 * repeatedly to multiple files. Instances of Grep are thread safe, they
 * may be shared.
 *
 * @author Michael Daudel ([email protected]) (original)
 * @author Ron Hitchens ([email protected]) (hacked)
 */
public class Grep {

    // 用于该实例的模式
    private Pattern pattern;

    /**
     * Instantiate a Grep object for the given pre-compiled Pattern object.
     *
     * @param pattern A java.util.regex.Pattern object specifying the
     *                pattern to search for.
     */
    public Grep(Pattern pattern) {
        this.pattern = pattern;
    }

    /**
     * Instantiate a Grep object and compile the given regular expression
     * string.
     *
     * @param regex      The regular expression string to compile into a
     *                   Pattern for internal use.
     * @param ignoreCase If true, pass Pattern.CASE_INSENSITIVE to the
     *                   Pattern constuctor so that seaches will be done without regard
     *                   to alphabetic case. Note, this only applies to the ASCII
     *                   character set. Use embedded expressions to set other options.
     */
    public Grep(String regex, boolean ignoreCase) {
        this.pattern = Pattern.compile(regex, (ignoreCase) ? Pattern.CASE_INSENSITIVE : 0);
    }

    /**
     * Instantiate a Grep object with the given regular expression string,
     * with default options.
     */
    public Grep(String regex) {
        this(regex, false);
    }
    // ---------------------------------------------------------------

    /**
     * Perform a grep on the given file.
     *
     * @param file A File object denoting the file to scan for the
     *             regex given when this Grep instance was constructed.
     * @return A type-safe array of Grep.MatchedLine objects describing
     * the lines of the file matched by the pattern.
     * @throws IOException If there is a problem reading the file.
     */
    public MatchedLine[] grep(File file)
            throws IOException {
        List list = grepList(file);
        MatchedLine matches[] = new MatchedLine[list.size()];
        list.toArray(matches);
        return (matches);
    }

    /**
     * Perform a grep on the given file.
     *
     * @param fileName A String filename denoting the file to scan for the
     *                 regex given when this Grep instance was constructed.
     * @return A type-safe array of Grep.MatchedLine objects describing
     * the lines of the file matched by the pattern.
     * @throws IOException If there is a problem reading the file.
     */
    public MatchedLine[] grep(String fileName)
            throws IOException {
        return (grep(new File(fileName)));
    }

    /**
     * Perform a grep on the given list of files. If a given file cannot
     * be read, it will be ignored as if empty.
     *
     * @param files An array of File objects to scan.
     * @return A type-safe array of Grep.MatchedLine objects describing
     * the lines of the file matched by the pattern.
     */
    public MatchedLine[] grep(File[] files) {
        List aggregate = new LinkedList();
        for (int i = 0; i < files.length; i++) {
            try {
                List temp = grepList(files[i]);
                aggregate.addAll(temp);
            } catch (IOException e) {
                // 忽略I/O异常
            }
        }
        MatchedLine matches[] = new MatchedLine[aggregate.size()];
        aggregate.toArray(matches);
        return (matches);
    }
// -------------------------------------------------------------

    /**
     * Encapsulation of a matched line from a file. This immutable
     * object has five read-only properties:
     * 
    *
  • getFile( ): The File this match pertains to.
  • *
  • getLineNumber( ): The line number (1-relative) within the file where the match was found.
  • *
  • getLineText( ): The text of the matching line
  • *
  • start( ): The index within the line where the matching pattern begins.
  • *
  • end( ): The index, plus one, of the end of the matched character sequence.
  • *
*/ public static class MatchedLine { private File file; private int lineNumber; private String lineText; private int start; private int end; MatchedLine(File file, int lineNumber, String lineText, int start, int end) { this.file = file; this.lineNumber = lineNumber; this.lineText = lineText; this.start = start; this.end = end; } public File getFile() { return (this.file); } public int getLineNumber() { return (this.lineNumber); } public String getLineText() { return (this.lineText); } public int start() { return (this.start); } public int end() { return (this.end); } } // ----------------------------------------------------------- /** * Run the grepper on the given File. * * @return A (non-type-safe) List of MatchedLine objects. */ private List grepList(File file) throws IOException { if (!file.exists()) { throw new IOException("Does not exist: " + file); } if (!file.isFile()) { throw new IOException("Not a regular file: " + file); } if (!file.canRead()) { throw new IOException("Unreadable file: " + file); } LinkedList list = new LinkedList(); FileReader fr = new FileReader(file); LineNumberReader lnr = new LineNumberReader(fr); Matcher matcher = this.pattern.matcher(""); String line; while ((line = lnr.readLine()) != null) { matcher.reset(line); if (matcher.find()) { list.add(new MatchedLine(file, lnr.getLineNumber(), line, matcher.start(), matcher.end())); } } lnr.close(); return (list); } // --------------------------------------------------------------- /** * Test code to run grep operations. Accepts two command-line * options: -i or --ignore-case, compile the given pattern so * that case of alpha characters is ignored. Or -1, which runs * the grep operation on each individual file, rather that passing * them all to one invocation. This is just to test the different * methods. The printed ouptut is slightly different when -1 is * specified. */ public static void main(String[] argv) { // 设置默认值 boolean ignoreCase = false; boolean onebyone = false; List argList = new LinkedList(); // 采集变量 // 循环遍历变量,查找转换并保存模式及文件名 for (int i = 0; i < argv.length; i++) { if (argv[i].startsWith("-")) { if (argv[i].equals("-i") || argv[i].equals("--ignore-case")) { ignoreCase = true; } if (argv[i].equals("-1")) { onebyone = true; } continue; } // 不是转移(switch),将其添加到列表中 argList.add(argv[i]); } // 是否有足够的变量可以运行? if (argList.size() < 2) { System.err.println("usage: [options] pattern filename ..."); return; } // 列表中第一个变量将被作为正则模式。 // 将模式及忽略大小写标志的当前值传递给新的Grep对象。 Grep grepper = new Grep((String) argList.remove(0), ignoreCase); // 随意点,拆分成调用grep程序和打印结果两种方式 if (onebyone) { Iterator it = argList.iterator(); // 循环遍历文件名并用grep处理它们 while (it.hasNext()) { String fileName = (String) it.next(); // 在每次grep前先打印文件名 System.out.println(fileName + ":"); MatchedLine[] matches = null; // 捕获异常 try { matches = grepper.grep(fileName); } catch ( IOException e) { System.err.println("\t*** " + e); continue; } // 打印匹配行的资料 for (int i = 0; i < matches.length; i++) { MatchedLine match = matches[i]; System.out.println(" " + match.getLineNumber() + " [" + match.start() + "-" + (match.end() - 1) + "]: " + match.getLineText()); } } } else { // 把文件名列表转换到File阵列中 File[] files = new File[argList.size()]; for (int i = 0; i < files.length; i++) { files[i] = new File((String) argList.get(i)); } // 运行grep程序;忽略无法读取的文件 MatchedLine[] matches = grepper.grep(files); // 打印匹配行的资料 for (int i = 0; i < matches.length; i++) { MatchedLine match = matches[i]; System.out.println(match.getFile().getName() + ", " + match.getLineNumber() + ": " + match.getLineText()); } } } }

 

总结

本章中我们讨论了期待已久的在 1.4 版本中添加到 J2SE 平台上的正则表达式 类:

CharSequence

在 5.2.1 节中我们了解了新的 CharSequence 接口,知道了它用若干个类实现了 抽象地描述字符序列。

Pattern

Pattern 类把正则表达式封装在不变的对象实例中。在 5.2.2 节我们看到了 Pattern 的 API 并学会了如何通过编译表达式字符串来创建实例。我们还知道了用于进行 单次匹配的一些静态实用程序。

Matcher

Matcher 类是状态机对象,它在输入字符序列上使用了 Pattern 对象来寻找输入中 匹配的模式。5.2.3 节描述了 Matcher API,包括如何在 Pattern 对象上创建新的 Matcher 实例及如何运行各种类型的匹配操作。

String

1.4 版本中添加的 String 类有了一些新的正则表达式公用程序。5.3 节对此作了总 结。

 

java.util.regex.Pattern 支持的正则表达式句法已在表 5-7 中列出。其句法与 Perl 5 很接近。

现在我们将这次游历划上圆满的句号。 在下一章中, 你将领略字符集这一充满 “异国情调”并且有时神秘的世界。

摘自JAVA NIO(中文版)

 

你可能感兴趣的:(JAVA,NIO,java,正则表达式,字符串)