滚动新闻 →
学生抗议影响 全法500所学校将全面或部分停课 Windows 11 应用无法打开怎么办?从权限、组件到重新安装逐项检查 24岁女狂吃2小时自助餐 腹胀如球开腹取出3斤食物 PHP 正则表达式怎么入门?从验证邮箱到提取文章内容逐步掌握 疑遭攻击阴谋锁定 美B-1轰炸机全数撤离英空军基地  李白为什么能够成为中国最著名的诗人之一 古代医书是怎样流传下来的 因高强训练心脏不适 邵兵儿子也停止录制《披哥》 台股涨逾千点冲49601点新高 挑战5万点关卡 围棋与中国古代棋文化的发展 涉监视赖清德之子 加州华裔经纪美机场被捕 如何培养孩子主动帮助弱者的意识 如何让人物肤色看起来更加自然 布什号美国航母抵达普吉岛 船员开启度假时光 金恩持险胜吴依铭 跻身IBK杯世界赛八强 汉谟拉比法典究竟反映了怎样的社会 大陆十一长假婚宴降温 “三无婚礼”受青睐 厨房柜子深处的东西总拿不到应该怎么解决 美海岸警卫队发现医疗机残骸 搜寻6名失踪者 母亲和祖母的家常菜为什么容易成为家庭记忆 俄连续四天轰炸基辅大桥 德国宣布对乌新军援 旅行中遇到道路变化应该怎么办 飞机上喝什么饮料好?空姐推荐每次都应点“它” 川普成立超级智能工作组 协调政府民间AI合作 驻日美军涉抢劫杀害日本女子 震动日本朝野 发动机高温后应该如何检查 巴西总统大选登场 备受中美关注 电动车在高速公路上为什么更依赖充电设施 木材维修需要哪些基本材料 多个 AI 模型共享 GPU 时应该怎样调度,模型之间会产生哪些资源竞争 俄实验室人员死于鼠疫 200人被隔离 白宫:在监测 Google Cloud VPN 和 Interconnect 怎么选,企业网络连接应该根据什么判断 SaaS API 是什么?企业软件之间的数据交换通常离不开它 显示器突然黑屏但电脑声音正常,维修时应该先检查什么 蔡康永现身沈伯洋造势大会 小粉红跳脚陆车企急切割 Windows 11 系统突然死机怎么办?常见软件和硬件因素全面分析 PHP 字符串处理有哪些常用函数?开发中文网站时尤其需要注意这些细节 从陶渊明看中国文学中的理想生活 地方财政捉襟见肘 远洋捕捞成解药? 芝加哥7岁女童遭陌生女当街刺死 路人合力缉凶

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

发布时间: 2026-10-04 22:00:02    最后更新: 2026-10-04 23:25:36    阅读:7  约4 分钟阅读     

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