你應該學習正則表達式

Regular Expressions (Regex):正則表達式,軟件工程中最為強大,且廣泛適用,令人信服的技術之一。從驗證電子郵件地址到執行復雜的代碼重構器,正則表達式的用途非常廣泛,是任何軟件工程師工具箱中必不可少的條目。

你應該學習正則表達式

什麼是正則表達式?

正則表達式(或Regex,或Regexp)是使用字符序列描述複雜搜索模式的一種方式。

然而,專門的Regex語法由於其複雜性使得有些表達式變得不可訪問。例如,下面的這個基本的正則表達式,它表示24小時制HH / MM格式的時間。

<code>\\b([01]?[0-9]|2[0-3]):([0-5]\\d)\\b/<code>

如果你覺得這看上去略顯複雜,別擔心,當我們完成這個教程時,理解這個表達式將會是小菜一碟。

Learn once, write anywhere

幾乎任何編程語言都可以使用Regex。Regex的知識對於驗證用戶輸入,與Unix shell進行交互,在你喜歡的文本編輯器中搜索/重構代碼,執行數據庫文本搜索等等都非常有用。

在本教程中,我將嘗試在各種場景、語言和環境中對Regex的語法和使用進行簡明易懂的介紹。

此Web應用程序是我用於構建、測試和調試Regex最喜歡的工具。我強烈推薦大家使用它來測試我們將在本教程中介紹的表達式。

本教程中的示例源代碼可以在Github存儲庫中找到——https://github.com/triestpa/You-Should-Learn-Regex

0 – 匹配任何數字行

我們將從一個非常簡單的例子開始——匹配任何只包含數字的行。

<code>^[0-9]+$/<code>

讓我們一點一點的解釋吧。

  • ^ ——表示一行的開始。
  • [0-9] ——匹配0到9之間的數字
  • + ——匹配前一個表達式的一個或多個實例。
  • $ ——表示行尾。

我們可以用偽英文重寫這個Regex為[start of line][one or more digits][end of line]。

很簡單,不是嗎?

我們可以用\\d替換[0-9],結果相同(匹配所有數字)。

這個表達式(和一般的正則表達式)的偉大之處在於它無需太多修改,就可以用到任何編程語言中。

為了演示,我們先快速瞭解如何使用16種最受歡迎的編程語言對文本文件執行此簡單的Regex搜索。

我們使用以下輸入文件(test.txt)為例。

<code>1234
abcde
12db2
5362

1/<code>

每個腳本都將使用這個正則表達式讀取並搜索test.txt文件,並將結果('1234', '5362', '1')輸出到控制檯。

語言範例

0.0 – Javascript / Node.js / Typescript

<code>const fs = require('fs')
const testFile = fs.readFileSync('test.txt', 'utf8')
const regex = /^([0-9]+)$/gm
let results = testFile.match(regex)
console.log(results)/<code>

0.1 – Python

<code>import re

with open('test.txt', 'r') as f:
test_string = f.read()
regex = re.compile(r'^([0-9]+)$', re.MULTILINE)
result = regex.findall(test_string)
print(result)/<code>

0.2 – R

<code>fileLines results print (results)/<code>

0.3 – Ruby

<code>File.open("test.txt", "rb") do |f|
test_str = f.read
re = /^[0-9]+$/m
test_str.scan(re) do |match|
puts match.to_s
end
end/<code>

0.4 – Haskell

<code>import Text.Regex.PCRE

main = do
fileContents let stringResult = fileContents =~ "^[0-9]+$" :: AllTextMatches [] String
print (getAllTextMatches stringResult)/<code>

0.5 – Perl

<code>open my $fh, 'read $fh, my $file_content, -s $fh;
close $fh;
my $regex = qr/^([0-9]+)$/mp;
my @matches = $file_content =~ /$regex/g;
print join(',', @matches);/<code>

0.6 – PHP

<code>$myfile = fopen("test.txt", "r") or die("Unable to open file.");
$test_str = fread($myfile,filesize("test.txt"));
fclose($myfile);
$re = '/^[0-9]+$/m';
preg_match_all($re, $test_str, $matches, PREG_SET_ORDER, 0);
var_dump($matches);
?>/<code>

