正则表达式是爬虫程序中经常使用的一种内容解析与提取技术。本节将主要介绍正则表达式的相关知识和应用。
正则表达式是一种用于匹配和查找文本的强大工具。它由一系列字符和特殊字符组合而成,用于描述要匹配的文本模式。
正则表达式中的字符分为普通字符和特殊字符两类。
普通字符(也称为字面字符)指的是那些在模式匹配中代表它们自身的字符。例如,大小写字母和数字等。
特殊字符(也称为元字符)具有特定的含义,而非字符的字面意思。特殊字符通常用于构建复杂的匹配模式。例如,字符“.”匹配除换行符外的任意单个字符。
在正则表达式中,特殊字符又可以分为转义字符、字符集合、定位字符、字符类、量词、逻辑字符和分组字符。
在正则表达式中,反斜杠“\”用作转义字符。转义字符可以使特殊字符被解释为字面意义上的字符,同时可以赋予普通字符特殊的意义。
在正则表达式中,字符集合由中括号“[]”表示。例如,[abc]匹配字母a、b、c中的任意一个。[0-9]则匹配数字0~9中的任意一个数字。
● ^:匹配输入字符串的开始。
● $:匹配输入字符串的结束。
● \b:匹配一个单词边界。
● \B:匹配非单词边界。
在编写正则表达式时,我们经常需要在同一表达式中频繁且多次匹配某些字符(例如数字)。为了简化正则表达式的编写,正则表达式语法中引入了字符类的概念。常用的字符类以及对应的字符集合表达式如表2-1所示。
表2-1 常用的字符类以及对应的字符集合表达式
在编写正则表达式的过程中,我们可能需要多次匹配某种类型的数字。例如,要匹配数字3次,可以编写表达式“\d\d\d”。如果要匹配11位的手机号码,则需要编写表达式“\d\d\d\d\d\d\d\d\d\d\d”,这看起来是一件令人感到恐怖的事情。幸运的是,量词为我们简化了这类表达式。例如,上面的表达式“\d\d\d\d\d\d\d\d\d\d\d”可以使用量词简写为“\d{11}”。正则表达式中常用的量词如下。
● *:匹配前面的子表达式零次或多次。
● +:匹配前面的子表达式一次或多次。
● ?:匹配前面的子表达式零次或一次。
● {n}:匹配前面的子表达式恰好 n 次。
● {n,}:匹配前面的子表达式至少 n 次。
● {n,m}:匹配前面的子表达式至少 n 次,但不超过 m 次。
在正则表达式中,逻辑运算符“|”用于表示逻辑或。例如,表达式“x|y”表示匹配x或者y。
在正则表达式中,捕获分组(capture group)指的是圆括号“()”中的子表达式。
● (…):捕获括号,匹配括号内的表达式,可以捕获此数据以供后续使用。
● (?:…):非捕获括号,匹配括号内的表达式,但不捕获匹配的数据。
下面的Java代码示例展示了如何引用捕获分组来提取匹配的子字符串。
String text = "Hello, my email address is zhangkai108@qq.com"; // 定义正则表达式模式,捕获电子邮件前缀 String regex = "\\b([a-zA-Z0-9._%+-]+)@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\b"; // 编译正则表达式模式 Pattern pattern = Pattern.compile(regex); // 创建 Matcher 对象 Matcher matcher = pattern.matcher(text); // 查找匹配的子字符串 if (matcher.find()) { // 获取整个正则表达式匹配的子字符串 String email = matcher.group(); System.out.println("Email found: " + email); // 获取第一个捕获分组 String prefix = matcher.group(1); System.out.println("Prefix: " + prefix); }
在上面的示例中,我们使用正则表达式来匹配电子邮件地址,并通过捕获分组提取电子邮件地址中的域名部分。在matcher.group(1)中,我们引用了第一个捕获分组来获取电子邮件地址的域名部分。
除了通过编号引用捕获分组外,我们还可以为捕获分组设置名称,并通过名称引用它。具体实现方式如下:
String text = "Hello, my email address is zhangkai108@qq.com"; // 定义正则表达式,使用命名捕获组 String regex = "\\b(?<prefix>[a-zA-Z0-9._%+-]+)@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\b"; // 编译正则表达式模式 Pattern pattern = Pattern.compile(regex); // 创建 Matcher 对象 Matcher matcher = pattern.matcher(text); // 查找匹配的子字符串 if (matcher.find()) { // 获取整个正则表达式匹配的子字符串 String email = matcher.group(); System.out.println("Email found: " + email); // 获取命名捕获组的值 String prefix = matcher.group("prefix"); System.out.println("Prefix: " + prefix); }
在前面的内容中,我们介绍了正则表达式的基础语法。本小节将介绍一些正则表达式的高级应用技巧。
零宽断言是正则表达式中的一种高级应用技巧。它们是一种特殊的匹配规则,本身表达式不匹配任何字符(即匹配零个字符,所以被称为零宽),而是用于判断字符串是否满足某种条件(即断言)。零宽断言通常以“(?…)”的形式出现在正则表达式中,其中“…”是具体的断言内容。零宽断言的工作方式有4种,如表2-2所示。
表2-2 零宽断言类型
非贪婪模式是正则表达式中的一个高级应用技巧。在正则表达式中,贪婪模式是指匹配尽可能多的字符,直到无法继续匹配或达到结尾。相反,非贪婪模式(也称为懒惰模式)是指匹配尽可能少的字符,只要满足匹配条件即可。非贪婪模式通常通过在量词后面添加“?”来实现。
假设现在有一段HTML代码片段,其内容为<div><p>Hello</p><p>World</p></div>,需求是提取出所有的<p>标签元素列表。
如果我们编写如下代码,得到的打印结果会是<p>Hello</p><p>World</p>,而不是<p>标签元素列表。
String htmlText = "<div><p>Hello</p><p>World</p></div>"; // 定义正则表达式模式 String regex = "<p>(.*)</p>"; // 编译正则表达式模式 Pattern pattern = Pattern.compile(regex); // 创建 Matcher 对象 Matcher matcher = pattern.matcher(htmlText); // 存储匹配结果 List<String> pTags = new ArrayList<>(); // 查找匹配的子字符串 while (matcher.find()) { // 获取匹配的子字符串 String pTag = matcher.group(1); pTags.add(pTag); } // 输出提取的<p>标签元素列表 for (String pTag : pTags) { System.out.println("<p>" + pTag + "</p>"); }
非贪婪模式可以帮助我们解决这个问题,我们只需要将正则表达式从<p>(.*)</p>修改成<p>(.*?)</p>即可。常用的非贪婪模式表达式如表2-3所示。
表2-3 非贪婪模式表达式
正则表达式中的反向引用功能可以帮助我们在表达式的后面部分引用前面部分已经匹配的子表达式。
在正则表达式中,我们可以通过圆括号“()”来创建捕获分组(子表达式),然后通过“\ n ”( n 为正整数)的形式来引用这些捕获分组。 n 代表捕获分组的顺序,例如“\1”引用第一个捕获分组,“\2”引用第二个捕获分组,以此类推。
假设我们需求检查文本中是否存在连续重复的单词。这个需求可以借助正则表达式的反向引用功能来实现。具体示例代码如下:
String text = "This is is a test test example."; // 正则表达式,使用反向引用匹配重复的单词 Pattern pattern = Pattern.compile("\\b(\\w+)\\b \\1\\b"); Matcher matcher = pattern.matcher(text); while (matcher.find()) { System.out.println("Duplicate word found: " + matcher.group(1)); }
上面的示例代码通过反向引用功能查找字符串中的重复单词,“\\1”引用了第一个捕获组(\\w+),这表示它会匹配与第一个捕获组相同的单词。
在爬虫程序中,正则表达式是一种常用的内容解析与提取工具。其主要应用场景如下。
在爬虫程序中,正则表达式通常用于清洗和筛选爬取到的数据。例如,去除多余的标签、空格或特殊字符,筛选出我们需要的内容。
假设我们有一个需求,目标是过滤掉一个HTML网页源码中的所有script标签元素和style标签元素。对于这个需求,可以通过正则表达式来实现。具体实现代码如下:
/** * 匹配script标签的正则表达式 **/ private static final String REGEX_SCRIPT = "<script[^>]*>[\\s\\S]*?<\\/script>"; /** * 匹配style标签的正则表达式 **/ private static final String REGEX_STYLE = "<style[^>]*>[\\s\\S]*?<\\/style>"; public static String removeHTMLTags(String htmlContent) { // 删除 script 标签 Pattern scriptPattern = Pattern.compile(REGEX_SCRIPT, Pattern.CASE_INSENSITIVE); Matcher scriptMatcher = scriptPattern.matcher(htmlContent); htmlContent = scriptMatcher.replaceAll(""); // 删除 style 标签 Pattern stylePattern = Pattern.compile(REGEX_STYLE, Pattern.CASE_INSENSITIVE); Matcher styleMatcher = stylePattern.matcher(htmlContent); htmlContent = styleMatcher.replaceAll(""); return htmlContent.trim(); }
上面的代码片段逻辑简单,下面我们主要解析一下上述正则表达式语句。
正则表达式<script[^>]*>[\\s\\S]*?<\\/script>用于匹配script标签及其包含的内容,具体解析如下:<script[^>]*>匹配script标签元素的起始内容,例如<script>或<script type="application/javascript">。
[\\s\\S]*?以非贪婪模式匹配任意字符0次或多次。注意,这里使用的是正则表达式中的非贪婪模式。在默认情况下,正则表达式采用贪婪匹配模式,即尽可能匹配最长的字符串。如果匹配到多个符合要求的字符串,它最终会选择最长的字符串。而在非贪婪模式下,正则表达式会匹配尽可能短的字符串。在正则表达式中,量词后面添加问号表示非贪婪模式。
举例来说,假设现在有一段HTML文本为:<script>alert(1)</script><div>test</div><script>alert(2)</script>。正则表达式<script[^>]*>[\\s\\S]*?<\\/script>会找到两个匹配字符串<script>alert(1)</script>和<script>alert(2)</script>,但是<script[^>]*>[\\s\\S]*<\\/script>会匹配到整个HTML文本字符串。
一般来说,我们可以通过Selenium中的元素定位器来提取网页链接。不过,有些网站的跳转链接并没有体现在HTML元素的href属性中,而是通过JavaScript脚本来实现网页的跳转操作。例如下面这段HTML代码:
<div onclick=showArticleDetail('8a81f6d88cf', 'abc')>文章1</div> <div onclick=showArticleDetail('8a81f6d88ef', 'abd')>文章2</div> <script> function showArticleDetail(id1, id2) { var url = "https://www.example.com?id1=" + id1 + "&id2=" + id2; window.location.href = url; } </script>
针对上面的HTML代码,可以采用正则表达式提取网页的跳转链接。具体实现思路可以参考下面的Java代码:
// 定义正则表达式,用于匹配showArticleDetail函数的参数 String regexPattern = "onclick=showArticleDetail\('([0-9a-z]+?)', '([0-9a-z]+?)'"; // 编译正则表达式,并且忽略字母大小写 Pattern compile = Pattern.compile(regexPattern, Pattern.CASE_INSENSITIVE); // 创建Matcher对象并使用正则表达式匹配articleElement(就是div元素) Matcher matcher = compile.matcher(articleElement); // 获取匹配分组 String[] groups = new String[matcher.groupCount()]; if (matcher.find()) { for (int i = 1; i <= matcher.groupCount(); i++) { groups[i - 1] = matcher.group(i); } // 拼接跳转链接 String url = String.format("https://www.example.com?id1=%s&id2=%s", groups); }
上述代码片段展示了如何在Java中使用正则表达式提取并构建基于JavaScript动态生成的URL。这种方法适用于网页内容动态生成且链接不直接出现在HTML href属性中的情况。正则表达式能够提取那些通过JavaScript动态生成的链接。当然,这也要求我们对页面的JavaScript代码逻辑有一定的理解,以便正确地构建正则表达式并提取链接。
需要注意的是,依赖正则表达式提取网页跳转链接时,需要定期检查网页结构的变化,因为页面结构的变化可能会导致正则表达式失效。后续还会介绍其他提取动态生成网页跳转链接的方法。
在网页内容中,有很多格式相对固定的信息。例如,文章的多级标题、招聘文章中的报名时间等。现在,我们以文章中的多级标题内容提取为例,演示正则表达式如何在格式化内容提取功能中发挥作用。
假设我们现在有一批网页文章,需求是提取出这批网页文章中的一级标题、二级标题和三级标题内容。如果文章内容中的标题级别不足三级,则展示的优先级为一级标题>二级标题>三级标题。
当面对这种需求时,可以利用多优先级正则表达式来完成。
首先,我们要做的第一项任务是统计样本文章的各级标题样式。假设统计的样本文章各级标题格式化内容如表2-4所示。
表2-4 标题格式统计结果
根据上面的统计结果,我们可以构建如下的优先级正则表达式列表:
[ { "patternStr": "^[一二三四五六七八九]、(.*)[^。]$", "priority": 1 }, { "patternStr": "^[一二三四五六七八九]、(.*?)。", "priority": 2 }, { "patternStr": "^[((][一二三四五六七八九十][))](.*)[^。]$", "priority": 3 }, { "patternStr": "^。[一二三四五六七八九]是(.*?)。", "priority": 4 } ]
根据上面的正则表达式JSON数组,可以编写代码如下:
List<String> firstLevelTitles = Lists.newArrayList(); List<String> secondLevelTitles = Lists.newArrayList(); List<String> thirdLevelTitles = Lists.newArrayList(); // 按照优先级从高到低,遍历优先级正则表达式列表 for(PriorityRegex regex : patterns) { List<String> tempTitles = Lists.newArrayList(); // 遍历文章所有段落,查找符合要求的标题文本 for(String para : paras) { para = para.trim(); Matcher matcher = regex.getPattern().matcher(para); if(matcher.find()) { tempTitles.add(matcher.group()); } } // 按照一级标题 > 二级标题 > 三级标题的优先级顺序存储标题内容 if(firstLevelTitles.isEmpty()) firstLevelTitles.addAll(tempTitles); else if(secondLevelTitles.isEmpty()) secondLevelTitles.addAll(tempTitles); else if(thirdLevelTitles.isEmpty()) thirdLevelTitles.addAll(tempTitles); }
正则表达式还有很多其他的应用场景,例如数据验证、关键词匹配等。因为篇幅原因,这里不再一一列举,读者在日常使用中注意灵活运用。