前言
正则表达式是匹配模式,要么匹配字符,要么匹配位置。
思考何时需要匹配字符,何时需要匹配位置?
一般在需要对原始字符串插入其他字符时需要匹配到位置,一般在需要将原始字符串中某些符合条件的字符串替换为其他字符串时需要先匹配到字符串
元字符
| 元字符 | 说明 | 实例 | |
|---|---|---|---|
基础元字符 | |||
| . | 匹配任意单个字符 | ||
| | | 逻辑或操作符 | abc|def表示 abc、def | |
| [ ] | 匹配该字符集中的一个 | [abc]表示 a、b、c | |
| [^ ] | 排除该字符集合 | [^abc]表示非a或b或c的单个字符 | |
| - | 定义一个范围 | [A-Z] 表示 A-Z 的单个字符 | |
| \ | 转义符 | \\ | |
量词元字符 | 匹配的次数 | ||
| * | 前一个字符匹配0次或多次 | a* 表示 不含a、aa、aaa | |
| + | 前一个字符匹配1次或多次 | a+ 表示 a、aaaaa | |
| ? | 前一个字符匹配0次或1次 | abc? 表示 ab、abc | |
| {n} | 前一个字符匹配n次 | ab{2}c 表示 abbc | |
| {m,n} | 前一个字符匹配m次到n次 | ab{1,2}c表示 abc、abbc | |
| {n, } | 前一个字符匹配n次以上 | ab{1, }c表示 abc、abbc、abbbc | |
位置元字符 | 匹配的位置 | ||
| 匹配字符串的开头 | ^abc表示 abc 且在一个字符串的开头 | ||
| \A | 匹配字符串的开头 | ||
| $ | 匹配字符串的结尾 | abc$表示 abc 且在一个字符串结尾 | |
| \Z | 匹配字符串的结尾 | ||
| \< | 匹配单词的开头 | ||
| \> | 匹配单词的结尾 | ||
| \b | 匹配单词的边界 | ① \w和\W之间的位置 ② ^与\w之间的位置 ③ \w与$之间的位置 | |
| \B | \b的反义,非单词的边界 | ① \w与\w之间的位置 ②\W与\W之间的位置 ③^与\W之间的位置 ④\W与$之间的位置 | |
匹配模式 | |||
| (?m) | 多行模式 | ||
特殊字符元字符 | |||
| [\b] | 退格字符 | ||
| \c | 匹配一个控制字符 | ||
| \d | 匹配任意数字字符 | 等价于0-9 | |
| \D | 匹配任意非数字 | ||
| \f | 换页符 | ||
| \n | 换行符 | ||
| \r | 回车符 | ||
| \s | 匹配任意空白字符 | ||
| \S | 匹配可见字符 | ||
| \t | 制表符tab键 | ||
| \v | 垂直制表符 | ||
| \w | 匹配任意字母数字下划线字符 | ||
| \x | 匹配一个十六进制数字 | ||
| \0 | 匹配一个八进制数字 | ||
反向引用和环视 | |||
| ( ) | 分组标记内部只能使用 | (abc)表示 abc,`(abc | def)`表示 abc、def |
| \1 | 匹配第一个子表达式;\2匹配第二个子表达式,以此类推 | ||
| ?= | 肯定式向前查看 | ||
| ?<= | 肯定式向后查看 | ||
| ?! | 否定式向前查看 | ||
| ?<! | 否定式向后查看 | ||
| ?() | 条件 if then | ||
| ?()| | 条件 if then else | ||
大小写转换 | |||
| \E | 结束\L或\U转换 | ||
| \l | 把下一个字符转换为小写 | ||
| \L | 把后面的字符转换为小写,直到遇到\E为止 | ||
| \u | 把下一个字符转换为大写 | ||
| \U | 把后面的字符转换为大写,直到遇到\E为止 |
修饰符
| 修饰符 | 说明 |
|---|---|
| i | ignoreCase 忽略大小写 |
| m | multiline 可以进行多行匹配 |
| g | global 全局匹配 |
常用正则表达式
手机号
以13,14,15,16,17,18,19开头
/^(?:(?:+|00)86)?1[3-9]\d{9}$/
座机
如: 027-17171717 0217-17171717
/^\d{3}-\d{8}$|^\d{4}-\d{7,8}$/
邮箱
/^(([^<>()[]\.,;:\s@"]+(.[^<>()[]\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/
用户名
校验4到16位(字母,数字,下划线,减号)
/^[a-zA-Z0-9_-]{4,16}$/
密码
强度校验,最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符
/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/
身份证号
(2代,18位数字),最后一位是校验位,可能为数字或字符X
/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/
1/2代(15位/18位数字)
/^\d{6}((((((19|20)\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|(((19|20)\d{2})(0[13578]|1[02])31)|((19|20)\d{2})02(0[1-9]|1\d|2[0-8])|((((19|20)([13579][26]|[2468][048]|0[48]))|(2000))0229))\d{3})|((((\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|((\d{2})(0[13578]|1[02])31)|((\d{2})02(0[1-9]|1\d|2[0-8]))|(([13579][26]|[2468][048]|0[048])0229))\d{2}))(\d|X|x)$/
1代,15位数字
/^[1-9]\d{7}(?:0\d|10|11|12)(?:0[1-9]|[1-2][\d]|30|31)\d{3}$/
匹配连续重复的字符
/(.)\1+/
html标签(宽松匹配)
/<(\w+)[^>]*>(.*?</\1>)?/
html注释
/<!--[\s\S]*?-->/g
中文/汉字
/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/
中文名
2-16位
/^(?:[\u4e00-\u9fa5·]{2,16})$/
英文名
/(^[a-zA-Z]{1}[a-zA-Z\s]{0,20}[a-zA-Z]{1}$)/
数字和字母组成
/^[A-Za-z0-9]+$/
不能包含字母
/^[^A-Za-z]*$/
中文和数字
/^((?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])|(\d))+$/
数字和英文字母组成,并且同时含有数字和英文字母
/^(?=.*[a-zA-Z])(?=.*\d).+$/
数字
/^\d{1,}$/
整数
/^-?[0-9]\d*$/
正整数
/^+?[1-9]\d*$/
负整数
/^-[1-9]\d*$/
小数
/^\d+.\d+$/
浮点数
/^(-?\d+)(.\d+)?$/
数字/货币金额(支持负数、千分位分隔符)
/^-?\d+(,\d{3})*(.\d{1,2})?$/
图片(image)链接地址(图片格式可按需增删)
/^https?://(.+/)+.+(.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i
base64格式
/^\s*data:(?:[a-z]+/[a-z0-9-+.]+(?:;[a-z-]+=[a-z0-9-]+)?)?(?:;base64)?,([a-z0-9!$&',()*+;=-._~:@/?%\s]*?)\s*$/i
大于等于0, 小于等于150, 支持小数位出现5, 如145.5, 用于判断考卷分数
/^150$|^(?:\d|[1-9]\d|1[0-4]\d)(?:.5)?$/
车牌号(新能源+非新能源)
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/
车牌号(非新能源)
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/
车牌号(新能源)
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](?:((\d{5}[A-HJK])|([A-HJK][A-HJ-NP-Z0-9][0-9]{4}))|[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳])$/
必须带端口号的网址(或ip)
/^((ht|f)tps?://)?[\w-]+(.[\w-]+)+:\d{1,5}/?$/
ip-v4[:端口]
/^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]).){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])(?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$/
ip-v6[:端口]
/(^(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$)|(^[(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b).){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))](?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$)/i
案例详解
题目1:数字的千分位分割法
将123456789转化为123,456,789
观察题目的规律就是从后往前,每三个数字前加一个逗号,(需要注意的是开头不需要加逗号,)。是不是很符合 (?=p)的规律呢?p可以表示每三个数字,要添加的逗号所处的位置正好是(?=p)匹配出来的位置。
第一步,尝试先把后面第一个逗号弄出来
let price = '123456789'
let priceReg = /(?=\d{3}$)/
console.log(price.replace(priceReg, ',')) // 123456,789
第二步,把所有的逗号都弄出来
要把所有的逗号都弄出来,主要要解决的问题是怎么表示三个数字一组,也就是3的倍数。我们知道正则中括号可以把一个p模式变成一个小整体,所以利用括号的性质,可以这样写
let price = '123456789'
let priceReg = /(?=(\d{3})+$)/g
console.log(price.replace(priceReg, ',')) // ,123,456,789
第三步,去掉首位的逗号,
上面已经基本上实现需求了,但是还不够,首位会出现,那怎么把首位的逗号去除呢?想想前面是不是有一个知识正好满足这个场景? 没错(?!p),就是他了,两者结合就是从后往前每三个数字的位置前添加逗号,但是这个位置不能是^首位。
let price = '123456789'
let priceReg = /(?!^)(?=(\d{3})+$)/g
console.log(price.replace(priceReg, ',')) // 123,456,789
题目2:手机号3-4-4分割
将手机号18379836654转化为183-7983-6654
有了上面数字的千分位分割法,做这个题相信会简单很多,也就是从后往前找到这样的位置:
每四个数字前的位置,并把这个位置替换为-
let mobile = '18379836654'
let mobileReg = /(?=(\d{4})+$)/g
console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654
题目3:手机号3-4-4分割扩展
将手机号11位以内的数字转化为3-4-4格式
回想一下这样的场景,有一个表单需要收集用户的手机号,用户是一个个数字输入的,我们需要在用户输入11位手机号的过程中把其转化为3-3-4格式。即
123 => 123
1234 => 123-4
12345 => 123-45
123456 => 123-456
1234567 => 123-4567
12345678 => 123-4567-8
123456789 => 123-4567-89
12345678911 => 123-4567-8911
这样用(?=p)就不太合适了,例如1234就会变成-1234。 想想前面的知识点有适合处理这种场景的吗?是的(?<=p)
第一步, 将第一个-弄出来
const formatMobile = (mobile) => {
return String(mobile).replace(/(?<=\d{3})\d+/, '-')
}
console.log(formatMobile(123)) // 123
console.log(formatMobile(1234)) // 123-
将第二个-弄出来
将第一个-弄出来之后,字符的长度多了一位,原本1234567(这个位置插入-)8,要变成往后移一位
const formatMobile = (mobile) => {
return String(mobile).slice(0,11)
.replace(/(?<=\d{3})\d+/, ($0) => '-' + $0)
.replace(/(?<=[\d-]{8})\d{1,4}/, ($0) => '-' + $0)
}
console.log(formatMobile(123)) // 123
console.log(formatMobile(1234)) // 123-4
console.log(formatMobile(12345)) // 123-45
console.log(formatMobile(123456)) // 123-456
console.log(formatMobile(1234567)) // 123-4567
console.log(formatMobile(12345678)) // 123-4567-8
console.log(formatMobile(123456789)) // 123-4567-89
console.log(formatMobile(12345678911)) // 123-4567-8911
题目4:验证密码的合法性
密码长度是6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符
题目由三个条件组成
① 密码长度是6-12位
② 由数字、小写字符和大写字母组成
③ 必须至少包括2种字符
第一步写出条件①和②和正则
let reg = /^[a-zA-Z\d]{6,12}$/
第二步,必须包含某种字符(数字、小写字母、大写字母)
let reg = /(?=.*\d)/
// 这个正则的意思是,匹配的是一个位置,这个位置需要满足`任意数量的符号,紧跟着是个数字`,注意它最终得到的是个位置,而不是数字或者是数字前面有任意的东西
console.log(reg.test('hello')) // false
console.log(reg.test('hello1')) // true
console.log(reg.test('hel2lo')) // true
// 其他类型同理
第三步,写出完整的正则
必须包含两种字符,有下面四种排列组合方式
① 数字和小写字母组合
② 数字和大写字母组合
③ 小写字母与大写字母组合
④ 数字、小写字母、大写字母一起组合(但其实前面三种已经覆盖了第四种了)
// 表示条件①和②
// let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))/
// 表示条件条件③
// let reg = /(?=.*[a-z])(?=.*[A-Z])/
// 表示条件①②③
// let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))|(?=.*[a-z])(?=.*[A-Z])/
// 表示题目所有条件
let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))|(?=.*[a-z])(?=.*[A-Z])^[a-zA-Z\d]{6,12}$/
console.log(reg.test('123456')) // false
console.log(reg.test('aaaaaa')) // false
console.log(reg.test('AAAAAAA')) // false
console.log(reg.test('1a1a1a')) // true
console.log(reg.test('1A1A1A')) // true
console.log(reg.test('aAaAaA')) // true
console.log(reg.test('1aA1aA1aA')) // true
题目5:匹配id
// 1
let regex = /id=".*?"/ // 想想为什么要加? 不加的话 连后面的class都会匹配到
let string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
// 2
let regex = /id="[^"]*"/
let string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
题目6:匹配16进制的颜色值
// 要求匹配如下颜色
/*
#ffbbad
#Fc01DF
#FFF
#ffE
*/
let regex = /#([a-fA-F\d]{6}|[a-fA-F\d]{3})/g
let string = "#ffbbad #Fc01DF #FFF #ffE";
console.log(string.match(regex))
// ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]
题目7:匹配24小时制时间
/*
要求匹配
23:59
02:07
*/
// 解析:
// 第一位:可以是0、1、2
// 第二位:当第一位位0或者1的时候,可以是0到9、第一位是2的时候,只可以是0到3
// 第三位:固定是冒号:
// 第四位:可以是0到5
// 第五位:0到9
let regex = /^([01]\d|2[0-3]):[0-5]\d$/
console.log(regex.test('23:59')) // true
console.log(regex.test('02:07'))// true
// 衍生题,可以是非0
let regex = /^(0?\d|1\d|2[0-3]):(0?|[1-5])\d/
console.log( regex.test("23:59") ) // true
console.log( regex.test("02:07") ) // true
console.log( regex.test("7:09") ) // true
题目8:匹配日期
/*
要求匹配
yyyy-mm-dd格式的日期
注意月份、和日的匹配
*/
let regex = /\d{4}-(0\d|1[0-2])-(0[1-9]|[12]\d|3[01])/
console.log( regex.test("2017-06-10") ) // true
console.log( regex.test("2017-11-10") ) // true
1.trim方法模拟
// 1. 提取中间关键字符, 使用的分组引用
const trim1 = (str) => {
return str.replace(/^\s*(.*?)\s*$/, '$1')
}
// 2. 去掉开头和结尾的空字符
const trim2 = (str) => {
return str.replace(/^\s*|\s*$/g, '')
}
2.将每个单词的首字母大写
关键是要找到每个单词的首字母
// my name is epeli
const titleize = (str) => {
return str.toLowerCase().replace(/(?:^|\s)\w/g, (c) => c.toUpperCase())
}
console.log(titleize('my name is epeli')) // My Name Is Epeli
// 拓展,横向转驼峰,例如base-act-tab => BaseActTab
'base-act-tab'.replace(/(?:^|-)(\w)/g, ($0, $1) => $1.toUpperCase()) // BaseActTab
3.驼峰化
// -moz-transform => MozTransform
const camelize = (str) => {
return str.replace(/[-_\s]+(\w)/g, (_, $1) => $1.toUpperCase())
}
console.log(camelize('-moz-transform')) // MozTransform
4.中划线化
// MozTransform => -moz-transform
const dasherize = (str) => {
return str.replace(/[A-Z]/g, ($0) => ('-' + $0).toLowerCase())
}
console.log(dasherize('MozTransform')) // -moz-transform
5.HTML转义和反转义
// html转义规则见https://blog.wpjam.com/m/character-entity/
const escapeHTML = (str) => {
const escapeChars = {
'<': 'lt',
'>': 'gt',
'"': 'quot',
''': '#39',
'&': 'amp'
}
let regexp = new RegExp(`[${Object.keys(escapeChars).join('')}]`, 'g') // 为了得到字符组[<>"'&]
return str.replace(regexp, (c) => `&${escapeChars[ c ]};`)
}
console.log( escapeHTML('<div>Blah blah blah</div>')) // <div>Blah blah blah</div>
// 反转义
const unescapseHTML = (str) => {
const htmlEntities = {
nbsp: ' ',
lt: '<',
gt: '>',
quot: '"',
amp: '&',
apos: '''
}
return str.replace(/&([^;]+);/g, ($0, $1) => {
return htmlEntities[ $1 ] || ''
})
}
console.log(unescapseHTML('<div>Blah blah blah</div>')) // <div>Blah blah blah</div>
6.匹配成对的标签
/*
匹配
<title>regular expression</title>
<p>laoyao bye bye</p>
不匹配
<title>wrong!</p>
*/
let reg = /<([^>]+)>.*?</\1>/g
console.log(reg.test('<title>regular expression</title>')) // true
console.log(reg.test('<p>laoyao bye bye</div>')) // false
总结
需要清楚想要匹配的是字符串还是位置。文章部分是从网上复制而来,主要供自己学习参考。后续慢慢修改优化。