如何在Node.js控制器中结合Promise/async/await使用OAI-PMH采集器?
我来一步步帮你搞定这个问题——先解决那个烦人的未处理Promise拒绝警告,再排查400状态码的根源,最后把你的OAI-PMH采集逻辑顺滑地整合到控制器里。
第一步:给异步逻辑套上错误捕获(解决UnhandledPromiseRejectionWarning)
你用async/await的时候,所有异步操作必须被try/catch包裹,不然一旦Promise拒绝(比如请求失败、数据库报错),就会抛出未处理的警告。之前demo里可能只是打印结果没处理错误,但移到控制器后,HTTP请求上下文需要明确处理错误,否则会直接崩掉。
举个实际的例子,假设你的demo核心采集函数是这样的:
// demo.js里的核心采集逻辑 async function fetchAllOAIArticles() { const oaiClient = createOAIClient('你的OAI仓库地址'); const articles = []; let resumptionToken = null; do { const response = await oaiClient.listRecords({ metadataPrefix: 'oai_dc', resumptionToken: resumptionToken }); // 提取标题 response.records.forEach(record => { articles.push({ title: record.metadata.dc.title[0] }); }); resumptionToken = response.resumptionToken; } while (resumptionToken); return articles; }
移到控制器里时,必须给整个流程加上错误捕获:
// 控制器中的GET接口处理函数 exports.scrapeAndStore = async (req, res) => { try { // 调用采集函数 const articles = await fetchAllOAIArticles(); // 存入MongoDB(记得await数据库操作) await ArticleModel.insertMany(articles); // 返回成功响应 res.status(200).json({ success: true, count: articles.length, message: '文章采集并存储成功' }); } catch (error) { // 捕获所有错误,包括请求失败、数据库错误等 console.error('采集失败:', error); // 根据错误类型返回对应HTTP状态码 if (error.message.includes('Unexpected status code 400')) { res.status(400).json({ success: false, message: 'OAI仓库返回无效请求——检查metadataPrefix或仓库地址是否正确' }); } else { res.status(500).json({ success: false, message: '采集过程中发生服务器内部错误' }); } } };
第二步:排查400状态码的根源
400错误是OAI仓库返回的,说明你的请求参数不符合仓库要求,常见原因有这几个:
metadataPrefix参数不合法:有些仓库只支持特定前缀(比如oai_dc是标准,但部分仓库用自定义前缀),你可以先调用ListMetadataFormats接口确认仓库支持的前缀。- 仓库地址错误:必须是OAI-PMH的专用接口地址(比如
https://example.com/oai-pmh),而不是仓库的主页URL。 - 缺少必填参数:部分仓库要求调用
listRecords时必须指定set(分类集)或from/until(时间范围)参数。
你可以先在demo里加个错误捕获,打印详细请求信息定位问题:
// 修改demo里的请求部分,添加错误日志 try { const response = await oaiClient.listRecords({ metadataPrefix: 'oai_dc', resumptionToken: resumptionToken }); } catch (error) { console.error('请求失败:', error); console.error('请求地址:', error.config?.url); console.error('请求参数:', error.config?.params); }
第三步:确保Promise逻辑的正确性
用async/await时要注意这几个细节:
- 所有返回Promise的函数必须加
await,不然会拿到Promise对象而非实际结果(比如MongoDB的insertMany必须await)。 - 分页循环中的异步操作:处理
resumptionToken分页时,要确保在循环内部用await,不要并行请求(OAI仓库大多限制频率)。 - 模块化拆分:把采集逻辑和控制器分离成单独模块,既方便测试,也让代码更清晰。
第四步:完整的控制器整合示例
把所有部分整合起来,一个可运行的控制器大概是这样的:
const OAIClient = require('oai-pmh-client'); // 假设你用这个OAI客户端包 const ArticleModel = require('../models/Article'); // 你的MongoDB模型 // 初始化OAI客户端 const oaiClient = new OAIClient({ baseUrl: 'https://你的OAI仓库地址/oai-pmh', timeout: 10000 }); // 封装采集逻辑为独立函数 async function fetchAllOAIArticles() { const articles = []; let resumptionToken = null; do { try { const response = await oaiClient.listRecords({ metadataPrefix: 'oai_dc', // 先确认仓库支持这个前缀 resumptionToken: resumptionToken }); // 提取需要的字段 const parsedRecords = response.records.map(record => ({ title: record.metadata?.dc?.title?.[0] || '无标题', identifier: record.header.identifier, date: record.header.datestamp })); articles.push(...parsedRecords); resumptionToken = response.resumptionToken; } catch (error) { console.error('分页采集失败(token:', resumptionToken, '):', error); throw error; // 若想终止采集就抛出,否则可改为continue跳过当前页 } } while (resumptionToken); return articles; } // GET接口控制器 exports.startScraping = async (req, res) => { try { // 可选:添加接口认证,防止滥用 if (!req.query.apiKey || req.query.apiKey !== process.env.SCRAPE_API_KEY) { return res.status(403).json({ success: false, message: '无权限访问' }); } // 执行采集 const articles = await fetchAllOAIArticles(); // 存入MongoDB const result = await ArticleModel.insertMany(articles); // 返回结果 res.status(200).json({ success: true, 采集数量: articles.length, 存储数量: result.length, message: '采集和存储完成' }); } catch (error) { console.error('采集流程失败:', error); // 按错误类型返回对应响应 if (error.message.includes('400')) { res.status(400).json({ success: false, message: 'OAI仓库拒绝请求——检查参数或地址', 详情: error.message }); } else if (error.name === 'MongoError') { res.status(500).json({ success: false, message: 'MongoDB存储失败', 详情: error.message }); } else { res.status(500).json({ success: false, message: '采集过程中发生未知错误', 详情: error.message }); } } };
最后提醒
- 生产环境中不要直接暴露这个接口,最好加API密钥或其他认证方式,防止被滥用。
- 可以给采集请求加小延迟(比如
await new Promise(resolve => setTimeout(resolve, 1000))),避免给OAI仓库造成过大压力。 - 可以添加断点续采逻辑,比如把已处理的
resumptionToken存在数据库,下次采集从断点开始。
内容的提问来源于stack exchange,提问作者Marc Barros
相关产品推荐
相关产品推荐

