你应该学习正则表达式
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]+$ 让我们一点一点的解释吧。
我们可以用伪英文重写这个Regex为 很简单,不是吗?
这个表达式(和一般的正则表达式)的伟大之处在于它无需太多修改,就可以用到任何编程语言中。 为了演示,我们先快速了解如何使用16种最受欢迎的编程语言对文本文件执行此简单的Regex搜索。 我们使用以下输入文件( 1234 abcde 12db2 5362 1 每个脚本都将使用这个正则表达式读取并搜索 语言范例0.0 – Javascript / Node.js / Typescriptconst 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 – Pythonimport 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 – RfileLines <- readLines("test.txt") results <- grep("^[0-9]+$",fileLines,value = TRUE) print (results) 0.3 – RubyFile.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 – Haskellimport Text.Regex.PCRE main = do fileContents <- readFile "test.txt" let stringResult = fileContents =~ "^[0-9]+$" :: AllTextMatches [] String print (getAllTextMatches stringResult) 0.5 – Perlopen 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 – Gopackage 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 – Javaimport 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 – Kotlinimport 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 – Scalaimport 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 – Swiftimport 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 – Rustextern 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 我们使用
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
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]}`)
上述脚本将产生以下输出。 The current hour is 16 The current minute is 24 作为额外的训练,你可以尝试修改此脚本,将24小时制转换为12小时制(am/pm)。 3 – 匹配日期现在我们来匹配一个 b(0?[1-9]|[12]d|3[01])([/-])(0?[1-9]|1[012])2(d{4}) 这个有点长,但它看起来与我们上面讲过的有些类似。
这里唯一新的概念是,我们使用 3.0 – 捕获组替换通过使用捕获组,我们可以动态地重组和转换我们的字符串输入。 引用捕获组的标准方法是使用 3.1 – 真实示例 – 日期格式转换假设我们的任务是将使用国际日期格式( 我们可以通过替换模式 让我们分解捕捉组。
替换模式( 以下是我们如何在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) 上述脚本将打印 同样的脚本在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'32124' result = re.sub(regex,subst,test_str) print(result) 4 – 电子邮件验证正则表达式也可用于输入验证。 ^[^@s]+@[^@s]+.w{2,6}$ 以上是一个(过于简单的)Regex,用来匹配电子邮件地址。
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)) 此脚本的输出应为
4.1 – 完整的电子邮件Regex这是一个非常简单的例子,它忽略了许多非常重要的电子邮件有效性边缘情况,例如无效的开始/结束字符以及连续的周期。我真的不建议在你的应用程序中使用上述表达式;最好是使用一个有信誉的电子邮件验证库或继续探索更完整的电子邮件验证Regex。 例如,下面是一个来自emailregex.com的更高级的表达式,它匹配99%的RFC 5322兼容的电子邮件地址。 (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[x01-x08x0bx0cx0e-x1fx21x23-x5bx5d-x7f]|[x01-x09x0bx0cx0e-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-x08x0bx0cx0e-x1fx21-x5ax53-x7f]|[x01-x09x0bx0cx0e-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注释以 要捕获任何单行CSS注释,我们可以使用以下表达式。 (/*+)(.*)(*+/)
注意,我们已经在上面的表达式中定义了三个捕获组:开放字符( 5.1 – 真实示例 – 将单行注释转换为多行注释我们可以使用此表达式通过执行以下替换将单行注释转换为多行注释。 $1n$2n$3 在这里,我们只是在每个捕获组之间添加了一个换行符 尝试在有以下内容的文件上执行此替换。 /* 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$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@:%_/+.~#?&=]*)?
6.0 – 命名捕获组你注意到没有,一些捕获组现在以 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,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!我们将介绍如何使用 7.0 – 真实示例 – 用
|