0.7 – Go

<code>package main

import (
"fmt"
"io/ioutil"
"regexp"
)

func main() {
testFile, err := ioutil.ReadFile("test.txt")
if err != nil { fmt.Print(err) }
testString := string(testFile)
var re = regexp.MustCompile(`(?m)^([0-9]+)$`)
var results = re.FindAllString(testString, -1)
fmt.Println(results)
}/<code>

0.8 – Java

<code>import java.util.regex.Matcher; 

import java.util.regex.Pattern;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;

class FileRegexExample {
public static void main(String[] args) {
try {
String content = new String(Files.readAllBytes(Paths.get("test.txt")));
Pattern pattern = Pattern.compile("^[0-9]+$", Pattern.MULTILINE);
Matcher matcher = pattern.matcher(content);
ArrayList<string> matchList = new ArrayList<string>();

while (matcher.find()) {
matchList.add(matcher.group());
}

System.out.println(matchList);
} catch (IOException e) {
e.printStackTrace();
}
}
}/<string>/<string>/<code>

0.9 – Kotlin

<code>import java.io.File
import kotlin.text.Regex
import kotlin.text.RegexOption

val file = File("test.txt")
val content:String = file.readText()
val regex = Regex("^[0-9]+$", RegexOption.MULTILINE)
val results = regex.findAll(content).map{ result -> result.value }.toList()
println(results)/<code>

0.10 – Scala

<code>import scala.io.Source
import scala.util.matching.Regex

object FileRegexExample {
def main(args: Array[String]) {
val fileContents = Source.fromFile("test.txt").getLines.mkString("\\n")
val pattern = "(?m)^[0-9]+$".r
val results = (pattern findAllIn fileContents).mkString(",")
println(results)
}
}/<code>

0.11 – Swift

<code>import Cocoa
do {
let fileText = try String(contentsOfFile: "test.txt", encoding: String.Encoding.utf8)
let regex = try! NSRegularExpression(pattern: "^[0-9]+$", options: [ .anchorsMatchLines ])
let results = regex.matches(in: fileText, options: [], range: NSRange(location: 0, length: fileText.characters.count))
let matches = results.map { String(fileText[Range($0.range, in: fileText)!]) }
print(matches)
} catch {
print(error)
}/<code>

0.12 – Rust

<code>extern crate regex;
use std::fs::File;
use std::io::prelude::*;
use regex::Regex;

fn main() {
let mut f = File::open("test.txt").expect("file not found");
let mut test_str = String::new();
f.read_to_string(&mut test_str).expect("something went wrong reading the file");

let regex = match Regex::new(r"(?m)^([0-9]+)$") {
Ok(r) => r,
Err(e) => {
println!("Could not compile regex: {}", e);
return;
}
};

let result = regex.find_iter(&test_str);
for mat in result {
println!("{}", &test_str[mat.start()..mat.end()]);
}
}/<code>

0.13 – C#

<code>using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;

namespace RegexExample
{

class FileRegexExample
{
static void Main()
{
string text = File.ReadAllText(@"./test.txt", Encoding.UTF8);
Regex regex = new Regex("^[0-9]+$", RegexOptions.Multiline);
MatchCollection mc = regex.Matches(text);
var matches = mc.OfType<match>().Select(m => m.Value).ToArray();
Console.WriteLine(string.Join(" ", matches));
}
}
}/<match>/<code>

0.14 – C++

<code>#include <string>
#include <fstream>
#include <iostream>
#include <sstream>
#include <regex>
using namespace std;

int main () {
ifstream t("test.txt");
stringstream buffer;
buffer << t.rdbuf();
string testString = buffer.str();

regex numberLineRegex("(^|\\n)([0-9]+)($|\\n)");
sregex_iterator it(testString.begin(), testString.end(), numberLineRegex);
sregex_iterator it_end;

while(it != it_end) {
cout << it -> str();
++it;
}
}/<regex>/<sstream>/<iostream>/<fstream>/<string>/<code>

0.15 – Bash

<code>#!bin/bash
grep -E '^[0-9]+$' test.txt/<code>

