React/Next.js导出Excel仅含表头无数据问题求助
React/Next.js爬虫导出Excel仅表头无数据问题排查
我想用React/Next.js爬取网站数据并导出成Excel,要求和手动录入的格式一致,但现在导出的Excel只有表头,没有行数据,请问哪里错了?
组件代码
'use client'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import cheerio from 'cheerio'; import Excel from 'exceljs'; import { saveAs } from 'file-saver'; import { Space, Input, InputNumber, Button } from 'antd'; export default function Home({ data }) { const $ = cheerio.load(data ?? '<></>'); const router = useRouter(); const [number, setNumber] = useState(1); const [url, setUrl] = useState(''); const workbook = new Excel.Workbook(); const worksheet = workbook.addWorksheet('World Countries'); const countryName = $(`h3.country-name:nth-child(${number})`).text(); const countryCapital = $(`span.country-capital:nth-child(${number})`).text(); const countryPopulation = $(`span.country-population:nth-child(${number})`).text(); const countryArea = $(`span.country-area:nth-child(${number})`).text(); worksheet.columns = [ { key: 'countryName', header: 'Country Name', width: '30' }, { key: 'countryCapital', header: 'Country Capital', width: '30' }, { key: 'countryPopulation', header: 'Country Population', width: '30' }, { key: 'countryArea', header: 'Country Area', width: '30' }, ]; useEffect(() => { router.push({ pathname: '/', query: url && `url=${decodeURIComponent(url)}` }) }, [url]); const getExcelFile = async () => { worksheet.addRow({ countryName, countryCapital, countryPopulation, countryArea }); const buffer = await workbook.xlsx.writeBuffer(); const fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; const fileExtension = '.xlsx'; const blob = new Blob([buffer], { type: fileType }); saveAs(blob, `world-countries${fileExtension}`); } return ( <main className="flex min-h-screen flex-col items-center justify-between bg-slate-800"> <h1 className='text-center'>Website Scraper</h1> <Space.Compact> <Input placeholder='输入目标网址...' value={url} onChange={(e) => setUrl(e.target.value)} /> <InputNumber value={number} onChange={(number) => setNumber(number)} /> <Button type="primary" htmlType='button' onClick={getExcelFile}>导出Excel</Button> </Space.Compact> </main> ) } export async function getServerSideProps({ query }) { const { url } = query; const response = await fetch(url); const data = await response.text(); return { props: { data }, }; }
输入字段截图

问题原因及解决方案
核心问题
- 数据未实时更新:
countryName等变量在组件初始化时计算,后续number或data变化时不会重新计算,导致导出时使用的是初始空值或过时数据。 - Excel实例复用问题:工作簿和工作表在组件顶层声明,每次组件渲染都会重建,但导出时可能使用的是旧实例,或者多次导出会重复添加数据。
- 路由参数传递错误:
useEffect中拼接query参数的方式不正确,可能导致后端获取的url无效,爬不到数据。 - 选择器可能错误:
nth-child选择器是基于父元素所有子元素的索引,而非同类型元素,容易选不到目标节点。
修复方案
1. 实时获取最新数据
将数据获取逻辑移到getExcelFile函数内,确保每次导出时使用最新的number和data:
const getExcelFile = async () => { // 每次导出时重新查询最新数据,Cheerio的eq从0开始索引 const targetIndex = number - 1; const countryName = $(`h3.country-name`).eq(targetIndex).text(); const countryCapital = $(`span.country-capital`).eq(targetIndex).text(); const countryPopulation = $(`span.country-population`).eq(targetIndex).text(); const countryArea = $(`span.country-area`).eq(targetIndex).text(); // 每次导出创建新的工作簿,避免数据重复或实例冲突 const workbook = new Excel.Workbook(); const worksheet = workbook.addWorksheet('World Countries'); worksheet.columns = [ { key: 'countryName', header: 'Country Name', width: 30 }, { key: 'countryCapital', header: 'Country Capital', width: 30 }, { key: 'countryPopulation', header: 'Country Population', width: 30 }, { key: 'countryArea', header: 'Country Area', width: 30 }, ]; // 仅当数据非空时添加行 if (countryName) { worksheet.addRow({ countryName, countryCapital, countryPopulation, countryArea }); } const buffer = await workbook.xlsx.writeBuffer(); const fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; const fileExtension = '.xlsx'; const blob = new Blob([buffer], { type: fileType }); saveAs(blob, `world-countries${fileExtension}`); }
2. 修复路由参数传递
正确传递query参数,避免拼接字符串导致的编码问题:
useEffect(() => { if (url) { router.push({ pathname: '/', query: { url: encodeURIComponent(url) } }); } }, [url, router]);
同时在getServerSideProps中处理解码:
export async function getServerSideProps({ query }) { const { url } = query; if (!url) return { props: { data: '' } }; try { const decodedUrl = decodeURIComponent(url); const response = await fetch(decodedUrl); const data = await response.text(); return { props: { data } }; } catch (err) { console.error('爬取失败:', err); return { props: { data: '' } }; } }
3. 验证选择器正确性
使用eq()替代nth-child(),确保选中第N个目标元素:
// 原选择器 // $(`h3.country-name:nth-child(${number})`).text() // 替换为 $(h3.country-name).eq(number - 1).text()
如果目标元素的父结构特殊,需要根据实际HTML结构调整选择器,确保能精准匹配到数据节点。
内容的提问来源于stack exchange,提问作者Mo1
相关产品推荐
相关产品推荐

