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

基于Puppeteer与Axios的Google Scholar PDF批量下载文件损坏问题

Google Scholar爬取PDF后部分文件损坏问题

我在Google Scholar中搜索关键词(例如machine learning),通过爬取页面div元素获取PDF链接,尝试下载类似https://arxiv.org/pdf/1611.09347的PDF文件,但部分链接无法正常工作,下载的文件出现损坏,比如多个下载文件里有一部分无法正常打开。

相关代码

const puppeteer = require("puppeteer");
const https = require("https");
const http = require("http");
const fs = require("fs");
const axios = require("axios");
const downloadPath =
  "C:\\Kodlamalar\\JavaScriptCodes\\WebScrapingProject\\public\\PDFFiles\\"; // 暂时没用到,之后会用

async function main() {
  const keyword = "machine learning"; // 可修改关键词
  console.log(keyword);
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  await page.goto("https://scholar.google.com");

  await page.setViewport({ width: 1080, height: 1024 });

  await page.type('input[name="q"]', keyword);

  const submitBtn = await page.$("button[type=submit]");
  await submitBtn.evaluate((btn) => btn.click());
  await page.waitForNavigation();

  async function scrapeData(page) {
    const divs = await page.$$eval(".gs_r.gs_or.gs_scl", (divs) => {
      const results = [];
      // results是div对象集合
      divs.forEach((div) => {
        const hasPdfText = div.textContent.includes("PDF");
        if (hasPdfText) {
          const links = div.querySelectorAll(".gs_ri > h3 > a");
          links.forEach((link) => {
            results.push({
              text: link.textContent.trim(),
              href: link.getAttribute("href"),
            });
          });
          // 爬取引用数的div元素(土耳其语:Alıntılanma sayısı 意为引用次数)
          const citations = div.querySelectorAll(".gs_fl.gs_flb > a");
          citations.forEach((citation) => {
            if (citation.textContent.includes("Alıntılanma sayısı")) {
              if (citation.textContent.trim()) {
                results.push({
                  divCitation: citation.textContent.trim(),
                });
              }
            }
          });
          const pdfLinks = div.querySelectorAll(".gs_or_ggsm > a");
          pdfLinks.forEach((link) => {
            results.push({
              pdfLink: link.getAttribute("href"),
            });
          });
        }
      });
      return results;
    });

    const titles = findTitles(divs);
    const urls = findUrls(divs);
    const citations = findCitations(divs);
    const pdfLinks = findPDFLinks(divs);

    return { titles, urls, citations, pdfLinks };
  }

  // 使用scrapeData函数
  const { titles, urls, citations, pdfLinks } = await scrapeData(page);
  extractCitationNumber(citations);
  console.log(titles);
  console.log(urls);
  console.log(citations);
  console.log(pdfLinks);
  // 从这里开始出现问题
  const promises = pdfLinks.map((url, i) => {
    return downloadPDF(url, `example${i}.pdf`)
      .then(() => ({ status: "fulfilled" }))
      .catch((error) => ({ status: "rejected", reason: error }));
  });
  const allPromise = Promise.all(promises);
  try {
    const values = await allPromise;
    console.log(values);
  } catch (error) {
    console.log(error);
  }

  // 到这里为止,尝试同时下载文件,但部分文件损坏
}
main();
function findTitles(divs) {
  const titles = [];
  divs.forEach(({ text }) => {
    if (text) {
      titles.push(text);
    }
  });
  return titles;
}
// 函数2:获取URL地址
function findUrls(divs) {
  const urls = [];
  divs.forEach(({ href }) => {
    if (href) {
      urls.push(href);
    }
  });
  return urls;
}
function findCitations(divs) {
  const citations = [];
  divs.forEach(({ divCitation }) => {
    if (divCitation) {
      citations.push(divCitation);
    }
  });
  return citations;
}
function findPDFLinks(divs) {
  const pdfLinks = [];
  divs.forEach(({ pdfLink }) => {
    if (pdfLink) {
      pdfLinks.push(pdfLink);
    }
  });
  return pdfLinks;
}
function extractCitationNumber(citations) {
  for (let i = 0; i < citations.length; i++) {
    citations[i] = citations[i].replace(/[^0-9]/g, "");
  }
}

async function downloadPDF(url, destination) {
  try {
    const response = await axios({
      method: 'GET',
      url: url,
      responseType: 'stream'
    });

    const writer = fs.createWriteStream(destination);
    response.data.pipe(writer);

    return new Promise((resolve, reject) => {
      writer.on('finish', resolve);
      writer.on('error', reject);
    });
  } catch (error) {
    throw new Error('下载时出错: ' + error.message);
  }
}

问题原因分析

  1. 并发限流:同时发起大量下载请求,目标服务器可能会限流,返回不完整的响应,导致文件损坏。
  2. 流处理不严谨:当前下载逻辑只监听了文件写入的错误,未监听请求数据流的错误,网络中断时会生成损坏文件。
  3. 链接重定向问题:Google Scholar的部分PDF链接是间接跳转链接,直接下载可能获取到非PDF内容。
  4. 反爬拦截:频繁请求触发反爬机制,服务器返回验证页面或无效内容,保存后显示为损坏文件。

修复方案

1. 限制并发下载数量

改为串行下载或限制并发数,避免被服务器限流:

// 替换原Promise.all部分,改为串行下载
for (let i = 0; i < pdfLinks.length; i++) {
  try {
    await downloadPDF(pdfLinks[i], `example${i}.pdf`);
    console.log(`文件example${i}.pdf下载完成`);
  } catch (error) {
    console.error(`文件example${i}.pdf下载失败:`, error.message);
  }
}

2. 完善下载函数的错误处理

添加请求超时、UA伪装、响应类型校验,以及错误时自动删除损坏文件:

async function downloadPDF(url, destination) {
  try {
    const response = await axios({
      method: 'GET',
      url: url,
      responseType: 'stream',
      timeout: 30000, // 30秒超时
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
      }
    });

    // 校验响应是否为PDF
    if (!response.headers['content-type']?.includes('application/pdf')) {
      throw new Error(`非PDF响应,Content-Type: ${response.headers['content-type']}`);
    }

    const writer = fs.createWriteStream(destination);

    return new Promise((resolve, reject) => {
      // 监听数据流错误
      response.data.on('error', (err) => {
        writer.close();
        fs.unlink(destination, () => {});
        reject(new Error('数据流错误: ' + err.message));
      });

      writer.on('finish', () => {
        writer.close();
        resolve();
      });

      writer.on('error', (err) => {
        writer.close();
        fs.unlink(destination, () => {});
        reject(new Error('文件写入错误: ' + err.message));
      });

      response.data.pipe(writer);
    });
  } catch (error) {
    if (fs.existsSync(destination)) fs.unlinkSync(destination);
    throw new Error('下载时出错: ' + error.message);
  }
}

3. 解析真实PDF链接

用Puppeteer获取跳转后的真实PDF地址:

// 新增解析真实链接的函数
async function getRealPdfUrl(browser, url) {
  try {
    const newPage = await browser.newPage();
    await newPage.goto(url, { waitUntil: 'networkidle2' });
    const realUrl = newPage.url();
    await newPage.close();
    return realUrl;
  } catch (error) {
    console.error('解析真实链接失败:', error.message);
    return url;
  }
}

// 在main函数中调用
const realPdfLinks = await Promise.all(pdfLinks.map(url => getRealPdfUrl(browser, url)));
// 之后使用realPdfLinks进行下载

4. 添加随机延迟规避反爬

在请求间隙添加随机延迟,模拟人工操作:

// 新增延迟函数
async function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 在下载循环中添加延迟
for (let i = 0; i < realPdfLinks.length; i++) {
  try {
    await downloadPDF(realPdfLinks[i], `example${i}.pdf`);
    console.log(`文件example${i}.pdf下载完成`);
    await delay(Math.random() * 2000 + 1000); // 1-3秒随机延迟
  } catch (error) {
    console.error(`文件example${i}.pdf下载失败:`, error.message);
    await delay(Math.random() * 3000 + 2000); // 失败后延迟2-5秒
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 01:09:54