以十六種語言編寫出相同的操作是一個有趣的練習,但是,接下來在本教程中,我們將主要使用Javascript和Python(最後還有一點Bash),因為這些語言(在我看來)傾向於產生最清晰和更可讀的實現。

1 – 年份匹配

我們來看看另外一個簡單的例子——匹配二十或二十一世紀中任何有效的一年。

<code>\\b(19|20)\\d{2}\\b/<code>

我們使用\\b而不是^和$來開始和結束這個正則表達式。\\b表示單詞邊界,或兩個單詞之間的空格。這允許我們在文本塊(而不是代碼行)中匹配年份,這對於搜索如段落文本非常有用。

  • \\b ——字邊界
  • (19|20) ——使用或(|)操作數匹配’19′或’20′。
  • \\d{2}——兩位數,與[0-9]{2}相同
  • \\b ——字邊界

請注意\\b不同於\\s,\\s是用於空格字符的代碼。\\b搜索一個單詞字符前面或者後面沒有另一個字符的地方,因此它搜索單詞字符的缺失,而\\s明確搜索空格字符。\\b特別適用於我們想要匹配特定序列/單詞的情況,而不是特定序列/單詞之前或之後有空格的情況。

1.0 – 真實示例 – 計數年份

我們可以在Python腳本中使用此表達式來查找維基百科歷史部分的文章中提及20或21世紀內年份的次數。

<code>import re
import urllib.request
import operator

# Download wiki page
url = "https://en.wikipedia.org/wiki/Diplomatic_history_of_World_War_II"
html = urllib.request.urlopen(url).read()

# Find all mentioned years in the 20th or 21st century
regex = r"\\b(?:19|20)\\d{2}\\b"
matches = re.findall(regex, str(html))

# Form a dict of the number of occurrences of each year
year_counts = dict((year, matches.count(year)) for year in set(matches))

# Print the dict sorted in descending order
for year in sorted(year_counts, key=year_counts.get, reverse=True):
print(year, year_counts[year])/<code>

上述腳本將按照提及的次數依次打印年份。

<code>1941 137
1943 80
1940 76
1945 73
1939 71
.../<code>

2 – 匹配時間

現在我們要定義一個正則表達式來匹配24小時格式(MM:HH,如16:59)的任何時間。

<code>\\b([01]?[0-9]|2[0-3]):([0-5]\\d)\\b/<code>
  • \\b——字邊界
  • [01]——0或1
  • ?——表示上述模式是可選的。
  • [0-9]—— 0到9之間的任何數字
  • |——OR操作數
  • 2[0-3]——2,後面跟0和3之間的任何數字(即20-23)
  • :——匹配:字符
  • [0-5]——0到5之間的任何數字
  • \\d——0到9之間的任何數字(與[0-9]相同)
  • \\b ——字邊界

2.0 – 捕獲組

你可能已經注意到上述模式中有了新內容—— 我們在括號 ( ... )中封裝小時和分鐘的捕獲片段。這允許我們將模式的每個部分定義為捕獲組。

捕獲組允許我們單獨提取、轉換和重新排列每個匹配模式的片段。

2.1 – 真實示例 – 時間分析

例如,在上述24小時模式中,我們定義了兩個捕獲組—— 時和分。

我們可以輕鬆地提取這些捕獲組。

以下是我們如何使用Javascript將24小時制的時間分解成小時和分鐘。

<code>const regex = /\\b([01]?[0-9]|2[0-3]):([0-5]\\d)/
const str = `The current time is 16:24`

const result = regex.exec(str)
console.log(`The current hour is ${result[1]}`)
console.log(`The current minute is ${result[2]}`)/<code>

第0個捕獲組始終是整個匹配表達式。

上述腳本將產生以下輸出。

<code>The current hour is 16
The current minute is 24/<code>

作為額外的訓練,你可以嘗試修改此腳本,將24小時制轉換為12小時制(am/pm)。

3 – 匹配日期

現在我們來匹配一個DAY/MONTH/YEAR樣式的日期模式。

<code>\\b(0?[1-9]|[12]\\d|3[01])([\\/\\-])(0?[1-9]|1[012])\\2(\\d{4})/<code>

