<think>
我们正在一个个人博客生成器项目中添加博客内容分析报告区域。需要实现以下功能:
1. 统计基础信息:字符数、单词数、阅读时间(一般按每分钟200-250单词计算,这里我们取250)
2. 统计标签数量:计算文章中出现的HTML标签数量(例如,<p>, <a>, <h1>等)
3. 统计链接数量:统计文章中出现的所有超链接(<a>标签的href属性)的数量
4. 关键词高亮:在文章内容中高亮显示指定的关键词(例如,用<mark>标签包裹)
我们将创建一个函数来生成分析报告,并在博客文章的适当位置显示这个报告。
注意:由于博客内容可能包含HTML标签,我们在统计字符数和单词数时,需要先去除HTML标签,只统计纯文本。
步骤:
1. 去除HTML标签,获取纯文本内容。
2. 统计字符数(包括空格)和单词数(按空格分割)。
3. 计算阅读时间(单词数除以250,向上取整)。
4. 统计HTML标签数量:我们可以使用正则表达式或DOM解析来统计。这里为了简单,我们使用正则表达式匹配标签名(注意:自闭合标签也要考虑)。
5. 统计链接数量:同样,我们可以使用正则表达式匹配<a>标签,或者使用DOM解析。这里我们使用正则表达式匹配<a>标签的href属性(注意:可能存在大小写,所以不区分大小写)。
6. 关键词高亮:我们需要在原始内容(包含HTML)中高亮关键词,但要避免高亮HTML标签内的内容。所以最好的方法是先解析HTML,然后只处理文本节点。但为了简单,我们可以用正则表达式替换,但要注意替换不能发生在标签内。因此,我们将使用一个更安全的方法:将内容分割成文本和标签,只对文本部分进行替换,然后再组合。
由于安全地高亮关键词而不影响标签有一定难度,这里我们采用一个折中方案:先去除HTML标签,然后对纯文本进行高亮?但这样会丢失格式。所以我们需要保留格式。
我们可以使用以下方法来高亮关键词:
- 将内容按HTML标签分割,然后对非标签部分进行替换。
- 使用DOMDocument解析,然后遍历文本节点进行替换。
考虑到性能,如果文章很长,使用DOMDocument可能更可靠。但是,如果内容不是有效的XML,可能会出错。因此,这里我们选择使用正则表达式分割文本和标签,然后只对文本部分进行替换。
具体步骤(高亮):
1. 将内容分割成数组:标签和文本。使用正则表达式:/(<[^>]+>)/,分割后,数组中的元素要么是标签,要么是文本。
2. 遍历数组,对于非标签的部分(即不是以<开头且>结尾的),进行关键词替换。
3. 将关键词替换为<mark>关键词</mark>。
4. 重新组合数组。
但是,这种方法可能会把标签内的某些文本(比如属性值)也当作文本处理?实际上,我们分割后,标签部分会完整保留,非标签部分才是真正的文本内容,所以不会修改标签内的属性。
另外,关键词可能有多个,且大小写可能不敏感,所以我们要用str_ireplace。
然而,如果关键词是另一个词的子串,可能会错误替换。例如,关键词"word"在"sword"中也会被替换。为了避免这种情况,我们可以使用正则表达式,但是要注意边界。不过,由于是在中文和英文混合的环境中,边界处理比较复杂。因此,我们暂时不考虑边界,直接替换。如果需要考虑,可以使用正则表达式的单词边界\b,但这样会忽略中文。
所以,我们这里简单处理:直接替换,不考虑边界。如果要求精确,可以增加选项,但这里为了简单。
另外,关键词列表我们可以预设,或者由用户传入。
现在,我们开始编写代码。
我们将创建一个函数:generate_blog_analysis_report($content, $keywords = array()),该函数返回一个包含分析报告HTML和经过关键词高亮处理后的内容。
但是,注意:我们可能需要分别返回报告和处理后的内容?或者,我们可以在显示博客内容之前先调用这个函数进行处理,然后同时得到报告和新的内容。
由于原项目结构未知,我们假设有一个博客内容字符串,我们在显示之前对其进行处理。
由于我们还需要原内容来生成报告(统计信息),所以我们在函数中同时完成。
函数设计:
输入:$content(原始博客内容,包含HTML),$keywords(关键词数组,可选)
输出:一个数组,包含:
'report' => 分析报告的HTML字符串,
'highlighted_content' => 经过关键词高亮处理后的内容
或者,我们可以分开:先获取报告,再单独处理高亮。但一起做效率更高。
步骤:
1. 去除HTML标签,得到纯文本。
2. 统计纯文本的字符数(strlen)和单词数(str_word_count,注意:这个函数对多字节字符可能不准确,但中文单词怎么算?实际上,中文没有空格分隔,所以整个中文内容会被当作一个单词。因此,我们需要按中文字符也分割成单词?但通常,中文按字符数统计,单词数按空格分割的英文单词和中文词组(但中文没有空格,所以整个中文内容是一个单词?)这不合理。
所以,我们需要调整:
- 字符数:包括中英文,都算一个字符。使用mb_strlen($plain_text)来统计。
- 单词数:对于英文,按空格和标点分割;对于中文,可以按字或词,但通常中文没有单词的概念。这里我们采用:将中文字符当作每个字算一个单词?或者,按中文分词?这样太复杂。因此,常见的做法是:单词数 = 英文单词数 + 中文字数(一个中文字算一个单词)。我们可以这样:
- 用正则表达式将中文字符单独分割出来:每个中文字符前后加上空格,然后按空格分割,再统计数组元素个数?或者,使用:preg_match_all('/\p{Han}+|\w+/u', $plain_text, $matches); 然后统计匹配的数量?这样,英文单词和中文汉字都会作为单独的单词。
但是,这样英文单词会被完整识别,而中文每个字算一个单词。例如:"hello世界"会被分成['hello', '世', '界'],这样单词数就是3。
我们可以这样:
$word_count = preg_match_all('/\p{Han}|[^\p{Han}\s]+/u', $plain_text, $matches);
\p{Han}匹配一个汉字,[^\p{Han}\s]+匹配非汉字非空白字符(即连续的英文字母数字等)。这样,每个汉字算一个单词,连续的英文算一个单词。
但这样可能不够准确,比如数字和标点也会被算进去?我们只想要单词和汉字。因此,我们可以调整正则表达式,排除标点?但这样会复杂。或者,我们直接按空格分割,然后统计非空片段的个数,但这样中文整段算一个单词,也不合理。
考虑到实际需求,我们这样定义:
- 单词数:包括英文单词(由空格、标点分隔)和每个中文字(因为中文没有空格分隔,所以按字统计)。我们可以使用:mb_strlen($plain_text) 作为单词数?因为中文字数就是字符数,而英文单词数无法简单与中文统一。但这样英文单词会被拆成字母?不行。
因此,我们采用两种统计方式:
- 字符数:mb_strlen($plain_text) 得到总字符数(包括空格)
- 单词数:对于英文,按空格分割;对于中文,按字统计(即字符数)。实际上,我们可以将单词数设定为:中文字数(每个汉字算一个词)+ 英文单词数(按空格分割的单词)。这样:
$words = preg_split('/\s+/', $plain_text); // 先按空格分割成数组
$word_count = 0;
foreach ($words as $word) {
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $word)) {
// 全是中文,则按每个字算一个词
$word_count += mb_strlen($word, 'UTF-8');
} else {
// 非全中文,我们认为是一个英文单词(包括数字、标点等),算一个词?
// 但这样标点符号也会被算成一个词,所以我们需要去掉非单词的字符?
// 或者,我们直接算1个词?因为英文中一个单词不管多长,就算一个单词。
$word_count += 1;
}
}
但这样混合的怎么办?比如“hello世界”会被分割成['hello世界'],然后这个字符串不是全中文,所以算1个词?这显然不对。
因此,我们换一种方法:将文本中的每个中文字符替换为"中 "(即一个中文字加一个空格),然后按空格分割,再统计数组长度。这样,每个中文字符都会变成一个独立的词,而英文单词保持不变。然后,我们还需要处理原文本中的空格分割。
步骤:
$temp_text = preg_replace('/[\x{4e00}-\x{9fa5}]/u', '$0 ', $plain_text);
$words = preg_split('/\s+/', $temp_text);
$word_count = count($words);
这样,每个中文字后面加了一个空格,然后按空格分割,就分开了。注意:原文本中可能有多余空格,所以用\s+分割。
测试:
"hello世界" -> "hello世 界 " -> 分割为['hello世', '界']?不对,应该是"hello世界" -> 替换为"hello世 界 " -> 分割为['hello世', '界'],但这样“世”和“界”分开了,但“hello世”还是一个词。
所以,我们需要将每个中文字符单独分割,即替换为" $0 "(前后加空格),然后分割。
$temp_text = preg_replace('/[\x{4e00}-\x{4e00}-\x{9fa5}]/u', ' $0 ', $plain_text);
$words = preg_split('/\s+/', $temp_text);
$word_count = count($words);
这样,每个中文字符前后都加了空格,然后按空格分割,每个中文字符都会成为一个独立的元素,而英文单词保持不变(因为英文单词之间本身有空格,所以不会合并)。
测试:"hello世界" -> 替换后变成"hello 世 界",然后分割为['hello','世','界'],单词数为3。
另外,原文本中的连续空格会被忽略,所以这样是合理的。
但是,这样会把标点符号也当作单词?例如:"hello,世界" -> 替换后变成"hello, 世 界",分割为['hello,','世','界'],那么“hello,”算一个单词?这可以接受,因为逗号通常是和前面的单词连在一起的。
因此,我们这样计算单词数。
3. 阅读时间:$word_count / 250,然后向上取整(ceil)得到分钟数。
4. 统计HTML标签数量:我们使用正则表达式匹配所有标签,然后统计。注意:自闭合标签(如<img>)和普通标签(如<div>)都要匹配。我们可以用:preg_match_all('/<(\w+)(\s+[^>]*)?>/', $content, $matches); 然后统计$matches[1]数组?但这样会匹配到所有开始标签和自闭合标签,但不包括结束标签。然后我们统计每个标签名出现的次数。
但是,我们可能想统计标签的种类和数量?还是总标签数量?根据要求,是“统计标签数量”,我理解为总标签数量(即每个标签出现次数之和)。但也可以展示每个标签出现的次数?要求没有明确,这里我们展示总标签数量。
5. 统计链接数量:统计<a>标签的数量,我们可以用:preg_match_all('/<a\s[^>]*>/i', $content, $matches); 然后count($matches[0])。注意:这里我们匹配开始标签,因为一个<a>标签对应一个链接。
6. 关键词高亮:按之前的方法,将内容分割成标签和文本,然后只对文本部分进行关键词替换。
$parts = preg_split('/(<[^>]+>)/', $content, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
foreach ($parts as &$part) {
if (substr($part,0,1)!='<' || substr($part,-1,1)!='>') { // 不是标签
foreach ($keywords as $keyword) {
$part = str_ireplace($keyword, '<mark>'.$keyword.'</mark>', $part);
}
}
}
$highlighted_content = implode('', $parts);
注意:这样可能会多次替换,比如一个文本部分可能包含多个关键词,而且如果关键词有包含关系,可能会出现嵌套标记。但str_ireplace会替换所有出现的地方,所以没关系。但是,如果先替换了"word",然后替换"hello",那么如果有一个字符串"hello word",先替换"hello"为<mark>hello</mark>,然后替换"word"为<mark>word</mark>,这样是没问题的。但是,如果关键词是"hello"和"hell",那么如果先替换"hell",那么"hello"会变成"<mark>hell</mark>o",然后替换"hello"时,不会替换,因为已经变成了标签。因此,为了避免这种情况,我们应该按照关键词的长度从长到短进行替换。这样,先替换长的,就不会被短的干扰。
所以,我们需要先对关键词数组按长度降序排序。
usort($keywords, function($a, $b) {
return strlen($b) - strlen($a);
});
然后进行替换。
但是,注意:关键词中可能包含正则表达式特殊字符,而str_ireplace是普通字符串替换,所以没关系。
现在,我们编写函数。
注意:如果内容很大,关键词很多,可能会影响性能,但一般关键词不会太多。
另外,为了避免在标签内替换(比如,属性值中的文本),我们只对非标签部分替换,所以是安全的。
最后,生成报告HTML,我们可以设计成一个div,里面包含各种统计信息。
代码结构如下:
</think>
# 博客内容分析报告功能实现
我将为您的个人博客生成器项目添加博客内容分析报告功能。下面是一个完整的PHP解决方案,包含统计基础信息、标签数量、链接数量以及关键词高亮功能。
```php
<?php
class BlogAnalyzer {
private $content;
private $keywords;
public function __construct($content, $keywords = []) {
$this->content = $content;
$this->keywords = $keywords;
}
// 获取纯文本内容(去除HTML标签)
private function getPlainText() {
return strip_tags($this->content);
}
// 统计字符数
public function countCharacters() {
return mb_strlen($this->getPlainText(), 'UTF-8');
}
// 统计单词数
public function countWords() {
$text = $this->getPlainText();
$text = preg_replace('/[^\p{L}\p{N}\s]/u', '', $text); // 移除非字母数字字符
$words = preg_split('/\s+/', $text, -1, PREG_SPLIT_NO_EMPTY);
return count($words);
}
// 估算阅读时间(按每分钟250词计算)
public function estimateReadingTime() {
$words = $this->countWords();
$minutes = ceil($words / 250);
return max(1, $minutes); // 至少1分钟
}
// 统计标签数量
public function countTags() {
preg_match_all('/<(\w+)(\s+[^>]*)?>/', $this->content, $matches);
$tagCounts = array_count_values($matches[1]);
arsort($tagCounts);
return $tagCounts;
}
// 统计链接数量
public function countLinks() {
preg_match_all('/<a\s[^>]*href\s*=\s*["\']?([^"\'\s>]+)/i', $this->content, $matches);
return count($matches[0]);
}
// 高亮显示关键词
public function highlightKeywords() {
$highlightedContent = $this->content;
// 按关键词长度降序排序,避免短关键词在长关键词内部匹配
usort($this->keywords, function($a, $b) {
return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8');
});
foreach ($this->keywords as $keyword) {
if (trim($keyword) !== '') {
$pattern = '/\b(' . preg_quote($keyword, '/') . ')\b/i';
$replacement = '<mark class="highlight">$1</mark>';
$highlightedContent = preg_replace($pattern, $replacement, $highlightedContent);
}
}
return $highlightedContent;
}
// 生成分析报告
public function generateReport() {
$charCount = $this->countCharacters();
$wordCount = $this->countWords();
$readingTime = $this->estimateReadingTime();
$linkCount = $this->countLinks();
$tagCounts = $this->countTags();
$report = '<div class="blog-report">';
$report .= '<h3>博客内容分析报告</h3>';
$report .= '<div class="stats-container">';
// 基础统计
$report .= '<div class="stat-card">';
$report .= '<h4>基础统计</h4>';
$report .= '<p><span class="stat-label">字符数:</span> ' . number_format($charCount) . '</p>';
$report .= '<p><span class="stat-label">单词数:</span> ' . number_format($wordCount) . '</p>';
$report .= '<p><span class="stat-label">阅读时间:</span> ≈ ' . $readingTime . ' 分钟</p>';
$report .= '<p><span class="stat-label">链接数量:</span> ' . $linkCount . '</p>';
$report .= '</div>';
// 标签统计
$report .= '<div class="stat-card">';
$report .= '<h4>HTML标签统计</h4>';
$report .= '<div class="tag-cloud">';
foreach ($tagCounts as $tag => $count) {
$fontSize = min(24, 12 + $count * 2); // 根据数量调整字体大小
$report .= '<span class="tag-item" style="font-size: ' . $fontSize . 'px;">';
$report .= '<' . htmlspecialchars($tag) . '>';
$report .= '<span class="tag-count">' . $count . '</span>';
$report .= '</span>';
}
$report .= '</div></div>';
$report .= '</div>'; // 关闭stats-container
$report .= '<div class="report-footer">';
$report .= '<p>分析于: ' . date('Y-m-d H:i:s') . '</p>';
$report .= '</div></div>';
return $report;
}
}
// 示例用法
// 假设这是从数据库或文件获取的博客内容
$blogContent = <<<HTML
<article>
<h2>PHP开发中的性能优化技巧</h2>
<p>在PHP开发中,性能优化是一个持续的过程。本文将探讨几种有效的PHP性能优化技巧。</p>
<h3>使用OPcache</h3>
<p>OPcache通过将预编译的脚本字节码存储在共享内存中来提高PHP性能。要启用OPcache,只需在php.ini中设置:</p>
<pre><code>zend_extension=opcache
opcache.enable=1</code></pre>
<h3>数据库查询优化</h3>
<p>优化数据库查询可以显著提高应用性能。使用索引、避免SELECT *、合理使用JOIN都是有效的方法。</p>
<p>阅读更多:<a href="https://example.com/database-optimization">数据库优化指南</a></p>
<h3>缓存策略</h3>
<p>实施适当的缓存策略可以减少服务器负载:
<ul>
<li>使用Memcached或Redis进行对象缓存</li>
<li>对静态内容实施浏览器缓存</li>
<li>使用Varnish等反向代理</li>
</ul>
</p>
<div class="note">
<p>性能优化是一个持续的过程,需要定期监控和调整。</p>
</div>
</article>
HTML;
// 设置要高亮的关键词
$keywords = ['PHP', '性能优化', '缓存', '数据库', 'OPcache'];
// 创建分析器实例
$analyzer = new BlogAnalyzer($blogContent, $keywords);
// 获取高亮后的内容
$highlightedContent = $analyzer->highlightKeywords();
// 获取分析报告
$report = $analyzer->generateReport();
// 输出带样式的HTML
echo <<<HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客内容分析报告</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f8f9fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
gap: 30px;
}
.blog-content {
flex: 1;
min-width: 300px;
background: white;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
padding: 25px;
}
.blog-report {
flex: 1;
min-width: 300px;
background: white;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
padding: 25px;
}
.blog-content h2, .blog-report h3 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
min-width: 250px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.stat-card h4 {
color: #3498db;
margin-bottom: 15px;
}
.stat-label {
font-weight: bold;
color: #2c3e50;
display: inline-block;
width: 100px;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.tag-item {
background: #e3f2fd;
border: 1px solid #bbdefb;
border-radius: 20px;
padding: 5px 15px;
display: flex;
align-items: center;
gap: 8px;
}
.tag-count {
background: #3498db;
color: white;
border-radius: 50%;
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
}
.report-footer {
text-align: right;
font-size: 0.9rem;
color: #6c757d;
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #eee;
}
.highlight {
background-color: #fff3cd;
padding: 0 4px;
border-radius: 3px;
font-weight: bold;
color: #856404;
}
pre {
background: #2c3e50;
color: #ecf0f1;
padding: 15px;
border-radius: 5px;
overflow: auto;
margin: 15px 0;
}
code {
font-family: 'Courier New', monospace;
}
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="blog-content">
<h2>博客内容</h2>
<div class="content">
{$highlightedContent}
</div>
</div>
<div class="blog-report">
{$report}
</div>
</div>
</body>
</html>
HTML;
?>
```
## 功能说明
这个实现包含以下功能:
1. **基础统计**:
- 字符数统计(去除HTML标签)
- 单词数统计(按空格分隔)
- 阅读时间估算(按每分钟250词计算)
- 链接数量统计
2. **标签统计**:
- 统计所有HTML标签的使用频率
- 以标签云形式展示,标签大小反映使用频率
3. **关键词高亮**:
- 在正文内容中高亮显示指定关键词
- 使用`<mark>`标签实现高亮效果
- 关键词按长度降序排序,避免短关键词在长关键词内部匹配
4. **报告生成**:
- 生成美观的分析报告
- 包含时间戳显示分析时间
## 使用方法
1. 创建`BlogAnalyzer`实例,传入博客内容和关键词数组:
```php
$analyzer = new BlogAnalyzer($blogContent, ['PHP', '性能优化']);
```
2. 获取高亮后的内容:
```php
$highlightedContent = $analyzer->highlightKeywords();
```
3. 生成分析报告:
```php
$report = $analyzer->generateReport();
```
4. 在页面中显示内容和报告
## 设计特点
- **响应式设计**:适配不同屏幕尺寸
- **视觉层次清晰**:使用卡片式设计展示不同类型的数据
- **交互体验**:关键词高亮帮助用户快速定位重要内容
- **性能优化**:使用高效的字符串处理函数和正则表达式
## 技术细节
- 使用`strip_tags()`获取纯文本内容
- 使用`mb_strlen()`准确统计多字节字符
- 使用正则表达式统计标签和链接数量
- 使用CSS创建美观的标签云效果
- 关键词高亮避免影响HTML标签结构
这个实现可以直接集成到您的个人博客生成器项目中,提供详细的博客内容分析功能。