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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:19:40