PHP 驱动的自动化营销工作流:从内容抓取、AI 改写到自动发布的全链路 PHP 闭环

PHP 驱动的自动化营销工作流:从内容抓取、AI 改写到自动发布的全链路 PHP 闭环

各位老铁,各位码农,各位那些在深夜里一边吃着泡面一边试图把项目赶上线的朋友们,大家早上好(或者是晚上好,我不确定现在的时区)。

今天我们不聊架构设计的七七八八,也不谈什么DDD(领域驱动设计)的鬼东西。今天我们要聊点“狠”的。我们要聊聊那个被贴了太多标签、被误解太深、实际上却像瑞士军刀一样锋利的语言——PHP

有人可能会说:“PHP?那不是写 WordPress 的吗?不是那个‘世界上最好的语言’自封的梗吗?”

嘿,说得好。没错,PHP 是能写 WordPress。但 PHP 也能驱动你那价值百万美元的自动化营销流水线。今天,我们就来一场硬核的技术秀,用 PHP 编写一套从“千里之外”抓取内容,交给“超级大脑”改写,最后自动分发到各个平台的闭环系统。

准备好了吗?让我们把代码敲得震天响!


第一部分:出发前的装备清单(不仅仅是 PHP)

在写代码之前,我们要先明确一下我们的“作战部队”。如果你只有一个 <?php echo "hello"; ?>,那你只能干瞪眼。我们需要更现代的工具,但在 PHP 生态里,有些老伙计依然很香。

  1. Guzzle HTTP Client:不要再用 file_get_contents 了,除非你想被人肉搜索并拉入黑名单。Guzzle 是 PHP 界的 curl 封装,它优雅、现代、支持异步,简直是爬虫界的宝马。
  2. Laravel:虽然你可以用原生 PHP 写,但我强烈建议你用 Laravel。为什么?因为它的队列、任务调度和依赖注入,能让你的代码像黄油一样顺滑。
  3. OpenAI API (或兼容接口):我们的 AI 智囊团。
  4. Redis:用来存队列,用来做缓存,用来当我们的内存数据库。
  5. Simple HTML DOM Parser (或原生 DOMDocument):解析 HTML 的神器。

好,假设我们已经安装了 Laravel,让我们开始构建这个“全自动营销绞肉机”。


第二部分:内容抓取——别被反爬虫卫士吃掉

首先,我们要去别的网站“偷点”素材。这听起来像黑客行为,但在营销领域,这叫“竞品分析”和“素材聚合”。

痛点:你的 IP 很快就会变成 403 Forbidden

互联网是有脾气的。如果你像蝗虫一样飞过去,把人家的页面一通乱抓,人家会立刻封你的 IP。所以,我们要学会做人,我们要伪装成一只正常的浏览器。

代码示例:伪装成 Chrome 的抓取器

我们在 Laravel 的 Service Provider 或者一个专门的 CrawlerService.php 里写下这样的逻辑:

<?php

namespace AppServices;

use GuzzleHttpClient;
use GuzzleHttpPool;
use GuzzleHttpRequestOptions;
use PsrHttpMessageResponseInterface;

class CrawlerService
{
    protected $client;

    public function __construct()
    {
        // 创建一个配置了 User-Agent 的 HTTP 客户端
        // 这里的 User-Agent 就像你的身份证,一定要写得像人类的浏览器
        $this->client = new Client([
            'headers' => [
                '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',
                'Accept-Encoding' => 'gzip, deflate',
            ],
            // 我们需要代理池吗?如果量大,是的。这里暂时省略,留给高级玩家。
            'timeout' => 30, // 别卡太久
            'verify' => false, // 有些自签名证书的站点会报错,关掉它(生产环境需谨慎)
        ]);
    }

    /**
     * 抓取单个页面的内容
     */
    public function fetchPage(string $url): string
    {
        try {
            $response = $this->client->get($url);
            return (string) $response->getBody();
        } catch (Exception $e) {
            Log::error("抓取失败: {$url}", [
                'error' => $e->getMessage()
            ]);
            return '';
        }
    }

    /**
     * 批量并发抓取 - 这里开始进入高并发领域
     */
    public function fetchBatch(array $urls): array
    {
        $requests = function ($urls) {
            foreach ($urls as $url) {
                // 只要那些以 http 开头的
                if (!filter_var($url, FILTER_VALIDATE_URL)) {
                    continue;
                }
                yield $url => $this->client->getAsync($url);
            }
        };

        $pool = new Pool($this->client, $requests($urls), [
            'concurrency' => 10, // 并发数,10个同时冲,别冲太猛,服务器会报警
            'fulfilled' => function (ResponseInterface $response, $index) {
                // 成功回调
                Log::info("成功抓取: {$index}");
            },
            'rejected' => function (ReasonException $reason, $index) {
                // 失败回调
                Log::warning("抓取被拒: {$index}, 原因: {$reason->getMessage()}");
            },
        ]);

        $promise = $pool->promise();
        $promise->wait(); // 等待所有任务完成
    }

