正则表达式随笔

正则表达式高级技巧
  文/余晟

环视结构(look-around)

分析日志(或普通数据文件),恐怕是大家在日常工作中经常遇到的问题,正则表达式应当是理所当然的选择,简单的正则表达式应用,大家应该都会,即使暂时不熟悉,查查资料也能解决。但是,有时候情况复杂,看起来正则表达式往往“束手无策”,其实事实并非如此。在这篇文章中,我们通过一个具体的例子,来讲解正则表达式的高级技巧。

事情源于朋友的一封来信:

“最近我遇到个小问题:公司让我处理日志文件,说实话我还真是巧,本来没有打算学正则,要是没有正则可能我这次还不知道怎么处理。简单说一下,主要任务是逐行读取数据,对每行内容进行分析,第一行是字段名,其余是日志内容,行与行之间没有联系,每行中字段内容用逗号隔开(但前两个字段和最后两个字段没有引号包围),逗号中的数据内容是用引号包围起来的,因为在生成日志的时候,没有考虑到在引号中的数据会存在逗号,所以无法整齐用切割函数类似split()的函数以逗号进行分割。所以我想了一个办法:把引号中的逗号全部换成别的符号,这样就可以切割了,我想了个正则表达式『("[^"]*")』,用它来找出引号字段,然后将其中的逗号替换掉,再处理。不知道有没有其它更好的办法?”

示例:

2007-11-6 0:41:37,15,"58.47.136.198","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NETCLR .1.4322)","gzzstresw,jgubrmkizefns55","","/ShowData.aspx","?db=cecdb&id=h17753&p=","大发财集团有限公司","http://www.somehost.net/somepath/07002.html","d1,浏览免费数据","","db=cecdb&dt=data&id=h17753&p=0",0,0

在这行数据中,共有两个字段包含引号且其中有逗号,它们分别是:

gzzstresw,jgubrmkizefns55

d1,浏览免费数据

类似的任务,许多读者应该都遇到过;类似的问题,或许大家都经历过——应用正则表达式时,有没有更好的办法?答案是:有,我们可以只用一个正则表达式,一个函数,实现完美的切分!

在详细的讲解之前,我们先罗列问题的特征:

1. 每行数据分为许多字段,以逗号分隔

2. 有的字段以引号包围,有的没有引号

3. 有引号的字段不可能出现在这一行的开头和结尾

4. 引号包围的字段中,可能存在逗号(补充一点,逗号不可能紧挨引号,而且这样的字段中逗号只有一个)

了解了这个问题,我们就可以着手考虑如何应用正则表达式了。我们知道,正则表达式的应用方式,通常有三大类:查找、替换和切分。查找,也叫提取、搜索,是从文本中提取具有某种结构特征的字符串;替换,类似于替换,是操作文本中具有某种结构特征的字符串,不过替换的字符串根据被替换字符串的内容动态生成;切分,是先在文本中进行查找,找到所有(或部分)具有某种结构特征的字符串,以它们为分隔符,将文本切分开来。针对这个具体的问题,我们最先想到的操作就是:切分。

如果使用切分,则作为切分符的逗号必须满足下面两个要求之一:

1. 之前和之后都是引号;

2. 不在引号字段之内。

针对第一个要求,选择的逗号之前和之后都必须有引号,写成正则表达式就是『(?<="),(?=")』,在这里我们使用了如今大多数正则表达式系统都支持环视(look-around)结构,用来规定需要查找的文本之前和之后的文本特征。其中的『(?<="),(?=")』称为逆序环视(look-behind)结构,表示逗号之前(左侧)必须出现一个引号,『(?<="),(?=")』是顺序环视(look-ahead)结构,表示逗号之后(右侧)必须出现一个引号。在这里,『(?<=")』和『(?=")』都是正则表达式预先定义的特殊结构,它们非常形象,大家在使用时可以按照具体的要求,把引号替换成自己需要的子表达式;

