代理IP、增量爬虫、分布式爬虫的必备利器—redis

来源:麦叔编程

作者:Kevin。

如果你真正写过爬虫,你一定遇到过这些问题:

爬取数据的时候IP被封或者被限制网页数据库时时刻刻都在更新,不可能每次爬取都爬整站,需要做增量爬取数据量巨大,即使用了scrapy等多线程框架也是杯水车薪

要解决这三种场景,都需要使用某种数据库,而redis是其中最合适的一种。

本文通过几个案例,学习用redis数据库解决以上问题:

使用基于redis的代理池,防止被封号使用redis管理爬取状态,实现增量式爬虫使用redis做分布式爬虫实现巨量数据爬取,著名的分布式爬虫方案scapy-redis也是类似原理

redis可以存储爬取的数据

当爬虫工程师想构建一个ip代理池的时候,redis绝对是首选。

下面我们来看一段代码:

import redis import requests from lxml import etree conn = redis.Redis(host=127.0.0.1, port=6379) headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 Edg/87.0.664.47"} def get_https_proxy(num): https_proxy_url = f"{num}/" resp = requests.get(url=https_proxy_url, headers=headers).text # 使用xpath提取代理ip的字段 tree = etree.HTML(resp) https_ip_list = tree.xpath(/html/body/div/div[1]/div/table//tr/td[1]/text()) # 将爬下来的代理ip以列表(键名为https)元素的形式推入数据库 [conn.lpush(https,ip) for ip in https_ip_list] print(Redis数据库有HTTPS代理IP数量为:,conn.llen(https)) # 获取代理网站1——6页的代理ip for n in range(1,6): get_https_proxy(n)

在redis命令交互端输入:

lrange https 0 -1

就可以看到爬到的代理ip:

取出代理ip也非常简单:

import redis conn = redis.Redis(host=127.0.0.1, port=6379) proxies_ip = conn.rpop(https).decode(utf-8) print(proxies_ip) >> 106.14.247.221:8080

是不是非常简单,学会这招,再也不怕被封IP了。

redis可以辅助实现增量爬虫

当爬虫工程师需要写增量式爬虫的时候,一定会考虑使用redis的set数据类型进行url“去重”,为什么呢?

现在假如有一个需求:

1. 爬取菜市场的历史菜价。

2. 需要每天更新当日的价格。

那么我的爬取思路就是:

1. 爬取每日菜价详情页的url,以set类型存入redis数据库。

2. 爬取redis数据库中所有的url对应菜价数据。

3. 然后第二天或者(第N天),再次爬取每日的菜价详情页的url,以set类型存入redis数据库。如果在存入数据库的时候返回0,则表示数据库中已存在相同的url,则不需要爬取该详情页,如果返回1,则表示数据库中未存在该url,则需要爬取该详情页。

上面看得有点绕,我们来实际操作下:

127.0.0.1:6379> sadd url wwww.baidu.com (integer) 1 127.0.0.1:6379> sadd url wwww.baidu.com (integer) 0

将http://www.baidu.com作为url的成员第一次存入数据库的时候,返回的是(integer) 1;然后我们第二次进行相同的操作时,数据库返回的是(integer) 0,这表示url成员中已经存在了该值。

利用redis这个特性,我们可以很便利地做到url去重的功能。

# 这是我写的某个爬虫项目的运行爬虫的实例方法 def run_spider(self): # 遍历爬取所有详情页的url for url in self.get_link_list(): # 将url以set类型存入redis数据库 j = self.conn.sadd(url,url) # 判断返回值是1或0 if j == 1: # 如果返回值为1,爬取该详情页数据,反之跳过 self.parse_detail(url) # 数据持久化存储 self.work_book.save(./price.xls)

redis可以用作分布式爬虫的调度器

鼎鼎大名的分布式爬虫框架:scrapy-redis一定有听说过吧?

分布式爬虫一听感觉很高大上,其实原理很简单,就是把scrapy的调度器共享到服务器上去,然后各个设备的爬虫从服务器上获取需要爬取数据的url。

我们来温习下scrapy五大组件之一的调度器的作用:

调度器(Scheduler):用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想象成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址。

下面我们分三步模拟部署一个简单的分布式爬虫:

1. 第一步,爬取需要解析数据的url;

import redis import requests from lxml import etree conn = redis.Redis(host=127.0.0.1, port=6379) headers = { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36, } # 获取一部小说所有的章节的url,并以list数据形式存入redis。 def get_catalogue(): response = requests.get( headers=headers) tree = etree.HTML(response.text) catalog_url_list = tree.xpath(//*[@id="chapterlist"]/li/a/@href) return catalog_url_list for i in get_catalogue(): full_link =+ i conn.lpush(catalogue,full_link)

这段代码实现了向redis注入所有需要爬取小说页面的url的功能。

