PHP如何实现自动采集文章并发布到WordPress网站系统

各位老铁,各位代码界的“铲屎官”们,晚上好!

我是你们的老朋友,一个坚信“能用脚本解决的事,绝不动手”的资深PHP工程师。

今天咱们不开课,咱们来聊点刺激的——“全自动采集与分发”

在这个万物互联的时代,数据就是石油,内容就是燃料。如果你每天还要手动打开浏览器,复制粘贴,去编辑器里调整格式,然后登录后台点发布,那我只能对你说一句:兄弟,你的CPU是拿来积灰的吗?

今天这场讲座,咱们就来实战演练一下:如何用PHP,像一只不知疲倦的智能蜘蛛,从互联网的各个角落“抓”来干货,然后神不知鬼不觉地搬运到你的WordPress站点里。这就叫“数字搬运工”,听起来是不是很有钱途?

准备好了吗?把IDE打开,把咖啡泡好,咱们开始吧。


第一幕:准备工作与环境认知

首先,咱们得明白这玩意儿到底是干嘛的。咱们要构建的是一个流水线:

  1. 猎人(爬虫): 负责在互联网丛林里扫描,发现猎物。
  2. 侦探(解析器): 负责把猎物身上的肉(内容)剥下来,把骨头(无关信息)扔掉。
  3. 邮递员(发布接口): 负责把剥好的肉,按照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过去。

准备工作:

  1. 在WordPress后台生成 Application Password(应用密码)。这是为了安全,不要用你的管理员账号密码直接写死在代码里。
  2. 确保你的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 循环太慢了。你可以使用 SwooleWorkerman 这种PHP异步框架,开启多进程并发采集。或者,最简单的,写个Shell脚本,用 nohup php script.php & 让它在后台跑。

4. 代码质量:使用框架

如果你要在生产环境部署这套系统,千万不要把所有代码都扔到一个 index.php 里。建议使用 Laravel。Laravel 内置的 HTTP Client (Guzzle) 非常好用,而且它的数据采集包 Spatie/PodcastFetcherLaravel-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,咱们把整个自动采集的流程串起来了。

这个技术听起来很酷,但请记住一句话:技术无罪,但滥用技术必死。

  1. 遵守robots.txt: 去对方网站看一眼 robots.txt,如果它写了 Disallow: /,你就别去爬它,这是江湖规矩。
  2. 控制频率: 千万别写成无限循环,那是DDoS攻击。设置合理的延迟,做一个有礼貌的爬虫。
  3. 数据质量: 自动采集的内容往往很粗糙。采集回来后,建议做个“人工审核”环节,或者用AI做简单的润色,不要让你的站点变成垃圾场。

好了,今天的“PHP自动采集与WordPress自动发布”讲座就到这里。现在,去你的IDE里敲下第一行代码吧!祝你的服务器24小时高能运转,祝你的文章数量指数级增长!如果遇到报错,别慌,查查日志,或者来群里吼一声,咱们再战!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注