Nodejs爬虫程序遇到的些许问题,特来讨教
Nodejs爬虫程序遇到的些许问题,特来讨教
刚刚写了一个爬虫,尝试读取豆瓣电影的评价及相关信息,并下载电影的一个海报图片,因为我自己的电脑是双核,所以,为了提高效率,利用cluster开了两个进程来跑,效率还可以,其中遇到了几个问题:
1、是关于链接地址的问题,因为之前没有写过类似的程序,所以没思路,当时我有两种想法(包括现在).第一种就是固定住一个链接地址,然后不听的变换页面的id,比如:http://movie.douban.com/subject/10485647/
每次只要替换掉id就可以了,但是这样的话,貌似效率不高,所以我舍弃了这种方法。第二种方法就是以一个固定的链接地址作为程序的入口,在读取完该页面的信息之后,再查找这个页面的所有的链接,找到有跟当前页面一样的链接,然后存入一个数组,接着循环爬这些地址页面,这样,避免了猜测链接地址和不必要的请求,但是带来的问题是因为每个页面的链接数不定,所以每爬到一个页面,盛放链接地址的数组就会增加很多,最终导致这个数组无限大,而内存又有点不够用了,所以这里不知道大家有没有什么好的想法。
2、关于访问受限的问题,因为知道很多网站为了防止爬虫爬取,如果发先在短时间内有大量的来自于同一ip的请求的话,这时该网站一般都是禁止ip访问。我这里的做法是每次请求都去换useragent,来达到尽量模仿浏览器行为的目的。其次是每次请求的时候都去伪造ip,这个伪造ip的方法就是在request的header里面加入x-forward-for属性,其实这么做完全是寄希望于对方的网站在做ip检查时的bug。其实如果还是担心ip被禁止的话,还可以限制爬取的频率,即多长时间发送一次请求,但这又在一定程度上影响了效率。在这里也想问问大家,在这个问题上都是怎么处理的。
3、遇到的地三个问题,可能是因为我没有做请求频率的限制,即同一时间发送了很多的请求出去(如前所说,将所有链接存入数组,然后循环这个数组发送请求。但是却没有发现豆瓣封掉我的ip).加上每个请求都会下载该页面的电影的一张海报下来,所以必然导致了我的I/O开销的增大。以至于后来我发现程序报错:Error: connect EMFILE,这明显是因为系统文件最大连接数限制造成的。所以按照网上的说法,是改掉这个值。但是我觉得这不能从根本上解决问题,所以,这里也想问问大家,这个问题应该如何解决。
4、综上:希望大家多多指点。
Node.js 爬虫程序遇到的些许问题,特来讨教
1. 链接地址的问题
在爬虫程序中,处理链接地址的方式直接影响到爬虫的效率和稳定性。我尝试了两种方法:
方法一:固定链接地址,变换页面ID
const id = 10485647;
let url = `http://movie.douban.com/subject/${id}/`;
这种方法虽然简单,但效率较低,因为需要频繁请求不同的页面。
方法二:动态获取链接
const axios = require('axios');
const cheerio = require('cheerio');
async function fetchLinks(url) {
const response = await axios.get(url);
const $ = cheerio.load(response.data);
let links = [];
$('a').each((index, element) => {
const link = $(element).attr('href');
if (link && link.includes('/subject/')) {
links.push(link);
}
});
return links;
}
fetchLinks('http://movie.douban.com/subject/10485647/')
.then(links => console.log(links))
.catch(err => console.error(err));
改进方案:
- 使用队列来管理待爬取的链接,而不是直接将所有链接存入数组。
- 每次只处理一定数量的链接,避免内存溢出。
const queue = require('queue');
const q = queue();
q.concurrency = 5; // 控制并发量
function processLink(link) {
return fetchLinks(link)
.then(sublinks => sublinks.forEach(q.push))
.catch(err => console.error(err));
}
q.push('http://movie.douban.com/subject/10485647/');
q.start();
2. 访问受限的问题
为了避免被网站封禁IP,可以采取以下措施:
- 更换User-Agent
axios({
url,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}
});
- 伪造IP
axios({
url,
headers: {
'X-Forwarded-For': '123.45.67.89'
}
});
- 限制请求频率
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function fetchPageWithDelay(url) {
await delay(1000); // 每秒请求一次
return axios.get(url);
}
3. I/O 开销过大
由于频繁的I/O操作导致系统文件最大连接数超限,可以采取以下措施:
- 使用流式处理
const fs = require('fs');
const axios = require('axios');
axios.get(url, { responseType: 'stream' })
.then(response => {
const writer = fs.createWriteStream(`poster-${id}.jpg`);
response.data.pipe(writer);
})
.catch(err => console.error(err));
- 限制并发请求数
q.concurrency = 5; // 控制并发量
4. 综上所述
通过以上方法,可以有效解决爬虫程序中遇到的问题。希望各位高手多多指教!
希望这些解决方案对你有所帮助!如果有任何问题或需要进一步讨论,请随时留言。
就一句,不要同时发送很多请求,最好是使用async来控制请求一个一个的处理,不要一股脑的全部发出去,
我也做过一个爬虫,https://github.com/dlutwuwei/CrawlerX ,可以设置并发请求数和爬网页的深度,首先没必要用cluster,nodejs,io是并发的,一个进程足够,处理返回的页面。网站防抓取都是通过IP地址,你也伪造不了,因为你改了源地址,ip包就返不回来了。
更改IP可以用代理的。正常的过程是做一个agent pool 然后随机选用代理来发送请求。
当然如果有条件和网络能用Tor 那就更好了。 随机匿名P2P代理,不过天朝各种被封。
我记得scrapy建议2秒左右的抓取间隔。user-agent一般也都够用了,抓数据不用太效率吧,如果不是像知道创宇那种做网页安全性扫描需要对速度有要求的话,慢点抓,数据总会来的。何况知道创宇为了提高速度也是用代理池的方式来实现的,一方面保证速度,一方面不被封掉,那只能一直变换着ip来搞。 做的再深点的话,可以再写一个爬虫用来搜集代理,然后测试代理的可用性,然后动态筛选和维护代理池。
你可以用redis,抓取页面后分析的url放置到redis中,以集合的方式来存储在A中。
不想抓重复页面的话,redis中对抓过的url以集合的形式在B中做个记录就行了。每次抓到的url要存到redis中之前先查询一下B中是否有该url,如果没有的话则存入到A中。
每次从A中pop一个数据进行页面抓取
总之,如果仅仅为了抓豆瓣的一些影评神马的,你说的这些问题都不是大问题。
针对你的问题,我会提供一些解决方案和示例代码,希望能帮到你:
1. 链接地址的问题
可以使用一种更高效的方式来遍历链接,而不是将所有链接一次性存入数组。你可以使用队列数据结构来管理待抓取的URL。每次从队列中取出一个URL进行抓取,抓取完成后,将新发现的URL添加到队列尾部。这样可以有效避免内存溢出的问题。
示例代码:
const cluster = require('cluster');
const url = require('url');
const request = require('request');
if (cluster.isMaster) {
const worker = cluster.fork();
} else {
let queue = ['http://movie.douban.com/subject/10485647/'];
let visited = new Set();
function processQueue() {
if (queue.length === 0) return;
let currentUrl = queue.shift();
if (!visited.has(currentUrl)) {
visited.add(currentUrl);
request(currentUrl, (err, res, body) => {
if (!err && res.statusCode === 200) {
// 解析页面中的链接并添加到队列
let links = parseLinks(body);
queue.push(...links.filter(link => !visited.has(link)));
}
processQueue(); // 处理下一个URL
});
}
}
function parseLinks(html) {
// 使用DOM解析器(如cheerio)来提取链接
// 这里假设你已经安装了cheerio
const $ = cheerio.load(html);
return $('a').map((i, el) => $(el).attr('href')).get().filter(Boolean);
}
processQueue();
}
2. 访问受限的问题
对于频繁访问被限制的问题,可以采取以下措施:
- IP轮换:使用代理服务器来改变请求的IP。
- 用户代理(User-Agent)轮换:模拟不同的浏览器或设备。
- 频率限制:限制每秒发送的请求次数。
示例代码:
function getRandomUserAgent() {
return [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
// 添加更多用户代理
][Math.floor(Math.random() * 2)];
}
request({
url: targetUrl,
headers: {
'User-Agent': getRandomUserAgent(),
'X-Forwarded-For': generateRandomIp()
},
timeout: 10000 // 设置超时时间
}, callback);
3. 文件描述符过多的问题
当发送大量请求时,可能会耗尽文件描述符。可以通过设置eventEmitters.maxListeners
和限制并发请求数量来解决这个问题。
示例代码:
require('events').EventEmitter.defaultMaxListeners = 15;
const MAX_CONCURRENT_REQUESTS = 10;
let activeRequests = 0;
function makeRequest(url) {
if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
setTimeout(() => makeRequest(url), 100); // 等待其他请求完成
return;
}
activeRequests++;
request(url, (err, res, body) => {
activeRequests--;
// 处理响应
});
}
makeRequest(targetUrl);
以上就是针对你提到的问题的一些解决方案,希望对你有所帮助。