简介:这是一份基于PHP实现的必应网页搜索程序v1.0源码,面向具备PHP基础、希望学习第三方搜索API集成的Web开发者与初学者。程序通过PHP中间层与微软Bing搜索API通信,完成请求发送、响应解析与结果展示,可用于搭建自定义搜索页面或作为API调用练手项目。压缩包共27个文件,约148KB,包含4个PHP核心脚本(配置、搜索入口与请求处理)、5个JavaScript交互脚本、3个CSS样式表,以及png、gif、jpg等界面素材和xml、txt说明文档,结构紧凑、便于阅读。目前已有256人学习下载。解压后可参考说明文档理解配置项与API密钥设置方式,学习cURL请求、JSON/XML解析、搜索表单与结果页渲染的完整链路,并可按需扩展缓存或数据库记录功能,是入门搜索接口开发的实用参考。
1. 一个 PHP 单文件搜索页,为什么还有人愿意拆
必应网页搜索的 PHP 程序源码,听起来像是个“套壳页面”——前端一个输入框,后端拼个 URL 跳过去就完事。但真正拆过这类源码的人知道,事情没这么简单。必应官方并不提供公开的网页搜索 API 给个人开发者直接调用,所以这类 PHP 程序的核心价值在于:它用服务端抓取或接口模拟的方式,把必应搜索结果页的数据取回来,再在自己的页面上重新渲染。你拿到的是一个完整的、可部署的搜索中间层。
这份 v1.0 源码适合三类人:一是想学 PHP 做搜索聚合的开发者,二是需要在自己的站内嵌入搜索能力但不想依赖第三方 JS SDK 的站长,三是想研究必应搜索结果页 DOM 结构和参数规律的技术爱好者。它解决的核心问题是:让你在不申请任何 API Key 的情况下,通过 PHP 后端完成一次完整的必应搜索请求并解析结果。下面我从代码结构、部署流程、参数调优到踩坑记录,把这套源码拆开讲清楚。
2. 源码结构与请求链路:从入口文件到结果渲染
2.1 目录骨架与文件职责
拿到压缩包解压后,常见的目录结构大致如下(不同版本可能略有差异,但核心文件不会少):
bing-search-php/ ├── index.php # 入口文件,渲染搜索框和结果页 ├── search.php # 核心搜索逻辑,接收关键词并请求必应 ├── config.php # 配置项:请求头、超时、结果数量等 ├── lib/ │ ├── Request.php # HTTP 请求封装类 │ └── Parser.php # HTML 解析类,提取搜索结果 ├── assets/ │ ├── style.css # 页面样式 │ └── script.js # 前端交互(搜索建议、分页等) └── cache/ # 搜索结果缓存目录(需可写)index.php负责展示搜索界面,表单提交到search.php。search.php接收q参数(关键词),调用lib/Request.php向必应发起请求,拿到 HTML 后用lib/Parser.php提取标题、链接、摘要,最后输出到页面。config.php里通常定义了请求头中的 User-Agent、Accept-Language、超时时间、每页结果数等。
注意:
cache/目录必须有写入权限,否则缓存功能会静默失败,每次搜索都重新请求,速度会明显变慢。
2.2 请求构造:必应搜索 URL 的参数拆解
必应网页搜索的基础 URL 是https://www.bing.com/search,核心参数如下:
| 参数 | 含义 | 常用值 |
|---|---|---|
q | 搜索关键词 | URL 编码后的字符串 |
count | 每页结果数 | 10 / 20 / 30 |
first | 结果偏移量 | 1, 11, 21... |
FORM | 来源标识 | 可自定义,不影响结果 |
setlang | 界面语言 | zh-Hans / en |
cc | 国家/地区 | CN / US |
源码里search.php构造请求的典型写法:
// search.php 核心请求片段 $keyword = isset($_GET['q']) ? trim($_GET['q']) : ''; $page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; $count = 10; $first = ($page - 1) * $count + 1; $params = http_build_query([ 'q' => $keyword, 'count' => $count, 'first' => $first, 'setlang' => 'zh-Hans', 'cc' => 'CN', ]); $url = 'https://www.bing.com/search?' . $params; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Language: zh-CN,zh;q=0.9', ], ]); $html = curl_exec($ch); curl_close($ch);这段代码的逻辑很直白:拼参数、发请求、拿 HTML。first参数控制分页偏移,第 1 页是 1,第 2 页是 11,以此类推。setlang和cc影响返回结果的地区和语言偏好,如果你做的是中文站,保持zh-Hans和CN就行。CURLOPT_TIMEOUT设 10 秒是常见做法,必应响应通常很快,超过 10 秒基本是网络问题。
2.3 HTML 解析:从结果页提取结构化数据
拿到 HTML 后,解析是第二个关键步骤。必应搜索结果页的结果条目通常包裹在<li class="b_algo">标签里,标题在<h2><a>中,摘要在<p>或<div class="b_caption">中。源码里Parser.php一般用 DOMDocument 或正则来提取:
// lib/Parser.php 解析片段 $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); $xpath = new DOMXPath($dom); $items = $xpath->query('//li[contains(@class, "b_algo")]'); $results = []; foreach ($items as $item) { $titleNode = $xpath->query('.//h2/a', $item)->item(0); $linkNode = $xpath->query('.//h2/a/@href', $item)->item(0); $descNode = $xpath->query('.//p', $item)->item(0); if ($titleNode && $linkNode) { $results[] = [ 'title' => trim($titleNode->textContent), 'url' => trim($linkNode->nodeValue), 'desc' => $descNode ? trim($descNode->textContent) : '', ]; } }用 DOMXPath 比正则稳,因为必应的 HTML 结构偶尔会调整,但b_algo这个 class 名多年没变过。libxml_use_internal_errors(true)是为了吞掉 HTML 不规范导致的警告,否则页面上会冒出一堆 warning。解析出来的$results数组直接交给index.php渲染即可。
提示:如果解析结果为空,先检查
$html是否真的包含b_algo,有时候必应会返回验证页或空结果页,这时候需要检查请求头是否被识别为爬虫。
3. 部署与调试:从零跑通一次搜索
3.1 环境准备与配置项调整
这套源码对环境要求不高,PHP 7.0 以上就能跑,但建议用 PHP 7.4 或 8.x,因为 8.x 对 DOMDocument 的性能有优化。需要开启的扩展:curl、dom、mbstring、json。如果你用的是宝塔面板或 phpstudy,这些扩展默认都有。
部署步骤:
- 把源码解压到网站根目录或子目录
- 确保
cache/目录可写(chmod 755或777,视服务器用户而定) - 修改
config.php中的超时时间和缓存开关 - 浏览器访问
index.php,输入关键词测试
config.php里几个值得关注的配置:
// config.php 关键配置 return [ 'timeout' => 10, // 请求超时秒数 'cache_enable' => true, // 是否开启缓存 'cache_time' => 3600, // 缓存有效期(秒) 'per_page' => 10, // 每页结果数 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ];cache_time设 3600 表示同一关键词一小时内只请求一次必应,后续直接读缓存文件。这对个人站来说够用,但如果你做的是新闻类搜索,建议降到 300 秒。per_page不要超过 30,必应对单页结果数有限制,超过 30 可能会被截断或触发验证。
3.2 搜索请求的完整调试流程
部署完第一次访问,大概率会遇到“搜索结果为空”或“页面报错”。我一般按这个顺序排查:
第一步,在search.php里把$html打印出来看看到底返回了什么。如果是一堆 JS 或者验证页面,说明请求头被识别了。这时候换一个更真实的 User-Agent,加上Accept和Referer头。
第二步,检查curl_exec的返回值。如果返回false,用curl_error($ch)看具体错误。常见的是 SSL 证书问题,加一行CURLOPT_SSL_VERIFYPEER => false可以临时绕过,但生产环境不建议。
第三步,如果$html正常但解析为空,把$html保存成文件,用浏览器打开,搜索b_algo看是否存在。如果不存在,说明必应改了结构或者返回了不同版本的结果页。
// 调试用:保存原始 HTML 到文件 file_put_contents(__DIR__ . '/debug_' . md5($keyword) . '.html', $html);这个调试代码我每次接手新的搜索类项目都会加,跑通后再删掉。它能帮你省下大量猜测时间。
3.3 分页与结果数量的参数边界
分页逻辑看似简单,但有几个边界要注意。first参数从 1 开始,每页递增per_page。但必应对first有上限,通常最多到 200 左右,再往后翻结果质量会急剧下降甚至返回空。所以你的分页组件不要做成无限翻页,建议最多显示 10 页。
另外,count参数虽然可以设到 50,但实际返回的结果数往往少于你请求的数量。必应会根据查询意图动态调整,有些关键词只返回 8 条,有些返回 20 条。你的解析代码要能处理“结果数少于预期”的情况,不要写死循环。
注意:如果你在
search.php里用了header('Location: ...')做跳转,记得在跳转前完成所有数据处理,否则缓存写入会被中断。
4. 避坑与常见问题:那些让你白忙半天的细节
4.1 请求被重定向到验证页
现象:$html里没有b_algo,而是一段 JavaScript 或者“请验证你是人类”的提示。
原因:必应对频繁请求或异常请求头会返回验证页。常见触发条件:同一 IP 短时间内大量请求、User-Agent 为空或过于简单、缺少Accept-Language头。
解决:在CURLOPT_HTTPHEADER里补全Accept、Accept-Language、Referer三个头。如果还是不行,降低请求频率,并在cache层做更激进的缓存策略。我一般会把cache_time调到 7200 秒,牺牲实时性换稳定性。
4.2 中文关键词乱码
现象:搜索中文关键词时,结果页标题和摘要显示为乱码。
原因:必应返回的 HTML 编码可能是 UTF-8,但DOMDocument::loadHTML默认按 ISO-8859-1 处理,导致中文被错误解码。
解决:在loadHTML之前做编码转换,或者直接在 HTML 字符串前面加<meta charset="UTF-8">:
$html = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . $html; $dom->loadHTML($html);这行代码看着不起眼,但少了它,中文搜索结果全是问号。
4.3 缓存目录权限导致静默失败
现象:搜索功能正常,但每次刷新页面速度都一样慢,缓存文件没有生成。
原因:cache/目录没有写入权限,file_put_contents返回false,但代码里没有做错误处理,所以静默失败。
解决:在缓存写入处加判断:
$cacheFile = __DIR__ . '/cache/' . md5($keyword) . '.json'; if (!is_dir(dirname($cacheFile))) { mkdir(dirname($cacheFile), 0755, true); } if (file_put_contents($cacheFile, json_encode($results)) === false) { error_log('Cache write failed: ' . $cacheFile); }同时检查cache/目录的所属用户是否和 PHP 运行用户一致。Linux 下ps aux | grep php可以看到运行用户,然后chown一下就行。
4.4 分页参数被忽略
现象:点击第二页,结果和第一页一样。
原因:search.php里读取page参数时用了$_GET['page'],但前端分页链接没有把page传过去,或者传了但被intval成了 0。
解决:检查前端分页链接的构造,确保page参数正确拼接。后端用max(1, intval($_GET['page'] ?? 1))做兜底。另外,first的计算要用($page - 1) * $count + 1,不要写成$page * $count,否则第一页会从第 11 条开始。
4.5 PHP 8 下的兼容性问题
现象:在 PHP 8.0 以上环境运行时,页面报Deprecated或TypeError。
原因:这套源码 v1.0 可能是在 PHP 7.x 下写的,PHP 8 对部分函数签名和类型检查更严格。常见问题:curl_close在 PHP 8 中不再是必须的(但也不报错),mb_convert_encoding的第二个参数在 PHP 8.2 后有所调整。
解决:把mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')改成mb_convert_encoding($html, 'UTF-8', 'UTF-8')或者直接用htmlspecialchars处理。如果报错在DOMDocument相关代码,升级到 PHP 8.1 以上通常会自动解决大部分兼容问题。
5. 进阶技巧:让搜索页跑得更稳的几个习惯
5.1 用多组 User-Agent 轮换降低被识别概率
单一 User-Agent 用久了容易被必应标记。我一般会在config.php里放一个数组,每次请求随机取一个:
$agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', ]; $ua = $agents[array_rand($agents)];配合cache_time拉长到 3600 秒以上,基本可以做到一天只发几十次请求,被验证的概率极低。
5.2 结果去重与域名过滤
必应搜索结果里经常出现同一域名的多条记录。如果你做的是聚合展示,去重能提升体验:
$seen = []; $unique = []; foreach ($results as $r) { $host = parse_url($r['url'], PHP_URL_HOST); if (!isset($seen[$host])) { $seen[$host] = true; $unique[] = $r; } } $results = $unique;这段代码按域名去重,每个域名只保留第一条结果。如果你还想过滤掉某些站点,加一个黑名单数组,在循环里continue掉就行。
5.3 验证搜索是否真正生效的检查清单
每次部署完或修改代码后,我习惯走一遍这个清单:
| 检查项 | 预期结果 | 不通过时看哪里 |
|---|---|---|
搜索框提交后 URL 带q参数 | 地址栏出现?q=关键词 | index.php表单 method 和 action |
| 页面显示至少 5 条结果 | 标题、链接、摘要齐全 | Parser.php的 XPath 查询 |
| 翻到第二页结果不同 | 第 11 条开始 | search.php的first计算 |
| 同一关键词第二次搜索变快 | 缓存文件生成 | cache/权限和cache_enable |
| 中文关键词不乱码 | 标题正常显示 | loadHTML前的编码声明 |
这个清单我每次改完搜索类项目都会跑一遍,五分钟内能定位到 90% 的问题。从那以后我每次部署新的搜索源码,都强制先跑一遍这个检查表,再去做样式和交互。希望帮到你。
本文还有配套的精品资源,点击获取