十五分钟认识正则表达式,解决所有文字难题

293 阅读4分钟

前言

在开发中,经常要对文字做处理,例如资料的搜索、获取并重组文字、验证使用者输入等等,遇到这类与字串有关的问题,依情况使用正规表达式可以免去很多处理上的麻烦,使程序更简单好懂。 这篇文章将带你用 15 分钟的时间,透过 MDN文档 与几个常见的例子,带你初步认识并使用这门技术,我们话不多说直接开始吧!

什麽是正规表达式 ?

正规表达式 (Regular Expression),是一种用来描述字串 符合某个语法规则 的模型 (pattern),可以用来做文字的搜寻、比对、萃取、替代、转换等等,在许多的程式语言中都支援正规表达式的使用,以下范例将以 Javascript 为例。

撰写正规表达式

撰写正规表达式时,使用两个斜线 // 或是 new RegExp() 来建立一个 RegExp 物件。

// 1. 使用 literal,这种方式会在 script 载入时就被编译,效能较好。
const regex = /some text/

// 2. 使用 new 建构一个 RegExp 物件,适合用在需要动态产生 pattern 的场合。
const regex = new RegExp('some text')

// 加上 flag 设定,使比对的能力更强大。i:不区分大小写,g:比对字串所有位置
const regex = /some text/i
const regex = new RegExp('some text', 'g')

使用正规表达式

建立了正规表达式后,就可以使用 RegExptest 以及 exec 来对字串做处理囉。

const regex = /hello world/i

// 使用 test 比对字串是否符合 pattern,回传 boolean
regex.test('Hello World !!') // true

// 使用 exec 取得比对的详细资讯,比对失败时回传 null
regex.exec('Hello World !!') // ["Hello World", index: 0, input: "Hello World !!", groups: undefined]
regex.exec('Hello Regex !!') // null

String 物件 中的 searchmatchreplacesplit 等方法中,也有支援正规表达式写法。

const paragraph = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'

// 使用 search 搜寻字串是否在段落中,有找到回传字串的起始位置,没找到回传 -1
paragraph.search('tExT') // -1
paragraph.search(/tExT/i) // 28

// 使用 match 找出第一个比对成功的详细资讯,加上 g flag 则会列出所有比对成功的字串
paragraph.match(/ing/) // ["ing", index: 45, ...]
paragraph.match(/ing/g) // ["ing", "ing"]

特殊字元

在正规表达式中,某些特定的字元或符号属于保留字,直接使用可能与预期的效果不同。

const  str ='Rails is a web framework written in Ruby'

// ^ 表示 pattern 必须在字串的开头
str.match(/^Rails/) // ["Rails", index: 0, ...]
str.match(/^Ruby/) // null

// $ 表示 pattern 必须在字串的结尾
str.match(/Ruby$/) // ["Ruby", index: 36, ...]
str.match(/Rails$/) // null

// | 表示 或(or), | 前后的字串都可以比对
const regex = /color|colour/
regex.exec('color') // ["color", index: 0, ...]
regex.exec('colour') // ["colour", index: 0, ...]

// 当要比对这些特殊符号时,使用反斜线'\'来跳脱特殊字元
const regex = /\$100/
regex.test('$100') // true

集合 []

在前面的例子中,pattern 都有指定明确的文字,如果想要比对的是英文、数字或是几种特定的组合,就可以使用集合 [ ] 来将它们一网打尽,集合代表著 这一个字元 可以是 [ ] 内的其中一种。

// 只要是英文大写字母,就比对成功
const regex = /[ABCDEFGHIJKLMNOPQRSTUVWXYZ]/
'K'.match(regex) // ["K", index: 0, ...]
'δ'.match(regex) // null

// 可以使用 '-' 来简化集合,'A-Z' 表示英文字母 A ~ Z 都符合
const regex = /[A-Z]/

// 若要比对的是英文或数字,可以这样表示
const regex = /[A-Za-z0-9]/

一些常用的集合有对应的特殊字元。

const regex = /./   // 比对换行符号外的任意一个字元
const regex = /\d/  // 比对一个数字,相等于 /[0-9]/
const regex = /\w/  // 比对一个英文、数字或底线,相等于 /[A-Za-z0-9_]/
const regex = /\s/  // 比对一个的空格 (ex: space, tab, 换行, ...)

使用排除法 [^ ] 来比对这个集合 以外 的字元

const regex = /[^\w]/
regex.test('a') // false
regex.test('!') // true

量词 {}

在集合的内容中我们提到,使用集合一次也只能比对一个文字,此时,若我们想比对连续的相同规则时,可以使用量词 { } 来修饰。

