You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Puppeteer中无需数组处理提取层级导航数据?

问题:简化导航层级数据提取方案

需要从给定的HTML导航结构中提取指定格式的层级数据,现有实现通过数组匹配关联父级和子级导航,希望简化代码,直接通过元素选择器关联父级与对应子级,无需事后处理数组。

给定HTML结构

<nav class='menu-315AG'>
    <a id='side-nav-1' href='something1' data-id='1'>text1</a>
    <div>
        <a class='side-sub-1 blue' href='something1/somethingA' data-id='1a'>text1a</a>
        <a class='side-sub-1 blue' href='something1/somethingB' data-id='1b'>text1b</a>
        <a class='side-sub-1 blue' href='something1/somethingC' data-id='1c'>text1c</a>
    </div>
    <a id='side-nav-2' href='something2' data-id='2'>text2</a>
    <div>
        <a class='side-sub-1 blue' href='something2/something2A' data-id='2a'>text2a</a>
        <a class='side-sub-1 blue' href='something2/something2B' data-id='2b'>text2b</a>
        <a class='side-sub-1 blue' href='something2/something2C' data-id='2c'>text2c</a>
        <a class='side-sub-1 blue' href='something2/something2D' data-id='2d'>text2d</a>
    </div>
    <a id='side-nav-3' href='something3' data-id='3'>text3</a>
    <div>
        <a class='side-sub-1 blue' href='something3/something3A' data-id='3a'>text3a</a>
        <a class='side-sub-1 blue' href='something3/something3B' data-id='3b'>text3b</a>
    </div>
</nav>

目标数据格式

[
    [
        1, 
        'text1', 
        'something1', 
        'side-nav-1', 
        [ 
            ['1a', 'text1a', 'something1/somethingA'],
            ['1b', 'text1b', 'something1/somethingB'],
            ['1c', 'text1c', 'something1/somethingC']
        ] 
    ],
    [
        2, 
        'text2', 
        'something2', 
        'side-nav-2', 
        [ 
            ['2a', 'text2a', 'something2/something2A'],
            ['2b', 'text2b', 'something2/something2B'],
            ['2c', 'text2c', 'something2/something2C'],
            ['2d', 'text2d', 'something2/something2D'] 
        ] 
    ],
    [
        3, 
        'text3', 
        'something3', 
        'side-nav-3', 
        [ 
            ['3a', 'text3a', 'something3/something3A'],
            ['3b', 'text3b', 'something3/something3B']
        ] 
    ]
]

当前实现代码(数组匹配方式)

const puppeteer = require("puppeteer");

const html = `
    <nav class='menu-315AG'>
        <a id='side-nav-1' href='something1' data-id='1'>text1</a>
        <div>
            <a class='side-sub-1 blue' href='something1/somethingA' data-id='1a'>text1a</a>
            <a class='side-sub-1 blue' href='something1/somethingB' data-id='1b'>text1b</a>
            <a class='side-sub-1 blue' href='something1/somethingC' data-id='1c'>text1c</a>
        </div>
        <a id='side-nav-2' href='something2' data-id='2'>text2</a>
        <div>
            <a class='side-sub-1 blue' href='something2/something2A' data-id='2a'>text2a</a>
            <a class='side-sub-1 blue' href='something2/something2B' data-id='2b'>text2b</a>
            <a class='side-sub-1 blue' href='something2/something2C' data-id='2c'>text2c</a>
            <a class='side-sub-1 blue' href='something2/something2D' data-id='2d'>text2c</a>
        </div>
        <a id='side-nav-3' href='something3' data-id='3'>text3</a>
        <div>
            <a class='side-sub-1 blue' href='something3/something3A' data-id='3a'>text3a</a>
            <a class='side-sub-1 blue' href='something3/something3B' data-id='3b'>text3b</a>
        </div>
    </nav>
`;

let browser;
(async () => {
    browser = await puppeteer.launch();
    const [page] = await browser.pages();
    await page.setContent(html);

    function ppp(str) {
        return console.dir(str, {depth: null,colors: true,maxArrayLength: null});
    }

    let categories = await page.$$eval('nav[class*=menu-]  a[id*=side-nav-]', el => el.map(x => {
        return [
            x.getAttribute('data-id'),              
            x.textContent.trim(), 
            x.getAttribute('href'),
            x.getAttribute('id'),
        ];
    }));

    let subcategories = await page.$$eval('nav[class*=menu-]  div > a[class*=side-sub-]', el => el.map(x => {
        return [
            x.getAttribute('data-id'),              
            x.textContent.trim(), 
            x.getAttribute('href'),
        ];
    }));

    for (let [i,] of categories.entries()) {
        categories[i].push([]); 
        for (let [j,] of subcategories.entries()) {
            if (subcategories[j][2].includes(categories[i][2])) {
                categories[i][4].push(subcategories[j]);
            }
        }
    }

    ppp(categories); 

})()
.catch(err => console.error(err))
.finally(() => browser ?. close());

