Cheerio 解析 HTML 表格:动态定位列索引

HTML 表格的列顺序经常变动,硬编码列索引(cells[2])在结构改变后立刻失效。正确做法是先扫描表头行确定目标列的实际位置,再按索引取数据。

核心思路

1. 找到目标表格
2. 读取表头行,确定"销售"列的索引
3. 遍历数据行,按列索引取值
4. 匹配产品名称,验证数值范围

完整实现

import * as cheerio from "cheerio";

class PriceParser {
    constructor() {
        this.validationRules = {
            gold:   { min: 400, max: 1000 },  // 元/克
            silver: { min: 3,   max: 20 },
        };
    }

    parsePrices(html) {
        const $ = cheerio.load(html);
        const result = { goldPrice: null, silverPrice: null };

        $("table").each((_, table) => {
            const rows = $(table).find("tr");
            if (rows.length < 2) return;

            // 扫描表头行,定位"销售"列
            let saleColumnIndex = -1;
            rows.first().find("th, td").each((i, cell) => {
                if ($(cell).text().trim().includes("销售")) {
                    saleColumnIndex = i;
                    return false; // break
                }
            });

            if (saleColumnIndex < 0) return;

            // 遍历数据行
            rows.each((rowIndex, row) => {
                if (rowIndex === 0) return; // 跳过表头

                const cells = $(row).find("td, th");
                if (!cells.length) return;

                const productName = $(cells[0]).text().trim();

                // 匹配黄金9999
                if (
                    (productName === "黄金9999" || productName === "黄金 9999") &&
                    !result.goldPrice
                ) {
                    result.goldPrice = this.extractPrice(
                        $(cells[saleColumnIndex]).text().trim(),
                        this.validationRules.gold
                    );
                }

                // 匹配白银(排除含"黄金"的行)
                if (
                    productName === "白银" &&
                    !productName.includes("黄金") &&
                    !result.silverPrice
                ) {
                    result.silverPrice = this.extractPrice(
                        $(cells[saleColumnIndex]).text().trim(),
                        this.validationRules.silver
                    );
                }
            });
        });

        return result;
    }

    extractPrice(text, { min, max }) {
        const match = text.match(/(\d+\.?\d*)/);
        if (!match) return null;

        const price = parseFloat(match[1]);
        if (price < min || price > max) return null;  // 超出合理范围

        return price.toFixed(2);
    }
}

// 使用
const parser = new PriceParser();
const { goldPrice, silverPrice } = parser.parsePrices(html);

关键点解析

动态列定位:用 .includes("销售") 而不是固定索引,表格新增或删除列时代码无需修改。

数值范围验证validationRules 定义合理价格区间,过滤掉格式异常或明显错误的数据(如抓到表格里的其他数字)。

产品名精确匹配productName === "黄金9999" 而不是 .includes("黄金"),防止把”黄金饰品9999”也匹配进来。!productName.includes("黄金") 在白银行再加一层保护。

多语言/别名的处理

如果表格里的产品名不统一(黄金9999黄金 9999Au9999 混用):

const GOLD_NAMES = new Set(["黄金9999", "黄金 9999", "Au9999", "足金9999"]);

if (GOLD_NAMES.has(productName) && !result.goldPrice) { ... }

调试技巧

遇到解析不到数据时,先打印表格 HTML 确认结构:

$("table").each((i, table) => {
    console.log(`Table ${i}:`, $.html(table).slice(0, 500));
});

再检查表头文本是否有空格或全角字符:

rows.first().find("th, td").each((i, cell) => {
    console.log(`Header ${i}: "${$(cell).text().trim()}"`);
});

页面改版后表格结构变化,这两步能快速定位问题所在。