    /**
     * 提取文章正文 (简单的 DOM 解析)
     */
    public function extractContent(string $html, string $selector = 'article, .post-content, #content')
    {
        $dom = new DOMDocument();
        // 抑制警告,因为 libxml 默认会对 HTML 噼里啪啦报错
        libxml_use_internal_errors(true);

        if (!$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'))) {
            return '';
        }
        libxml_clear_errors();

        $xpath = new DOMXPath($dom);
        $elements = $xpath->query("//{$selector}");

        if ($elements->length > 0) {
            $content = '';
            foreach ($elements as $element) {
                $content .= $element->nodeValue;
            }
            return strip_tags(trim($content));
        }

        // 如果没找到,就暴力清洗一下 HTML
        return strip_tags(trim($html));
    }
}

技术揭秘

上面的代码里,我用了 fetchBatch 方法。这是一个经典的 Guzzle Pool 用法。想象一下,你是一个拥有 10 个手下的经理,你把他们派出去同时抓取 100 个网站。这就是并发。

注意那个 User-Agent,那是你的伪装。如果网站检测到你的请求头发丝都是 Curl 而不是 Chrome,它就会把你拒之门外。在这个阶段,我们要像忍者一样,悄无声息地获取数据。


第三部分:AI 改写——把“垃圾”变成“金子”

现在,我们手里有了原始的 HTML 字符串,可能是一堆乱七八糟的标签和乱码。我们要怎么做?我们要把它变成一篇SEO友好、引人入胜、甚至带有“网感”的营销文案。

这里我们引入 OpenAI 的 API。但别直接把 HTML 扔进去,那会让 AI 瞎的。我们需要一个 Prompt(提示词)工程。

代码示例:Prompt 工程师

<?php

namespace AppServices;

use GuzzleHttpClient;

class AIWriterService
{
    protected $client;
    protected $apiKey;
    protected $model;

    public function __construct()
    {
        $this->client = new Client();
        $this->apiKey = env('OPENAI_API_KEY');
        $this->model = 'gpt-4'; // 或者是 GPT-3.5-turbo,省钱嘛
    }

    /**
     * 核心魔法:改写内容
     */
    public function rewriteContent(string $rawContent, string $targetAudience, string $tone = 'professional'): string
    {
        $prompt = "你是一位资深的内容营销专家。请根据以下原始内容,为 {$targetAudience} 人群撰写一篇引人入胜的文章。

        目标受众:{$targetAudience}
        基调风格:{$tone} (比如:幽默风趣、专业严谨、煽情等)

        原始内容:
        ---START---
        {$rawContent}
        ---END---

        要求:
        1. 不要直接翻译,要理解核心观点并进行重述。
        2. 使用恰当的修辞手法和口语化表达,避免生硬的翻译腔。
        3. 加入吸引人的小标题。
        4. 字数控制在 500-800 字之间。
        5. 输出纯文本,不要 Markdown 格式。";

        try {
            $response = $this->client->post('https://api.openai.com/v1/chat/completions', [
                'headers' => [
                    'Authorization' => 'Bearer ' . $this->apiKey,
                    'Content-Type' => 'application/json',
                ],
                'json' => [
                    'model' => $this->model,
                    'messages' => [
                        ['role' => 'system', 'content' => 'You are a helpful marketing assistant.'],
                        ['role' => 'user', 'content' => $prompt],
                    ],
                    'temperature' => 0.7, // 创造力系数,0-1
                ],
            ]);

            $body = json_decode($response->getBody(), true);

            return $body['choices'][0]['message']['content'] ?? 'AI 空空如也';
        } catch (Exception $e) {
            Log::error("AI 改写失败", ['error' => $e->getMessage()]);
            return $rawContent; // 失败了,就退回到原文,别崩
        }
    }

    /**
     * 处理长文章(分块处理)
     */
    public function handleLongArticle(string $content, $chunkSize = 1000)
    {
        // 简单的字符串截断逻辑,实际生产中可以用 NLP 分句
        $chunks = str_split($content, $chunkSize);

        $rewrittenChunks = [];
        foreach ($chunks as $chunk) {
            // 递归调用,或者这里可以加个缓存防止重复改写
            $rewrittenChunks[] = $this->rewriteContent($chunk, '大众读者', '幽默');
        }

        return implode("nn", $rewrittenChunks);
    }
}

