滚动新闻 →
生宣和熟宣有什么区别 如何让孩子学会真诚地赞美别人 欧洲遭遇双风暴袭击 逾15万户断电 数百航班取消 日本逮捕中国女子 指称其携带22公斤香肠入境 腾冲辣椒厂火灾致全城空气刺鼻 市民集体咳嗽 拍视频如何避免重要素材丢失 郑和下西洋究竟改变了什么 山东日照渔民捕获268斤鲅鱼王 做成水饺享美食 东欧强人接连失势 武契奇败选塞尔维亚总理选举 家里的动线不合理应该如何调整 粽子为什么在不同地区有完全不同的做法 加拿大佩吉湾的岩石海岸,适合把一个下午交给海浪 清除故障码后故障又出现怎么办 河北患者肛瘘手术后死亡 家属维权遭暴力驱逐 广州近300户玻璃自爆引争议 物业称已过质保期 激光雷达到底有什么作用 美国独立屋长期维护需要考虑哪些费用 巴西总统决选:卢拉逆转胜出 如何确定适合自己的应急资金规模 游艇建造年份对价格影响有多大 沈玉琳抗癌重生首获金钟奖:活下来就是等这个 中共二十届五中全会或超45名高官被除名 张又侠等在列 明星之间为什么存在看不见的片酬参照体系 加拿大如何查看CRA保存的税务资料 沈阳保洁员因外貌酷似胡歌走红 真实身份曝光 唐嘉鸿世锦赛单杠夺金 创台湾体操史纪录 美国新闻自由的法律基础 圣奥古斯丁为何成为美国最古老的欧洲城市 房屋外墙防水应该注意什么 Kubernetes 为什么逐渐进入 AI 基础设施,传统虚拟机架构有哪些不足 委内瑞拉反对派领袖马查多结束流亡回国 加拿大卑诗省政坛格局巨变 保守党时隔近百年重返执政 五中全会前胡春华突“失踪” 内部人士曝真实内情 川普密西根集会:力阻穆斯林候选人参议院席位 黑海安全成焦点 保加利亚大选聚焦对俄立场 Google Cloud Storage 成本怎么优化,生命周期和存储类别应该如何组合 MySQL 游标分页怎么实现?适合 SaaS 大数据列表的实际场景 佛罗里达农工大学附近发生枪击案 至少12人受伤 USB设备插入以后电脑立即重启,可能存在什么硬件故障 Windows 11 PIN 登录怎么设置?它和传统密码有哪些实际区别?

PHP 正则表达式怎么入门?从验证邮箱到提取文章内容逐步掌握

发布时间: 2026-10-04 22:00:02    最后更新: 2026-10-26 02:40:40    阅读:72  约4 分钟阅读     