第二点要求逗号不能在引号字段内,如何判断逗号是否在引号字段内呢?我们不妨这样想:如果逗号在引号字段内,那么它与之前(左边)的第一个逗号(或者行开头位置)之间,必然存在引号(因为引号字段内不可能存在多于一个的逗号)。也就是说,我们要寻找的逗号与它之前的第一个逗号之间,不容许存在引号。仍然使用环视结构,我们得到这样一个正则表达式『(?<=(,|^)[^"]+),』。其中『(?<=(,|^)[^"]+),』对应之前的逗号或是行开头位置,而『(?<=(,|^)[^"]+),』表示两个逗号(或行开头与逗号)之间的字符不能是引号,否则就会匹配失败。这个表达式能够找到的逗号,应该就是我们需要的逗号了。

最后,因为这两个要求是“或(or)”的关系,我们使用多选分支将它们并列起来,最终得到的表达式就是『((?<="),(?=")| (?<=(,|^)[^"]+),)』。

事情到这里就结束了吗?答案是否定的。虽然我们的思路正确,表达式的格式和结构也没问题,但问题并没有解决,因为在大多数语言和系统中,逆序环视结构中出现的子表达式必须有确定的长度(有的系统中可以使用量词『?』,但无法使用『*』和『+』),只有.NET是个例外,也就是说,这种方法只有在.NET系统中才有现实意义(使用.NET的程序员这下该高兴了)。

那么,是否存在其他途径呢?我们知道,在进行日志分析时,除了切分,还有一种常用的操作,就是查找——在日志中迭代应用正则表达式进行查找,依次提取出需要的字段。下面我们尝试使用查找来解决这个问题。

如果使用查找,我们需要总结出字段的公共特征,这样,就能保证每次迭代,都能找到一个字段——无论它是有引号包围,还是没有引号包围的。

同样,我们首先还是分情况考虑(注意,因为逗号只是分隔符,所以我们提取的字段中不应该包含逗号,另外,也不应当包含引号):

1. 如果是没有引号包围的字段,可能以上一个逗号之后的位置,或行开头位置为起点,以下一个逗号之前的位置,或者行结束位置为终点,并且,之中不能包含引号或逗号;

2. 如果是引号包围的字段,则以引号为起点,以引号为终点,之中不能包含引号。补充一点,为了保证匹配的准确性,我们把条件设置得更强一些,开头的引号之前必须还有一个逗号,结尾的引号之后必须还有一个逗号,在实际应用正则表达式时,将条件设置得更强一些,保证正确性,是一个好习惯。

先来考虑第一种情况,对起点和终点的判断,都可以以环视结构来进行,判断起点的表达式是『(?<=(,|^))』,这里的逆序环视结构中,又出现了不定长度的表达式,在某些系统中无法编译通过,但这种简单的情况,我们有办法突破限制,即将表达式改写为『((?<=,)|(?<=^))』,判断终点的表达式『(?=(,|$))』,不包含引号或逗号的表达式『[^",]+』的联合,就得到了第一种情况对应的表达式『((?<=,)|(?<=^))[^",]+(?=(,|$))』;对应第二种情况的表达式非常容易,是『(?<=,")[^"]+(?= ",)』。

同样,两种情况是“或(or)”的关系,我们使用多选分支将它们并列起来,最终得到的表达式就是『(((?<=,)|(?<=^))[^",]+(?=(,|$))|(?<=,")[^"]+(?= ",))』。

在Java和Python语言中,这个正则表达式(注意,是这个正则表达式,不是这个字符串,编译时我们还需要转义,这个问题会在下一节文章详细谈到)都可以编译通过,没有问题。

好了,到这里,这个问题已经解决完毕,解决问题的思路和步骤,有兴趣的朋友可以再推敲推敲,在最后,我们详细介绍环视功能:相信我,它很有用,但知道的人并不多。

环视(look-around)用来检查某个位置两侧的文本,但不会把检查时匹配的文本加入匹配的最终结果。通常情况下,表达式『/bJeff/b』只能匹配“Jeff”这个单词,如果我们需要精确匹配“Jeffrey”这个单词中的“Jeff”,就可以使用环视『Jeff(?=rey)』,后面的『(?=rey)』表示,如果匹配成功,“Jeff”之后必须出现“rey”(但是这三个字符并不会包含在最终的匹配结果之中)。有的读者可能会说,那我直接使用『(Jeff)rey』,先找出来,再提取分组,不是一样吗?请注意,环视的对象又可以是正则表达式,『Jeff(?=(rey|erson))』就可以找到“Jeffrey”或“Jefferson”中的“Jeff”,这种灵活性是前一种做法无法提供的;再者说,『(Jeff)rey』使用括号来捕获文本,正则表达式在匹配时必须保存处理括号,保存文本,效率有所降低;而且,环视在处理中文文本时有独特的价值,因为中文的字符是连在一起的,单词之间没有空格分隔:如果一段文本中包含许多句子,有些只包含“北京”,有些包含“北京市”,我们需要仅仅将包含“北京”的句子都筛出来,就必须使用环视功能。此外,环视结构也可用于匹配的定位,保证准确性,在上面的例子中,我们就用环视结构,保证了字段两端引号匹配的准确性。

按照环视的方向不同,可以分为顺序环视(lookahead,表示从左向右检查)和逆序环视(lookbehind,从右向左检查);按照环视成立的条件不同,又可分为肯定环视(positive lookaround,只有在环视对象能匹配时才成功)和否定环视(negative lookaround,只有在环视对象无法匹配时才成功)。两者组合起来,就得到四种环视:

肯定顺序环视,要求右侧的文本必须能被环视内的表达式匹配

肯定逆序环视,要求左侧的文本必须能被环视内的表达式匹配

否定顺序环视,要求右侧的文本必须不能被环视内的表达式匹配

否定逆序环视,要求左侧的文本必须不能被环视内的表达式匹配

所使用的标记也很好识别,『(?=Regex)』表示肯定顺序环视,『(?!Regex)』表示否定顺序环视,『(?<=Regex)』表示肯定逆序环视,『(?<!Regex)』表示否定逆序环视。

在常见的HTML解析中,如果我们需要精确获得“src=...”中的资源地址(这里假定“src=...”的格式统一规范,等号两端没有空格,也没有引号),可以在表达式之前添加『(?<=src=)』,我们还可以用『(?<=<B>).*?(?=</B>)』来精确匹配“<B>...</B>”之中的内容。在这两个例子中,当然也可以使用匹配-括号提取的办法,但使用环视的效率更高,也更切合程序的本意。

还需要提到的一点是,在大多数系统(也就是.NET之外)中,逆序环视结构存在限制,一般来说其中的表达式所匹配的文本的长度必须固定,或者必须有上限。如果我们能确定表达式能匹配的文本有几种情况,就可以先列出这几种情况对应的环视结构,再用多选分支连立起来——在上文中,我们就是用这种方法绕过这种限制的——在编辑Apache的Rewrite规则时,这是一条很有用的经验。

转义符

在日常应用正则表达式时,我们经常会遇到这样的问题,正则表达式中到底该如何转义——最明显的表现就是,搞不懂究竟要使用多少个反斜线(你能迅速准确回答下面的问题吗:正则表达式中的一个反斜线,在Java语言中,究竟需要多少个反斜线来表示?)。结果,在大部分时候,我们盲目尝试,直到测试成功为止。但是,许多时候,这个办法实现起来并不方便。

为了彻底解决这类问题,我们需要弄清楚正则表达式与字符串的关系:它其实很简单,根据本人的经验,我们只需要牢记下面两条原则即可:

1.正则表达式必须以字符串的形式指定,但它不等于字符串

大多数语言中都存在正则表达式(regex)对象,譬如Java语言中的Pattern,.NET中的Regex。如果没有提供专用对象,一般需要用某些特殊的字符来标注正则表达式,譬如PHP中常用的反斜线'/';另一方面,正则表达式对某些字符或字符序列有自己的规定,不同于字符串的规定,譬如字符'/b',在正则表达式中,它表示单词分界符(word-boundary,用来匹配这样的位置,一侧是英文单词字符,一侧是非单词字符,关于单词字符的规定,请参考具体的语言文档),而在普通字符串中表示退格符(backspace)。因此我们可以说,正则表达式对文本的规定,并不等同于普通的字符串。

但是,正则表达式又终究是一种处理文本的语言,我们给出的所有正则表达式,大都是以字符串形式指定的。

所以,在正则表达式的应用过程中,往往需要进行从字符串到正则表达式本身的转换;我们也知道,从源代码中的字符序列,到语言中的字符串,也需要经过一个转换的过程。综合起来,我们在源代码中指定的字符序列,到最终生成正则表达式,需要经过两步转换:

“源代码中的字符序列”->“字符串”->“正则表达式”

我们来看下面这个例子(用Java语言举例)

Pattern pattern = Pattern.compile('//b');

其中,源代码中的字符序列是" //b ",经过转义,生成的字符串(String对象)包含两个字符:反斜线和小写字母'b',以正则表达式的方式解析这个字符串,得到的正则表达式对应单词分界符(word-boundary)。如果我们这样写:

Pattern pattern = Pattern.compile('/b');

仍然能够编译通过,但此时生成的字符串仅包含一个字符 :退格符,于是正则表达式接收到的也就是单个退格符。

这里有一点需要指出:在Java和C#之类的语言中,如源代码中的字符序列无法识别,编译会出错,譬如这样:

Pattern pattern = Pattern.compile('/w');

尽管我们知道,在正则表达式中,/w匹配单词字符(一般来说,是数字、字母和下划线),编译仍然会报错。因为根据针对字符串的规定,'/w'不是一个合法的转义序列,也就是说,我们无法由字符序列/w生成一个合法的字符串:

String s = '/w'; //编译出错!

但是PHP和Python之类的语言却不存在这样的问题。原因在于,如果PHP和Python发现字符串中有无法识别的转义序列,会原封不动地保存下来。如果我们在Python中这么写:

p = re.compile("/w")

是没有问题的,因为尽管/w无法识别,仍然会保存下来,在正则表达式中被正确解析。

当然,我们也可以在这些语言中使用'//w',结果是一样的,因为此时,在进行字符串处理时,第1个反斜线转义了第2个反斜线,正则表达式接受到的,同样是'/w'。

在实际开发中,这样的问题可能非常迷惑人,但只要我们弄清了正则表达式和字符串的关系,就不会再被它困扰。

2.正则表达式中单独出现的反斜线也需要转义

与字符串一样,在正则表达式中,反斜线通常与其他字符一起构成特殊的结构,譬如'/d'用来匹配数字字符,'/s'用来匹配空白字符,'/1'用来反向引用第一个括号内的字表达式(也就是编号为1的分组)捕获的文本,等等等等。

可是,如果我们的正则表达式中仅仅需要“反斜线”本身,也就是字符'/',该如何做呢?

其实,正则表达式对这个问题的处理,与字符串的处理是一样的,也就是说,在正则表达式中,必须用转义序列'//'来表示单个反斜线。

这个规定会带来一个有趣的问题:正则表达式中单独出现的反斜线字符,在生成正则表达式的时候,必须以转义序列'//'来表示,而这其中的每个反斜线字符,在表示正则表达式的字符串中,又必须以转义序列'//'来表示。所以,在字符串中,必须写出四个反斜线'////',才能对应到正则表达式中单独出现的一个反斜线字符:生成的字符串中,只包含两个反斜线字符'//';由这个字符串生成的正则表达式,就只包含一个反斜线字符'/'。

牢记这两条原则,在以后的开发中,面对正则表达式的转义问题,我们就不会感到迷惑了。 

作者简介:

余晟,抓虾网高级顾问,历任高级程序员,技术经理;解决过大量文本解析和数据抽取的问题;本科毕业于东北师范大学,主修计算机,副修中文,现居北京。对程序语言、算法、数据库和敏捷开发都有兴趣,译有《精通正则表达式》(第3版)。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>我的个人博客生成器</title> <style> /* 页面基础样式 */ body { font-family: 'Microsoft YaHei', Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; } /* 容器样式 - 左右布局 */ .container { display: flex; gap: 20px; max-width: 1200px; margin: 0 auto; } /* 输入区域样式 */ .input-area, .output-area { flex: 1; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } /* 文本区域样式 */ textarea { width: 100%; height: 300px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; line-height: 1.5; resize: vertical; box-sizing: border-box; } /* 提交按钮样式 */ input[type="submit"] { background-color: #0077cc; color: white; padding: 12px 30px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; } input[type="submit"]:hover { background-color: #005fa3; } /* 预览iframe样式 */ iframe { width: 100%; height: 400px; border: 1px solid #ccc; border-radius: 4px; background: white; } /* 标签样式 */ .tag { display: inline-block; background-color: #e0e0e0; color: #555; padding: 4px 12px; margin: 2px; border-radius: 15px; font-size: 0.8em; text-decoration: none; } .tag:hover { background-color: #d0d0d0; } /* 详情展开样式 */ details { margin-top: 20px; background: #f9f9f9; padding: 15px; border-radius: 4px; } summary { cursor: pointer; font-weight: bold; color: #0077cc; } pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; } h1 { text-align: center; color: #333; margin-bottom: 30px; } h3 { color: #555; border-bottom: 1px solid #eee; padding-bottom: 10px; } </style> </head> <body> <h1>🎨 我的个人博客生成器</h1> <div class="container"> <!-- 左侧输入区 --> <div class="input-area"> <form method="POST"> <h3>📝 请输入你的博客内容:</h3> <textarea name="blog_content" placeholder="在这里写下你的博客内容...例如: 今天发现了一个很棒的学习网站,https://www.php.net,这里有最权威的PHP文档。 还找到了一个不错的编程社区:https://stackoverflow.com 在这里可以找到各种编程问题的答案。 #PHP #编程学习 #Web开发 今天学习了字符串处理和正则表达式,感觉对文本处理有了新的认识。函数封装让代码更加模块化,易于维护。 期待后续学习更多有趣的知识!"><?php // 在文本区域中显示之前提交的内容,使用htmlspecialchars防止XSS攻击 if(isset($_POST['blog_content'])) { echo htmlspecialchars($_POST['blog_content']); } ?></textarea> <br><br> <input type="submit" value="🚀 生成博客!"> </form> </div> <!-- 右侧输出区 --> <div class="output-area"> <h3>👀 生成的博客预览:</h3> <?php /** * 任务1:将文本中的URL地址转换为可点击的HTML链接 * @param string $text 原始文本 * @return string 处理后的文本(包含HTML链接) */ function makeClickableLinks($text) { // 使用正则表达式匹配以http://或https://开头的URL // 正则表达式说明:/(https?:\/\/[^\s]+)/ // - `https?`:匹配 http 或 https(s?表示s出现0次或1次) // - `:\/\/`:匹配 :// // - `[^\s]+`:匹配一个或多个非空格的字符 $pattern = '/(https?:\/\/[^\s]+)/'; // 替换模式:将匹配到的URL用<a>标签包裹起来 // `$1` 是对正则中第一个括号内匹配到的内容的引用,即完整的URL $replacement = '<a href="$1" target="_blank" style="color: #0077cc; text-decoration: none;">$1</a>'; // 使用preg_replace函数执行正则替换 return preg_replace($pattern, $replacement, $text); } /** * 任务2:将文本中的标签(如#PHP)格式化为带样式的Span元素 * @param string $text 原始文本 * @return string 处理后的文本(包含格式化后的标签) */ function formatHashtags($text) { // 使用正则表达式匹配以#开头、后跟一个或多个字母/数字/下划线的标签 // 正则表达式说明:/#(\w+)/ // - `#`:匹配字符# // - `\w+`:匹配一个或多个字母、数字或下划线 $pattern = '/#(\w+)/'; // 替换模式:将匹配到的标签用<span>标签包裹,并添加一个CSS类`tag` // `$1` 是对正则中第一个括号内匹配到的内容的引用,即`#`后面的文字(如PHP) $replacement = '<span class="tag">#$1</span>'; // 使用preg_replace函数执行正则替换 return preg_replace($pattern, $replacement, $text); } /** * 任务3:将处理好的博客内容包装成一个完整的HTML页面 * @param string $content 已经过URL和标签处理的博客正文 * @return string 完整的HTML页面代码 */ function wrapBlogContent($content) { // 使用date函数获取当前日期,并格式化为"年-月-日"的形式 $currentDate = date("Y年m月d日"); // 使用nowdoc语法定义一个多行字符串,用于构建完整的HTML // 这种语法非常适合编写大段的、内嵌HTML的字符串 $html = <<<'HTML' <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>我的生成博客</title> <style> body { font-family: 'Microsoft YaHei', sans-serif; line-height: 1.6; padding: 40px; max-width: 800px; margin: 0 auto; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .blog-container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } .blog-title { color: #333; border-bottom: 3px solid #667eea; padding-bottom: 15px; margin-bottom: 20px; } .blog-date { color: #666; font-size: 0.9em; margin-bottom: 30px; padding: 10px; background: #f8f9fa; border-radius: 5px; } .blog-content { margin-top: 20px; font-size: 16px; } .blog-content p { margin-bottom: 20px; } .tag { display: inline-block; background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 5px 15px; margin: 5px; border-radius: 20px; font-size: 0.8em; font-weight: bold; } a { color: #667eea; text-decoration: none; font-weight: bold; } a:hover { text-decoration: underline; } </style> </head> <body> <div class="blog-container"> <h1 class="blog-title">✍️ 我的博客随笔</h1> <p class="blog-date">📅 发布日期: HTML; // 拼接日期 $html .= $currentDate . '</p>'; // 拼接内容 $html .= '<div class="blog-content">' . nl2br($content) . '</div>'; // 闭合标签 $html .= '</div></body></html>'; // 返回最终生成的完整HTML字符串 return $html; } // 检查是否是POST请求并且有博客内容 if ($_SERVER["REQUEST_METHOD"] == "POST" && !empty($_POST['blog_content'])) { // 获取原始博客内容 $originalContent = $_POST['blog_content']; echo "<h4>✅ 处理流程:</h4>"; echo "<ol>"; echo "<li>原始内容长度: " . strlen($originalContent) . " 字符</li>"; // !!!核心处理流程!!! // 步骤1:调用函数,转换URL为链接 $contentWithLinks = makeClickableLinks($originalContent); echo "<li>已识别并转换URL链接</li>"; // 步骤2:调用函数,格式化标签 $formattedContent = formatHashtags($contentWithLinks); echo "<li>已格式化标签</li>"; // 步骤3:调用函数,将最终内容包装成完整HTML页面 $finalBlogHTML = wrapBlogContent($formattedContent); echo "<li>已生成完整HTML页面</li>"; echo "</ol>"; // 在预览区域显示生成的博客 echo "<h4>🎯 博客预览:</h4>"; echo "<iframe srcdoc=\"" . htmlspecialchars($finalBlogHTML) . "\" title=\"博客预览\"></iframe>"; // 显示生成的HTML源代码,供调试学习 echo "<details>"; echo "<summary>🔍 查看生成的HTML源码</summary>"; echo "<pre>" . htmlspecialchars($finalBlogHTML) . "</pre>"; echo "</details>"; } elseif ($_SERVER["REQUEST_METHOD"] == "POST") { // 如果提交了表单但内容为空 echo "<div style='color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 4px;'>"; echo "⚠️ 请输入博客内容后再点击生成按钮!"; echo "</div>"; } else { // 首次访问页面时的提示 echo "<div style='color: #666; text-align: center; padding: 40px;'>"; echo "<p>👆 请在左侧输入你的博客内容,然后点击生成按钮</p>"; echo "<p>✨ 系统会自动:</p>"; echo "<ul style='text-align: left; display: inline-block;'>"; echo "<li>将网址转换为可点击链接</li>"; echo "<li>格式化 #标签 为美观样式</li>"; echo "<li>生成完整的博客页面</li>"; echo "</ul>"; echo "</div>"; } ?> </div> </div> </body> </html>一、基于补充的“个人博客生成器”项目,执行进阶版任务,具体任务要求: 在原来“博客预览”的下方,新增一个“博客内容分析报告”区域。学生需要编写新的函数,实现以下四个核心统计功能: 统计基础信息:总字符数、总单词数、预计阅读时间。 统计标签数量:识别并统计文章中使用了多少个#标签。 统计链接数量:识别并统计文章中包含了多少个http/https链接。 关键词高亮:让用户输入一个自定义关键词(如“PHP”),并在原文中高亮显示它。 WEB页面样式不限; 二、任务要点: 1、创建统计函数:创建至少3个新的自定义函数来完成统计。 getBasicStats($text): 统计字符数、单词数,并计算阅读时间(假设每分钟阅读200个单词)。 countHashtags($text): 使用正则表达式统计并返回所有#标签的数量和列表。 countLinks($text): 使用正则表达式统计文本中http/https链接的数量。 2、实现关键词高亮:在左侧表单中新增一个文本输入框,用于让用户输入想要高亮的关键词。 编写一个 highlightKeyword($text, $keyword) 函数,使用字符串替换功能,将文中所有的关键词替换为 <mark style="background-color: yellow;">$keyword</mark>。 3、整合与显示: 在生成博客后,调用这些统计函数。 将统计结果以清晰、美观的格式显示在“博客预览”区域下方。 三、关键知识点提示: 字符串函数:strlen(), str_word_count(), str_ireplace() 正则表达式:preg_match_all() 这是完成统计的关键匹配步骤。 自定义函数:如何设计函数接收参数并返回结果。 表单处理:如何获取新增的输入框的值 ($_POST['keyword'])。输出修改后的完整php代码
最新发布
11-28
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值