基于Redis的分布式爬虫

项目的目标是爬取电子工业出版社网上书店中图书排行榜栏目下所有分类的图书信息

详情页汇总的作译者、出版时间、页数、ISBN和定价是爬虫程序的目标。

这是一种拥有递进关系的网站,从栏目页到列表页,再到详情页。如果采用对等分布式,那么程序会将栏目页URL、列表页URL和详情页URL都放到待爬队列中,每个爬虫程序的作用都是相同的。如果采用主从分布式,那么主机上的爬虫程序负责将栏目页URL和列表页URL放入待爬队列,而从机上的爬虫程序则负责从待爬队列中取出URL,向其发出请求并从响应内容中抽取数据即可。

对等分布式爬虫的实现

ManySpider项目的目录结构如下:
目录结构
其中的equals代表的是对等分布式爬虫代码。项目所用的Redis是作者在本机启动的Redis服务,实际应用中需要使用多机可访问的Redis服务。details.py文件中的代码,即对等分布式爬虫代码如下:

import requests
import parsel
import redis

from urllib.parse import urljoin

# 建立Redis 链接
r = redis.Redis(host='localhost', port=6379, db=0)
# 提前设立待爬取队列和已爬取队列的名称
wait_key_name = "waits"
down_key_name = "downs"

# 进入栏目页
category = requests.get("https://www.phei.com.cn/module/publishinghouse/moresalseranking.jsp")
category_html = parsel.Selector(category.text)
category_url = category_html.css("div.book_ranking_list_area li a::attr('href')").extract()

# 拼接 URL 并逐条放入待爬队列中(Redis)
for half_url in category_url:
    url = "https://www.phei.com.cn" + half_url
    r.sadd(wait_key_name, url)

while True:
    # 从待爬队列中弹出一条URL
    if not r.spop(wait_key_name):
        pass
    else:
        target = str(r.spop(wait_key_name), encoding="utf8")
        resp = requests.get(target)
        # 将请求过的URL放入已爬队列
        r.sadd(down_key_name, target)
        # 使用 parsel 库解析相应正文
        html = parsel.Selector(resp.text)
        # 判断用于区分列表页和详情页
        if "bookid" not in target:
            # 从列表页中提取详情页的URL
            detail_url = html.css("div.book_ranking_list_area span.book_title a::attr('href')").extract()
            for detail in detail_url:
                # 循环拼接详情页URL,并添加到待爬队列
                d = "https://www.phei.com.cn" + detail
                r.sadd(wait_key_name, d)
        else:
            # 如果请求的详情页,那么直接提取数据
            title = html.css("div.content_book_info h1::text").extract_first()
            author = html.css("div.content_book_info p:nth-child(3)::text").extract_first()
            year = html.css("div.content_book_info p:nth-child(4) span:first-child::text").extract_first()
            number = html.css("div.content_book_info p:nth-child(4) span:nth-child(2)::text").extract_first()
            pages = html.css("div.content_book_info p:nth-child(4) span:nth-child(4)::text").extract_first()
            price = html.css("div.content_book_info p.book_price span::text").extract_first()
            print(title, author, year, number, pages, price)

在运行之前,需要安装用于连接Redis数据库的redis库、用于发出网络请求的Requests库和用于解析网页的Parsel库,使用Python的包管理工具pip进行安装即可。

代码导入了需要用到的3个库,先与Redis建立连接并提前设定待爬队列和已爬队列的名称。然后使用Requests库的get()方法向栏目页的URL发出请求,使用Parsel库中的Selector对象解析网页内容并从中提取出列表页的URL。接着循环刚才提取的列表页,由于URL不完整,所以这里需要将URL拼接完整后逐条放入待爬队列中。放入待爬队列使用的是redis库提供的sadd()方法,对应的是Redis数据库的SADD命令,也就是将URL放到Redis数据库指定的集合中。

使用while True保持程序持续运行,这里用redis库中的spop()方法从Redis数据库指定的集合中随机弹出一条URL。由于SPOP命令返回的是bytes类型的数据,所以这里需要转为str格式。考虑到如果Redis数据库指定的集合中没有数据,这种情况下返回的是None,会导致str()方法抛出异常,于是这里使用if语句来预防这样的异常产生。

