如何用PHP编写代码巧妙抓取特定小说网站内容?
- 内容介绍
- 文章标签
- 相关推荐
在实际项目中。很多开发者都会遇到以下痛点:
- 抓取速度慢,频繁超时导致使用者体验下降。
- 网站频繁更改页面结构。正则表达式失效,需要频繁维护代码。
- IP 被封或被网站检测到爬虫行为,导致抓取中断。
- 中文乱码、字符集不统一,导致采集的章节内容出现乱码。
- 采集后数据保存不规范,后期检索和展示困难。
一、整体思路概览
二、准备工作
1. PHP 环境要求
- PHP>= 7.4
-
已开启
allow_url_fopen或安装cURL -
(或直接使用原生
DOMDocument/XPath) - Mysql/MariaDB或本地文件程序
2. 常用工具库安装
# 使用 Composer 安装 phpQuery
composer require electrolinux/phpquery
# 若想使用 Guzzle 替代 cURL,可额外安装
composer require guzzlehttp/guzzle
3、获取页面内容的四种方式及对比
| 方式 | 优点 | 缺点 / 注意点 |
|---|---|---|
file_get_contents | 代码最简洁;怎么说呢,无需额外 | 不支持复杂请求头;对大文件易超时, |
fopen/fread | 可自定义流上下文,灵活控制超时。 | PHP 配置需要允许 URL fopen;一样受限于单线程, |
CURL(curl_exec) | 支持自定义 Header、Cookie、代理;性能最佳, | LAMP 环境需安装 curl 代码略显冗长。 |
| Guzzle HTTP 客户端 | Psr-7 标准;处理友好,支持并发请求。 | Larger dependency;对新手有学习成本, |
CURL 示例
$url = 'https://www.example.com/book/12345.html';$ch = curl_init;curl_setopt_array($ch,,]);$html = curl_exec;if ) {
// 记录日志,便于后期排查
error_log);}
curl_close;>
四、解析页面结构
a) 使用 phpQuery 简化 DOM 操作
// 引入 phpQuery 自动加载文件
require_once __DIR__.'/vendor/autoload.php';use phpQuery;// 将获取到的 HTML 转为 phpQuery 对象
$doc = phpQuery::newDocumentHTML;// 示例:获取章节标题 & 内容
$title = $doc->find->text;$content = $doc->find->html;// 去除多余的换行与空格
$content = preg_replace;>
b) 原生 DOMDocument + XPath
$dom = new DOMDocument;话说回来,// 抑制因非法 HTML 导致的警告
libxml_use_internal_errors;$dom->loadHTML);libxml_clear_errors;$xpath = new DOMXPath;// 获取章节标题
$titleNode = $xpath->query;$title = $titleNode->length?trim->nodeValue) : '未知标题';// 获取正文内容
$contentNode = $xpath->query;$contentHtml = '';foreach {
$contentHtml .= $dom->saveHTML;}
>
五、正则匹配细节
虽然推荐使用 DOM 解析,但在某些仅有纯文本的页面仍需要正则抽取。下面给出常见的章节标题和正文匹配模式:
// 匹配类似 “第12章 奇怪的旅程”
$chapterPattern = '/第\s*\s*章\s*\s*/u';// 匹配正文块:去除广告或脚本标签
$contentPattern = '/
<\/div>/si';>
TIPS:Curl 抓取后若出现乱码,可在正则前先统一字符集:
$html = mb_convert_encoding;>
六、数据持久化方案
a) 写入本地 TXT 文件
$filename = __DIR__.'/novel/'.$title.'.txt';file_put_contents;echo "已写入文件:{$filename}";>
b) 保存至 MySQL 数据库
// PDO 示例,确保使用预处理防止注入
$pdo = new PDO('mysql:host=127.0.0.1;dbname=novel;charset=utf8mb4'。'root','password',);$sql = "INSERT INTO chapters VALUES )
ON DUPLICATE KEY UPDATE content=:content";$stmt = $pdo->prepare;$stmt->execute();>
七、定时任务实现自动更新
TIPS:Cron 是最常用且可靠的方式。怎么说呢,若服务器不支持 Cron。可考虑 Laravel Scheduler 或 Symfony Console 定时执行。
# 在 Linux 主机上编辑 crontab -e
30 2 * * * /usr/bin/php /var/www/html/novelcrawler.php>> /var/log/novelcrawler.log 2>&1
php // novel_crawler.php 简易入口文件
requireonce DIR.'/vendor/autoload.php';requireonce DIR.'/crawl_functions.php';// 包含所有业务函数
// 假设已有书籍列表存于 DB 中,需要遍历每本书进行增量更新 $books = getAllBooks;// 返回数组,... ]
foreach {
try {
updateBookChapters;} catch {
error_log);}
}
>
八、防反爬技巧
- User-Agent 随机化: 每次请求随机挑选一个常见浏览器 UA。
- X-Forwarded-For & Proxy IP 池: 使用国内外多个代理 IP,降低单一 IP 的访问频率。
-
SLEEP 与随机间隔: 在每次请求之间加入
,避免触发频率阈值。 - Caching 本地 HTML: 同一章节多次访问时直接读取缓存,可显著降低请求次数。
- DDoS 防护规避: 如发现返回验证码或 Cloudflare 挑战页。可改为使用 Selenium/Puppeteer 等无头浏览器,但这会增加资源开销,仅在必要时采用。
九、完整示例代码汇总
iniset;errorreporting;
require_once DIR.'/vendor/autoload.php';use phpQuery;
// ---------- 配置 ----------
$baseUrl = 'https://www.biquge.com.cn/book/12345/';$startPageUrl = $baseUrl . 'index.html';话说回来,// 起始目录页
$userAgents =;
// ---------- 数据库 ----------
$pdo = new PDO('mysql:host=127.0.0.1;dbname=novel;charset=utf8mb4'。'root','password',);
// ---------- 工具函数 ----------
function curlGet: string{
static $idx=0;$ch=curlinit;curlsetoptarray($ch。CURLOPTHTTPHEADER=>,]);$data=curlexec;if){
throw new RuntimeException);}
curlclose;// 防止被封:随机延迟 500~1500ms
usleep);怎么说呢,return mbconvertencoding;}
function fetchChapterList: array{
// 用 phpQuery 抽取章节链接与标题
phpQuery::newDocumentHTML;$list=,foreach as $a){
/** @var DOMElement $a */
$href=pq->attr;if{continue,}
// 相对方法转绝对方法
if!==0){
global $baseUrl;$href=$baseUrl.ltrim;}
$list=,}
return $list;}
function parseChapter: string{
// 使用 XPath 更精准定位正文区域,兼容不同站点结构
libxmluseinternalerrors;$dom=new DOMDocument;@$dom->loadHTML);libxmlclear_errors;
// 常见正文容器 ID / CLASS 列表,可自行
:
$candidates=;
foreach {
if===0){
// ID 查询
@$node=$dom->getElementById);if{ break,说起来,}
}else{
// Class 查询
@$xpath=new DOMXPath;@$nodes=$xpath->query . "')]");if {
@$node=$nodes->item;break,}
}
php
}
if){
throw new RuntimeException;}
$contentHtml=$node->ownerDocument->saveHTML;
// 清理广告脚本等噪声:
$content=pregreplace*?#is','',$contentHtml);$content=pregreplace;// 转换成纯文本并去除多余空行:
$content=striptags));$content=pregreplace;
return trim;}
// ---------- 主流程 ----------
try{
// 第一步先:获取目录页并抽取所有章节链接
$catalogHtml=curlGet;$chapters=fetchChapterList;
foreach{
echo "正在处理第".."章:{$chapter}
";$html=curlGet;$text=parseChapter;
// 保存至数据库
$stmt=$pdo->prepare(
"INSERT INTO chapters
VALUES)
ON DUPLICATE KEY UPDATE content=:cont"
);$stmt->execute(,':url' => $chapter。':cont' => $text,]);
// 同步写入本地 txt
$filePath=DIR.'/output/'.pregreplace.'.txt';fileput_contents;不过,}
echo "全部完成!",
}catch{
error_log);}
>
十、常见问题 FAQ
- A: "抓取到的章节全是空白" B: 检查是否因为页面采用了iframe 加载正文或 JS 动态渲染”。此类情况只能使用无头浏览器,或者寻找接口返回纯文本 JSON 的 Ajax 地址再抓取。
- A: "出现中文乱码" B: 先通过 `mb_detect_encoding` 判断原始字符集,再统一转成 UTF‑8;在 MySQL 表字段设置为 `utf8mb4` 编码。怎么说呢,
- A: "IP 被目标站点封禁" B: ① 引入代理池并轮询;② 设置合理的请求间隔与随机 User‑Agent;③ 如仍被拦截,可考虑切换目标站点或申请官方 API 授权。
- A: "正则匹配不到章节标题" B: 优先使用 DOM/XPath。因为它们不依赖具体字符模式,只要结构不变就能稳定工作。如果必须用正则,请先打印 `$html` 并手动定位关键标记。再调试 `preg_match_all` 的模式修饰符 `u` 与 `s`。
- A: "Cron 没有按计划执行" B: 确认程序时间与时区一致;在命令行手动运行一次脚本看是否报错;将日志输出重定向至文件以便排查,如 `/var/log/novel_crawler.log`。
在实际项目中。很多开发者都会遇到以下痛点:
- 抓取速度慢,频繁超时导致使用者体验下降。
- 网站频繁更改页面结构。正则表达式失效,需要频繁维护代码。
- IP 被封或被网站检测到爬虫行为,导致抓取中断。
- 中文乱码、字符集不统一,导致采集的章节内容出现乱码。
- 采集后数据保存不规范,后期检索和展示困难。
一、整体思路概览
二、准备工作
1. PHP 环境要求
- PHP>= 7.4
-
已开启
allow_url_fopen或安装cURL -
(或直接使用原生
DOMDocument/XPath) - Mysql/MariaDB或本地文件程序
2. 常用工具库安装
# 使用 Composer 安装 phpQuery
composer require electrolinux/phpquery
# 若想使用 Guzzle 替代 cURL,可额外安装
composer require guzzlehttp/guzzle
3、获取页面内容的四种方式及对比
| 方式 | 优点 | 缺点 / 注意点 |
|---|---|---|
file_get_contents | 代码最简洁;怎么说呢,无需额外 | 不支持复杂请求头;对大文件易超时, |
fopen/fread | 可自定义流上下文,灵活控制超时。 | PHP 配置需要允许 URL fopen;一样受限于单线程, |
CURL(curl_exec) | 支持自定义 Header、Cookie、代理;性能最佳, | LAMP 环境需安装 curl 代码略显冗长。 |
| Guzzle HTTP 客户端 | Psr-7 标准;处理友好,支持并发请求。 | Larger dependency;对新手有学习成本, |
CURL 示例
$url = 'https://www.example.com/book/12345.html';$ch = curl_init;curl_setopt_array($ch,,]);$html = curl_exec;if ) {
// 记录日志,便于后期排查
error_log);}
curl_close;>
四、解析页面结构
a) 使用 phpQuery 简化 DOM 操作
// 引入 phpQuery 自动加载文件
require_once __DIR__.'/vendor/autoload.php';use phpQuery;// 将获取到的 HTML 转为 phpQuery 对象
$doc = phpQuery::newDocumentHTML;// 示例:获取章节标题 & 内容
$title = $doc->find->text;$content = $doc->find->html;// 去除多余的换行与空格
$content = preg_replace;>
b) 原生 DOMDocument + XPath
$dom = new DOMDocument;话说回来,// 抑制因非法 HTML 导致的警告
libxml_use_internal_errors;$dom->loadHTML);libxml_clear_errors;$xpath = new DOMXPath;// 获取章节标题
$titleNode = $xpath->query;$title = $titleNode->length?trim->nodeValue) : '未知标题';// 获取正文内容
$contentNode = $xpath->query;$contentHtml = '';foreach {
$contentHtml .= $dom->saveHTML;}
>
五、正则匹配细节
虽然推荐使用 DOM 解析,但在某些仅有纯文本的页面仍需要正则抽取。下面给出常见的章节标题和正文匹配模式:
// 匹配类似 “第12章 奇怪的旅程”
$chapterPattern = '/第\s*\s*章\s*\s*/u';// 匹配正文块:去除广告或脚本标签
$contentPattern = '/
<\/div>/si';>
TIPS:Curl 抓取后若出现乱码,可在正则前先统一字符集:
$html = mb_convert_encoding;>
六、数据持久化方案
a) 写入本地 TXT 文件
$filename = __DIR__.'/novel/'.$title.'.txt';file_put_contents;echo "已写入文件:{$filename}";>
b) 保存至 MySQL 数据库
// PDO 示例,确保使用预处理防止注入
$pdo = new PDO('mysql:host=127.0.0.1;dbname=novel;charset=utf8mb4'。'root','password',);$sql = "INSERT INTO chapters VALUES )
ON DUPLICATE KEY UPDATE content=:content";$stmt = $pdo->prepare;$stmt->execute();>
七、定时任务实现自动更新
TIPS:Cron 是最常用且可靠的方式。怎么说呢,若服务器不支持 Cron。可考虑 Laravel Scheduler 或 Symfony Console 定时执行。
# 在 Linux 主机上编辑 crontab -e
30 2 * * * /usr/bin/php /var/www/html/novelcrawler.php>> /var/log/novelcrawler.log 2>&1
php // novel_crawler.php 简易入口文件
requireonce DIR.'/vendor/autoload.php';requireonce DIR.'/crawl_functions.php';// 包含所有业务函数
// 假设已有书籍列表存于 DB 中,需要遍历每本书进行增量更新 $books = getAllBooks;// 返回数组,... ]
foreach {
try {
updateBookChapters;} catch {
error_log);}
}
>
八、防反爬技巧
- User-Agent 随机化: 每次请求随机挑选一个常见浏览器 UA。
- X-Forwarded-For & Proxy IP 池: 使用国内外多个代理 IP,降低单一 IP 的访问频率。
-
SLEEP 与随机间隔: 在每次请求之间加入
,避免触发频率阈值。 - Caching 本地 HTML: 同一章节多次访问时直接读取缓存,可显著降低请求次数。
- DDoS 防护规避: 如发现返回验证码或 Cloudflare 挑战页。可改为使用 Selenium/Puppeteer 等无头浏览器,但这会增加资源开销,仅在必要时采用。
九、完整示例代码汇总
iniset;errorreporting;
require_once DIR.'/vendor/autoload.php';use phpQuery;
// ---------- 配置 ----------
$baseUrl = 'https://www.biquge.com.cn/book/12345/';$startPageUrl = $baseUrl . 'index.html';话说回来,// 起始目录页
$userAgents =;
// ---------- 数据库 ----------
$pdo = new PDO('mysql:host=127.0.0.1;dbname=novel;charset=utf8mb4'。'root','password',);
// ---------- 工具函数 ----------
function curlGet: string{
static $idx=0;$ch=curlinit;curlsetoptarray($ch。CURLOPTHTTPHEADER=>,]);$data=curlexec;if){
throw new RuntimeException);}
curlclose;// 防止被封:随机延迟 500~1500ms
usleep);怎么说呢,return mbconvertencoding;}
function fetchChapterList: array{
// 用 phpQuery 抽取章节链接与标题
phpQuery::newDocumentHTML;$list=,foreach as $a){
/** @var DOMElement $a */
$href=pq->attr;if{continue,}
// 相对方法转绝对方法
if!==0){
global $baseUrl;$href=$baseUrl.ltrim;}
$list=,}
return $list;}
function parseChapter: string{
// 使用 XPath 更精准定位正文区域,兼容不同站点结构
libxmluseinternalerrors;$dom=new DOMDocument;@$dom->loadHTML);libxmlclear_errors;
// 常见正文容器 ID / CLASS 列表,可自行
:
$candidates=;
foreach {
if===0){
// ID 查询
@$node=$dom->getElementById);if{ break,说起来,}
}else{
// Class 查询
@$xpath=new DOMXPath;@$nodes=$xpath->query . "')]");if {
@$node=$nodes->item;break,}
}
php
}
if){
throw new RuntimeException;}
$contentHtml=$node->ownerDocument->saveHTML;
// 清理广告脚本等噪声:
$content=pregreplace*?#is','',$contentHtml);$content=pregreplace;// 转换成纯文本并去除多余空行:
$content=striptags));$content=pregreplace;
return trim;}
// ---------- 主流程 ----------
try{
// 第一步先:获取目录页并抽取所有章节链接
$catalogHtml=curlGet;$chapters=fetchChapterList;
foreach{
echo "正在处理第".."章:{$chapter}
";$html=curlGet;$text=parseChapter;
// 保存至数据库
$stmt=$pdo->prepare(
"INSERT INTO chapters
VALUES)
ON DUPLICATE KEY UPDATE content=:cont"
);$stmt->execute(,':url' => $chapter。':cont' => $text,]);
// 同步写入本地 txt
$filePath=DIR.'/output/'.pregreplace.'.txt';fileput_contents;不过,}
echo "全部完成!",
}catch{
error_log);}
>
十、常见问题 FAQ
- A: "抓取到的章节全是空白" B: 检查是否因为页面采用了iframe 加载正文或 JS 动态渲染”。此类情况只能使用无头浏览器,或者寻找接口返回纯文本 JSON 的 Ajax 地址再抓取。
- A: "出现中文乱码" B: 先通过 `mb_detect_encoding` 判断原始字符集,再统一转成 UTF‑8;在 MySQL 表字段设置为 `utf8mb4` 编码。怎么说呢,
- A: "IP 被目标站点封禁" B: ① 引入代理池并轮询;② 设置合理的请求间隔与随机 User‑Agent;③ 如仍被拦截,可考虑切换目标站点或申请官方 API 授权。
- A: "正则匹配不到章节标题" B: 优先使用 DOM/XPath。因为它们不依赖具体字符模式,只要结构不变就能稳定工作。如果必须用正则,请先打印 `$html` 并手动定位关键标记。再调试 `preg_match_all` 的模式修饰符 `u` 与 `s`。
- A: "Cron 没有按计划执行" B: 确认程序时间与时区一致;在命令行手动运行一次脚本看是否报错;将日志输出重定向至文件以便排查,如 `/var/log/novel_crawler.log`。