這個有點長,但它看起來與我們上面講過的有些類似。

  • (0?[1-9]|[12]\\d|3[01])——匹配1到31之間的任何數字(前面的0是可選的)
  • ([\\/\\-])——匹配分隔符/或-
  • (0?[1-9]|1[012])—— 匹配1到12之間的數字
  • \\2——匹配第二個捕獲組(分隔符)
  • \\d{4}——匹配任意4位數(0000 – 9999)

這裡唯一新的概念是,我們使用\\2來匹配第二個捕獲組,即分隔符(/或-)。這使得我們能夠避免重複模式匹配規範,並且要求分隔符是一致的(如果第一個分隔符是/,那麼第二個分隔符也必須一樣)。

3.0 – 捕獲組替換

通過使用捕獲組,我們可以動態地重組和轉換我們的字符串輸入。

引用捕獲組的標準方法是使用$或\\符號,以及捕獲組的索引(請記住捕獲組元素是完整的捕獲文本)。

3.1 – 真實示例 – 日期格式轉換

假設我們的任務是將使用國際日期格式(DAY/MONTH/YEAR)的文檔集合轉換為美式(MONTH/DAY/YEAR)日期樣式。

我們可以通過替換模式$3$2$1$2$4或\\3\\2\\1\\2\\4使用上述正則表達式。

讓我們分解捕捉組。

  • $1——第一個捕獲組:日期。
  • $2——第二個捕捉組:分隔符。
  • $3——第三個捕獲組:月份。
  • $4——第四個捕獲組:年份。

替換模式(\\3\\2\\1\\2\\4)簡單地交換了表達式中月份和日期的內容。

以下是我們如何在Javascript中進行這種轉換:

<code>const regex = /\\b(0?[1-9]|[12]\\d|3[01])([ \\/\\-])(0?[1-9]|1[012])\\2(\\d{4})/
const str = `Today's date is 18/09/2017`
const subst = `$3$2$1$2$4`
const result = str.replace(regex, subst)
console.log(result)/<code>

上述腳本將打印Today's date is 09/18/2017到控制檯。

同樣的腳本在Python中是這樣的:

<code>import re
regex = r'\\b(0?[1-9]|[12]\\d|3[01])([ \\/\\-])(0?[1-9]|1[012])\\2(\\d{4})'
test_str = "Today's date is 18/09/2017"
subst = r'\\3\\2\\1\\2\\4'
result = re.sub(regex, subst, test_str)
print(result)/<code>

4 – 電子郵件驗證

正則表達式也可用於輸入驗證。

<code>^[^@\\s]+@[^@\\s]+\\.\\w{2,6}$/<code>

以上是一個(過於簡單的)Regex,用來匹配電子郵件地址。

  • ^——輸入開始
  • [^@\\s]——匹配除@和空格\\s之外的任何字符
  • +——1+次數
  • @——匹配’@'符號
  • [^@\\s]+——匹配除@和空格之外的任何字符,1+次數
  • \\.——匹配’.'字符。
  • \\w{2,6}——匹配任何字符(字母,數字或下劃線),2-6次
  • $——輸入結束

4.0 – 真實示例 – 驗證電子郵件

假設我們要創建一個簡單的Javascript函數以檢查輸入是否為有效的電子郵件。

<code>function isValidEmail (input) {
const regex = /^[^@\\s]+@[^@\\s]+\\.\\w{2,6}$/g;
const result = regex.exec(input)

// If result is null, no match was found
return !!result
}

const tests = [
`[email protected]`, // Valid
'', // Invalid
`test.test`, // Invalid
'@[email protected]', // Invalid
'invalid@@test.com', // Invalid
`gmail.com`, // Invalid
`this is a [email protected]`, // Invalid
`[email protected]@gmail.com` // Invalid
]

console.log(tests.map(isValidEmail))/<code>

此腳本的輸出應為[ true, false, false, false, false, false, false, false ]。

注意——在現實應用程序中,使用Regex驗證電子郵件地址對於許多情況,例如用戶註冊,是不夠的。但是一旦你確認輸入的文本是電子郵件地址,那麼你應該始終遵循發送確認/激活電子郵件的標準做法。

4.1 – 完整的電子郵件Regex

