你應該學習正規表示式

2017-10-18    分類:作業系統、程式設計開發、首頁精華0人評論發表於2017-10-18

本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

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

什麼是正規表示式?

正規表示式(或Regex,或Regexp)是使用字元序列描述複雜搜尋模式的一種方式。

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

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

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

Learn once, write anywhere

幾乎任何程式語言都可以使用Regex。Regex的知識對於驗證使用者輸入,與Unix shell進行互動,在你喜歡的文字編輯器中搜尋/重構程式碼,執行資料庫文字搜尋等等都非常有用。

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

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

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

0 – 匹配任何數字行

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

^[0-9]+$

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

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

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

很簡單,不是嗎?

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

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

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

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

1234
abcde
12db2
5362

1

每個指令碼都將使用這個正規表示式讀取並搜尋test.txt檔案,並將結果('1234', '5362', '1')輸出到控制檯。

語言範例

0.0 – Javascript / Node.js / Typescript

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

0.1 – Python

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)

0.2 – R

fileLines <- readLines("test.txt")
results <- grep("^[0-9]+$", fileLines, value = TRUE)
print (results)

0.3 – Ruby

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

0.4 – Haskell

import Text.Regex.PCRE

main = do
  fileContents <- readFile "test.txt"
  let stringResult = fileContents =~ "^[0-9]+$" :: AllTextMatches [] String
  print (getAllTextMatches stringResult)

0.5 – Perl

open my $fh, '<', 'test.txt' or die "Unable to open file $!";
read $fh, my $file_content, -s $fh;
close $fh;
my $regex = qr/^([0-9]+)$/mp;
my @matches = $file_content =~ /$regex/g;
print join(',', @matches);

0.6 – PHP

<?php
$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);
?>

0.7 – Go

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)
}

0.8 – Java

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();
    }
  }
}

0.9 – Kotlin

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)

0.10 – Scala

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)
  }
}

0.11 – Swift

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)
}

0.12 – Rust

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()]);
  }
}

0.13 – C#

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));
        }
    }
}

0.14 – C++

#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;
  }
}

0.15 – Bash

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

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

1 – 年份匹配

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

\b(19|20)\d{2}\b

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

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

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

1.0 – 真實示例 – 計數年份

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

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])

上述指令碼將按照提及的次數依次列印年份。

1941 137
1943 80
1940 76
1945 73
1939 71
...

2 – 匹配時間

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

\b([01]?[0-9]|2[0-3]):([0-5]\d)\b
  • \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小時制的時間分解成小時和分鐘。

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]}`)

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

上述指令碼將產生以下輸出。

The current hour is 16
The current minute is 24

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

3 – 匹配日期

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

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

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

  • (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中進行這種轉換:

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)

上述指令碼將列印Today's date is 09/18/2017到控制檯。

同樣的指令碼在Python中是這樣的:

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)

4 – 電子郵件驗證

正規表示式也可用於輸入驗證。

^[^@\s]+@[^@\s]+\.\w{2,6}$

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

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

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

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

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 = [
  `test.test@gmail.com`, // Valid
  '', // Invalid
  `test.test`, // Invalid
  '@invalid@test.com', // Invalid
  'invalid@@test.com', // Invalid
  `gmail.com`, // Invalid
  `this is a test@test.com`, // Invalid
  `test.test@gmail.comtest.test@gmail.com` // Invalid
]

console.log(tests.map(isValidEmail))

此指令碼的輸出應為[ true, false, false, false, false, false, false, false ]

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

4.1 – 完整的電子郵件Regex

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

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

(?:[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])+)\])

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

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註釋,我們可以使用以下表示式。

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

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

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

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

$1\n$2\n$3

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

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

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

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

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

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

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

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

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

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

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

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

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

(\/\*{2,})(.*)(\*+\/)

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

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

/*$2$3

讓我們在以下測試CSS檔案上執行此替換。

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

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

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

結果將是與標準註釋開頭相同的檔案。

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

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

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

6 – 匹配網址

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

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

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

6.0 – 命名捕獲組

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

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

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

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'))

指令碼將列印在原始網頁HTML內容中找到的每個域名。

...
facebook.com
twitter.com
google.com
youtube.com
linkedin.com
wordpress.org
instagram.com
pinterest.com
wikipedia.org
wordpress.com
...

7 – 命令列的用法

許多Unix命令列實用程式也支援Regex!我們將介紹如何使用grep查詢特定檔案,以及使用sed替換文字檔案內容。

7.0 – 真實示例 – 用grep匹配影像檔案

我們將定義另一個基本的Regex,這次是用於匹配影像檔案。

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

以下是如何列出Downloads目錄中所有影像檔案的方法。

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

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

bash命令中正規表示式的另一個好處是在文字檔案中修改電子郵件。

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

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

我們可以在一個示例test.txt檔案上執行上面的替換命令。

My email is patrick.triest@gmail.com

命令執行後,電子郵件將從test.txt檔案中進行編輯。

My email is {redacted}

警告——此命令將自動從你傳遞的任何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語法特性沒有覆蓋,lookaheadlookbehindatomic groupsrecursionsubroutines

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

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

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

譯文連結:http://www.codeceo.com/article/you-should-learn-regex.html
英文原文:YOU SHOULD LEARN REGEX
翻譯作者:碼農網 – 小峰
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章