]*>\s*)?]*>\s*免费节点订阅链接\s*[::]?\s*(?:\s*

)?~u', $html, $heading, PREG_OFFSET_CAPTURE)) return null; if (!preg_match('~]*>\s*温馨提示\s*[::]?\s*

~u', $html, $help, PREG_OFFSET_CAPTURE, $heading[0][1])) return null; $start = $heading[0][1];$from = $start + strlen($heading[0][0]);$end = $help[0][1]; $intro = self::fragment(str_replace(ArticleRecommendations::SLOT, '', substr($html, 0, $start))); $subscriptions = self::fragment(substr($html, $from, $end - $from)); if (!$intro || !$subscriptions) return null; $result = ['count' => null, 'speed' => null, 'regions' => [], 'community' => [], 'intro' => '', 'notes' => '', 'subscriptions' => [], 'usage' => substr($html, $end + strlen($help[0][0]))]; foreach (iterator_to_array($intro->childNodes) as $node) { $text = self::text($node->textContent); if ($text === '') continue; if (preg_match('/^本次更新共\s*(\d+)\s*个可用节点[,,]\s*最高速度\s*([^。]+)[。]?$/u', $text, $m)) { $result['count'] = (int)$m[1];$result['speed'] = trim($m[2]);continue; } if (preg_match('/^覆盖(.+?)等多个区域[。]?$/u', $text, $m)) { $result['regions'] = array_values(array_unique(array_filter(array_map('trim', preg_split('/[、,,]/u', $m[1])))));continue; } if (str_starts_with($text, '复制下方订阅链接')) continue; if ($node instanceof DOMElement && preg_match('/点击加入我们的tg频道|点击关注我们的github仓库/u', $text)) { $recognized = false; foreach ($node->getElementsByTagName('a') as $a) { $url = $a->getAttribute('href');$host = strtolower(parse_url($url, PHP_URL_HOST) ?? ''); if (parse_url($url, PHP_URL_SCHEME) !== 'https' || !in_array($host, ['t.me', 'github.com'], true)) continue; $result['community'][] = ['url' => $url, 'label' => $host === 't.me' ? 'Telegram 更新频道' : 'GitHub 项目仓库'];$recognized = true; } if ($recognized) continue; } $result['intro'] .= $intro->ownerDocument->saveHTML($node); } $urls = []; foreach (iterator_to_array($subscriptions->getElementsByTagName('a')) as $a) { if (!$a->hasAttribute('data-clipboard-text')) continue; $url = $a->getAttribute('data-clipboard-text'); if (!filter_var($url, FILTER_VALIDATE_URL) || !in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) return null; $path = parse_url($url, PHP_URL_PATH) ?? ''; $type = match (true) { (bool)preg_match('/-clash\.(yaml|yml)$/i', $path) => ['key'=>'clash', 'title'=>'Clash / Mihomo', 'format'=>'YAML', 'clients'=>'Clash Verge Rev、Mihomo 等客户端', 'symbol'=>'C'], (bool)preg_match('/-singbox\.(txt|json)$/i', $path) => ['key'=>'singbox', 'title'=>'Sing-box / Hiddify', 'format'=>'SING-BOX', 'clients'=>'Sing-box、Hiddify 等客户端', 'symbol'=>'S'], (bool)preg_match('/-v2ray\.txt$/i', $path) => ['key'=>'v2ray', 'title'=>'V2ray / Shadowrocket', 'format'=>'通用订阅', 'clients'=>'v2rayNG、Shadowrocket 等客户端', 'symbol'=>'V'], default => null, }; // Unknown formats keep the original article rather than dropping a subscription. if (!$type || isset($result['subscriptions'][$type['key']])) return null; $result['subscriptions'][$type['key']] = $type + ['url'=>$url];$urls[] = $url; } if ($result['subscriptions'] === []) return null; // Keep every supplementary note while removing only labels/URLs now shown in cards. foreach (iterator_to_array($subscriptions->childNodes) as $node) { $text = self::text($node->textContent); if ($text === '' || in_array($text, $urls, true) || preg_match('/订阅链接\s*[::]$/u', $text)) continue; if ($node instanceof DOMElement && ($node->hasAttribute('data-clipboard-text') || $node->getElementsByTagName('a')->length)) { foreach (iterator_to_array($node->getElementsByTagName('a')) as $link) { if ($link->hasAttribute('data-clipboard-text')) $link->parentNode->removeChild($link); } if (self::text($node->textContent) === '' || in_array(self::text($node->textContent), $urls, true)) continue; } $result['notes'] .= $subscriptions->ownerDocument->saveHTML($node); } $order = ['clash'=>0, 'v2ray'=>1, 'singbox'=>2]; uasort($result['subscriptions'], fn ($a, $b) => $order[$a['key']] <=> $order[$b['key']]); $result['subscriptions'] = array_values($result['subscriptions']); return $result; } private static function text(string $text): string { return trim(preg_replace('/\s+/u', ' ', $text)); } private static function fragment(string $html): ?DOMElement { $dom = new DOMDocument('1.0', 'UTF-8');$before = libxml_use_internal_errors(true); try { $dom->loadHTML('
'.$html.'
', LIBXML_NONET); return $dom->getElementById('fn-root'); } catch (\Throwable) { return null; } finally { libxml_clear_errors();libxml_use_internal_errors($before); } } }