這是一個非常簡單的例子,它忽略了許多非常重要的電子郵件有效性邊緣情況,例如無效的開始/結束字符以及連續的週期。我真的不建議在你的應用程序中使用上述表達式;最好是使用一個有信譽的電子郵件驗證庫或繼續探索更完整的電子郵件驗證Regex。

例如,下面是一個來自emailregex.com的更高級的表達式,它匹配99%的RFC 5322兼容的電子郵件地址。

<code>(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21\\\\x23-\\\\x5b\\\\x5d-\\\\x7f]|\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\\\x01-\\\\x08\\\\x0b\\\\x0c\\\\x0e-\\\\x1f\\\\x21-\\\\x5a\\\\x53-\\\\x7f]|\\\\[\\\\x01-\\\\x09\\\\x0b\\\\x0c\\\\x0e-\\\\x7f])+)\\])/<code>

不過今天我們不打算深入探討。

5 – 代碼註釋模式匹配

Regex最有用的特殊用法之一是可以成為代碼重構器。大多數代碼編輯器支持基於Regex的查找/替換操作。一個格式正確的Regex替換可以將繁瑣的需要半小時忙碌的工作變成一個漂亮的Regex重構魔法。

不要編寫腳本來執行這些操作,試著在你選擇的文本編輯器中去做。幾乎每個文本編輯器都支持基於Regex的查找和替換。

Sublime中的Regex替換——http://docs.sublimetext.info/en/latest/search_and_replace/search_and_replace_overview.html#using-regular-expressions-in-sublime-text

Vim中的Regex替換——http://vimregex.com/#backreferences

VSCode中的Regex替換——https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options

Emacs中的Regex替換——https://www.gnu.org/software/emacs/manual/html_node/emacs/Regexp-Replace.html

5.0 – 提取單行CSS註釋

如果我們想要查找CSS文件中的所有單行註釋怎麼辦?

CSS註釋以/* Comment Here */的格式出現。

要捕獲任何單行CSS註釋,我們可以使用以下表達式。

<code>(\\/\\*+)(.*)(\\*+\\/)/<code>
  • \\/——匹配/符號(我們有轉義/字符)
  • \\*+——匹配一個或多個*符號(再次,我們使用\\來轉義*字符)。
  • (.*)——匹配任何字符(除了換行符\\n),任意次數
  • \\*+——匹配一個或多個*字符
  • \\/——匹配關閉/符號。

注意,我們已經在上面的表達式中定義了三個捕獲組:開放字符((\\/\\*+)),註釋內容((.*))和結束字符((\\*+\\/))。

5.1 – 真實示例 – 將單行註釋轉換為多行註釋

我們可以使用此表達式通過執行以下替換將單行註釋轉換為多行註釋。

<code>$1\\n$2\\n$3/<code>

在這裡,我們只是在每個捕獲組之間添加了一個換行符\\n。

嘗試在有以下內容的文件上執行此替換。

<code>/* Single Line Comment */
body {
background-color: pink;
}

/*
Multiline Comment
*/
h1 {
font-size: 2rem;
}

/* Another Single Line Comment */
h2 {
font-size: 1rem;
}/<code>

替換將產生相同的文件,但每個單行註釋轉換為多行註釋。

<code>/*
Single Line Comment
*/
body {
background-color: pink;
}

/*
Multiline Comment
*/
h1 {
font-size: 2rem;
}

/*
Another Single Line Comment
*/
h2 {

font-size: 1rem;
}/<code>

5.2 – 真實示例 – 標準化CSS註釋開頭

假設我們有一個又大又雜亂的CSS文件,是由幾個不同的人寫的。在這個文件中,有些註釋以/*開頭,有些以/**開頭,還有些以/*****開頭。

讓我們來寫一個Regex替換以標準化所有的單行CSS註釋,以/*開頭。

為了做到這一點,我們將擴展表達式,只匹配以兩個或更多星號開頭的註釋。

<code>(\\/\\*{2,})(.*)(\\*+\\/)/<code>

這個表達式與原來的非常相似。主要區別在於開頭我們用\\*{2,}替換了\\*+。\\*{2,}語法表示*的“兩個或多個”實例。

為了規範每個註釋的開頭,我們可以通過以下替代。

<code>/*$2$3/<code>

讓我們在以下測試CSS文件上運行此替換。

<code>/** Double Asterisk Comment */
body {
background-color: pink;
}