尝试的错误代码

let categoryPages = await this.page.$$('nav[class*=menu-]  a[id*=side-nav-]');

let categories = [];
for (let cat of categoryPages) {

    let text = await cat.evaluate(el => el.textContent.trim());
    let href = await cat.evaluate(el => el.getAttribute('href'));
    let id = await cat.evaluate(el => el.getAttribute('id'));
    let data_id = await cat.evaluate(el => el.getAttribute('data-id'));              

    let subCategories = await cat.page.$$eval('div > a[class*=side-sub-]', el => el.map(x => {
            return [
                x.getAttribute('data-id'),              
                x.textContent.trim(), 
                x.getAttribute('href'),
            ];
    }))

    categories.push([data_id, text, href, id, subCategories]);
}

解决方案

核心思路

每个父级导航<a id*=side-nav->的直接下一个兄弟元素就是包含子导航的<div>,因此可以通过el.nextElementSibling直接获取对应子元素容器,在父元素的上下文中提取子导航数据,彻底避免事后数组匹配的繁琐步骤。

优化后代码(单次$$eval实现)

const puppeteer = require("puppeteer");

const html = `
    <nav class='menu-315AG'>
        <a id='side-nav-1' href='something1' data-id='1'>text1</a>
        <div>
            <a class='side-sub-1 blue' href='something1/somethingA' data-id='1a'>text1a</a>
            <a class='side-sub-1 blue' href='something1/somethingB' data-id='1b'>text1b</a>
            <a class='side-sub-1 blue' href='something1/somethingC' data-id='1c'>text1c</a>
        </div>
        <a id='side-nav-2' href='something2' data-id='2'>text2</a>
        <div>
            <a class='side-sub-1 blue' href='something2/something2A' data-id='2a'>text2a</a>
            <a class='side-sub-1 blue' href='something2/something2B' data-id='2b'>text2b</a>
            <a class='side-sub-1 blue' href='something2/something2C' data-id='2c'>text2c</a>
            <a class='side-sub-1 blue' href='something2/something2D' data-id='2d'>text2d</a>
        </div>
        <a id='side-nav-3' href='something3' data-id='3'>text3</a>
        <div>
            <a class='side-sub-1 blue' href='something3/something3A' data-id='3a'>text3a</a>
            <a class='side-sub-1 blue' href='something3/something3B' data-id='3b'>text3b</a>
        </div>
    </nav>
`;

let browser;
(async () => {
    browser = await puppeteer.launch();
    const [page] = await browser.pages();
    await page.setContent(html);

    function ppp(str) {
        return console.dir(str, {depth: null, colors: true, maxArrayLength: null});
    }

    // 直接在父元素上下文中关联子导航数据
    const categories = await page.$$eval('nav[class*=menu-] a[id*=side-nav-]', els => 
        els.map(el => {
            // 获取当前父级a对应的子导航容器
            const subContainer = el.nextElementSibling;
            // 提取子导航数据
            const subCategories = Array.from(subContainer.querySelectorAll('a[class*=side-sub-]')).map(subEl => [
                subEl.getAttribute('data-id'),
                subEl.textContent.trim(),
                subEl.getAttribute('href')
            ]);
            // 返回完整的父级+子级数据
            return [
                el.getAttribute('data-id'),
                el.textContent.trim(),
                el.getAttribute('href'),
                el.getAttribute('id'),
                subCategories
            ];
        })
    );

    ppp(categories);

})()
.catch(err => console.error(err))
.finally(() => browser?.close());

错误代码修正说明

之前的错误代码中cat.page.$$eval是错误用法:cat是ElementHandle实例,不存在page属性。如果需要用循环ElementHandle的方式实现,可改为在每个元素的evaluate回调中直接获取子元素:

// 循环ElementHandle的修正写法
let categoryPages = await page.$$('nav[class*=menu-] a[id*=side-nav-]');
let categories = [];

for (let cat of categoryPages) {
    const categoryData = await cat.evaluate(el => {
        const subContainer = el.nextElementSibling;
        const subCategories = Array.from(subContainer.querySelectorAll('a[class*=side-sub-]')).map(subEl => [
            subEl.getAttribute('data-id'),
            subEl.textContent.trim(),
            subEl.getAttribute('href')
        ]);
        return [
            el.getAttribute('data-id'),
            el.textContent.trim(),
            el.getAttribute('href'),
            el.getAttribute('id'),
            subCategories
        ];
    });
    categories.push(categoryData);
}

内容的提问来源于stack exchange,提问作者user21473566

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 11:37:06