Java正则表达式学习笔记
|字数总计:6.4k|阅读时长:25分钟|阅读量:
Java正则表达式学习笔记
学习来源:韩顺平讲Java
学习时间:2022年3月11日
1 概述
正则表达式:regular expression
,简写为RegExp
或regex
,专门用于解决处理文本问题,是对字符串执行模式匹配的技术。
很多编程语言都支持正则表达式进行字符串操作。
regex体验
给出一段文本:
1
| 1995年,互联网的蓬勃发展给了Oak机会。业界为了使死板、单调的静态网页能够“灵活”起来,急需一种软件技术来开发一种程序,这种程序可以通过网络传播并且能够跨平台运行。于是,世界各大IT企业为此纷纷投入了大量的人力、物力和财力。这个时候,Sun公司想起了那个被搁置起来很久的Oak,并且重新审视了那个用软件编写的试验平台,由于它是按照嵌入式系统硬件平台体系结构进行编写的,所以非常小,特别适用于网络上的传输系统,而Oak也是一种精简的语言,程序非常小,适合在网络上传输。Sun公司首先推出了可以嵌入网页并且可以随同网页在网络上传输的Applet(Applet是一种将小程序嵌入到网页中进行执行的技术),并将Oak更名为Java(在申请注册商标时,发现Oak已经被人使用了,再想了一系列名字之后,最终,使用了提议者在喝一杯Java咖啡时无意提到的Java词语)。5月23日,Sun公司在Sun world会议上正式发布Java和HotJava浏览器。IBM、Apple、DEC、Adobe、HP、Oracle、Netscape和微软等各大公司都纷纷停止了自己的相关开发项目,竞相购买了Java使用许可证,并为自己的产品开发了相应的Java平台。
|
需求:
- 提取文章中所有的英文单词
- 提取文章中所有的数字
- 提取文章中所有的英文单词和数字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class Regexp {
public static void main(String[] args) { String content = "1995年,互联网的蓬勃发展给了Oak机会。" + "业界为了使死板、单调的静态网页能够“灵活”起来,急" + "需一种软件技术来开发一种程序,这种程序可以通过网" + "络传播并且能够跨平台运行。于是,世界各大IT企业为" + "此纷纷投入了大量的人力、物力和财力。这个时候,Sun" + "公司想起了那个被搁置起来很久的Oak,并且重新审视了" + "那个用软件编写的试验平台,由于它是按照嵌入式系统硬" + "件平台体系结构进行编写的,所以非常小,特别适用于网" + "络上的传输系统,而Oak也是一种精简的语言,程序非常小" + ",适合在网络上传输。Sun公司首先推出了可以嵌入网页并" + "且可以随同网页在网络上传输的Applet(Applet是一种将" + "小程序嵌入到网页中进行执行的技术),并将Oak更名为Java" + "(在申请注册商标时,发现Oak已经被人使用了,再想了一系" + "列名字之后,最终,使用了提议者在喝一杯Java咖啡时无意提" + "到的Java词语)。5月23日,Sun公司在Sun world会议上正" + "式发布Java和HotJava浏览器。IBM、Apple、DEC、Adobe、" + "HP、Oracle、Netscape和微软等各大公司都纷纷停止了自己" + "的相关开发项目,竞相购买了Java使用许可证,并为自己的产" + "品开发了相应的Java平台。";
Pattern pattern = Pattern.compile("([0-9]+)|([a-zA-Z]+)"); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println("找到: " + matcher.group(0)); } } }
|
打印结果:(列举部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 找到: 1995 找到: Oak 找到: IT 找到: Sun 找到: Oak 找到: Oak 找到: Sun 找到: Applet 找到: Applet 找到: Oak 找到: Java 找到: Oak 找到: Java 找到: Java 找到: 5 找到: 23 找到: Sun 找到: Sun
|
2 底层实现
2.1 代码示例1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class RegTheory { public static void main(String[] args) { String content = "1998年12月8日,第二代Java平台的企业版J2EE发" + "布。1999年6月,Sun公司发布了第二代Java平台(简称为Java" + "2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的" + "微型版),应用于移动、无线及有限资源的环境;J2SE(Java 2" + " Standard Edition,Java 2平台的标准版),应用于桌面环境" + ";J2EE(Java 2Enterprise Edition,Java 2平台的企业版)" + ",应用于3443基于Java的应用服务器。Java 2平台的发布,是Java发展" + "过程中最重要的一个里程碑,标志着Java的应用开始普及9889"; String regStr = "\\d\\d\\d\\d"; Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
matcher.find()
:
matcher.group(0)
- 根据
groups[0] = 0
和 groups[1] = 4
的记录的位置,从content
开始截取子字符串并返回,即[0, 4)
之间的子串
1 2 3 4 5 6 7 8 9
| public String group(int group) { if (first < 0) throw new IllegalStateException("No match found"); if (group < 0 || group > groupCount()) throw new IndexOutOfBoundsException("No group " + group); if ((groups[group*2] == -1) || (groups[group*2+1] == -1)) return null; return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString(); }
|
- 输出1998后,再次进行匹配,定位至1999,此时:
1 2 3 4
| groups[0] = 31;
groups[1] = 35;
|
2.2 代码示例2
考虑分组的情况,即regex中含有小括号的情况。第一对小括号表示第一组,以此类推。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class RegTheory { public static void main(String[] args) { String content = "1998年12月8日,第二代Java平台的企业版J2EE发" + "布。1999年6月,Sun公司发布了第二代Java平台(简称为Java" + "2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的" + "微型版),应用于移动、无线及有限资源的环境;J2SE(Java 2" + " Standard Edition,Java 2平台的标准版),应用于桌面环境" + ";J2EE(Java 2Enterprise Edition,Java 2平台的企业版)" + ",应用于3443基于Java的应用服务器。Java 2平台的发布,是Java发展" + "过程中最重要的一个里程碑,标志着Java的应用开始普及9889"; String regStr = "(\\d\\d)(\\d\\d)"; Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println("find: " + matcher.group(0)); System.out.println("第1组()匹配到的值: " + matcher.group(1)); System.out.println("第2组()匹配到的值: " + matcher.group(2)); } } }
|
打印结果:
1 2 3 4 5 6 7 8 9 10 11 12
| find: 1998 第1组()匹配到的值: 19 第2组()匹配到的值: 98 find: 1999 第1组()匹配到的值: 19 第2组()匹配到的值: 99 find: 3443 第1组()匹配到的值: 34 第2组()匹配到的值: 43 find: 9889 第1组()匹配到的值: 98 第2组()匹配到的值: 89
|
3 正则表达式语法
正则表达式中的元字符从功能上大致分为:
- 限定符
- 选择匹配符
- 分组组合和反向引用符
- 特殊字符
- 字符匹配符
- 定位符
3.1 正则转义符
转义符\\
:使用正则表达式去撇皮某些特殊字符时,需要用到转义符。注意在java中,转义符号用两个反斜杠\
。
需要用到转义符号的字符有:. * + ( ) $ / \ ? [ ] ^ { }
,注意,在[]
中不需要转义,会自动转义,例如[?]
匹配的就是一个实实在在的?
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Regex01 { public static void main(String[] args) { String content = "abc$(abc(123("; String regStr = "\\("; Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
输出:
3.2 字符匹配符
3.2.1 概述
符号 |
含义 |
示例 |
说明 |
[] |
可接受的字符列表 |
[efgh] |
efgh中的任意一个字符,可自动转义 |
[^] |
不可接受的字符列表 |
[^abc] |
除abc之外的任意一个字符,包括数字和特殊符号 |
- |
连字符 |
A-Z |
任意单个大写字母 |
. |
匹配除\n 以外的任何一个字符 |
a..b |
以a开头,b结尾,中间包括2个任意字符的长度为4的字符串 |
\\d |
匹配单个数字字符,相当于[0-9] |
\\d{3}(\\d)? |
包含3个或4个数字的字符串 |
\\D |
匹配单个非数字字符,相当于[^0-9] |
\\D(\\d)* |
以单个非数字字符开头,后接任意个数字的字符串 |
\\w |
匹配单个数字、大小写字母字符,下划线,相当于[0-9a-zA-Z_] |
\\d{3}\\w{4} |
以3个数字字符开头的长度为7的数字字母字符串 |
\\W |
匹配单个非数字、非大小写字母字符,非下划线,相当于[^0-9a-zA-Z_] |
\\W+\\d{2} |
以至少1个非数字字母字符开头,2个数字字符结尾的字符串 |
\\s |
匹配单个任意空白字符(例如空格,制表符等) |
|
|
\\S |
匹配单个任意非空白字符 |
|
3.2.2 代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class Regex02 { public static void main(String[] args) { String content = "a11c8";
String regStr1 = "[a-z]";
String regStr2 = "[A-Z]";
String regStr3 = "abc";
String regStr4 = "(?i)abc"; String regStr5 = "[^a-z]{2}";
String regStr6 = "[^0-9]"; Pattern pattern = Pattern.compile(regStr1); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
3.3 选择匹配符
在匹配某个字符串的时候是选择性的,即:既可以匹配这个,又可以匹配那个,这时候需要使用选择匹配符。
符号 |
含义 |
示例 |
说明 |
` |
` |
匹配` |
`之前或之后的表达式 |
`ab |
cd` |
ab或者cd |
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Regex03 { public static void main(String[] args) { String content = "Hongyi Mark 四川省成都市"; String regStr = "(?i)hong|四川|成都";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
打印结果:
1 2 3
| find: Hong find: 四川 find: 成都
|
3.4 限定符
限定符用于指定前面的字符和组合项连续出现多少次。
符号 |
含义 |
示例 |
说明 |
* |
指定字符重复0次或n次,零到多 |
(abc)* |
仅包含任意个abc的字符串 |
+ |
指定字符重复1次或n次,一到多 |
m+(abc)* |
以至少1个m开头,后接任意个abc的字符串 |
? |
指定字符重复0次或1次,零到一 |
m+abc? |
以至少1个m开头,后接abc或ab的字符串 |
{n} |
只能输出n个字符 |
[abcd]{3} |
由abcd中的字母组成的任意长度为3的字符串 |
{n,} |
指定至少n个匹配 |
[abcd]{3,} |
由abcd中的字母组成的任意长度不小于3的字符串 |
{n,m} |
指定至少n个但不多于m个匹配 |
[abcd]{3,5} |
由abcd中的字母组成的任意长度不小于3但不大于5的字符串 |
代码示例
细节:java匹配默认贪婪匹配,即尽可能匹配多的字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Regex04 { public static void main(String[] args) { String content = "11111111aaaaaa2345";
String regStr1 = "a{3,4}"; String regStr2 = "1+"; String regStr2 = "\\d+";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); }
} }
|
3.5 定位符
定位符的作用是规定要匹配的字符串出现的位置,比如在字符串的开始还是结束的位置。
符号 |
含义 |
示例 |
说明 |
^ |
指定起始字符 |
^[0-9]+[a-z]* |
以至少1个数字开头,后接任意个小写字母的字符串 |
$$` |
指定结束字符 |
`^[0-9]\-[a-z]+$$ |
以1个数字开头,后接连字符- ,并以至少一个小写字母结尾的字符串 |
\\b |
匹配目标字符串的边界 |
han\\b |
这里说的字符串边界指的是子串间有空格,或者是目标字符串的结束位置 |
\\B |
匹配目标字符串的非边界 |
han\\B |
和\\b 含义相反 |
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Regex05 { public static void main(String[] args) { String content = "a123abc";
String regStr = "^[0-9]+[a-z]*";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
3.6 分组 捕获 反向引用
- 分组:我们可以用圆括号组成一个比较复杂的匹配模式,一个圆括号的部分就可以看作是一个子表达式
- 捕获:把正则表达式中子表达式/分组匹配的内容,保存到内存中,以数字编号或显式命名的组里,方便后面引用。注意组0代表的是整个正则式,分组编号从1开始。
- 反向引用:圆括号的内容被捕获后,可以在这个括号后被使用,从而写出一个比较实用的匹配模式。这种引用既可以在正则表达式内部,也可以在外部。内部反向引用使用
\\分组号
,外部引用使用$分组号
3.6.1 捕获分组
常用分组构造形式 |
说明 |
(pattern) |
非命名捕获。捕获匹配的子字符串。编号为零的第一个捕获是由整个正则表达式模式匹配的文本,其他捕获结果则根据左括号的顺序从1开始自动编号 |
(?<name>pattern) |
命名捕获。将匹配的子字符串捕获到一个组名称或编号名称中。用于name的字符串不能包含任何标点符号,并且不能以数字开头。可以使用单引号代替尖括号,例如(?'name') |
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Regex06 { public static void main(String[] args) { String content = "Hongyi s7788 nn1198han";
String regStr = "(\\d\\d)(\\d\\d)";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); System.out.println("第一个分组的内容: " + matcher.group(1)); System.out.println("第二个分组的内容: " + matcher.group(2)); } } }
|
1 2 3 4 5 6
| find: 7788 第一个分组的内容: 77 第二个分组的内容: 88 find: 1198 第一个分组的内容: 11 第二个分组的内容: 98
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Regex06 { public static void main(String[] args) { String content = "Hongyi s7788 nn1198han";
String regStr = "(?<g1>\\d\\d)(?<g2>\\d\\d)";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); System.out.println("第一个分组的内容: " + matcher.group("g1")); System.out.println("第二个分组的内容: " + matcher.group("g2")); } } }
|
3.6.2 非捕获分组
(?:pattern)
:匹配pattern但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用or(|)
组合模式部件的情况很有用。
industr(?:y|ies)
是比industry|industries
更经济的表达式
(?=pattern)
:非捕获匹配
Windows(?=95|98|NT|2000)
匹配Windows 2000
中的Windows
,但不匹配Windows 3.1
中的Windows
(?!pattern)
:非捕获匹配。匹配不处于匹配pattern
的字符串的起始点的搜索字符串,即与第二条规则匹配相反的内容
代码演示
给定字符串:
1
| hello韩顺平教育 jack韩顺平老师 韩顺平同学hello
|
- 需求1:找到韩顺平教育、韩顺平老师、韩顺平同学三个字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Regex08 { public static void main(String[] args) { String content = "hello韩顺平教育 jack韩顺平老师 韩顺平同学hello";
String regStr = "韩顺平(?:教育|老师|同学)";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
1 2 3
| find: 韩顺平教育 find: 韩顺平老师 find: 韩顺平同学
|
注意:这里并没有分组,不能使用matcher.group(1)
- 需求2:查找韩顺平关键字,但只找韩顺平教育和韩顺平老师含有的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Regex08 { public static void main(String[] args) { String content = "hello韩顺平教育 jack韩顺平老师 韩顺平同学hello";
String regStr = "韩顺平(?=教育|老师)";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
- 需求3:查找韩顺平关键字,但不找韩顺平教育和韩顺平老师含有的
1
| String regStr = "韩顺平(?!教育|老师)";
|
3.6.3 反向引用
1 2
| String content = "hello jack11 tom22 Hongyi"; String regStr = "(\\d)\\1";
|
1 2
| String content = "hello jack1111 tom2222 Hongyi"; String regStr = "(\\d)\\1{4}";
|
1 2
| String content = "hello jack1221 tom2332 Hongyi"; String regStr = "(\\d)(\\d)\\2\\1";
|
- 需求:形如
12319-333999111
,前面是一个五位数,紧随连字符,然后是一个九位数,连续的三位要相同
1
| String regStr = "\\d{5}-(\\d)\\1{2}(\\d)\\2{2}(\\d)\\3{2}";
|
- 需求:结巴去重,把类似
我...我要...学学学学...编程java!
修改为我要学编程java!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Regex04 { public static void main(String[] args) { String content = "我...我要...学学学学...编程java!"; Pattern pattern = Pattern.compile("\\."); Matcher matcher = pattern.matcher(content); content = matcher.replaceAll("");
pattern = Pattern.compile("(.)\\1+"); matcher = pattern.matcher(content);
content = matcher.replaceAll("$1"); System.out.println(content); } }
|
更简洁的方法(去掉.
之后用一条语句完成):
1
| content = Pattern.compile("(.)\\1+").matcher(content).replaceAll("$1");
|
3.7 非贪婪匹配
前面提到java的匹配默认是贪婪匹配,但?
字符紧随其他任何限定符(* + ? {n} {n,} {n,m}
)之后时,转变为非贪婪匹配。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Regex09 { public static void main(String[] args) { String content = "hello 1111 ok";
String regStr = "\\d+"; String regStr1 = "\\d+?";
Pattern pattern = Pattern.compile(regStr1); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("find: " + matcher.group(0)); } } }
|
3.8 实践案例
3.8.1 汉字
需求:给定一段文字,检验文字是否全部为汉字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Regex10 { public static void main(String[] args) { String content = "韩顺平教a育"; String regStr = "^[\u0391-\uffe5]+$";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
if (matcher.find()) { System.out.println("满足格式"); } else { System.out.println("不满足格式"); } } }
|
3.8.2 邮政编码
需求:验证是否为邮政编码
特点:1-9开头的六位数
1
| String regStr = "^[1-9]\\d{5}$";
|
3.8.3 QQ号
需求:验证qq号
特点:1-9开头的一个(5-10位数)
1
| String regStr = "^[1-9]\\d{4,9}$";
|
3.8.4 手机号码
需求:验证是否为手机号码
特点:必须以13,14,15,18开头的11位数
1
| String regStr = "^[1](?:3|4|5|8)\\d{9}$";
|
3.8.5 URL
- 需求:验证URL
- 例如:
https://www.bilibili.com/video/BV1Eq4y1E79W?p=17&spm_id_from=pageDriver
1
| String regStr = "^((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-?=&/%.#]*)?";
|
4 常用类
java.util.regex
包主要包括三个类:
Pattern
类
Matcher
类
PatternSyntaxException
类:非强制异常类,表示一个正则表达式模式中的语法错误。
4.1 Pattern类
pattern对象是一个正则表达式对象。Pattern类没有公共构造方法。要创造一个Pattern对象,需要调用其公共静态方法,返回一个Pattern类对象,该方法接收一个正则表达式作为它的第一个参数。
1
| Pattern r = Pattern.conpile(pattern);
|
matches
方法
用于整体匹配(从头开始检验),在验证输入的字符串是否满足条件时使用。
1 2 3 4 5 6 7 8 9 10 11 12
| public class Regex01 { public static void main(String[] args) { String content = "hello abc hello, 韩顺平教育";
String regStr = "hello"; String regStr1 = "hello.*";
boolean matches = Pattern.matches(regStr1, content); System.out.println("整体匹配=" + matches); } }
|
源码
1 2 3 4 5
| public static boolean matches(String regex, CharSequence input) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(input); return m.matches(); }
|
4.2 Matcher类
Matcher对象是对输入字符串进行解释和匹配的引擎,它也没有公共构造方法,需要调用Pattern对象的matcher方法来获得一个Matcher对象。
常用方法
public int start()
:返回以前匹配的初始索引。
public int start(int group)
:返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引
public int end()
:返回最后匹配字符之后的偏移量。
public int end(int group)
:返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Regex02 { public static void main(String[] args) { String content = "hello abc jack hello atm Hongyi";
String regStr = "hello";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content);
while (matcher.find()) { System.out.println("=============="); System.out.println(matcher.start()); System.out.println(matcher.end()); System.out.println("find: " + content.substring(matcher.start(), matcher.end())); } } }
|
1 2 3 4 5 6 7 8
| ============== 0 5 find: hello ============== 15 20 find: hello
|
public boolean matches()
:尝试将整个区域与模式匹配(整体匹配)
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Regex02 { public static void main(String[] args) { String content = "hello abc jack hello atm Hongyi";
String regStr = "hello"; String regStr1 = "hello.*";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content); System.out.println("整体匹配: " + matcher.matches()); } }
|
public String replaceAll(String replacement)
:替换模式与给定替换字符串相匹配的输入序列的每个子序列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Regex02 { public static void main(String[] args) { String content = "hello abc jack hello atm Hongyi";
String regStr = "hello";
Pattern pattern = Pattern.compile(regStr); Matcher matcher = pattern.matcher(content); String newContent = matcher.replaceAll("world"); System.out.println(content); System.out.println(newContent); } }
|
1 2
| hello abc jack hello atm Hongyi world abc jack world atm Hongyi
|
版权声明: 此文章版权归Kisugi Takumi所有,如有转载,请註明来自原作者