技术揭秘

看到那个 prompt 了吗?那是魔法咒语。
$temperature 参数很有意思。设为 0,AI 就像个复读机;设为 1,AI 就像个疯子。我们要在这个中间找个平衡点。

如果文章很长,比如 5000 字,你不能一次性喂给 GPT,它会超时或者忘掉前面说了什么。所以,代码里的 handleLongArticle 逻辑是把文章切成 1000 字的小块,一块一块喂给 AI,然后再拼回去。这就好比吃巨无霸汉堡,一口一口吃。


第四部分:自动化工作流——让 PHP 变得有节奏感

现在,我们有数据(抓取),我们有内容(改写)。但这只是死水。我们需要一条河,让水流起来。

在 Laravel 里,这叫 Jobs(任务)Queues(队列)

代码示例:任务定义

<?php

namespace AppJobs;

use IlluminateBusQueueable;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateQueueInteractsWithQueue;
use IlluminateQueueSerializesModels;
use AppServicesCrawlerService;
use AppServicesAIWriterService;

class ProcessMarketingArticle implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $url;
    public $tries = 3; // 失败重试 3 次
    public $timeout = 60; // 这个任务最多跑 60 秒

    /**
     * Create a new job instance.
     */
    public function __construct(string $url)
    {
        $this->url = $url;
    }

    /**
     * Execute the job.
     */
    public function handle(CrawlerService $crawler, AIWriterService $ai)
    {
        // 1. 爬取
        Log::info("开始处理 URL: {$this->url}");
        $html = $crawler->fetchPage($this->url);

        if (empty($html)) {
            throw new Exception("无法抓取页面内容");
        }

        // 2. 提取文本
        $textContent = $crawler->extractContent($html);

        // 3. AI 改写
        $article = $ai->rewriteContent($textContent, '科技爱好者', '硬核科普风');

        // 4. 这里可以接上保存到数据库、生成图片、或者发布到社交媒体
        Log::info("改写完成,内容长度: " . strlen($article));

        // 模拟发布
        $this->publishToWordPress($article);
    }

    private function publishToWordPress(string $content)
    {
        // 实际上你会调用 WordPress REST API 或者 XML-RPC
        // 这里省略具体的 cURL 调用代码
        Log::info("已发布到 WordPress");
    }

    /**
     * 任务失败处理
     */
    public function failed(Exception $exception)
    {
        Log::error("任务彻底失败: {$this->url}", [
            'exception' => $exception->getMessage()
        ]);

        // 发送钉钉通知?
        // $this->sendDingTalkAlert("文章发布失败: " . $this->url);
    }
}

触发机制:Cron Jobs

现在我们定义了一个 Job,怎么让它跑起来?你需要一个 kernel.php 或者一个计划任务。

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // 每天凌晨 3 点,跑一次
    $schedule->call(function () {
        // 去数据库里找一批待抓取的 URL
        $urls = DB::table('crawl_queue')->where('status', 'pending')->limit(10)->pluck('url');

        foreach ($urls as $url) {
            // 分发任务
            ProcessMarketingArticle::dispatch($url);

            // 更新状态
            DB::table('crawl_queue')->where('url', $url)->update(['status' => 'processing']);
        }
    })->dailyAt('03:00');
}

技术揭秘

这是整个系统的“心脏”。ShouldQueue 接口意味着这个任务不会被阻塞。如果你有 1000 个 URL 要抓取,它们不会排队跑死你的服务器,而是会把任务扔进 Redis,然后你的队列 Worker(工作进程)会像勤劳的工蜂一样,一个个把它们拿起来吃掉。

即使其中某个 URL 抓取超时了,tries = 3 会确保系统不会轻易放弃,而是会重试。如果还是失败,触发生命周期的 failed 方法,给你发个邮件哭诉一下。


第五部分:全链路闭环——从服务器到屏幕

我们已经有了抓取、有了改写、有了调度。现在,我们把这个闭环补全。

假设我们抓取了一篇关于“PHP 新特性”的硬核文章,AI 把它改写成了“为什么 PHP 依然能统治服务器世界”。