/* Single Asterisk Comment */
h1 {
font-size: 2rem;
}


/***** Many Asterisk Comment */
h2 {
font-size: 1rem;
}/<code>

結果將是與標準註釋開頭相同的文件。

<code>/* Double Asterisk Comment */
body {
background-color: pink;
}

/* Single Asterisk Comment */
h1 {
font-size: 2rem;
}

/* Many Asterisk Comment */
h2 {
font-size: 1rem;
}/<code>

6 – 匹配網址

另一個非常有用的Regex是在文本中匹配URL。

下面是一個來自Stack Overflow的URL匹配表達式的示例。

<code>(https?:\\/\\/)(www\\.)?(?<domain>[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6})(?<path>\\/[-a-zA-Z0-9@:%_\\/+.~#?&=]*)?/<path>/<domain>/<code>
  • (https?:\\/\\/)——匹配http(s)
  • (www\\.)?——可選的“www”前綴
  • (?<domain>[-a-zA-Z0-9@:%._\\+~#=]{2,256}——匹配有效的域名/<domain>
  • \\.[a-z]{2,6})——匹配域擴展擴展名(即“.com”或“.org”)
  • (?<path>\\/[-a-zA-Z0-9@:%_\\/+.~#?&=]*)?——匹配URL路徑(/posts)、查詢字符串(?limit=1)和/或文件擴展名(.html),這些都是可選的。/<path>

6.0 – 命名捕獲組

你注意到沒有,一些捕獲組現在以?<name>標識符開頭。這是命名捕獲組的語法,可以使得數據提取更加清晰。/<name>

6.1 – 真實示例 – 從Web頁面上的URL解析域名

以下是我們如何使用命名捕獲組來提取使用Python語言的網頁中每個URL的域名。

<code>import re
import urllib.request

html = str(urllib.request.urlopen("https://moz.com/top500").read())
regex = r"(https?:\\/\\/)(www\\.)?(?P<domain>[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6})(?P<path>\\/[-a-zA-Z0-9@:%_\\/+.~#?&=]*)?"
matches = re.finditer(regex, html)

for match in matches:
print(match.group('domain'))/<path>/<domain>/<code>

腳本將打印在原始網頁HTML內容中找到的每個域名。

<code>...
facebook.com
twitter.com
google.com
youtube.com
linkedin.com
wordpress.org
instagram.com
pinterest.com
wikipedia.org
wordpress.com
.../<code>

7 – 命令行的用法

許多Unix命令行實用程序也支持Regex!我們將介紹如何使用grep查找特定文件,以及使用sed替換文本文件內容。

7.0 – 真實示例 – 用grep匹配圖像文件

我們將定義另一個基本的Regex,這次是用於匹配圖像文件。

<code>^.+\\.(?i)(png|jpg|jpeg|gif|webp)$/<code>
  • ^——開始行。
  • .+——匹配任何字符(字母,數字,符號),除了\\n(換行)之外,1+次數。
  • \\.——匹配 ‘.’字符。
  • (?i)——表示下一個序列不區分大小寫。
  • (png|jpg|jpeg|gif|webp)——匹配常見的圖像文件擴展名
  • $——結束行

以下是如何列出Downloads目錄中所有圖像文件的方法。

<code>ls ~/Downloads | grep -E '^.+\\.(?i)(png|jpg|jpeg|gif|webp)$'/<code>
  • ls ~/Downloads——列出Downloads目錄中的文件
  • |——將輸出管道輸送到下一個命令
  • grep -E——使用正則表達式過濾輸入

7.1 – 真實例子 – 用sed進行電子郵件替換

bash命令中正則表達式的另一個好處是在文本文件中修改電子郵件。

這可以通過使用sed命令以及前面的電子郵件Regex的修改版本完成。

<code>sed -E -i 's/^(.*?\\s|)[^@]+@[^\\s]+/\\1\\{redacted\\}/g' test.txt/<code>
  • sed——Unix的“流編輯器”實用程序,允許強大的文本文件轉換。
  • -E——使用擴展的Regex模式匹配
  • -i——原位替換文件流
  • 's/^(.*?\\s|)——將行的開頭包裝在捕獲組中
  • [^@]+@[^\\s]+——電子郵件Regex的簡化版本。
  • /\\1\\{redacted\\}/g'——用{redacted}替換每個電子郵件地址。
  • test.txt——對test.txt文件執行操作。

我們可以在一個示例test.txt文件上運行上面的替換命令。

<code>My email is [email protected]/<code>

命令運行後,電子郵件將從test.txt文件中進行編輯。

<code>My email is {redacted}/<code>

警告——此命令將自動從你傳遞的任何test.txt中刪除所有電子郵件地址,因此,在運行它的時候要小心,因為此操作無法逆轉。要在終端中預覽結果,而不是替換原來的文本,只需省略-i標誌。

注意——儘管上述命令適用於大多數Linux發行版,但是macOS使用BSD實現是sed,它在其支持的Regex語法中受到更多的限制。要在MacOS上使用sed,並具有體面的正則表達式支持,我建議使用brew install gnu-sed安裝sed的GNU實現,然後從命令行使用gsed而不是sed。

8 – 什麼時候不使用Regex

好的,知道Regex是一個強大又靈活的工具了吧?!那麼,有沒有應該避免編寫Regex的時候?有!

8.0 – 語言解析

解析結構化語言,從英語到Java到JSON,使用正則表達式都是一種真正的痛苦。

當數據源中的邊緣情況或次要語法錯誤導致表達式失敗時,將導致最終(或即時)的災難,出於此目的去編寫你自己的正則表達式可能會讓你心情沮喪。

強化的解析器幾乎可用於所有機器可讀的語言,而NLP工具可用於人類語言——我強烈建議你使用其中一種,而不是嘗試編寫自己的語言。

8.1 – 安全 – 輸入過濾和黑名單

使用Regex過濾用戶輸入(例如來自Web表單),以及防止黑客嚮應用程序發送惡意命令(例如SQL注入),看上去似乎很誘人。

在這裡使用自定義的Regex是不明智的,因為它很難覆蓋每個潛在的攻擊向量或惡意命令。例如,黑客可以使用替代字符編碼繞過編寫得不全面的輸入黑名單過濾器。

這是另一個實例,對此我強烈建議你使用經過良好測試的庫和/或服務,以及使用白名單而不是黑名單,以保護你的應用程序免受惡意輸入。

8.2 – 性能密集的應用程序

正則表達式的匹配速度可以從不是非常快到極慢的範圍變動,取決於表達式寫得怎麼樣。對於大多數用例來說,這很好,特別是如果匹配的文本很短(例如電子郵件地址表單)的話。然而,對於高性能服務器應用程序,正則表達式會成為性能瓶頸,特別是如果表達式寫得不好或被搜索的文本很長的話。

8.3 – 對於不需要Regex的地方

正則表達式是一個非常有用的工具,但這並不意味著你應該在任何地方使用它。

如果問題有替代的解決方案,解決方案更簡單和/或不需要使用Regex,那麼請不要只是為了顯擺而使用Regex。Regex很棒,但它也是最不可讀的編程工具之一,而且很容易出現邊緣情況和bug。

過度使用Regex會讓你的同事(以及需要工作在你的代碼上的任何人)生氣惱怒,甚至恨不得揍你一頓。

結論

我希望這是對Regex的許多用途的一個有用的介紹。

還有很多Regex的用例是我們沒有涵蓋的。例如,可以在PostgreSQL查詢中使用Regex來動態地搜索數據庫中的文本模式。

我們還漏下了許多強大的Regex語法特性沒有覆蓋,如lookahead,lookbehind,atomic groups,recursion和subroutines。

要提高正則表達式技能並瞭解有關這些功能的更多信息,我推薦以下資源。

  • Learn Regex The Easy Way - https://github.com/zeeshanu/learn-regex
  • Regex101 - https://regex101.com/
  • HackerRank Regex Course - https://www.hackerrank.com/domains/regex/re-introduction

本教程中示例的源代碼可以在Github存儲庫中找到—— https://github.com/triestpa/You-Should-Learn-Regex

歡迎隨時對本教程提出任何建議、看法或批評。


分享到:


相關文章: