基于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); } }
问题原因分析
- 并发限流:同时发起大量下载请求,目标服务器可能会限流,返回不完整的响应,导致文件损坏。
- 流处理不严谨:当前下载逻辑只监听了文件写入的错误,未监听请求数据流的错误,网络中断时会生成损坏文件。
- 链接重定向问题:Google Scholar的部分PDF链接是间接跳转链接,直接下载可能获取到非PDF内容。
- 反爬拦截:频繁请求触发反爬机制,服务器返回验证页面或无效内容,保存后显示为损坏文件。
修复方案
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
相关产品推荐
相关产品推荐