PHP 正则表达式怎么入门?从验证邮箱到提取文章内容逐步掌握
图片说明:示意图   图片来源:Public Domain(公有领域)
PHP正则表达式怎么入门?从验证邮箱到提取文章内容逐步掌握 正则表达式是PHP中处理文本和字符串的重要工具,但它并不是解决所有字符串问题的万能方法。对于初学者来说,最好的学习方式不是一开始记忆复杂语法,而是从实际需求入手,逐步理解字符匹配、重复、分组和边界等基本概念。 邮箱验证就是一个比较容易理解的例子。不过,在实际PHP开发中,如果只是判断一个字符串是否符合常见邮箱格式,通常应该优先考虑PHP提供的验证函数,而不是自己编写一套复杂的邮箱正则表达式。 一、从最简单的正则表达式开始 PHP使用PCRE正则表达式引擎,可以通过preg_match()进行匹配。 例如: if (preg_match('/^hello$/', $text)) { echo "匹配成功"; } 这里的^表示字符串开始,$表示字符串结束。因此,这个表达式要求整个字符串必须是hello。 再例如: if (preg_match('/^[0-9]+$/', $text)) { echo "只包含数字"; } 其中[0-9]表示数字字符,+表示前面的模式至少出现一次。 掌握字符类、重复次数、开始和结束位置等基本概念后,再学习捕获组和更复杂的模式会容易得多。 二、邮箱验证不要盲目自己写正则 邮箱地址看起来很简单,但实际格式比普通字符串复杂得多。 如果只是判断用户输入是否符合PHP常见的邮箱格式,可以直接使用: if (filter_var($email, FILTER_VALIDATE_EMAIL)) { echo "邮箱格式正确"; } 这种方法通常比自己编写复杂正则更加方便,也更容易维护。 需要注意的是,格式验证并不能证明邮箱地址真实存在。一个格式正确的地址可能根本没有注册,或者用户并不能接收邮件。如果业务要求确认邮箱有效,通常还需要通过发送验证邮件完成验证。 因此,应该区分“格式正确”和“邮箱真实可用”这两个概念。 三、正则表达式适合提取简单文本 正则表达式真正有价值的场景之一,是从具有相对稳定格式的文本中提取信息。 例如,从一段简单文本中提取日期: preg_match('/(\d{4})-(\d{2})-(\d{2})/', $text, $matches); 匹配成功后,$matches[0]通常保存完整匹配结果,而捕获组中的内容则保存在后续数组元素中。 这里的括号()就是捕获组,可以把匹配结果中的某一部分单独提取出来。 例如: echo $matches[1]; echo $matches[2]; echo $matches[3]; 分别可以得到年份、月份和日期。 理解捕获组以后,就可以进一步学习preg_match_all(),一次提取文本中出现的多个匹配结果。 四、为什么不建议用正则解析整个HTML 如果只是从非常简单的HTML片段中寻找特定文本,正则有时可以使用。 例如: preg_match_all('/loadHTML($html); $xpath = new DOMXPath($dom); $articles = $xpath->query('//div[@class="article"]'); 这种方式是按照HTML结构寻找节点,而不是单纯依靠字符模式匹配,因此更适合处理结构复杂的网页内容。 五、JSON应该使用JSON解析工具 类似的问题也存在于JSON。 例如下面的数据: {"title":"测试文章","id":123} 如果需要读取title,没有必要使用正则: $data = json_decode($json, true); echo $data['title']; json_decode()能够按照JSON的数据结构进行解析,因此比正则更加可靠。 正则可以处理普通文本中的特定模式,但JSON、HTML、XML等具有明确结构的数据,通常应该优先使用对应的解析工具。 六、理解贪婪匹配和非贪婪匹配 正则表达式中的*、+等重复符号通常具有贪婪匹配特性,也就是尽可能匹配更多内容。 例如: preg_match('/.*<\/title>/', $html, $matches); 如果文本中存在多个<title>相关内容,.*可能匹配过多内容。 在适合的场景下,可以使用非贪婪形式: preg_match('/<title>.*?<\/title>/', $html, $matches); .*?倾向于匹配尽可能少的内容。 但需要注意,非贪婪匹配并不意味着正则表达式一定安全,也不能简单地认为把.*全部换成.*?就可以防止ReDoS。对于复杂正则,应避免容易产生大量回溯的模式,并对用户可控输入设置合理限制。 七、正则表达式与安全问题要分开处理 正则表达式经常会出现在用户输入处理过程中,但正则本身不能替代其他安全措施。 例如,防止SQL注入应该使用PDO预处理语句: $stmt = $pdo->prepare( 'SELECT * FROM users WHERE email = ?' ); $stmt->execute([$email]); 输出用户提交的内容时,则应该根据具体输出环境进行HTML转义,例如: echo htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); 而防范正则表达式拒绝服务,则需要从正则模式设计、输入长度限制和执行环境等方面进行处理。 这些问题虽然都与输入数据有关,但解决方法并不相同,不能用一个“过滤输入”概括。 八、PHP中还有很多时候根本不需要正则 初学者容易产生一个误区:看到字符串处理就想使用正则。 实际上,如果只是查找字符串: strpos($text, 'PHP'); 如果只是替换: str_replace('PHP', 'Python', $text); 如果只是截取字符串,在确定编码和使用场景的情况下,也可以使用相应的字符串函数。 只有当需求涉及比较复杂的模式匹配时,正则表达式才真正具有优势。 例如: “查找所有连续数字” “提取固定格式的日期” “判断某种特定文本结构” “从日志中提取符合模式的数据” 这些场景都比较适合使用正则。 九、初学者应该怎样学习 学习PHP正则表达式,不需要一开始记住大量复杂语法。 可以按照下面的顺序逐步学习: 字符和字符类,例如[0-9]、[a-z]。 重复符号,例如*、+、?和{n,m}。 字符串边界,例如^和$。 分组和捕获,例如()。 或条件,例如|。 转义字符,例如\.、\d、\s。 preg_match()和preg_match_all()的使用方法。 贪婪和非贪婪匹配。 最后再学习复杂的断言、命名捕获组等高级功能。 这种学习方式比直接背诵一大串正则表达式更加有效。 正则表达式的价值,在于处理适合用“模式”描述的文本,而不是替代所有字符串函数和数据解析工具。PHP开发中,邮箱格式可以使用filter_var(),JSON应该使用json_decode(),复杂HTML应该使用DOM解析器,普通字符串查找则可以直接使用字符串函数。 真正掌握正则表达式,并不是记住多少条复杂规则,而是知道什么时候应该使用它,以及什么时候应该选择更合适的工具。 </div> <!-- ============================================================ MNewsTV Reader Tools ============================================================ --> <div class="mnews-reader-tools" id="mnewsReaderTools" > <div class="mnews-reader-tools-inner"> <!-- ==================================================== 标题 ===================================================== --> <div class="mnews-reader-heading"> <h2> 喜欢这篇报道? </h2> <p> 使用下面的功能,方便以后继续阅读和分享 MNewsTV </p> </div> <!-- ==================================================== Google News ===================================================== --> <a href="https://www.google.com/preferences/source?q=https%3A%2F%2Fwww.mnewstv.com%2F" target="_blank" rel="noopener noreferrer" class="mnews-reader-google" > <span class="mnews-reader-google-icon"> <span></span> <span></span> <span></span> </span> <span class="mnews-reader-google-text"> <strong> 设为 Google 新闻首选来源 </strong> <small> 让 Google 新闻优先显示 MNewsTV 的最新报道 </small> </span> <span class="mnews-reader-google-arrow"> › </span> </a> <!-- ==================================================== 功能按钮 ===================================================== --> <div class="mnews-reader-grid"> <!-- 收藏 --> <button type="button" class="mnews-reader-button" id="mnewsFavoriteButton" > <span class="mnews-reader-icon"> ☆ </span> <span class="mnews-reader-button-text"> <strong> 加入收藏 </strong> <small> 收藏这篇报道,方便以后查看 </small> </span> </button> <!-- 我的收藏 --> <a href="/my-favorites.php" class="mnews-reader-button" > <span class="mnews-reader-icon"> ★ </span> <span class="mnews-reader-button-text"> <strong> 我的收藏 </strong> <small> 查看已经收藏的文章 </small> </span> </a> <!-- 下载 --> <button type="button" class="mnews-reader-button" id="mnewsDownloadButton" > <span class="mnews-reader-icon"> ↓ </span> <span class="mnews-reader-button-text"> <strong> 下载文章 </strong> <small> 将文章保存为文本文件 </small> </span> </button> <!-- 分享 --> <button type="button" class="mnews-reader-button" id="mnewsShareButton" > <span class="mnews-reader-icon"> ↗ </span> <span class="mnews-reader-button-text"> <strong> 分享这篇报道 </strong> <small> 分享给朋友或社交平台 </small> </span> </button> <!-- 听文章 --> <button type="button" class="mnews-reader-button" id="mnewsReadButton" > <span class="mnews-reader-icon"> 🔊 </span> <span class="mnews-reader-button-text"> <strong> 听文章 </strong> <small> 使用设备中文语音朗读文章 </small> </span> </button> <!-- 停止 --> <button type="button" class="mnews-reader-button" id="mnewsStopButton" > <span class="mnews-reader-icon"> ■ </span> <span class="mnews-reader-button-text"> <strong> 停止朗读 </strong> <small> 停止当前语音播放 </small> </span> </button> </div> <!-- ==================================================== 收藏说明 ===================================================== --> <div class="mnews-reader-note"> <strong> 关于文章收藏 </strong> <span> 收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。 删除收藏请进入「我的收藏」进行管理。 </span> </div> <!-- ==================================================== 分享 ===================================================== --> <div class="mnews-reader-share" id="mnewsSharePanel" > <div class="mnews-reader-share-inner"> <div class="mnews-reader-share-title"> 分享这篇报道 </div> <div class="mnews-reader-share-grid"> <button type="button" class="mnews-reader-share-item" id="mnewsCopyButton" > <span> 🔗 </span> <strong> 复制链接 </strong> </button> <a href="#" target="_blank" rel="noopener noreferrer" class="mnews-reader-share-item" id="mnewsFacebook" > <span> f </span> <strong> Facebook </strong> </a> <a href="#" target="_blank" rel="noopener noreferrer" class="mnews-reader-share-item" id="mnewsX" > <span> 𝕏 </span> <strong> X </strong> </a> <a href="#" target="_blank" rel="noopener noreferrer" class="mnews-reader-share-item" id="mnewsWhatsApp" > <span> W </span> <strong> WhatsApp </strong> </a> <a href="#" class="mnews-reader-share-item" id="mnewsEmail" > <span> ✉ </span> <strong> 邮件 </strong> </a> <!-- Reddit --> <a href="#" target="_blank" rel="noopener noreferrer" class="mnews-reader-share-item" id="mnewsReddit" > <span> reddit </span> <strong> Reddit </strong> </a> <!-- 微信 --> <button type="button" class="mnews-reader-share-item" id="mnewsWeChat" > <span> 微信 </span> <strong> WeChat </strong> </button> </div> </div> </div> <!-- ==================================================== 状态 ===================================================== --> <div class="mnews-reader-message" id="mnewsReaderMessage" aria-live="polite" ></div> </div> </div> <style> /* ============================================================ 基础 ============================================================ */ .mnews-reader-tools, .mnews-reader-tools * { box-sizing: border-box; } .mnews-reader-tools { width: 100%; margin: 32px 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "Noto Sans SC", Arial, sans-serif; color: #202124; } .mnews-reader-tools-inner { width: 100%; max-width: 900px; margin: 0 auto; padding: 26px; background: #fff; border: 1px solid #e1e5ea; border-radius: 14px; box-shadow: 0 2px 6px rgba(0,0,0,.04), 0 8px 25px rgba(0,0,0,.04); } /* ============================================================ 标题 ============================================================ */ .mnews-reader-heading { text-align: center; margin-bottom: 22px; } .mnews-reader-heading h2 { margin: 0; font-size: 20px; line-height: 1.4; font-weight: 650; } .mnews-reader-heading p { margin: 6px 0 0; font-size: 13px; line-height: 1.6; color: #70757a; } /* ============================================================ Google News ============================================================ */ .mnews-reader-google { display: flex; align-items: center; width: 100%; min-height: 72px; padding: 13px 17px; color: #202124; text-decoration: none; background: #f7faff; border: 1px solid #d7e2f1; border-radius: 11px; transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .2s ease; } .mnews-reader-google:hover { background: #f1f6ff; border-color: #b9cce7; box-shadow: 0 4px 12px rgba(66,133,244,.10); transform: translateY(-1px); } .mnews-reader-google-icon { flex: 0 0 46px; width: 46px; height: 46px; display: flex; align-items: center; justify-content: center; gap: 4px; background: #fff; border: 1px solid #e0e4ea; border-radius: 10px; } .mnews-reader-google-icon span { display: block; width: 5px; border-radius: 3px; } .mnews-reader-google-icon span:nth-child(1) { height: 18px; background: #4285f4; } .mnews-reader-google-icon span:nth-child(2) { height: 28px; background: #34a853; } .mnews-reader-google-icon span:nth-child(3) { height: 22px; background: #fbbc05; } .mnews-reader-google-text { flex: 1; min-width: 0; margin-left: 14px; } .mnews-reader-google-text strong { display: block; font-size: 15px; line-height: 22px; font-weight: 650; } .mnews-reader-google-text small { display: block; margin-top: 2px; font-size: 12px; line-height: 18px; color: #6b7280; } .mnews-reader-google-arrow { margin-left: 12px; font-size: 27px; color: #6b7280; transition: transform .2s ease; } .mnews-reader-google:hover .mnews-reader-google-arrow { color: #1a73e8; transform: translateX(3px); } /* ============================================================ 功能网格 ============================================================ */ .mnews-reader-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; margin-top: 14px; } /* ============================================================ 功能按钮 ============================================================ */ .mnews-reader-button { display: flex; align-items: center; width: 100%; min-height: 68px; padding: 12px 14px; background: #fff; border: 1px solid #e1e5ea; border-radius: 10px; color: #202124; font-family: inherit; font-size: inherit; text-align: left; text-decoration: none; cursor: pointer; appearance: none; transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .2s ease; } .mnews-reader-button:hover { background: #f8fafc; border-color: #cbd5e1; box-shadow: 0 3px 10px rgba(0,0,0,.06); transform: translateY(-1px); } .mnews-reader-button:active { transform: translateY(0); } .mnews-reader-icon { flex: 0 0 40px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: #f1f3f4; border-radius: 9px; font-size: 19px; color: #3c4043; } .mnews-reader-button-text { flex: 1; min-width: 0; margin-left: 11px; } .mnews-reader-button-text strong { display: block; font-size: 14px; line-height: 20px; font-weight: 600; } .mnews-reader-button-text small { display: block; margin-top: 2px; font-size: 11px; line-height: 16px; color: #70757a; } /* ============================================================ 收藏说明 ============================================================ */ .mnews-reader-note { display: flex; flex-direction: column; gap: 4px; margin-top: 12px; padding: 12px 14px; background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 9px; } .mnews-reader-note strong { font-size: 12px; color: #4b5563; } .mnews-reader-note span { font-size: 11px; line-height: 17px; color: #6b7280; } /* ============================================================ 分享面板 ============================================================ */ .mnews-reader-share { display: none; margin-top: 12px; } .mnews-reader-share.mnews-share-open { display: block; } .mnews-reader-share-inner { padding: 16px; background: #f8fafc; border: 1px solid #e1e5ea; border-radius: 10px; } .mnews-reader-share-title { margin-bottom: 12px; font-size: 14px; font-weight: 600; } .mnews-reader-share-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; } .mnews-reader-share-item { min-height: 50px; padding: 7px 5px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; background: #fff; border: 1px solid #e1e5ea; border-radius: 8px; color: #202124; text-decoration: none; font-family: inherit; cursor: pointer; appearance: none; } .mnews-reader-share-item:hover { border-color: #c7cdd5; background: #fafafa; } .mnews-reader-share-item span { font-size: 15px; font-weight: 600; } .mnews-reader-share-item strong { font-size: 11px; font-weight: 500; } /* ============================================================ 状态消息 ============================================================ */ .mnews-reader-message { display: none; width: 100%; margin-top: 14px; padding: 10px 14px; background: #f1f8f4; border: 1px solid #d6eadc; border-radius: 8px; color: #287343; text-align: center; font-size: 13px; line-height: 1.5; } .mnews-reader-message.mnews-message-show { display: block; } /* ============================================================ 手机 ============================================================ */ @media (max-width: 600px) { .mnews-reader-tools { margin: 24px 0; } .mnews-reader-tools-inner { padding: 18px 13px; border-radius: 12px; } .mnews-reader-heading h2 { font-size: 18px; } .mnews-reader-google { min-height: 64px; padding: 11px 12px; } .mnews-reader-google-icon { flex: 0 0 40px; width: 40px; height: 40px; } .mnews-reader-google-text { margin-left: 11px; } .mnews-reader-google-text strong { font-size: 14px; } .mnews-reader-google-text small { font-size: 10px; } .mnews-reader-grid { grid-template-columns: 1fr; gap: 8px; } .mnews-reader-button { min-height: 60px; padding: 10px 12px; } .mnews-reader-share-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (max-width: 360px) { .mnews-reader-tools-inner { padding: 15px 10px; } .mnews-reader-google-text small, .mnews-reader-button-text small { display: none; } .mnews-reader-share-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } </style> <script> (function () { "use strict"; /* ======================================================== PHP 数据 ======================================================== */ var articleTitle = "PHP 正则表达式怎么入门?从验证邮箱到提取文章内容逐步掌握"; var articleUrl = "https://mnewstv.com/news/45539.html"; var articleContent = "PHP正则表达式怎么入门?从验证邮箱到提取文章内容逐步掌握\n\n正则表达式是PHP中处理文本和字符串的重要工具,但它并不是解决所有字符串问题的万能方法。对于初学者来说,最好的学习方式不是一开始记忆复杂语法,而是从实际需求入手,逐步理解字符匹配、重复、分组和边界等基本概念。\n\n邮箱验证就是一个比较容易理解的例子。不过,在实际PHP开发中,如果只是判断一个字符串是否符合常见邮箱格式,通常应该优先考虑PHP提供的验证函数,而不是自己编写一套复杂的邮箱正则表达式。\n\n一、从最简单的正则表达式开始\n\nPHP使用PCRE正则表达式引擎,可以通过preg_match()进行匹配。\n\n例如:\n\nif (preg_match(\u0027/^hello$/\u0027, $text)) {\n echo \u0022匹配成功\u0022;\n}\n\n这里的^表示字符串开始,$表示字符串结束。因此,这个表达式要求整个字符串必须是hello。\n\n再例如:\n\nif (preg_match(\u0027/^[0-9]+$/\u0027, $text)) {\n echo \u0022只包含数字\u0022;\n}\n\n其中[0-9]表示数字字符,+表示前面的模式至少出现一次。\n\n掌握字符类、重复次数、开始和结束位置等基本概念后,再学习捕获组和更复杂的模式会容易得多。\n\n二、邮箱验证不要盲目自己写正则\n\n邮箱地址看起来很简单,但实际格式比普通字符串复杂得多。\n\n如果只是判断用户输入是否符合PHP常见的邮箱格式,可以直接使用:\n\nif (filter_var($email, FILTER_VALIDATE_EMAIL)) {\n echo \u0022邮箱格式正确\u0022;\n}\n\n这种方法通常比自己编写复杂正则更加方便,也更容易维护。\n\n需要注意的是,格式验证并不能证明邮箱地址真实存在。一个格式正确的地址可能根本没有注册,或者用户并不能接收邮件。如果业务要求确认邮箱有效,通常还需要通过发送验证邮件完成验证。\n\n因此,应该区分“格式正确”和“邮箱真实可用”这两个概念。\n\n三、正则表达式适合提取简单文本\n\n正则表达式真正有价值的场景之一,是从具有相对稳定格式的文本中提取信息。\n\n例如,从一段简单文本中提取日期:\n\npreg_match(\u0027/(\\d{4})-(\\d{2})-(\\d{2})/\u0027, $text, $matches);\n\n匹配成功后,$matches[0]通常保存完整匹配结果,而捕获组中的内容则保存在后续数组元素中。\n\n这里的括号()就是捕获组,可以把匹配结果中的某一部分单独提取出来。\n\n例如:\n\necho $matches[1];\necho $matches[2];\necho $matches[3];\n\n分别可以得到年份、月份和日期。\n\n理解捕获组以后,就可以进一步学习preg_match_all(),一次提取文本中出现的多个匹配结果。\n\n四、为什么不建议用正则解析整个HTML\n\n如果只是从非常简单的HTML片段中寻找特定文本,正则有时可以使用。\n\n例如:\n\npreg_match_all(\u0027/\u003Ca\\s+href=\u0022([^\u0022]+)\u0022/i\u0027, $html, $matches);\n\n这个例子可以用于演示如何寻找使用双引号包围的href属性。\n\n但是,HTML本身具有复杂的嵌套结构,属性还可能使用单引号、属性顺序也可能不同,链接文字内部也可能包含其他HTML标签。因此,不能把一个简单正则表达式当成完整的HTML解析器。\n\n如果任务是提取文章正文、标题、链接节点等结构化HTML内容,更合适的方法通常是使用PHP的DOMDocument和DOMXPath。\n\n例如:\n\n$dom = new DOMDocument();\n\nlibxml_use_internal_errors(true);\n$dom-\u003EloadHTML($html);\n\n$xpath = new DOMXPath($dom);\n$articles = $xpath-\u003Equery(\u0027//div[@class=\u0022article\u0022]\u0027);\n\n这种方式是按照HTML结构寻找节点,而不是单纯依靠字符模式匹配,因此更适合处理结构复杂的网页内容。\n\n五、JSON应该使用JSON解析工具\n\n类似的问题也存在于JSON。\n\n例如下面的数据:\n\n{\u0022title\u0022:\u0022测试文章\u0022,\u0022id\u0022:123}\n\n如果需要读取title,没有必要使用正则:\n\n$data = json_decode($json, true);\n\necho $data[\u0027title\u0027];\n\njson_decode()能够按照JSON的数据结构进行解析,因此比正则更加可靠。\n\n正则可以处理普通文本中的特定模式,但JSON、HTML、XML等具有明确结构的数据,通常应该优先使用对应的解析工具。\n\n六、理解贪婪匹配和非贪婪匹配\n\n正则表达式中的*、+等重复符号通常具有贪婪匹配特性,也就是尽可能匹配更多内容。\n\n例如:\n\npreg_match(\u0027/\u003Ctitle\u003E.*\u003C\\/title\u003E/\u0027, $html, $matches);\n\n如果文本中存在多个\u003Ctitle\u003E相关内容,.*可能匹配过多内容。\n\n在适合的场景下,可以使用非贪婪形式:\n\npreg_match(\u0027/\u003Ctitle\u003E.*?\u003C\\/title\u003E/\u0027, $html, $matches);\n\n.*?倾向于匹配尽可能少的内容。\n\n但需要注意,非贪婪匹配并不意味着正则表达式一定安全,也不能简单地认为把.*全部换成.*?就可以防止ReDoS。对于复杂正则,应避免容易产生大量回溯的模式,并对用户可控输入设置合理限制。\n\n七、正则表达式与安全问题要分开处理\n\n正则表达式经常会出现在用户输入处理过程中,但正则本身不能替代其他安全措施。\n\n例如,防止SQL注入应该使用PDO预处理语句:\n\n$stmt = $pdo-\u003Eprepare(\n \u0027SELECT * FROM users WHERE email = ?\u0027\n);\n\n$stmt-\u003Eexecute([$email]);\n\n输出用户提交的内容时,则应该根据具体输出环境进行HTML转义,例如:\n\necho htmlspecialchars($text, ENT_QUOTES, \u0027UTF-8\u0027);\n\n而防范正则表达式拒绝服务,则需要从正则模式设计、输入长度限制和执行环境等方面进行处理。\n\n这些问题虽然都与输入数据有关,但解决方法并不相同,不能用一个“过滤输入”概括。\n\n八、PHP中还有很多时候根本不需要正则\n\n初学者容易产生一个误区:看到字符串处理就想使用正则。\n\n实际上,如果只是查找字符串:\n\nstrpos($text, \u0027PHP\u0027);\n\n如果只是替换:\n\nstr_replace(\u0027PHP\u0027, \u0027Python\u0027, $text);\n\n如果只是截取字符串,在确定编码和使用场景的情况下,也可以使用相应的字符串函数。\n\n只有当需求涉及比较复杂的模式匹配时,正则表达式才真正具有优势。\n\n例如:\n\n“查找所有连续数字”\n\n“提取固定格式的日期”\n\n“判断某种特定文本结构”\n\n“从日志中提取符合模式的数据”\n\n这些场景都比较适合使用正则。\n\n九、初学者应该怎样学习\n\n学习PHP正则表达式,不需要一开始记住大量复杂语法。\n\n可以按照下面的顺序逐步学习:\n\n字符和字符类,例如[0-9]、[a-z]。\n重复符号,例如*、+、?和{n,m}。\n字符串边界,例如^和$。\n分组和捕获,例如()。\n或条件,例如|。\n转义字符,例如\\.、\\d、\\s。\npreg_match()和preg_match_all()的使用方法。\n贪婪和非贪婪匹配。\n最后再学习复杂的断言、命名捕获组等高级功能。\n\n这种学习方式比直接背诵一大串正则表达式更加有效。\n\n正则表达式的价值,在于处理适合用“模式”描述的文本,而不是替代所有字符串函数和数据解析工具。PHP开发中,邮箱格式可以使用filter_var(),JSON应该使用json_decode(),复杂HTML应该使用DOM解析器,普通字符串查找则可以直接使用字符串函数。\n\n真正掌握正则表达式,并不是记住多少条复杂规则,而是知道什么时候应该使用它,以及什么时候应该选择更合适的工具。"; /* ======================================================== DOM ======================================================== */ var tools = document.getElementById( "mnewsReaderTools" ); if (!tools) { return; } var message = document.getElementById( "mnewsReaderMessage" ); var favoriteButton = document.getElementById( "mnewsFavoriteButton" ); var downloadButton = document.getElementById( "mnewsDownloadButton" ); var shareButton = document.getElementById( "mnewsShareButton" ); var readButton = document.getElementById( "mnewsReadButton" ); var stopButton = document.getElementById( "mnewsStopButton" ); var sharePanel = document.getElementById( "mnewsSharePanel" ); var copyButton = document.getElementById( "mnewsCopyButton" ); var facebook = document.getElementById( "mnewsFacebook" ); var xButton = document.getElementById( "mnewsX" ); var whatsapp = document.getElementById( "mnewsWhatsApp" ); var email = document.getElementById( "mnewsEmail" ); var reddit = document.getElementById( "mnewsReddit" ); var wechat = document.getElementById( "mnewsWeChat" ); /* ======================================================== 常量 ======================================================== */ var FAVORITES_KEY = "mnewstv_favorites"; var MAX_FAVORITES = 500; /* ======================================================== 当前 URL ======================================================== */ function getCurrentUrl() { var url = ""; try { url = window.location.href; } catch (error) { url = articleUrl || ""; } if (!url) { return ""; } var hash = url.indexOf("#"); if (hash !== -1) { url = url.substring( 0, hash ); } return url.trim(); } /* ======================================================== 当前标题 ======================================================== */ function getCurrentTitle() { var title = articleTitle || ""; if (!title.trim()) { title = document.title || ""; } return title.trim(); } /* ======================================================== 消息 ======================================================== */ function showMessage(text) { if (!message) { return; } message.textContent = text; message.classList.add( "mnews-message-show" ); clearTimeout( window.mnewsReaderMessageTimer ); window.mnewsReaderMessageTimer = setTimeout(function () { message.classList.remove( "mnews-message-show" ); }, 3500); } /* ======================================================== 获取收藏 ======================================================== */ function getFavorites() { try { var data = localStorage.getItem( FAVORITES_KEY ); if (!data) { return []; } var result = JSON.parse(data); if (!Array.isArray(result)) { return []; } return result; } catch (error) { return []; } } /* ======================================================== 保存收藏 注意: 这里只能保存。 本文件不存在任何删除收藏功能。 ======================================================== */ function saveFavorites(favorites) { try { localStorage.setItem( FAVORITES_KEY, JSON.stringify(favorites) ); return true; } catch (error) { return false; } } /* ======================================================== 加入收藏 绝对不会删除。 ======================================================== */ function addFavorite() { var url = getCurrentUrl(); if (!url) { showMessage( "无法获取当前文章地址。" ); return; } var title = getCurrentTitle(); var favorites = getFavorites(); /* * 检查是否已经存在。 */ for ( var i = 0; i < favorites.length; i++ ) { if ( !favorites[i] || typeof favorites[i].url !== "string" ) { continue; } var savedUrl = favorites[i].url .split("#")[0] .trim(); if ( savedUrl === url ) { showMessage( "这篇文章已经收藏。" ); return; } } /* * 添加。 */ favorites.unshift({ url: url, title: title, time: Date.now() }); /* * 最多保存 500 篇。 */ if ( favorites.length > MAX_FAVORITES ) { favorites = favorites.slice( 0, MAX_FAVORITES ); } if ( saveFavorites( favorites ) ) { showMessage( "文章已经加入收藏。" ); } else { showMessage( "收藏失败,请检查浏览器是否允许网站保存数据。" ); } } /* ======================================================== 下载文章成文本 ======================================================== */ function downloadArticleTxt() { var title = getCurrentTitle(); var url = getCurrentUrl(); var text = getReadingText(); if (!text) { showMessage( "没有找到当前文章正文。" ); return; } var content = title + "\r\n\r\n" + text + "\r\n\r\n" + "文章来源:MNewsTV\r\n" + "文章地址:" + url; var blob = new Blob( [content], { type: "text/plain;charset=utf-8" } ); var downloadUrl = URL.createObjectURL( blob ); var filename = sanitizeFilename( title ); if (!filename) { filename = "MNewsTV-article"; } filename += ".txt"; var link = document.createElement( "a" ); link.href = downloadUrl; link.download = filename; link.style.display = "none"; document.body.appendChild( link ); link.click(); document.body.removeChild( link ); setTimeout(function () { URL.revokeObjectURL( downloadUrl ); }, 1000); showMessage( "文章已经开始下载。" ); } /* ======================================================== 下载文章成html ======================================================== */function downloadArticle() { var title = getCurrentTitle(); var url = getCurrentUrl(); var content = articleContent || ""; /* ==================================================== 如果 PHP 正文为空 从当前页面寻找文章正文 ==================================================== */ if ( !content || content.trim().length < 50 ) { var articleElement = findArticleElement(); if (articleElement) { var clone = articleElement.cloneNode(true); removeUnwantedElements( clone ); content = clone.innerHTML || ""; } } /* ==================================================== 检查正文 ==================================================== */ if ( !content || content.trim().length < 20 ) { showMessage( "没有找到当前文章正文,暂时无法下载。" ); return; } /* ==================================================== 判断是不是纯文本 如果没有 HTML 标签, 就按照原文章的空行恢复段落。 ==================================================== */ var plainText = content .replace(/<br\s*\/?>/gi, "\n") .replace(/<\/p>/gi, "\n\n") .replace(/<\/div>/gi, "\n\n") .replace(/<\/h[1-6]>/gi, "\n\n") .replace(/<[^>]+>/g, "") .trim(); var hasHtml = /<[a-z][\s\S]*>/i.test( content ); if (!hasHtml) { var paragraphs = plainText .split(/\n\s*\n+/) .map(function (paragraph) { return paragraph.trim(); }) .filter(function (paragraph) { return paragraph.length > 0; }); if (paragraphs.length > 0) { content = paragraphs .map(function (paragraph) { var safeParagraph = paragraph .replace( /&/g, "&" ) .replace( /</g, "<" ) .replace( />/g, ">" ); return ( "<p>" + safeParagraph + "</p>" ); }) .join(""); } } /* ==================================================== 文件名 ==================================================== */ var safeTitle = sanitizeFilename( title ); if (!safeTitle) { safeTitle = "MNewsTV文章"; } /* ==================================================== 安全处理标题 ==================================================== */ var safeTitleHtml = title .replace( /&/g, "&" ) .replace( /</g, "<" ) .replace( />/g, ">" ) .replace( /"/g, """ ); /* ==================================================== 安全处理 URL ==================================================== */ var safeUrl = url .replace( /&/g, "&" ) .replace( /"/g, """ ) .replace( /</g, "<" ) .replace( />/g, ">" ); /* ==================================================== HTML 文件 ==================================================== */ var html = '<!DOCTYPE html>' + '<html lang="zh-CN">' + '<head>' + '<meta charset="UTF-8">' + '<meta name="viewport" content="width=device-width, initial-scale=1.0">' + '<title>' + safeTitleHtml + '' + '' + '' + '' + '

' + safeTitleHtml + '

' + '
' + 'MNewsTV
' + '原文:' + '' + safeUrl + '' + '
' + '
' + content + '
' + '' + ''; /* ==================================================== 创建 HTML 文件 ==================================================== */ var blob = new Blob( [html], { type: "text/html;charset=utf-8" } ); var downloadUrl = URL.createObjectURL( blob ); var link = document.createElement( "a" ); link.href = downloadUrl; link.download = safeTitle + ".html"; link.style.display = "none"; document.body.appendChild( link ); link.click(); document.body.removeChild( link ); setTimeout( function () { URL.revokeObjectURL( downloadUrl ); }, 1000 ); showMessage( "文章 HTML 文件已经开始下载。" ); } /* ======================================================== 文件名清理 ======================================================== */ function sanitizeFilename(name) { return String(name || "") .replace( /[\\\/:*?"<>|]/g, "" ) .replace( /\s+/g, " " ) .trim() .substring( 0, 100 ); } /* ======================================================== 复制链接 ======================================================== */ function copyUrl() { var url = getCurrentUrl(); if (!url) { showMessage( "无法获取文章地址。" ); return; } if ( navigator.clipboard && window.isSecureContext ) { navigator.clipboard .writeText(url) .then(function () { showMessage( "文章链接已经复制。" ); }) .catch(function () { fallbackCopy(url); }); } else { fallbackCopy(url); } } /* ======================================================== 旧浏览器复制 ======================================================== */ function fallbackCopy( text, successMessage ) { var textarea = document.createElement( "textarea" ); textarea.value = text; textarea.setAttribute( "readonly", "" ); textarea.style.position = "fixed"; textarea.style.left = "-9999px"; textarea.style.top = "0"; document.body.appendChild( textarea ); textarea.focus(); textarea.select(); var success = false; try { success = document.execCommand( "copy" ); } catch (error) { success = false; } document.body.removeChild( textarea ); if (success) { showMessage( successMessage || "文章链接已经复制。" ); } else { showMessage( "复制失败,请手动复制浏览器地址。" ); } } /* ======================================================== 更新分享链接 ======================================================== */ function updateShareLinks() { var url = getCurrentUrl(); var title = getCurrentTitle(); var encodedUrl = encodeURIComponent( url ); var encodedTitle = encodeURIComponent( title ); if (facebook) { facebook.href = "https://www.facebook.com/sharer/sharer.php?u=" + encodedUrl; } if (xButton) { xButton.href = "https://twitter.com/intent/tweet?url=" + encodedUrl + "&text=" + encodedTitle; } if (whatsapp) { whatsapp.href = "https://api.whatsapp.com/send?text=" + encodeURIComponent( title + " " + url ); } if (email) { email.href = "mailto:?subject=" + encodedTitle + "&body=" + encodeURIComponent( title + "\n\n" + url ); } if (reddit) { reddit.href = "https://www.reddit.com/submit?url=" + encodedUrl + "&title=" + encodedTitle; } } /* ======================================================== 打开分享 ======================================================== */ function openSharePanel() { if (!sharePanel) { return; } sharePanel.classList.toggle( "mnews-share-open" ); } /* ======================================================== 系统分享 ======================================================== */ function shareArticle() { var url = getCurrentUrl(); var title = getCurrentTitle(); if ( navigator.share && typeof navigator.share === "function" ) { navigator.share({ title: title, text: title, url: url }) .then(function () { }) .catch(function (error) { if ( error && error.name === "AbortError" ) { return; } openSharePanel(); }); return; } openSharePanel(); } /* ======================================================== 微信分享 ======================================================== */ function shareToWeChat() { var url = getCurrentUrl(); var title = getCurrentTitle(); if (!url) { showMessage( "无法获取当前文章地址。" ); return; } /* * 微信没有提供普通网页直接调用微信分享 * 的公开 URL 接口。 * * 优先尝试系统分享。 */ if ( navigator.share && typeof navigator.share === "function" ) { navigator.share({ title: title, text: title, url: url }) .then(function () { showMessage( "请选择微信分享。" ); }) .catch(function (error) { if ( error && error.name === "AbortError" ) { return; } copyUrlForWeChat(); }); return; } /* * 不支持系统分享: * 自动复制文章链接。 */ copyUrlForWeChat(); } function copyUrlForWeChat() { var url = getCurrentUrl(); var title = getCurrentTitle(); var shareText = title + "\n\n" + url; if ( navigator.clipboard && window.isSecureContext ) { navigator.clipboard .writeText( shareText ) .then(function () { showMessage( "文章标题和链接已经复制,请打开微信粘贴分享。" ); }) .catch(function () { fallbackCopy( shareText ); }); } else { fallbackCopy( shareText ); } } /* ======================================================== 获取正文 ======================================================== */ function getReadingText() { var text = ""; /* * 第一优先: * PHP 提供的正文。 */ if ( articleContent && articleContent.trim().length > 50 ) { text = htmlToText( articleContent ); } /* * PHP 没有正文: * 从页面自动寻找。 */ if ( !text || text.length < 50 ) { var element = findArticleElement(); if (!element) { return ""; } var clone = element.cloneNode(true); removeUnwantedElements( clone ); text = clone.innerText || clone.textContent || ""; text = cleanText(text); } /* * 去掉文章开头的新闻标记。 */ text = removeNewsHeader( text ); return cleanText(text); } /* ======================================================== HTML 转文字 ======================================================== */ function htmlToText(html) { var container = document.createElement( "div" ); container.innerHTML = html; removeUnwantedElements( container ); var text = container.innerText || container.textContent || ""; return cleanText(text); } /* ======================================================== 删除不需要朗读的元素 ======================================================== */ function removeUnwantedElements( container ) { if (!container) { return; } var selectors = [ "script", "style", "noscript", "iframe", "video", "audio", "button", "nav", "header", "footer", "aside", "figure", "figcaption", ".share-buttons", ".social-buttons", ".mnews-reader-tools", ".related-news", ".related-articles", ".comments", ".comment", ".advertisement", ".advertisements", ".advertising", ".ads", ".ad", ".ad-container", ".sidebar", ".menu", ".navigation" ]; for ( var i = 0; i < selectors.length; i++ ) { var elements = container.querySelectorAll( selectors[i] ); for ( var j = 0; j < elements.length; j++ ) { elements[j].remove(); } } } /* ======================================================== 清理文字 ======================================================== */ function cleanText(text) { if (!text) { return ""; } text = text .replace( /\u00a0/g, " " ) .replace( /[ \t]+/g, " " ) .replace( /\r\n/g, "\n" ) .replace( /\r/g, "\n" ) .replace( /\n[ \t]+/g, "\n" ) .replace( /[ \t]+\n/g, "\n" ) .replace( /\n{3,}/g, "\n\n" ) .trim(); return text; } /* ======================================================== 去掉新闻头部 例如: 【中国观察北京时间2026年08月09日讯】 只朗读后面的正文。 ======================================================== */ function removeNewsHeader(text) { if (!text) { return ""; } var pattern = /【中国观察北京时间[^】]*?日讯】/; var match = text.match(pattern); if (match) { text = text.substring( match.index + match[0].length ); } return text; } /* ======================================================== 寻找文章正文 ======================================================== */ function findArticleElement() { var selectors = [ "#mnews-article-content", "#article-content", "#articleContent", ".article-content", ".article-body", ".article-details", ".article-text", ".news-content", ".news-details", ".news-body", ".post-content", ".post-body", ".entry-content", "article" ]; for ( var i = 0; i < selectors.length; i++ ) { var element = document.querySelector( selectors[i] ); if (!element) { continue; } if ( element.closest && element.closest( "#mnewsReaderTools" ) ) { continue; } var text = element.innerText || element.textContent || ""; if ( text.trim().length > 200 ) { return element; } } /* * 找不到明确正文时, * 从 main / section / div 中寻找 * 内容最多的区域。 */ var candidates = document.querySelectorAll( "main, section, div" ); var best = null; var bestLength = 0; for ( var j = 0; j < candidates.length; j++ ) { var candidate = candidates[j]; if ( candidate.closest && candidate.closest( "#mnewsReaderTools" ) ) { continue; } if ( candidate.closest && candidate.closest( "nav, header, footer, aside" ) ) { continue; } if ( candidate === document.body || candidate === document.documentElement ) { continue; } var className = typeof candidate.className === "string" ? candidate.className.toLowerCase() : ""; var idName = typeof candidate.id === "string" ? candidate.id.toLowerCase() : ""; var name = className + " " + idName; var excludedWords = [ "menu", "navigation", "header", "footer", "sidebar", "comment", "related", "recommend", "share", "social", "advert", "advertisement", "ads", "banner", "popup", "modal" ]; var excluded = false; for ( var k = 0; k < excludedWords.length; k++ ) { if ( name.indexOf( excludedWords[k] ) !== -1 ) { excluded = true; break; } } if (excluded) { continue; } var candidateText = candidate.innerText || candidate.textContent || ""; candidateText = candidateText.trim(); if ( candidateText.length < 300 ) { continue; } if ( candidateText.length > bestLength ) { bestLength = candidateText.length; best = candidate; } } return best; } /* ======================================================== 分割文章 ======================================================== */ function splitSpeechText( text, maxLength ) { var chunks = []; var start = 0; while ( start < text.length ) { var end = Math.min( start + maxLength, text.length ); if ( end < text.length ) { var positions = [ text.lastIndexOf( "。", end ), text.lastIndexOf( "?", end ), text.lastIndexOf( "!", end ), text.lastIndexOf( ";", end ), text.lastIndexOf( "\n", end ) ]; var best = Math.max.apply( null, positions ); if ( best > start + 500 ) { end = best + 1; } } chunks.push( text.substring( start, end ) ); start = end; } return chunks; } /* ======================================================== 语音系统 重要: 不再简单地拿第一个 zh-CN。 会综合: 1. 浏览器默认 Voice 2. 中文语言 3. Voice 名称 4. 系统 Voice 5. 在线 Voice 尽量选择质量较好的 Voice。 ======================================================== */ function getAllVoices() { if ( !window.speechSynthesis ) { return []; } try { return ( window.speechSynthesis .getVoices() || [] ); } catch (error) { return []; } } /* ======================================================== 判断中文 ======================================================== */ function isChineseVoice(voice) { if (!voice) { return false; } var lang = String( voice.lang || "" ).toLowerCase(); return ( lang.indexOf("zh") === 0 || lang.indexOf("cmn") === 0 ); } /* ======================================================== Voice 名称评分 这里故意不写死某一个浏览器。 不同系统名称可能不同。 ======================================================== */ function scoreVoice( voice ) { if (!isChineseVoice(voice)) { return -10000; } var score = 0; var name = String( voice.name || "" ).toLowerCase(); var lang = String( voice.lang || "" ).toLowerCase(); /* * 浏览器标记为 default * 非常重要。 * * 这意味着系统/浏览器认为它 * 是当前最适合的默认语音。 */ if (voice.default) { score += 100; } /* * 简体中文优先。 */ if ( lang === "zh-cn" || lang === "zh-hans" || lang === "zh-sg" ) { score += 40; } /* * 常见高质量中文 Voice 名称。 */ var preferredNames = [ "xiaoxiao", "xiaoyi", "yunxi", "yunyang", "yunxia", "yunxia", "xiaoxuan", "ting-ting", "tingting", "mei-jia", "meijia", "sin-ji", "sinji", "lili" ]; for ( var i = 0; i < preferredNames.length; i++ ) { if ( name.indexOf( preferredNames[i] ) !== -1 ) { score += 70; break; } } /* * Apple 中文 Voice。 */ if ( name.indexOf("ting") !== -1 || name.indexOf("mei") !== -1 || name.indexOf("sin") !== -1 ) { score += 35; } /* * Microsoft 新一代中文语音。 */ if ( name.indexOf("microsoft") !== -1 ) { score += 25; } /* * Google 中文语音。 */ if ( name.indexOf("google") !== -1 ) { score += 20; } /* * Online 通常代表在线神经语音。 */ if ( name.indexOf("online") !== -1 ) { score += 20; } /* * Enhanced / Premium。 */ if ( name.indexOf("enhanced") !== -1 || name.indexOf("premium") !== -1 ) { score += 25; } /* * 明显的机械/测试语音降低优先级。 */ var badNames = [ "espeak", "festival", "dummy", "test", "compact" ]; for ( var j = 0; j < badNames.length; j++ ) { if ( name.indexOf( badNames[j] ) !== -1 ) { score -= 80; } } /* * 中文普通话。 */ if ( lang === "zh-cn" ) { score += 15; } return score; } /* ======================================================== 选择最佳中文语音 ======================================================== */ function chooseChineseVoice() { var voices = getAllVoices(); if ( !voices.length ) { return null; } var chineseVoices = []; for ( var i = 0; i < voices.length; i++ ) { if ( isChineseVoice( voices[i] ) ) { chineseVoices.push( voices[i] ); } } if ( !chineseVoices.length ) { return null; } /* * 给每个 Voice 打分。 */ var best = chineseVoices[0]; var bestScore = scoreVoice(best); for ( var j = 1; j < chineseVoices.length; j++ ) { var current = chineseVoices[j]; var currentScore = scoreVoice(current); if ( currentScore > bestScore ) { best = current; bestScore = currentScore; } } return best; } /* ======================================================== 等待 Voice 列表 Firefox / Safari 有时候不是页面加载时 就把 Voice 列表提供出来。 ======================================================== */ function waitForChineseVoice( callback ) { var voice = chooseChineseVoice(); if (voice) { callback(voice); return; } var attempts = 0; var timer = setInterval( function () { attempts++; var found = chooseChineseVoice(); if (found) { clearInterval( timer ); callback( found ); return; } if ( attempts >= 30 ) { clearInterval( timer ); callback( null ); } }, 100 ); } /* ======================================================== 开始朗读 ======================================================== */ function readArticle() { if ( !window.speechSynthesis || !window.SpeechSynthesisUtterance ) { showMessage( "抱歉,您的浏览器不支持文章朗读功能。" ); return; } /* * 停止上一次。 */ window.speechSynthesis.cancel(); /* * 找 Voice。 */ waitForChineseVoice( function (voice) { if (!voice) { showMessage( "您的设备目前没有可用的中文语音,请在系统朗读设置中启用中文语音。" ); return; } var text = getReadingText(); if ( !text || text.length < 50 ) { showMessage( "没有找到当前文章正文。" ); return; } /* * 保存当前 Voice。 */ window.mnewsReaderVoice = voice; /* * 分段。 */ window.mnewsReaderChunks = splitSpeechText( text, 2200 ); window.mnewsReaderIndex = 0; speakNext(); } ); } /* ======================================================== 下一段 ======================================================== */ function speakNext() { if ( !window.mnewsReaderChunks || window.mnewsReaderIndex >= window.mnewsReaderChunks.length ) { showMessage( "文章朗读结束。" ); window.mnewsReaderChunks = null; window.mnewsReaderIndex = 0; return; } var voice = window.mnewsReaderVoice; if (!voice) { window.speechSynthesis.cancel(); showMessage( "中文语音不可用,朗读已经停止。" ); return; } var text = window.mnewsReaderChunks[ window.mnewsReaderIndex ]; var utterance = new SpeechSynthesisUtterance( text ); /* * 语言设为中文。 * * 但 Voice 使用实际选择出来的 Voice。 */ utterance.lang = voice.lang || "zh-CN"; utterance.voice = voice; /* * 不要把速度调得过慢。 * * 0.95 对中文新闻比较自然。 */ utterance.rate = 0.95; utterance.pitch = 1; utterance.volume = 1; utterance.onstart = function () { showMessage( "正在朗读文章……" ); }; utterance.onend = function () { window.mnewsReaderIndex++; setTimeout( function () { speakNext(); }, 80 ); }; utterance.onerror = function () { window.speechSynthesis.cancel(); window.mnewsReaderChunks = null; window.mnewsReaderIndex = 0; showMessage( "中文语音无法正常播放,请检查设备的语音设置。" ); }; window.mnewsReaderUtterance = utterance; window.speechSynthesis.speak( utterance ); } /* ======================================================== 停止朗读 注意: 这里也不涉及收藏。 ======================================================== */ function stopReading() { if ( window.speechSynthesis ) { window.speechSynthesis.cancel(); } window.mnewsReaderChunks = null; window.mnewsReaderIndex = 0; window.mnewsReaderVoice = null; showMessage( "文章朗读已经停止。" ); } /* ======================================================== 初始化 Voice ======================================================== */ if ( window.speechSynthesis ) { /* * 第一次主动请求 Voice。 */ window.speechSynthesis .getVoices(); /* * Safari / Firefox / Chrome * 在 Voice 列表加载后通知。 */ if ( "onvoiceschanged" in window.speechSynthesis ) { window.speechSynthesis.onvoiceschanged = function () { window.mnewsReaderVoices = window.speechSynthesis .getVoices(); }; } } /* ======================================================== 事件:收藏 ======================================================== */ if (favoriteButton) { favoriteButton.addEventListener( "click", function () { addFavorite(); } ); } /* ======================================================== 事件:下载 ======================================================== */ if (downloadButton) { downloadButton.addEventListener( "click", function () { downloadArticle(); } ); } /* ======================================================== 事件:分享 ======================================================== */ if (shareButton) { shareButton.addEventListener( "click", function () { shareArticle(); } ); } /* ======================================================== 事件:复制 ======================================================== */ if (copyButton) { copyButton.addEventListener( "click", function () { copyUrl(); } ); } /* ======================================================== 事件:微信 ======================================================== */ if (wechat) { wechat.addEventListener( "click", function () { shareToWeChat(); } ); } /* ======================================================== 事件:朗读 ======================================================== */ if (readButton) { readButton.addEventListener( "click", function () { readArticle(); } ); } /* ======================================================== 事件:停止 ======================================================== */ if (stopButton) { stopButton.addEventListener( "click", function () { stopReading(); } ); } /* ======================================================== 初始化分享链接 ======================================================== */ updateShareLinks(); /* ======================================================== 页面返回 ======================================================== */ window.addEventListener( "pageshow", function () { updateShareLinks(); } ); /* ======================================================== 浏览器历史变化 ======================================================== */ window.addEventListener( "popstate", function () { updateShareLinks(); } ); /* ======================================================== 页面重新获得焦点 ======================================================== */ window.addEventListener( "focus", function () { updateShareLinks(); } ); /* ======================================================== 点击外部关闭分享 ======================================================== */ document.addEventListener( "click", function (event) { if ( !sharePanel || !shareButton ) { return; } if ( sharePanel.contains( event.target ) || shareButton.contains( event.target ) ) { return; } sharePanel.classList.remove( "mnews-share-open" ); } ); })();

捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP