首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >将 Lazada 爬取结果落地 MySQL:PHP 数据持久化实战

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

原创
作者头像
小白学大数据
发布2026-07-21 16:58:35
发布2026-07-21 16:58:35
2320
举报

引言

在跨境电商选品和竞品监控中,Lazada 的商品页包含标题、价格、评分、库存等结构化字段,是比价与趋势分析的数据来源。爬虫把页面拉到内存只是流程的前半段:内存中的数据进程结束后即丢弃,无法支撑跨周期的查询、去重和增量更新。本文描述一条从采集到落库的完整链路,使用 PHP 作为采集与写入语言,MySQL 作为存储,全文覆盖表结构设计、代理接入、字段解析、批量 upsert 与容错策略。

一、为什么采集后必须落库

爬虫输出停留在内存或临时 CSV 时,每一次分析都要重新发起请求,既消耗带宽,又放大触达风控的概率。落库要解决三个具体的工程问题:

首先是幂等性。同一商品会被周期性重复抓取,写入逻辑必须保证重复执行不产生重复行,否则后续统计会被脏数据污染。

其次是可查询性。价格曲线、库存波动、类目分布都需要按时间维度和商品维度做聚合,关系型数据库比文件系统更胜任这件事。

最后是可追溯性。保留每条记录的抓取时间和来源站点,出现解析异常时能定位是哪一次采集引入了错误字段。

MySQL 在百万级商品规模下,配合合理索引和批量写入,吞吐和运维成本都可控,适合作为该类项目的主存储。

二、Lazada 的访问限制与亿牛云隧道代理

Lazada 对来源 IP 的请求频率敏感。单 IP 短时间高频访问会触发 403、验证码页或按 IP 的限流,部分接口还会校验 X-CSRF-TOKEN 之类的动态参数。直连采集几千条商品后失败率通常明显上升。

亿牛云(16yun)提供面向采集场景的代理产品,其中隧道代理(Tunnel)适合本场景:业务侧只配置一个固定的入口地址,由亿牛云在后端自动轮换出口 IP,开发者不需要自建 IP 池或做可用性探活。对 Lazada 这类按 IP 限流的站点,分散出口能直接降低单一 IP 被封的概率。

接入方式是带账密鉴权的代理 URL:

代码语言:javascript
复制
http://<亿牛云用户名>:<密码>@tunnel.16yun.cn:<端口>

凭证应放在环境变量或配置文件中,不要硬编码进源码。需要留意两点:隧道代理相比直连会引入额外延迟,QPS 规划时要预留这部分开销;亿牛云按套餐限制并发通道数,采集并发度不能超过套餐上限,否则请求会在代理层排队。

三、MySQL 表结构设计

商品数据兼有宽表属性和缓慢变化特征:标题、类目相对静态,价格、库存频繁变动。设计两张表,主表存最新快照,历史表存价格时序。

代码语言:javascript
复制
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_itemitem_idcountry 作为唯一约束,是因为同一商品 ID 在不同国家站点(sg、my、ph 等)是不同货品,需要区分后各自 upsert。

四、PHP 采集与字段解析

Lazada 商品页是前端渲染的 SPA,核心数据放在页面的 __INITIAL_STATE__ 这个内联 JSON 状态块里,比解析 DOM 更稳定,因为 DOM 结构随改版变动更频繁。先安装依赖:

代码语言:javascript
复制
composer require guzzlehttp/guzzle symfony/dom-crawler symfony/css-selector

采集与解析代码:

代码语言:javascript
复制
<?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 时更新快照字段,未命中时插入新行:

代码语言:javascript
复制
<?php
$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,代理决定采集能否持续。把这些约束前置到表结构和代码里,后续的比价、预警和趋势分析才站得住脚。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 引言
  • 一、为什么采集后必须落库
  • 二、Lazada 的访问限制与亿牛云隧道代理
  • 三、MySQL 表结构设计
  • 四、PHP 采集与字段解析
  • 五、数据清洗与批量 upsert
  • 六、性能优化与容错
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档