状态流转图(脑补)

  1. Input: URL 列表。
  2. Queue: 任务进入 Redis 队列。
  3. Process: Worker 抓取 HTML -> 提取文本 -> 调用 GPT -> 获得文案。
  4. Store: 将文案存入 MySQL,生成缩略图(如果需要)。
  5. Output: 自动调用第三方平台的 API(微博、Twitter、微信公众号)发布。

代码示例:自动发布逻辑(伪代码)

// 假设我们要发布到微信公众号
private function publishToWeChat(string $content)
{
    // 微信公众平台 API
    $accessToken = $this->getAccessToken(); // 缓存获取 token

    $data = [
        'articles' => [
            [
                'title' => '深度解析:PHP 的魔法',
                'author' => '自动化机器',
                'content' => $content, // 这里通常是 HTML,需要做富文本处理
                'digest' => 'AI 改写的精华摘要...',
                'show_cover_pic' => 1,
            ]
        ]
    ];

    $response = $this->client->post("https://api.weixin.qq.com/cgi-bin/material/add_news?access_token={$accessToken}", [
        'json' => $data
    ]);

    $result = json_decode($response->getBody(), true);

    if (isset($result['media_id'])) {
        // 获取素材 ID 后,推送到图文消息
        $this->pushToMpNews($result['media_id']);
    }
}

进阶:使用 Docker 编排

如果你想把这套东西部署上去,最爽的方式就是 Docker。在一个 docker-compose.yml 里,我们可以这样搞:

version: '3.8'
services:
  # 数据库
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
    volumes:
      - db_data:/var/lib/mysql

  # 队列监听器
  worker:
    build: .
    command: php artisan queue:work redis --tries=3
    volumes:
      - .:/app
    depends_on:
      - mysql
      - redis

  # 调度器 (虽然最好在宿主机跑,但这里演示)
  scheduler:
    build: .
    command: php artisan schedule:run
    volumes:
      - .:/app
    depends_on:
      - worker

volumes:
  db_data:

有了这个,你的自动化营销系统就是一个独立的容器。挂在一个云服务器上,它就开始自动转动了。


第六部分:避坑指南与实战中的“坑爹”时刻

作为资深程序员,我得给你们提个醒。这个流程看着很美,但跑起来全是坑。

  1. 反爬虫的高级形态:现在很多网站用了 Cloudflare 或 WAF(Web 应用防火墙)。普通的 Guzzle 请求会被瞬间拦截。这时候,你可能需要用到 v2ray-php 或者 Selenium/Playwright 来模拟真实的浏览器操作(但这会非常消耗资源)。或者,你需要去买那个贵得要死的 residential proxy(住宅代理)。记住,免费的代理就是你的噩梦,全是垃圾数据。
  2. AI 的幻觉:AI 有时候会胡说八道。比如它可能会编造一个不存在的数据点。在营销号领域,这叫“夸大宣传”;但在严肃场景,这叫“风险”。记得在发布前人工抽检几条,或者加一个过滤逻辑,如果生成的文章包含“绝对化用语”或“虚假新闻”,自动拦截。
  3. Token 成本:GPT-4 很贵。如果你每天要改写 1000 篇文章,每篇 2000 字,那成本是天文数字。建议用 GPT-3.5-turbo 做初稿,GPT-4 做润色。或者使用本地的开源大模型(比如 Llama 3),部署在本地 GPU 服务器上,成本几乎为零。
  4. HTML 解析的坑:很多网站的结构非常糟糕,表哥嵌套表弟,还带很多注释和脚本。Simple HTML DOM Parser 有时候会崩溃。这时候,Python 的 BeautifulSoup 可能更好用,但你得通过 API 去调用 Python 服务。这是跨语言协作的优雅方式。

结语:PHP 的不死传说

好了,朋友们,看看我们今天做了什么?

我们从零开始,用 PHP 的 Guzzle 去获取数据,用 PHP 的逻辑去处理文本,用 PHP 的 Queue 去管理并发,最后用 PHP 调用了 OpenAI 的接口。

这不仅仅是代码,这是生产力

PHP 是一种极具侵略性的语言。它的语法简单,生态庞大,尤其是结合了现代的框架和工具链后,它依然能像十年前一样,统治着互联网的后台。

那些嘲笑 PHP 过时的人,可能还在手动复制粘贴文章,或者还在用 Python 写个脚本跑一下然后关掉。而我们,正在用 PHP 构建一条永不停歇的自动化生产线。

所以,别再说 PHP 只能写博客了。把你的腰杆挺直了,打开编辑器,敲下 php artisan make:command

去吧,去构建属于你的自动化帝国!

(屏幕暗下,出现一行字:The code is poetry, automation is life.)

发表回复

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