// 不使用量词时,要比对 5 个连续的数字就必须写 5 次
const regex = /\d\d\d\d\d/
regex.test('12345') // true

// 使用 {5} 表示连续出现 5 次
const regex = /\d{5}/
regex.exec('abcde12345') // ["12345", index: 5, ...]
regex.exec('a1b2c3d4e5') // null

// 使用 {2,} 表示连续出现 2 次以上
const regex = /\w\+{2,}/
regex.exec('a+') // null
regex.exec('a++') // ["a++", index: 0, ...]

// 使用 {2, 5} 表示连续出现 2 ~ 5 次
const regex = /^\w{2,5}!/
regex.exec('Hi!') // ["Hi!", index: 0, ...]
regex.exec('Helloooo!') // null

量词也有特殊字元可以替代。

// 使用 ? 表示出现 0 或 1 次,等同于 {0,1}
const regex = /\w?/
// 使用 + 表示出现 1 次或以上,等同于 {1,}
const regex = /\w+/
// 使用 * 表示出现 0 次或以上,等同于 {0,}
const regex = /\w*/

使用上,+、?、、{2, 5} 都是属于 Greedy 量词,意思是会以连续出现次数 越多 为优先,相反的,在量词后面加上一个问号 +?、??、?、{2, 5}? 就变成 Lazy 量词,意思是以连续出现次数 越少 为优先。

// '+' 出现的次数越多优先
const regex = /a\+{2,}/
regex.exec('a+++++') // ["a+++++", index: 0, ...]
// '+' 出现的次数越少优先
const regex = /a\+{2,}?/
regex.exec('a+++++') // ["a++", index: 0, ...]

断言 (Assertions)

最后一个要介绍的主题是断言 ,根据维基百科的解释,

断言是一种放在程式中的一阶逻辑 ( 例如一个结果为 true 或 false 的判断式 ) ,当程式执行到断言的位置时就会执行判断,若结果为 true 则继续执行,结果为 false 则中止执行。

在正规表达式中,断言可以用来指定字串中的某个锚点要符合一些条件,例如前面介绍的特殊字元 在字串开头 ^在字串的结尾 $ 就是被归类在断言的用法中,常见的断言还有 文字边界 \b环顾 Lookaround

// 假设要找出 'Java' 而不是 'Javascript'
const str = 'difference between Javascript and Java.'
// 不使用断言会找到 'Javascript' 的 'Java'
str.match(/Java/) // ["Java", index: 19, ...]
// 改使用文字边界 \b 来比对,就能找到想要的结果了
str.match(/\bJava\b/) // ["Java", index: 34, ...]

// 文字边界指的是在比对到 \b 的位置时,前后相邻的字元必须有一个不是文字
// 使用 replace 把 \b 替换成 '|' 来看看它的效果
const regex = /\b/g
str.replace(regex, '|') // "|difference| |between| |Javascript| |and| |Java|."

字串与锚点的描述,建议到 regex101 实际操作一下,配合网站的视觉效果可以更好的理解它,如果想要判断的是更複杂的条件,可以使用 环顾 Lookaround

// Lookaround 分为两种 `Lookahead` 以及 `Lookbehind`,各自又有 positive 与 negative 两种判断方式
// Positive Lookahead: A(?=B) → A 后方的条件要符合 B
// Negative Lookahead: A(?!B) → A 后方的条件不能符合 B
// Positive Lookbehind: (?<=A)B → B 前方的条件要符合 A
// Negative Lookbehind: (?<!A)B → B 前方的条件不能符合 A

// 假设要取出商品的金额
const str = '数量 2,实付金额 990元'
// 分析一下要撷取的资料 "前方有一个空格 + 金额 + 后方有一个'元'"
// 把规则写成正规表达式
const regex = /(?<=\s)\d+(?=元)/
str.match(regex) // ["990", index: 10, ...]

要注意的是,断言与群组的语法虽然很像,但是断言的条件不会出现在比对的结果中,所以在正规表达式中也被称为 Zero-Length Assertions。

总结

正规表达式这项技术除了在 web 开发上常被使用之外,在爬虫、数据分析等应用也用得上,本期文章使用十五分钟的时间带大家初步认识正规表达式的用法,更多的内容例如比对的优先顺序、多重条件的撰写、正规表达式的效能等主题可以透过 MDN文檔 、网路搜寻或是相关的书籍来做进一步的学习,以上就是这次的全部内容,若有错误或需要补充的部分还请不吝指出,感谢你的观看。

参考资料