127.0.0.1:6379> lrange catalogue 0 -1 1) "/16/16814/13348772.html" 2) "/16/16814/13348771.html" 3) "/16/16814/13348770.html" 4) "/16/16814/13348769.html" 5) "/16/16814/13348768.html" 6) "/16/16814/13348767.html" 7) "/16/16814/13348766.html" 8) "/16/16814/13348765.html" 9) "/16/16814/13348764.html" 10) "/16/16814/13348763.html" 11) "/16/16814/13348762.html" 12) "/16/16814/13348761.html" 13) "/16/16814/13348760.html" 14) "/16/16814/13348759.html" 15) "/16/16814/13348758.html" 16) "/16/16814/13348757.html" 17) "/16/16814/13348756.html" 18) "/16/16814/13348755.html" 19) "/16/16814/13348754.html" 20) "/16/16814/13348753.html" 21) "/16/16814/13348752.html" 22) "/16/16814/13348751.html" 23) "/16/16814/13348750.html" 24) "/16/16814/13348749.html" 25) "/16/16814/13348748.html" 26) "/16/16814/13348747.html" 27) "/16/16814/13348746.html" 28) "/16/16814/13348745.html" 29) "/16/16814/13348744.html" 30) "/16/16814/13348743.html" 31) "/16/16814/13348742.html" 32) "/16/16814/13348741.html" 33) "/16/16814/13348740.html" 34) "/16/16814/13348739.html" 35) "/16/16814/13348738.html" 36) "/16/16814/13348737.html" 37) "/16/16814/13348736.html" 38) "/16/16814/13348735.html" 39) "/16/16814/13348734.html" 40) "/16/16814/13348733.html" 41) "/16/16814/13348732.html" 42) "/16/16814/13348731.html" 43) "/16/16814/13348730.html" 44) "/16/16814/13348729.html" 45) "/16/16814/13348728.html" 46) "/16/16814/13348727.html" 47) "/16/16814/13348726.html" 48) "/16/16814/13348725.html" 49) "/16/16814/13348724.html" 50) "/16/16814/13348723.html" 51) "/16/16814/13348722.html" 52) "/16/16814/13348721.html" 53) "/16/16814/13348720.html" 54) "/16/16814/13348719.html" 55) "/16/16814/13348718.html" 56) "/16/16814/13348717.html" 57) "/16/16814/13348716.html" 58) "/16/16814/13348715.html" 59) "/16/16814/13348714.html" 60) "/16/16814/13348713.html" 61) "/16/16814/13348712.html" 62) "/16/16814/13348711.html" 63) "/16/16814/13348710.html" 64) "/16/16814/13348709.html" 65) "/16/16814/13348708.html" 66) "/16/16814/13348707.html" 67) "/16/16814/13348706.html" 68) "/16/16814/13348705.html" 69) "/16/16814/13348704.html" 70) "/16/16814/13348703.html" 71) "/16/16814/13348702.html" 72) "/16/16814/13348701.html" 73) "/16/16814/13348700.html" 74) "/16/16814/13348699.html" 75) "/16/16814/13348698.html" 76) "/16/16814/13348697.html" 77) "/16/16814/13348696.html" 78) "/16/16814/13348695.html" 79) "/16/16814/13348694.html" 80) "/16/16814/13348693.html" 81) "/16/16814/13348692.html" 82) "/16/16814/13348691.html" 83) "/16/16814/13348690.html" 84) "/16/16814/13348689.html" 85) "/16/16814/13348688.html" 86) "/16/16814/13348687.html" 87) "/16/16814/13348686.html" 88) "/16/16814/13348685.html" 89) "/16/16814/13348684.html" 90) "/16/16814/13348683.html" 91) "/16/16814/13348682.html" 92) "/16/16814/13348681.html" 93) "/16/16814/13348680.html" 94) "/16/16814/13348679.html" 95) "/16/16814/13348678.html" 96) "/16/16814/13348677.html" 97) "/16/16814/13348676.html" 98) "/16/16814/13348675.html" 99) "/16/16814/13348674.html" 100) "/16/16814/13348673.html" 101) "/16/16814/13348672.html" 102) "/16/16814/13348671.html" 103) "/16/16814/13348670.html" 104) "/16/16814/13348669.html" 105) "/16/16814/13348668.html"

2. 第二步,配置redis的配置文件

linux或者mac:redis.confwindows:redis.windows.conf代开配置文件修改:将bind 127.0.0.1进行删除关闭保护模式:protected-mode yes改为no结合着配置文件开启redis服务redis-server 配置文件启动客户端:redis-cli

将bind 127.0.0.1进行删除

protected-mode yes改为no

3. 第三步,使用局域网内别的设备访问redis服务;

开始爬取,

使用redis的lpop命令,执行后从数据库返回url,数据库中则清除该url。

import redis import requests from lxml import etree conn = redis.Redis(host=192.168.125.101, port=6379) headers = { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36, } def extract_data(per_link): response = requests.get(url=per_link, headers=headers).content tree = etree.HTML(response.decode(gbk)) title = tree.xpath(//*[@id="mains"]/div[1]/h1/text())[0] content = tree.xpath(//*[@id="book_text"]//text())[0] return title, content def save_to_pc(title, content): print(title + "开始下载!") with open(title+.txt,w,encoding=utf-8)as f: f.write(content) print(title + "下载结束!") def run_spider(): print(开始运行爬虫!) link = conn.lpop(catalogue).decode(utf-8) title, content = extract_data(link) save_to_pc(title, content) print(下载结束!) run_spider() >> 开始运行爬虫! 章节目录 第105章 同样是君子开始下载! 章节目录 第105章 同样是君子下载结束! 下载结束!

就这样爬取一个url,redis中少一个url,直到被全部爬取完毕为止。