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、综上:希望大家多多指点。


7 回复

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);

以上就是针对你提到的问题的一些解决方案,希望对你有所帮助。

回到顶部