各位老铁,各位代码界的“铲屎官”们,晚上好!
我是你们的老朋友,一个坚信“能用脚本解决的事,绝不动手”的资深PHP工程师。
今天咱们不开课,咱们来聊点刺激的——“全自动采集与分发”。
在这个万物互联的时代,数据就是石油,内容就是燃料。如果你每天还要手动打开浏览器,复制粘贴,去编辑器里调整格式,然后登录后台点发布,那我只能对你说一句:兄弟,你的CPU是拿来积灰的吗?
今天这场讲座,咱们就来实战演练一下:如何用PHP,像一只不知疲倦的智能蜘蛛,从互联网的各个角落“抓”来干货,然后神不知鬼不觉地搬运到你的WordPress站点里。这就叫“数字搬运工”,听起来是不是很有钱途?
准备好了吗?把IDE打开,把咖啡泡好,咱们开始吧。
第一幕:准备工作与环境认知
首先,咱们得明白这玩意儿到底是干嘛的。咱们要构建的是一个流水线:
- 猎人(爬虫): 负责在互联网丛林里扫描,发现猎物。
- 侦探(解析器): 负责把猎物身上的肉(内容)剥下来,把骨头(无关信息)扔掉。
- 邮递员(发布接口): 负责把剥好的肉,按照WordPress的语法,打包发货。
在开始写代码之前,你得先认识一下你的主角——cURL。在PHP世界里,cURL就是那个隐形的巨人,也是那个无所不能的显眼包。它不仅能处理HTTP请求,还能处理代理、Cookie、压缩、上传下载,简直是全能选手。
但如果你想稍微“进阶”一点,避免被对方的防火墙当成黑客攻击,强烈建议引入 Guzzle HTTP Client。当然,为了保持代码的纯粹和低依赖,咱们今天的演示,主要以原生PHP和cURL为主,毕竟咱们要的是“大巧不工”。
第二幕:猎人上线——cURL 的实战艺术
采集的第一步,是获取目标网页的HTML源码。很多人喜欢用 file_get_contents,但我劝你千万别用。为什么?因为它太笨了,遇到跳转、遇到404、遇到需要登录的页面,它就只会给你报个错,或者直接给你个空文件,连个招呼都不打。
咱们要用cURL,把伪装做到极致。
代码示例:一个优雅的cURL请求
<?php
/**
* 发起一个GET请求
* 这里的逻辑是:我们要装作一个正常的Chrome浏览器,而不是一个饥渴的PHP脚本
*/
function fetchUrl($url) {
// 初始化
$ch = curl_init($url);
// 设置选项
curl_setopt_array($ch, [
// 1. 返回数据而不是直接输出,方便后续处理
CURLOPT_RETURNTRANSFER => true,
// 2. 跟随重定向(比如页面跳转了,cURL要跟着跳)
CURLOPT_FOLLOWLOCATION => true,
// 3. 超时设置,别让你的脚本卡死在某个垃圾网站上
CURLOPT_TIMEOUT => 30,
// 4. 关键点:伪装Headers
CURLOPT_HTTPHEADER => [
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
// 如果对方有反爬机制,可能需要Cookie,这里先留空
// 'Cookie: session_id=xxxxx'
],
// 5. SSL验证(为了安全,建议开启,但有些老旧网站证书配置有问题,可能会报错,这时候可以设为false,但仅限测试环境)
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
// 6. 处理gzip压缩
CURLOPT_ENCODING => 'gzip',
]);
// 执行并获取内容
$content = curl_exec($ch);
// 检查错误
if (curl_errno($ch)) {
// 记录错误日志,别让它悄悄跑了
error_log('CURL Error: ' . curl_error($ch));
return false;
}
// 获取HTTP状态码
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode != 200) {
error_log('HTTP Error Code: ' . $httpCode . ' for URL: ' . $url);
curl_close($ch);
return false;
}
curl_close($ch);
return $content;
}
// 使用示例
$html = fetchUrl('https://www.example-news-site.com/article-123');
if ($html) {
// 假装我们拿到了数据
echo "采集成功,数据长度:" . strlen($html);
}
专家点评:
看懂了吗?这行代码里,User-Agent 是灵魂。如果不伪装,很多网站会直接返回空,或者返回一个“拒绝访问”的页面。这就像是你去人家公司面试,如果不穿西装不打领带,保安直接把你轰出来,你连门都进不去。
第三幕:侦探上线——解析HTML的混乱与秩序
拿到了HTML代码,你会发现它跟马蜂窝一样乱。标签嵌套错乱、属性没闭合、script和style标签里全是垃圾代码。这时候,你需要一个侦探,从垃圾堆里淘金。
PHP自带的 DOMDocument 类就是最好的侦探。虽然它经常被吐槽“脾气暴躁”,但在处理结构化数据时,它是王者。
代码示例:提取文章标题、正文和图片
/**
* 从HTML中提取文章信息
*/
function parseArticle($html) {
// 创建DOM对象
$dom = new DOMDocument();
// 抑制警告(因为很多网站HTML写得不规范,loadHTML会报错,用 @ 忍一忍)
@$dom->loadHTML($html);
// 清理多余的乱码(通常loadHTML会加上xmlns,导致后续操作属性出错)
libxml_clear_errors();
// 获取所有H1标签,假设文章标题在H1里
$titles = $dom->getElementsByTagName('h1');
$title = $titles->length > 0 ? $titles->item(0)->nodeValue : '无标题文章';
// 获取所有div或者p标签,假设正文在class="content"或者class="article-body"里
// 这里为了演示,我们粗暴地获取所有p标签,实际项目中请用CSS选择器(如QueryPath或SimpleHtmlDom)
$bodyP = $dom->getElementsByTagName('p');
$content = '';
foreach ($bodyP as $p) {
// 过滤掉纯空白字符
if (trim($p->nodeValue) !== '') {
// 这里可以加一些过滤逻辑,比如过滤掉广告div
$content .= '<p>' . $p->nodeValue . '</p>';
}
}
// 获取第一张图片
$imgs = $dom->getElementsByTagName('img');
$imageSrc = '';
if ($imgs->length > 0) {
$imageSrc = $imgs->item(0)->getAttribute('src');
}
return [
'title' => $title,
'content' => $content,
'image' => $imageSrc
];
}
// 测试
// $data = parseArticle($html);
// print_r($data);
专家点评:
这段代码非常简陋,对吧?现实中的网页结构千奇百怪,有的标题在H2里,有的正文在article标签里。这就是所谓的“坑爹”时刻。作为专家,你需要根据目标网站的HTML结构,不断调整你的选择器。这就像是在拆弹,稍微动错一个节点,文章就全乱了。
第四幕:邮递员上线——WordPress 自动发布
好了,现在我们有了标题、正文和图片链接。下一步,怎么把它扔进WordPress?
WordPress 提供了两种主要方式:XML-RPC 和 REST API。
XML-RPC 是老古董了,虽然简单,但安全性较差,很多新版的WordPress默认禁用了它。
REST API 是现代趋势,它是王道。我们需要通过API,带上你的认证信息,把数据POST过去。
准备工作:
- 在WordPress后台生成 Application Password(应用密码)。这是为了安全,不要用你的管理员账号密码直接写死在代码里。
- 确保你的WordPress支持REST API(基本版就支持)。
代码示例:通过REST API发布文章
/**
* 发布文章到WordPress
*/
function publishToWordpress($title, $content, $imageUrl, $wpUrl, $username, $appPassword) {
// 1. 构建REST API的URL
// 注意:/wp-json/wp/v2/posts 是固定的端点
$apiUrl = rtrim($wpUrl, '/') . '/wp-json/wp/v2/posts';
// 2. 准备认证
// WordPress REST API 支持Basic Auth
// 格式:username:password
$credentials = base64_encode($username . ':' . $appPassword);
// 3. 准备数据
$postData = [
'title' => $title,
'content' => $content,
'status' => 'publish', // 'draft' 表示存为草稿,'pending' 表示待审核
'excerpt' => '这是自动采集的摘要,稍后可手动修改。',
// 尝试设置文章的Featured Image(特色图片)
'featured_media' => uploadImageIfNeeded($imageUrl, $wpUrl, $username, $appPassword)
];
// 4. 发送请求
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => [
'Authorization: Basic ' . $credentials,
'Content-Type: application/json'
],
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
error_log("Curl Error: " . curl_error($ch));
return false;
}
curl_close($ch);
// 5. 解析响应
if ($httpCode == 201) {
$result = json_decode($response, true);
error_log("文章发布成功!Post ID: " . $result['id']);
return true;
} else {
error_log("发布失败,HTTP Code: " . $httpCode . ", Response: " . $response);
return false;
}
}
/**
* 辅助函数:如果没有图片,就先下载图片再上传
* 这是一个进阶功能,为了完整性,咱们简单写一下
*/
function uploadImageIfNeeded($url, $wpUrl, $username, $appPassword) {
// 1. 获取图片内容
$imageData = fetchUrl($url);
if (!$imageData) return 0;
// 2. 解析扩展名
$ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) return 0;
// 3. 上传API
$uploadUrl = rtrim($wpUrl, '/') . '/wp-json/wp/v2/media';
$credentials = base64_encode($username . ':' . $appPassword);
$ch = curl_init($uploadUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'file' => new CURLFile($url, 'image/' . $ext) // 注意:这里需要把URL保存为临时文件,CURLFile要求是文件路径
],
CURLOPT_HTTPHEADER => [
'Authorization: Basic ' . $credentials,
'Content-Type: multipart/form-data'
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 201) {
$result = json_decode($response, true);
return $result['id'];
}
return 0;
}
专家点评:
看到了吗?publishToWordpress 函数就是咱们的大动脉。它不仅发布了文章,还尝试处理了图片。但是,注意看那个 uploadImageIfNeeded,这仅仅是“如果需要”的上传。在实际开发中,你可能需要更复杂的逻辑:比如判断图片是否已经是WordPress里的,如果是,直接用ID,不是才去下载。而且,直接用 CURLFile(new CURLFile(...)) 在某些PHP版本里是有坑的,你需要先下载到本地临时文件。
第五幕:整合与调度——打造你的“掘金机”
现在,猎手、侦探和邮递员都已经就位了。接下来,你需要一个调度中心,来控制他们什么时候动,动多少次。
代码示例:调度器
// 配置参数
$config = [
'sourceUrl' => 'https://news-source.com/feed',
'targetWp' => 'https://your-wordpress-site.com',
'username' => 'your_wp_user',
'password' => 'your_application_password'
];
// 1. 获取源站的文章列表(这里假设我们有一个RSS Feed,这是最稳的方式)
$rssContent = fetchUrl($config['sourceUrl']);
if (!$rssContent) {
die("无法连接源站!");
}
// 解析RSS
$rss = simplexml_load_string($rssContent);
$items = $rss->channel->item;
echo "发现 " . count($items) . " 篇文章,准备开始搬运...n";
foreach ($items as $item) {
$title = (string)$item->title;
$link = (string)$item->link;
// --- 核心逻辑:去重 ---
// 我们不能把同一篇文章搬运两遍,否则你的站点就被垃圾文章淹没了
// 这里可以用WordPress的API查询是否存在,或者用数据库记录URL
if (isArticleExist($link, $config['targetWp'])) {
echo "文章已存在,跳过:{$title}n";
continue;
}
// --- 采集内容 ---
// 很多RSS只有标题和链接,内容在详情页。这时候我们需要再次请求详情页
$html = fetchUrl($link);
if (!$html) {
echo "采集失败,跳过:{$title}n";
continue;
}
// --- 解析内容 ---
$articleData = parseArticle($html);
// --- 发布 ---
if (publishToWordpress($articleData['title'], $articleData['content'], $articleData['image'], $config['targetWp'], $config['username'], $config['password'])) {
echo "发布成功:{$title}n";
// 记录数据库,防止重复
saveArticleUrl($link);
} else {
echo "发布失败:{$title}n";
}
// --- 延迟 ---
// 千万别狂发!会被封IP的,或者被对方反爬软件屏蔽
sleep(3);
}
echo "所有任务执行完毕!";
第六幕:进阶生存指南——避坑与反封
代码写完了,你以为这就结束了?天真。互联网丛林里的生存法则比这残酷得多。
1. 遇到验证码怎么办?
这是采集最大的拦路虎。如果你遇到验证码,纯PHP是无解的。你需要接入打码平台(如超级鹰、2Captcha)或者用OCR技术(Tesseract)。这属于人工智能领域了,咱们今天就不展开了。
2. 403 Forbidden 怎么办?
对方服务器检测到你是机器人,直接拒绝访问。除了伪装User-Agent,你还可以:
- IP代理池: 这是最常用的手段。购买或自建代理IP,每次请求换个IP。当然,免费的代理大多是假的,贵的很贵。
- Cookie池: 维护一个包含Cookie的池子,模拟不同的用户登录状态。
3. 性能优化与僵尸进程
如果你要采集成千上万篇文章,上面的 foreach 循环太慢了。你可以使用 Swoole 或 Workerman 这种PHP异步框架,开启多进程并发采集。或者,最简单的,写个Shell脚本,用 nohup php script.php & 让它在后台跑。
4. 代码质量:使用框架
如果你要在生产环境部署这套系统,千万不要把所有代码都扔到一个 index.php 里。建议使用 Laravel。Laravel 内置的 HTTP Client (Guzzle) 非常好用,而且它的数据采集包 Spatie/PodcastFetcher 或 Laravel-Scraper 已经封装好了很多复杂的逻辑,比如自动去重、自动清洗HTML。
5. 数据清洗(脏活累活)
这是最折磨人的。采集下来的文章,可能有乱码、多余的 <p> 标签、多余的换行符。
你需要编写正则表达式清洗数据。例如:
// 清理多余的换行符和空格
$content = preg_replace('/s+/', ' ', $content);
// 删除script和style标签
$content = preg_replace('/<scriptb[^>]*>(.*?)</script>/is', '', $content);
$content = preg_replace('/<styleb[^>]*>(.*?)</style>/is', '', $content);
第七幕:终极代码整合——给你一个开箱即用的框架
为了让大家少走弯路,我把上面所有的零散代码整合成一个稍微完整点的演示脚本。
<?php
require 'vendor/autoload.php'; // 假设你用了Guzzle,如果是原生PHP,请删掉这行
// 配置信息
$CONFIG = [
'target_url' => 'https://your-wordpress.com',
'wp_user' => 'your_username',
'app_pass' => 'your_app_password',
'source_rss' => 'https://target-site.com/feed', // 或者是爬虫抓取到的URL列表
'sleep_time' => 2
];
use GuzzleHttpClient;
class AutoPublisher {
private $client;
private $targetUrl;
private $wpUser;
private $wpPass;
public function __construct($config) {
$this->targetUrl = rtrim($config['target_url'], '/');
$this->wpUser = $config['wp_user'];
$this->wpPass = $config['app_pass'];
// 初始化 Guzzle 客户端
$this->client = new Client([
'timeout' => 30,
'verify' => false, // 生产环境建议开启
]);
}
// 1. 发送 HTTP 请求
private function getHtml($url) {
try {
$response = $this->client->get($url, [
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
]
]);
return (string) $response->getBody();
} catch (Exception $e) {
echo "请求失败: {$url} n";
return false;
}
}
// 2. 解析 HTML
private function parseContent($html) {
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->loadHTML($html);
libxml_clear_errors();
// 获取标题
$title = $dom->getElementsByTagName('h1')->item(0)->nodeValue ?? '未命名文章';
// 获取正文 (简单粗暴获取所有P标签)
$content = '';
$pTags = $dom->getElementsByTagName('p');
foreach ($pTags as $tag) {
if (trim($tag->nodeValue)) {
$content .= '<p>' . $tag->nodeValue . '</p>';
}
}
// 获取图片
$imgTag = $dom->getElementsByTagName('img')->item(0);
$image = $imgTag ? $imgTag->getAttribute('src') : '';
return compact('title', 'content', 'image');
}
// 3. 发布到 WP
public function publish($title, $content, $imageUrl) {
// Basic Auth
$auth = base64_encode($this->wpUser . ':' . $this->wpPass);
try {
// 发布文章
$response = $this->client->post($this->targetUrl . '/wp-json/wp/v2/posts', [
'headers' => [
'Authorization' => 'Basic ' . $auth,
'Content-Type' => 'application/json'
],
'json' => [
'title' => $title,
'content' => $content,
'status' => 'publish'
]
]);
if ($response->getStatusCode() == 201) {
echo "发布成功: {$title}n";
return true;
}
} catch (Exception $e) {
echo "发布出错: {$title} - " . $e->getMessage() . "n";
}
return false;
}
// 主流程
public function run() {
// 获取 RSS
$rssHtml = $this->getHtml($this->CONFIG['source_rss']);
$xml = simplexml_load_string($rssHtml);
foreach ($xml->channel->item as $item) {
$url = (string)$item->link;
$title = (string)$item->title;
// 简单去重(这里实际应该查数据库)
// if (file_exists($url . '.done')) continue;
echo "开始处理: {$title} n";
// 采集详情页
$detailHtml = $this->getHtml($url);
if ($detailHtml) {
$data = $this->parseContent($detailHtml);
$this->publish($data['title'], $data['content'], $data['image']);
}
// 礼貌性休眠
sleep($this->CONFIG['sleep_time']);
}
}
}
// 执行
$config = [
'target_url' => 'https://your-wordpress.com',
'wp_user' => 'admin',
'app_pass' => 'xxxx-xxxx',
'source_rss' => 'https://www.zhihu.com/rss',
'sleep_time' => 3
];
(new AutoPublisher($config))->run();
结语:拥抱自动化,但保持敬畏
老铁们,讲了这么多,从 cURL 到 DOM 解析,再到 REST API,咱们把整个自动采集的流程串起来了。
这个技术听起来很酷,但请记住一句话:技术无罪,但滥用技术必死。
- 遵守robots.txt: 去对方网站看一眼
robots.txt,如果它写了Disallow: /,你就别去爬它,这是江湖规矩。 - 控制频率: 千万别写成无限循环,那是DDoS攻击。设置合理的延迟,做一个有礼貌的爬虫。
- 数据质量: 自动采集的内容往往很粗糙。采集回来后,建议做个“人工审核”环节,或者用AI做简单的润色,不要让你的站点变成垃圾场。
好了,今天的“PHP自动采集与WordPress自动发布”讲座就到这里。现在,去你的IDE里敲下第一行代码吧!祝你的服务器24小时高能运转,祝你的文章数量指数级增长!如果遇到报错,别慌,查查日志,或者来群里吼一声,咱们再战!