如果弹出了URL,那么就使用Requests库的get()方法发出请求,同时将URL放入已爬队列。对于返回的响应正文,依旧使用Parsel库中的Selector对象对其进行解析。

待爬队列中既有列表页URL又有详情页URL,如何让程序区分呢?
观察到详情页的URL中带有bookid关键字,例如《Kubernetes in Action中文版 》一书的URL为:

https://www.phei.com.cn/module/goods/wssd_content.jsp?bookid=53240

而栏目页URL却没有bookid关键字,于是便将其作为区分栏目页和详情页的依据,用if语句进行判断。

如果判断本次请求的是栏目页,那么就从栏目页的列表处提取详情页的URL。同样,在进行URL拼接后将完整的URL添加到Redis数据库指定的集合中。如果判断本次请求的是详情页,那么就从页面中抽取所需要的数据。

URL和Redis数据库关系
代码运行后,终端显示的结果如下:
运行结果

主从分布式爬虫的实现

master-slave代表的是主从分布式爬虫代码,master负责获取栏目页的URL并将其添加到待爬队列,slave负责从待爬队列中取出URL。master.py中的代码如下:

import requests
import parsel
import redis

# 建立Redis 链接
r = redis.Redis(host='localhost', port=6379, db=0)
# 提前设立待爬取队列和已爬取队列的名称
wait_key_name = "waits"
down_key_name = "downs"

# 进入栏目页
category = requests.get("https://www.phei.com.cn/module/publishinghouse/moresalseranking.jsp")
category_html = parsel.Selector(category.text)
category_url = category_html.css("div.book_ranking_list_area li a::attr('href')").extract()

# 拼接 URL 并逐条放入待爬队列中(Redis)
for half_url in category_url:
    url = "https://www.phei.com.cn" + half_url
    r.sadd(wait_key_name, url)

slave.py中的代码如下:

import requests
import parsel
import redis

# 建立Redis 链接
r = redis.Redis(host='localhost', port=6379, db=0)
# 提前设立待爬取队列和已爬取队列的名称
wait_key_name = "waits"
down_key_name = "downs"

while True:
    # 从待爬队列中弹出一条URL
    if not r.spop(wait_key_name):
        pass
    else:
        target = str(r.spop(wait_key_name), encoding="utf8")
        resp = requests.get(target)
        # 将请求过的URL放入已爬队列
        r.sadd(down_key_name, target)
        # 使用 parsel 库解析相应正文
        html = parsel.Selector(resp.text)
        # 判断用于区分列表页和详情页
        if "bookid" not in target:
            # 从列表页中提取详情页的URL
            detail_url = html.css("div.book_ranking_list_area span.book_title a::attr('href')").extract()
            for detail in detail_url:
                # 循环拼接详情页URL,并添加到待爬队列
                d = "https://www.phei.com.cn" + detail
                r.sadd(wait_key_name, d)
        else:
            # 如果请求的详情页,那么直接提取数据
            title = html.css("div.content_book_info h1::text").extract_first()
            author = html.css("div.content_book_info p:nth-child(3)::text").extract_first()
            year = html.css("div.content_book_info p:nth-child(4) span:first-child::text").extract_first()
            number = html.css("div.content_book_info p:nth-child(4) span:nth-child(2)::text").extract_first()
            pages = html.css("div.content_book_info p:nth-child(4) span:nth-child(4)::text").extract_first()
            price = html.css("div.content_book_info p.book_price span::text").extract_first()
            print(title, author, year, number, pages, price)

主从分布式结构下URL和Redis数据库关系:
关系
由于slave中使用了while True语句,slave如果检测不到待爬队列中的URL就会空跑,所以程序启动时master和slave的先后顺序并不重要。
无论是equals结构还是master-slave结构,都可以添加多台计算机,由于使用了Redis集合作为待爬队列和已爬队列,所以我们并不需要担心会有重复爬取的情况出现。

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值