正则表达式
正则表达式又称正规表达法,常规表达法(Regular Expression)。正则表达式使用单个字符串来描述,匹配一系列符合某个句法规则的字符串,通常被用来检索,替换那些符合某个模式的文本,正则表达式通常有元字符和限定修饰符组成。
使用正则表达式最便捷的方式是使用String类内置的功能(matches()),见下例:
System.out.println(“-1234”.matches(“-?\d+”));
正则表达式还能结合String的其他方法,比如:split(),例如:split(“\W+”); 还有replaceFirst() 例如:str.replaceFirst(“f\w+”,”located”)
元字符:
正则表达式含有一些具有特殊意义的字符,这些特殊字符称为正则表达式的元字符
元字符 含义
\ 通用转义字符,表示下一个字符标记为特殊字符、原义字符、一个向后引用或一个八进制转义符
. 任意一个字符
\s 表示空白字符(space键、Tab、换行、换页、回车)
\S 表示非空字符(相当于[^\s])
\d 表示一个数字字符(相当于[0-9])
\D 表示一个非数字的字符(相当于[^0-9])
\w 表示一个单词字符(word character)
\W 表示一个非单词的字符(相当于[^\w])
限定修饰符:
在正则表达式中用来限定元字符的出现位置及出现次数的修饰符,称为正则表达式的限定修饰符。
元字符都是针对单个字符匹配的,要想同时匹配多个字符,还需要借助限定符。
限定修饰符 含义
* | 匹配0到多个元字符,相当于{0,} |
---|---|
? | 匹配0到1个元字符,相当于{0,1} |
+ | 匹配至少一个元字符,相当于{1,} |
{n} | 匹配n个元字符 |
{n,} | 匹配至少n个元字符 |
{n,m} | 匹配n~m个元字符 |
[ ] | 匹配方括号内的任意一个字符,例如:[abc] 表示a,b,c中的任意一个 相当于a,b,c;[^abc] 表示不是a,b,c中的任意一个;[a-zA-Z] 表示a-z或A-Z中的任一个;[abc[hij]] 表示a,b,c,h,i,j的任意一个 相当于a,b,c,h,i,j;[a-z&&[hij]] 表示h,i,j中的任意一个 |
- | 用在原义字符中,指定字符的范围 |
^ | 若用在方括号之外,表示匹配目标字符串的开头;若用在方括号之内,表示不接受该字符集合(非。。。) |
$ | 表示匹配目标字符串的结尾 |
\b | 匹配一个单词边界,也就是指单词和空格键的位置 |
\B | 匹配非单词边界 |
正则表达中的逻辑运算符
逻辑运算符 含义
XY X后接Y
X | Y X或Y
(X) 捕获组。之后可以在表达式中用\i引用第i个捕获的组。
正则表达式中的字符
字符 含义
B 特殊字符B
\xhh 十六进制值为oxhh的字符
\uhhh 十六进制表示的Unicode字符0xhhhh
\t Tab
\n 换行符
\r 回车(Carriage return)
\f 换页符(Form feed)
\e 转义字符
见下例:
//: strings/Rudolph.java
public class Rudolph {
public static void main(String[] args) {
for(String pattern : new String[]{ "Rudolph", "[rR]udolph", "[rR][aeiou][a-z]ol.*", "R.*" }) System.out.println("Rudolph".matches(pattern));
}
} /* Output:
true
true
true
true
*///:~
又比如
\W+ 表示非单词的字符
n\W+ 表示‘n’后接非单词的字符
(-|\+)? 指的是这部分字符串可能是一个‘-’或者一个‘+‘(\+ +在此表示普通字符)或者什么也没有(因为?)
当然并不是正则表达式一定要写的多复杂,才能显出技术有多高超,相反简洁、实用、易懂才是王道。同时你会发现要写新的正则表达式时通常会拿自己已有的正则表达式作为参照。
量词(Quantifiers)
量词描述的是类型吸收输入文本的方式:
贪婪型(Greedy):
不情愿型(Reluctant):
占有型(Possessive):
正则表达式的实现
Java正则表达式是通过java.util.regex包下的Pattern类与Matcher类实现的,基本实现步骤如下:
import java.util.regex; //导入 java.util.regex包
Pattern pattern = Pattern.compile(expression); //使用Pattern的静态方法compile 导入的 //参数为正则表达式,返回Pattern
Matcher matcher = pattern.matcher(str); //使用Pattern的方法matcher,传入的参数是要 //操作的字符串,返回的是Matcher
boolean b = matcher.matches(); //返回是否匹配的结果,也可以使用lookingAt()或find()
参看下例代码
//: strings/TestRegularExpression.java
// Allows you to easily try out regular expressions.
// {Args: abcabcabcdefabc "abc+" "(abc)+" "(abc){2,}" }
import java.util.regex.*;
import static net.mindview.util.Print.*;
public class TestRegularExpression {
public static void main(String[] args) {
if(args.length < 2) {
print("Usage:\njava TestRegularExpression " + "characterSequence regularExpression+");
System.exit(0);
}
print("Input: \"" + args[0] + "\"");
for(String arg : args) {
print("Regular expression: \"" + arg + "\"");
Pattern p = Pattern.compile(arg);
Matcher m = p.matcher(args[0]);
while(m.find()){
print("Match \"" + m.group() + "\" at positions " +
m.start() + "-" + (m.end() - 1));
}
}
}
} /* Output:
Input: "abcabcabcdefabc"
Regular expression: "abcabcabcdefabc"
Match "abcabcabcdefabc" at positions 0-14
Regular expression: "abc+"
Match "abc" at positions 0-2
Match "abc" at positions 3-5
Match "abc" at positions 6-8
Match "abc" at positions 12-14
Regular expression: "(abc)+"
Match "abcabcabc" at positions 0-8
Match "abc" at positions 12-14
Regular expression: "(abc){2,}"
Match "abcabcabc" at positions 0-8
*///:~
Matcher提供3个匹配的方法:
- matches() 对整个字符串进行匹配,只有整个字符串都匹配才返回true
- lookingAt() 对前面的字符串进行匹配,只有匹配到的字符串在最前面才返回true
- find() 对字符串进行匹配,匹配到的字符串可以在任何位置
组(Groups)
组是由括号隔开的正则表达式,之后可以使用组号调用。组0表示整个表达式,组 l指的是第一个带括号的组,见下例:
A(B©)D
在上例中,有三个组,分别是:ABCD, BC, C
Matcher对象有方法展示组的信息:
public int groupCount() 返回组的个数,组0不包含在该计数中
public String group() 返回上一个匹配操作(例如find())而来的组0(全匹配)(entire match)
public String gourp(int i) 返回上一个匹配操作中指定组号的组。如果匹配成功,但指定的组不匹配输入的字符串,则返回null
public int start(int group) 返回在上一个匹配操作中找到的组的开始索引
public int end(int group) 返回在上一个匹配操作中找到的组的最后一个字符的索引,再加1
见下例:
//: strings/Groups.java
import java.util.regex.*;
import static net.mindview.util.Print.*;
public class Groups {
static public final String POEM =
"Twas brillig, and the slithy toves\n" +
"Did gyre and gimble in the wabe.\n" +
"All mimsy were the borogoves,\n" +
"And the mome raths outgrabe.\n\n" +
"Beware the Jabberwock, my son,\n" +
"The jaws that bite, the claws that catch.\n" +
"Beware the Jubjub bird, and shun\n" +
"The frumious Bandersnatch.";
public static void main(String[] args) {
Matcher m =
Pattern.compile("(?m)(\\S+)\\s+((\\S+)\\s+(\\S+))$").matcher(POEM); while(m.find()) {
for(int j = 0; j <= m.groupCount(); j++)
printnb("[" + m.group(j) + "]");
print();
}
}
} /* Output:
[the slithy toves][the][slithy toves][slithy][toves]
[in the wabe.][in][the wabe.][the][wabe.]
[were the borogoves,][were][the borogoves,][the][borogoves,]
[mome raths outgrabe.][mome][raths outgrabe.][raths][outgrabe.]
[Jabberwock, my son,][Jabberwock,][my son,][my][son,]
[claws that catch.][claws][that catch.][that][catch.]
[bird, and shun][bird,][and shun][and][shun]
[The frumious Bandersnatch.][The][frumious Bandersnatch.][frumious][Bandersnatch.] *///:~
注意此处的正则表达式,该正则表达式有许多被括号括起来的组,由任意数量的非空白字符(‘\S+‘)和任意数量的空白字符(’\s+‘)组成。该表达式的目标是获取每行最后三个单词;行尾用“ ” 分 隔 。 但 是 , 正 常 行 为 是 将 “ ”分隔。但是,正常行为是将“ ”分隔。但是,正常行为是将“”与整个输入序列的结尾匹配,因此必须显式地告诉正则表达式注意输入中的换行符。这是通过序列开头的模式标志’(?m)’来完成的。
start()和end()
在匹配操作失败(或在尝试匹配操作之前)之后调用start()或end()会产生IllegalstateException 见下例:
//: strings/StartEnd.java
import java.util.regex.*;
import static net.mindview.util.Print.*;
public class StartEnd {
public static String input =
"As long as there is injustice, whenever a\n" +
"Targathian baby cries out, wherever a distress\n" +
"signal sounds among the stars ... We’ll be there.\n" +
"This fine ship, and this fine crew ...\n" +
"Never give up! Never surrender!";
private static class Display {
private boolean regexPrinted = false;
private String regex;
Display(String regex) { this.regex = regex; }
void display(String message) {
if(!regexPrinted) {
print(regex);
regexPrinted = true;
}
print(message);
}
}
static void examine(String s, String regex) {
Display d = new Display(regex);
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(s);
while(m.find())
d.display("find() ‘" + m.group() +
"‘ start = "+ m.start() + " end = " + m.end());
if(m.lookingAt()) // No reset() necessary
d.display("lookingAt() start = " + m.start() + " end = " + m.end());
if(m.matches()) // No reset() necessary
d.display("matches() start = " + m.start() + " end = " + m.end());
}
public static void main(String[] args) {
for(String in : input.split("\n")) {
print("input : " + in);
for(String regex : new String[]{"\\w*ere\\w*", "\\w*ever", "T\\w+", "Never.*?!"}) examine(in, regex);
}
}
} /* Output:
input : As long as there is injustice, whenever a
\w*ere\w*
find() ‘there’ start = 11 end = 16
\w*ever
find() ‘whenever’ start = 31 end = 39
input : Targathian baby cries out, wherever a distress
\w*ere\w*
find() ‘wherever’ start = 27 end = 35
\w*ever
find() ‘wherever’ start = 27 end = 35
T\w+
find() ‘Targathian’ start = 0 end = 10
lookingAt() start = 0 end = 10
input : signal sounds among the stars ... We’ll be there.
\w*ere\w*
find() ‘there’ start = 43 end = 48 input : This fine ship, and this fine crew ...
T\w+
find() ‘This’ start = 0 end = 4
lookingAt() start = 0 end = 4
input : Never give up! Never surrender!
\w*ever
find() ‘Never’ start = 0 end = 5
find() ‘Never’ start = 15 end = 20
lookingAt() start = 0 end = 5
Never.*?!
find() ‘Never give up!’ start = 0 end = 14
find() ‘Never surrender!’ start = 15 end = 31
lookingAt() start = 0 end = 14
matches() start = 0 end = 31
*///:~
Pattern flags
Pattern Pattern.compile(String regex,int flag)
其中flag会影响匹配行为,flag是从以下Pattern类常量中提取的:
编译Flag 影响
Pattern.CANON_EQ 只有当两个字符的完整规范分解(canonical decompositions)匹配时,才认为这两个字符匹配。例如,当指定此标志时,表达式’\u003f’将匹配字符串’?’。默认情况下,匹配不考虑规范等价(canonical equivalence)。
Pattern.CASE INSENSITIVE
(?i) 默认情况下,不区分大小写的匹配(case-insensitive matching)假定仅匹配US-ASCII字符集的字符。此标志允许不考虑大小写进行匹配。通过一起指定UNICODE_CASE标志与此标志,可以启用识别Unicode而不区分大小写的匹配。
Pattern.COMMENTS
(?x) 在这种模式下,空白被忽略,以#开头的嵌入注释被忽略,直到行尾。也可以通过嵌入的标志表达式启用Unix行模式。
Pattern.DOTALL
(?s) 在dotall模式下,表达式“.”匹配任何字符,包括行终止符。默认情况下,“.”表达式与行终止符不匹配。行终止符即句号。
Pattern.MULTILINE
(?m)
在多行模式下,表达式“^”和“
”
分
别
匹
配
行
的
开
头
和
结
尾
。
“
”
还
匹
配
输
入
字
符
串
的
开
头
,
“
”分别匹配行的开头和结尾。“^”还匹配输入字符串的开头,“
”分别匹配行的开头和结尾。“”还匹配输入字符串的开头,“”还匹配输入字符串的结尾。默认情况下,这些表达式只在整个输入字符串的开头和结尾匹配。在此模式下,KaTeX parse error: Undefined control sequence: \n at position 11: 的 精确含意是:匹配\̲n̲之前的位置以及字符串结束前的位…的行为中被识别。
现实中用的最多的是Pattern.CASE_INSENSITIVE,Pattern.MULTILINE和Pattern.COMMENTS。
可以通过“OR”(‘|’)操作将这些标志的和其他标志结合。
//: strings/ReFlags.java
import java.util.regex.*;
public class ReFlags {
public static void main(String[] args) {
Pattern p = Pattern.compile("^java",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher(
"java has regex\nJava has regex\n" +
"JAVA has pretty good regular expressions\n" +
"Regular expressions are in Java");
while(m.find())
System.out.println(m.group());
}
} /* Output:
java
Java
JAVA *///:~
注意group方法只生成匹配的部分。
正则表达式常见的应用
- 字符匹配
Pattern p = Pattern.compile(expression); //正则表达式
Matcher m = p.matcher(str); //操作的字符串
boolean b = m.matches(); //返回是否匹配的结果,或使用lookingAt()和find()方法 - 分割字符
Pattern pattern = Pattern.compile(expression); //正则表达式
String[] strs = pattern.split(str); //操作字符串得到返回的字符串数组
其中split()是Pattern类中的方法,其表示形式为:
String[] split(CharSequence input)
String[] split(CharSequence input, int limit)
第二种形式split()限制splits的个数
- 替换字符串
正则表达式在替换文本上十分有用。以下是Mathcer类的替换方法
replaceFirst(String replacement) 用replacement替换输入字符串第一个匹配部分
replaceAll(String replacement) 用replacement替换输入字符串所有匹配部分
appendReplacement(StringBuffer sbuf, String replacement) 在sbuf中执行逐步替换,而不是仅替换第一个或全部替换。这个方法非常重要,因为通过该方法可以调用多个方法并执行其他处理以生成replacement(replaceFirst()和replaceAll()只能放入固定字符串)。使用此方法,可以通过编程方式分离组并创建强大的替换。
appendTrail(StringBuffer sbuf, String replacement) 在一到多个appendReplacement()方法调用后调用,以便复制输入字符串的其余部分。
见下例:
//: strings/TheReplacements.java
import java.util.regex.;
import net.mindview.util.;
import static net.mindview.util.Print.;
/! 这里有一个文本块用作正则表达式匹配器的输入。注意,我们将首先通过查找特殊的分隔符来提取文本块,然后处理提取的块。
!*/
public class TheReplacements {
public static void main(String[] args) throws Exception {
String s = TextFile.read("TheReplacements.java");
// 匹配上面特别注释的文本块:
Matcher mInput = Pattern.compile("/\\*!(.*)!\\*/", Pattern.DOTALL) .matcher(s); if(mInput.find())
s = mInput.group(1); // Captured by parentheses
// 用一个空格替换两个或多个空格:
s = s.replaceAll(" {2,}", " ");
// 将每行开头的一个或多个空格替换为无空格。必须启用多行模式:
s = s.replaceAll("(?m)^ +", ""); //注意此处正则表达式指的是每行开头的一 //个或多个空格
print(s);
s = s.replaceFirst("[aeiou]", "(VOWEL1)");
StringBuffer sbuf = new StringBuffer();
Pattern p = Pattern.compile("[aeiou]");
Matcher m = p.matcher(s);
// 执行替换时处理查找信息
while(m.find())
m.appendReplacement(sbuf, m.group().toUpperCase()); //效果是把元 //音字母变成大写
// Put in the remainder of the text:
m.appendTail(sbuf); //从截取点将后面的字符串接上
print(sbuf);
}
} /* Output:
Here’s a block of text to use as input to
the regular expression matcher. Note that we’ll
first extract the block of text by looking for
the special delimiters, then process the
extracted block.
H(VOWEL1)rE’s A blOck Of tExt tO UsE As InpUt tO
thE rEgUlAr ExprEssIOn mAtchEr. NOtE thAt wE’ll
fIrst ExtrAct thE blOck Of tExt by lOOkIng fOr thE
spEcIAl dElImItErs, thEn prOcEss thE
ExtrActEd blOck.
*///:~
appendReplacement()还允许在替换字符串中直接引用捕获的组,方法是使用“$g”, 其中“g”是组号。但是,这是为了更简单的处理,不会在前面的程序中提供想要的结果
- 查找输出字符串
Pattern p = Pattern.compile(expression);
Matcher m = p.matcher(str);
while(m.find()){
//m.start(); 返回匹配到的子字符串在字符串中的索引位置
//m.end(); 返回匹配到的子字符串的最后一个字符在字符串中的索引位置
//m.group(); 返回匹配到的子字符串
}
扫描输入(Scanning input)
目前为止,从人类可读的文件或标准输入中读取数据是比较痛苦的。通常的解决方案是读取一行文本,将其标记化(tokenize),然后使用Integer、Double等解析方法来解析数据
//: strings/SimpleRead.java
import java.io.*;
public class SimpleRead {
public static BufferedReader input =
new BufferedReader( new StringReader("Sir Robin of Camelot\n22 1.61803"));
public static void main(String[] args) {
try {
System.out.println("What is your name?");
String name = input.readLine();
System.out.println(name);
System.out.println( "How old are you? What is your favorite double?");
System.out.println("(input: <age> <double>)");
String numbers = input.readLine();
System.out.println(numbers);
String[] numArray = numbers.split(" ");
int age = Integer.parseInt(numArray[0]);
double favorite = Double.parseDouble(numArray[1]);
System.out.format("Hi %s.\n", name);
System.out.format("In 5 years you will be %d.\n", age + 5);
System.out.format("My favorite double is %f.", favorite / 2);
} catch(IOException e) {
System.err.println("I/O exception");
}
}
} /* Output:
What is your name?
Sir Robin of Camelot
How old are you? What is your favorite double?
(input: <age> <double>)
22 1.61803
Hi Sir Robin of Camelot.
In 5 years you will be 27.
My favorite double is 0.809015. *///:~
readline()是用来获取每行输入的字符串,在解析之前要把字符串分组
使用Scanner类,会更加轻松:
//: strings/BetterRead.java
import java.util.*;
public class BetterRead {
public static void main(String[] args) {
Scanner stdin = new Scanner(SimpleRead.input);
System.out.println("What is your name?");
String name = stdin.nextLine();
System.out.println(name);
System.out.println( "How old are you? What is your favorite double?");
System.out.println("(input: <age> <double>)");
int age = stdin.nextInt();
double favorite = stdin.nextDouble();
System.out.println(age);
System.out.println(favorite);
System.out.format("Hi %s.\n", name);
System.out.format("In 5 years you will be %d.\n", age + 5);
System.out.format("My favorite double is %f.", favorite / 2);
}
} /* Output:
What is your name?
Sir Robin of Camelot
How old are you? What is your favorite double?
(input: <age> <double>)
22
1.61803
Hi Sir Robin of Camelot.
In 5 years you will be 27.
My favorite double is 0.809015. *///:~
有了Scanner类,输入、标记化和解析都包含在各种不同的“next”方法中。
Scanner分隔符(Scanner delimiters)
默认情况下,Scanner都是用空白来分割输入token的,但也可以以正则表达式指定自己的分隔符类型:
//: strings/ScannerDelimiter.java
import java.util.*;
public class ScannerDelimiter {
public static void main(String[] args) {
Scanner scanner = new Scanner("12, 42, 78, 99, 42");
scanner.useDelimiter("\\s*,\\s*");
while(scanner.hasNextInt())
System.out.println(scanner.nextInt());
}
} /* Output:
12
42
78
99
42
*///:~
此示例在读取给定字符串时使用逗号(由任意数量的空格包围)作为分隔符。同样的技术也可以用来读取用逗号作为分隔符的文件。除了使用useDelimiter()设置分隔符模式外,还可以使用delimeter(),该方法返回当前用作分隔符的Pattern。
使用正则表达式扫描
除了扫描预定义的基本类型之外,还可以扫描用户定义的模式,这在扫描更复杂的数据时很有用。此示例扫描来自日志的威胁数据,一般防火墙会生成这类数据:
//: strings/ThreatAnalyzer.java
import java.util.regex.*;
import java.util.*;
public class ThreatAnalyzer {
static String threatData =
"58.27.82.161@02/10/2005\n" +
"204.45.234.40@02/11/2005\n" +
"58.27.82.161@02/11/2005\n" +
"58.27.82.161@02/12/2005\n" +
"58.27.82.161@02/12/2005\n" +
"[Next log section with different data format]";
public static void main(String[] args) {
Scanner scanner = new Scanner(threatData);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@" + "(\\d{2}/\\d{2}/\\d{4})";
while(scanner.hasNext(pattern)) {
scanner.next(pattern);
MatchResult match = scanner.match();
String ip = match.group(1);
String date = match.group(2);
System.out.format("Threat on %s from %s\n", date,ip);
}
}
} /* Output:
Threat on 02/10/2005 from 58.27.82.161
Threat on 02/11/2005 from 204.45.234.40
Threat on 02/11/2005 from 58.27.82.161
Threat on 02/12/2005 from 58.27.82.161
Threat on 02/12/2005 from 58.27.82.161
*///:~
将next()用于特定模式时,该模式将与下一个输入标记匹配。结果由match()方法提供,如上所见,其工作方式与前面看到的匹配的正则表达式类似。
使用正则表达式扫描要注意一点。模式只与下一个输入token匹配,因此如果你的模式包含分隔符,那么该模式将永远不会匹配。(the patern is matched against the next input token only, so if your pattern contains a delimiter it will never be matched)
关于正则表达式的一些概念
反斜杠、转义字符和引用符(Backslashes,escapes, and quoting)
反斜杠(“\”)用来引入转义结构,并引用使用反斜杠就会被当作转义的字符。因此,\表示一个反斜杠,{表示一个大括号。
在不表示转义结构的任何字母字符前使用反斜杠都是不对的,因为这些字母是为了以后正则表达式扩展保留的。反斜杠可以在非字母字符前使用,不论该字符属不属于转义结构。
Java源代码中的字符串文本中的反斜杠根据Java语言规范被解释为Unicode转义符或其他字符转义符,因此在表示正则表达式的字符串文本中要使用两个反斜杠。例如:“\b”在正则表达式中表示单个字符匹配,而\b则表示单词边界。“(hello)”是非法的会导致编译错误,要想匹配字符串(hello),则要写成“\(hello\)”
行终止符(Line terminators)
行终止符是一个或两个字符序列,用来标记输入字符序列行的结尾,以下是行终止符:
“\n”
“\r\n”
“\r”
等等
如果UNIX_LINES(?d)模式开启,能识别的行终止符只有换行符
在DOTALL模式下,表达式“.”匹配任何字符,包括行终止符。默认情况下,“.”表达式与行终止符不匹配。
默认情况下,正则表达式中的^
和$
忽略行终止符,只匹配整个输入序列的开始和结尾。如果激活MULTILINE模式,则^
在输入的开头和任何行终止符之后匹配(包含行终止符)。而$
只匹配到行终止符之前或者可以说成是输入序列的末尾。(^
包含,$
不包含)