PHP 驱动的自动化营销工作流:从内容抓取、AI 改写到自动发布的全链路 PHP 闭环
各位老铁,各位码农,各位那些在深夜里一边吃着泡面一边试图把项目赶上线的朋友们,大家早上好(或者是晚上好,我不确定现在的时区)。
今天我们不聊架构设计的七七八八,也不谈什么DDD(领域驱动设计)的鬼东西。今天我们要聊点“狠”的。我们要聊聊那个被贴了太多标签、被误解太深、实际上却像瑞士军刀一样锋利的语言——PHP。
有人可能会说:“PHP?那不是写 WordPress 的吗?不是那个‘世界上最好的语言’自封的梗吗?”
嘿,说得好。没错,PHP 是能写 WordPress。但 PHP 也能驱动你那价值百万美元的自动化营销流水线。今天,我们就来一场硬核的技术秀,用 PHP 编写一套从“千里之外”抓取内容,交给“超级大脑”改写,最后自动分发到各个平台的闭环系统。
准备好了吗?让我们把代码敲得震天响!
第一部分:出发前的装备清单(不仅仅是 PHP)
在写代码之前,我们要先明确一下我们的“作战部队”。如果你只有一个 <?php echo "hello"; ?>,那你只能干瞪眼。我们需要更现代的工具,但在 PHP 生态里,有些老伙计依然很香。
- Guzzle HTTP Client:不要再用
file_get_contents了,除非你想被人肉搜索并拉入黑名单。Guzzle 是 PHP 界的 curl 封装,它优雅、现代、支持异步,简直是爬虫界的宝马。 - Laravel:虽然你可以用原生 PHP 写,但我强烈建议你用 Laravel。为什么?因为它的队列、任务调度和依赖注入,能让你的代码像黄油一样顺滑。
- OpenAI API (或兼容接口):我们的 AI 智囊团。
- Redis:用来存队列,用来做缓存,用来当我们的内存数据库。
- 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 依然能统治服务器世界”。
状态流转图(脑补)
- Input: URL 列表。
- Queue: 任务进入 Redis 队列。
- Process: Worker 抓取 HTML -> 提取文本 -> 调用 GPT -> 获得文案。
- Store: 将文案存入 MySQL,生成缩略图(如果需要)。
- 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:
有了这个,你的自动化营销系统就是一个独立的容器。挂在一个云服务器上,它就开始自动转动了。
第六部分:避坑指南与实战中的“坑爹”时刻
作为资深程序员,我得给你们提个醒。这个流程看着很美,但跑起来全是坑。
- 反爬虫的高级形态:现在很多网站用了 Cloudflare 或 WAF(Web 应用防火墙)。普通的 Guzzle 请求会被瞬间拦截。这时候,你可能需要用到
v2ray-php或者 Selenium/Playwright 来模拟真实的浏览器操作(但这会非常消耗资源)。或者,你需要去买那个贵得要死的 residential proxy(住宅代理)。记住,免费的代理就是你的噩梦,全是垃圾数据。 - AI 的幻觉:AI 有时候会胡说八道。比如它可能会编造一个不存在的数据点。在营销号领域,这叫“夸大宣传”;但在严肃场景,这叫“风险”。记得在发布前人工抽检几条,或者加一个过滤逻辑,如果生成的文章包含“绝对化用语”或“虚假新闻”,自动拦截。
- Token 成本:GPT-4 很贵。如果你每天要改写 1000 篇文章,每篇 2000 字,那成本是天文数字。建议用 GPT-3.5-turbo 做初稿,GPT-4 做润色。或者使用本地的开源大模型(比如 Llama 3),部署在本地 GPU 服务器上,成本几乎为零。
- 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.)