将 Lazada 爬取结果落地 MySQL:PHP 数据持久化实战

简介: 将 Lazada 爬取结果落地 MySQL:PHP 数据持久化实战

引言
在跨境电商选品和竞品监控中,Lazada 的商品页包含标题、价格、评分、库存等结构化字段,是比价与趋势分析的数据来源。爬虫把页面拉到内存只是流程的前半段:内存中的数据进程结束后即丢弃,无法支撑跨周期的查询、去重和增量更新。本文描述一条从采集到落库的完整链路,使用 PHP 作为采集与写入语言,MySQL 作为存储,全文覆盖表结构设计、代理接入、字段解析、批量 upsert 与容错策略。
一、为什么采集后必须落库
爬虫输出停留在内存或临时 CSV 时,每一次分析都要重新发起请求,既消耗带宽,又放大触达风控的概率。落库要解决三个具体的工程问题:
首先是幂等性。同一商品会被周期性重复抓取,写入逻辑必须保证重复执行不产生重复行,否则后续统计会被脏数据污染。
其次是可查询性。价格曲线、库存波动、类目分布都需要按时间维度和商品维度做聚合,关系型数据库比文件系统更胜任这件事。
最后是可追溯性。保留每条记录的抓取时间和来源站点,出现解析异常时能定位是哪一次采集引入了错误字段。
MySQL 在百万级商品规模下,配合合理索引和批量写入,吞吐和运维成本都可控,适合作为该类项目的主存储。
二、Lazada 的访问限制与亿牛云隧道代理
Lazada 对来源 IP 的请求频率敏感。单 IP 短时间高频访问会触发 403、验证码页或按 IP 的限流,部分接口还会校验 X-CSRF-TOKEN 之类的动态参数。直连采集几千条商品后失败率通常明显上升。
亿牛云(16yun)提供面向采集场景的代理产品,其中隧道代理(Tunnel)适合本场景:业务侧只配置一个固定的入口地址,由亿牛云在后端自动轮换出口 IP,开发者不需要自建 IP 池或做可用性探活。对 Lazada 这类按 IP 限流的站点,分散出口能直接降低单一 IP 被封的概率。
接入方式是带账密鉴权的代理 URL:


凭证应放在环境变量或配置文件中,不要硬编码进源码。需要留意两点:隧道代理相比直连会引入额外延迟,QPS 规划时要预留这部分开销;亿牛云按套餐限制并发通道数,采集并发度不能超过套餐上限,否则请求会在代理层排队。
三、MySQL 表结构设计
商品数据兼有宽表属性和缓慢变化特征:标题、类目相对静态,价格、库存频繁变动。设计两张表,主表存最新快照,历史表存价格时序。
```CREATE TABLE lazada_products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
item_id VARCHAR(32) NOT NULL COMMENT 'Lazada 商品ID',
title VARCHAR(512) NOT NULL DEFAULT '',
seller_name VARCHAR(128) NOT NULL DEFAULT '',
price DECIMAL(12,2) NOT NULL DEFAULT 0,
original_price DECIMAL(12,2) NOT NULL DEFAULT 0,
currency CHAR(3) NOT NULL DEFAULT 'USD',
rating DECIMAL(3,2) NOT NULL DEFAULT 0,
review_count INT UNSIGNED NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
category VARCHAR(128) NOT NULL DEFAULT '',
image_url VARCHAR(1024) NOT NULL DEFAULT '',
url VARCHAR(1024) NOT NULL DEFAULT '',
country CHAR(2) NOT NULL DEFAULT 'SG' COMMENT '站点国家',
last_crawled DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_item (item_id, country)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE lazada_price_history (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
item_id VARCHAR(32) NOT NULL,
price DECIMAL(12,2) NOT NULL,
crawled_at DATETIME NOT NULL,
KEY idx_item (item_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


设计要点说明。字符集用 utf8mb4 而不是 utf8,因为 Lazada 商品标题会出现 emoji 和超出基本多文种平面的字符,utf8 会截断。金额用 DECIMAL 而非 FLOAT,避免二进制浮点带来的精度误差,价格比较和汇总才有确定结果。uk_item 以 item_id 加 country 作为唯一约束,是因为同一商品 ID 在不同国家站点(sg、my、ph 等)是不同货品,需要区分后各自 upsert。
四、PHP 采集与字段解析
Lazada 商品页是前端渲染的 SPA,核心数据放在页面的 __INITIAL_STATE__ 这个内联 JSON 状态块里,比解析 DOM 更稳定,因为 DOM 结构随改版变动更频繁。先安装依赖:
composer require guzzlehttp/guzzle symfony/dom-crawler symfony/css-selector
采集与解析代码:
```<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;

$proxy = sprintf(
    'http://%s:%s@tunnel.16yun.cn:3100',
    getenv('YINIU_USER'),
    getenv('YINIU_PASS')
);

$client = new Client([
    'proxy' => $proxy,
    'timeout' => 15,
    'headers' => [
        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept-Language' => 'en-US,en;q=0.9',
    ],
]);

function fetchProduct(Client $client, string $url): array
{
    $html = $client->get($url)->getBody()->getContents();
    $crawler = new Crawler($html);

    $script = $crawler->filter('script:contains("__INITIAL_STATE__")')->first()->text();
    preg_match('/__INITIAL_STATE__\s*=\s*(\{.*?\});/', $script, $m);
    $data = json_decode($m[1], true);

    $p = $data['mods']['pageProduct'] ?? [];
    return [
        'item_id'     => $p['itemId'] ?? '',
        'title'       => $p['title'] ?? '',
        'price'       => ($p['price'] ?? 0) / 100,   // Lazada 金额以最小货币单位存储,需除以 100
        'seller_name' => $p['sellerName'] ?? '',
        'rating'      => $p['ratingScore'] ?? 0,
        'review_count'=> $p['review'] ?? 0,
        'stock'       => $p['stock'] ?? 0,
        'image_url'   => $p['image'] ?? '',
        'url'         => $url,
    ];
}

Guzzle 的 Client 实例复用连接,避免每条请求重建 TCP 握手。经隧道代理发出的请求由亿牛云轮换出口,配合真实 UA 和语言头,能通过 Lazada 的基础反爬校验。解析时需要注意,Lazada 接口返回的金额以最小货币单位(分)计,必须除以 100 还原成元,否则落库价格会放大一百倍。
五、数据清洗与批量 upsert
写入前先做清洗:修剪首尾空白、过滤 item_id 为空的记录、校验价格非负。然后用 INSERT ... ON DUPLICATE KEY UPDATE 做 upsert,命中 uk_item 时更新快照字段,未命中时插入新行:

$pdo = new PDO(
    'mysql:host=127.0.0.1;dbname=lazada;charset=utf8mb4',
    getenv('DB_USER'), getenv('DB_PASS'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

function persist(PDO $pdo, array $row, string $country)
{
    $now = date('Y-m-d H:i:s');
    $sql = "INSERT INTO lazada_products
      (item_id, title, seller_name, price, currency, rating, review_count, stock, image_url, url, country, last_crawled)
      VALUES (?,?,?,?, 'USD', ?,?,?,?,?,?,?)
      ON DUPLICATE KEY UPDATE
        title=VALUES(title), price=VALUES(price), rating=VALUES(rating),
        review_count=VALUES(review_count), stock=VALUES(stock),
        last_crawled=VALUES(last_crawled)";
    $pdo->prepare($sql)->execute([
        $row['item_id'], $row['title'], $row['seller_name'],
        $row['price'], $row['rating'], $row['review_count'],
        $row['stock'], $row['image_url'], $row['url'], $country, $now
    ]);

    // 仅当价格相对上次发生变化时记录历史,避免历史表被重复值撑大
    $prev = $pdo->prepare("SELECT price FROM lazada_price_history WHERE item_id = ? ORDER BY crawled_at DESC LIMIT 1");
    $prev->execute([$row['item_id']]);
    if ($prev->fetchColumn() != $row['price']) {
        $pdo->prepare("INSERT INTO lazada_price_history (item_id, price, crawled_at) VALUES (?,?,?)")
            ->execute([$row['item_id'], $row['price'], $now]);
    }
}

参数化查询(? 占位符)是强制项,不能直接拼接 $row 到 SQL 字符串,否则商品标题中的特殊字符会破坏语句,甚至引入注入风险。价格历史只在价格相对上次记录发生变化时才写入,否则每天的全量巡检会把历史表撑成大量重复行,拖慢按 item_id 的检索。
批量场景用事务包裹,每累积 500 行提交一次,比逐条 autocommit 减少磁盘 fsync 次数,写入吞吐可提升数倍。注意事务不要开得过大,否则长事务会占用 undo 日志并阻塞备份。
六、性能优化与容错
连接复用。PDO 可加 PDO::ATTR_PERSISTENT => true 复用数据库连接,Guzzle Client 实例在循环外创建,两者都避免重复建连的开销。
并发与限流。Guzzle 的异步 Promise 或 Swoole 协程能把串行等待改成并发,但并发度要受亿牛云套餐通道数和 Lazada 频率限制的双重约束,不能只看本机 CPU。
失败重试。对 403 和超时做指数退避重试,间隔取 1s、2s、4s,并加入随机抖动避免多个任务同步重试形成尖峰。超过重试上限的记录写入 failed_queue 表,由独立任务延后补偿,不阻塞主流程。
校验与隔离。入库前校验 item_id 非空、price >= 0、评分在合理区间。不合规则的记录进异常表,不写主表,保证主表数据可直接用于分析。
增量调度。按 last_crawled 排序,只抓取超过设定阈值(如 24 小时)未更新的商品,整体请求量随之下降。
结语
从页面解析到 MySQL 落库,链路的每个环节都有明确的工程约束:字符集决定能否完整存下标题,DECIMAL 决定金额是否可信,唯一键决定能否安全 upsert,代理决定采集能否持续。把这些约束前置到表结构和代码里,后续的比价、预警和趋势分析才站得住脚。

相关文章
|
2月前
|
人工智能 缓存 编解码
把 AI 视频剪辑搬进浏览器:Timeline Studio 的本地优先架构与工程实践
视频剪辑、语音合成、自动字幕和 AI 视觉处理,过去往往意味着桌面客户端、服务端转码集群以及漫长的素材上传。Timeline Studio 尝试了另一条路线:以浏览器为完整运行环境,把多轨时间线、ONNX 端侧推理、Canvas 合成、离线音频混音和 WebCodecs 编码连接成一个可用的 AI 视频编辑器。本文结合项目实践,拆解这套架构为何成立、最难的三个工程问题,以及我们如何处理预览与导出一致性、模型缓存、时间线精度和兼容回退
|
2月前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2823 13
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
8月前
|
数据采集 JSON Java
Java 异步爬虫高效获取小红书短视频内容
Java 异步爬虫高效获取小红书短视频内容
|
9月前
|
数据采集 文字识别 JavaScript
基于文本检测的 Python 爬虫弹窗图片定位与拖动实现
基于文本检测的 Python 爬虫弹窗图片定位与拖动实现
|
3月前
|
数据采集 存储 自然语言处理
信息筛选耗时?Python 爬虫搭配大模型,一键抓取资讯并智能总结
信息筛选耗时?Python 爬虫搭配大模型,一键抓取资讯并智能总结
|
4月前
|
数据采集 Web App开发 JavaScript
Python 爬虫动态 JS 渲染与无头浏览器实战选型指南
Python 爬虫动态 JS 渲染与无头浏览器实战选型指南
|
4月前
|
数据采集 Web App开发 JSON
基于大模型的Python智能爬虫:语义识别与数据清洗实践
基于大模型的Python智能爬虫:语义识别与数据清洗实践
|
4月前
|
数据采集 JSON 数据挖掘
抖音搜索页数据批量爬取,多关键词同步采集实现
抖音搜索页数据批量爬取,多关键词同步采集实现
|
5月前
|
数据采集 Web App开发 开发者
解决 Python 爬虫被限制:延迟抓取指令深度解析
解决 Python 爬虫被限制:延迟抓取指令深度解析
|
6月前
|
数据采集 搜索推荐 应用服务中间件
如何判断网站流量飙升是搜索引擎爬虫导致的?
如何判断网站流量飙升是搜索引擎爬虫导致的?