Python3网络爬虫教程20——分布式爬虫爬取10000个百度百科词条

本文详细介绍了一种基于Python的分布式爬虫系统设计与实现,使用Scrapy框架爬取百度百科词条,涵盖控制节点与爬虫节点的交互、队列管理、数据存储等关键环节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上接:Python3网络爬虫教程19——分布式爬虫Scrapy实例(爬取多个页面)
https://blog.youkuaiyun.com/u011318077/article/details/86692733

利用分布式爬虫爬取1000、10000个百度百科词条

分布式爬虫基本结构见下图

在这里插入图片描述
在这里插入图片描述

项目结构图如下

控制节点
- 控制器
- URL管理器
- 数据存储器
爬虫节点
- 爬虫程序
- HTML下载器
- HTML解析器
在这里插入图片描述

控制节点

# coding: utf-8
'''
第一步: 创建一个分布式管理器
第二步: 创建URL管理进程、 数据提取进程、数据存储进程
URL管理进程里面有两个队列:url_q和result_q,和爬虫节点中的两个节点(self.task和self.result)是联络在一起的
数据提取进程里面有两个队列:conn_q和store_q,分别用于放置待提交给URL管理进程的数据和数据存储进程的数据
第三步: 启动三个进程和管理器

爬取流程逻辑:
整个控制节点和爬取节点靠的是4个队列进行联络:
    url_q:在爬虫节点SpiderWork中被重命名为self.task,其实本质都是get_task_queue队列
           控制节点中向url_q放入数据,爬虫节点从self.task取出数据,然后用于下载HTML页面和解析页面
           解析出来的数据放进self.result队列,返回给控制节点
    result_q:在爬虫节点SpiderWork中被重命名为self.result,其实本质都是get_result_queue队列
           爬虫节点中向self.result放入数据,控制节点从result_q取出数据,
           取出的数据中的URL交给了conn_q队列,然后又循环给url_q队列,提交给爬虫节点,进行下一轮爬取
           其它数据交给了store_q,然后存储到文件中,一直循环下去,到达限定条件结束爬取。
    store_q:该队列只在控制节点中使用,数据提取进程用于放置待提交给数据存储进程的数据,
            数据来源于result_q,实际是SpiderWork中的self.result队列
    conn_q:该队列只在控制节点中使用,数据提取进程用于放置待提交给URL管理进程的URL,
            数据来源于result_q,实际是SpiderWork中的self.result队列

运行步骤:先启动控制节点NodeManager.py,再启动爬虫节点SpiderWorker.py

'''


from multiprocessing.managers import BaseManager
import time
from multiprocessing import Process, Queue
from ControlNode.DataOutput import DataOutput
from ControlNode.URLManager import UrlManager

class NodeManager(object):

    # 第一步:创建分布式管理器
    def start_Manager(self, url_q, result_q):
        '''
        创建一个分布式管理器
        :param url_q: url地址队列,URL管理进程将URL传递给爬虫节点的通道(爬虫节点:SpiderWork.py)
        :param result_q: 结果队列, 爬虫节点将数据返回给数据提取进程的通道
        :return:
        '''
        # 把创建的两个队列注册在网络上,利用register方法, call参数关联了Queue对象
        # 将Queue对象在网络中暴露,网络中一个任务队列一个结果队列
        # 队列经过BaseManager封装后进行重命名,其实get_task_queue队列就是url_q队列
        BaseManager.register('get_task_queue', callable=lambda: url_q)
        BaseManager.register('get_result_queue', callable=lambda: result_q)
        # 绑定端口8001,设置验证口令‘baike',这个相当于对象的初始化
        manager = BaseManager(address=('127.0.0.1', 8001), authkey='baike'.encode('utf-8'))
        # 返回manager对象
        return manager

    # 第二步:创建URL管理进程、 数据提取进程、数据存储进程
    # URL管理进程(利用URLManager.py)
    def url_manager_proc(self, url_q, conn_q, root_url):
        '''
        URL管理进程
        :param url_q: 任务进程队列,放置即将爬取URL,URL管理进程将URL传递给爬虫节点的通道(爬虫节点:SpiderWork.py)
        :param conn_q: 数据提取进程将新的URL提交给URL管理进程的通道
        :param root_url: 最原始的url,第一个用于爬取的网址
        :return:
        '''
        # Url管理器实例化
        url_manager = UrlManager()
        # 将第一个网址加入到未爬取的URL集合中
        url_manager.add_new_url(root_url)
        while True:
            # 判断是否有新的未爬取的URL
            while(url_manager.has_new_url()):
                # 从未爬取的URL集合中取出新的URL
                new_url = url_manager.get_new_url()
                # 将新的URL放入任务进程队列,放置即将爬取的URL
                url_q.put(new_url)
                # 打印已爬取过URL集合的大小
                print('old_url=', url_manager.old_url_size())
                # 加一个判断条件,当爬取2000个链接后就关闭爬行节点,并保存进度
                if(url_manager.old_url_size() > 30):
                    # 通知爬行节点工作结束,即使未爬取的URL集合中还有URL或者任务队列中还有未爬取的URL
                    url_q.put('end')
                    print("控制节点发起结束通知!")
                    # 关闭管理节点,同时存储set状态
                    url_manager.save_progerss('new_urls.txt', url_manager.new_urls)
                    url_manager.save_progerss('old_urls.txt', url_manager.old_urls)
                    return
            # 将从result_solve_proc获取到的urls添加到URL管理器之间
            try:
                urls =conn_q.get()
                url_manager.add_new_urls(urls)
            except BaseException as e:
                # 延时休息一会儿
                time.sleep(0.1)

    # 数据提取进程
    def result_solve_proc(self, result_q, conn_q, store_q):
        '''
        数据提取进程
        :param result_q: 结果队列, 爬虫节点将数据返回给数据提取进程的通道
        :param conn_q: 数据提取进程将新的URL提交给URL管理进程的通道
        :param store_q: 数据提取进程将获取的数据提交给数据存储进程的通道
        :return:
        '''
        while(True):
            try:
                if not result_q.empty():
                    content = result_q.get(True) # 从result_q就是SpiderWork中的self.result队列,里面放置了网页分析后的数据
                    if content['new_urls']=='end':
                        #结果分析进程接受通知然后结束
                        print('数据提取进程收到结束通知,立刻结束!')
                        store_q.put('end')
                        return
                    conn_q.put(content['new_urls'])#url为set类型
                    store_q.put(content['data'])#解析出来的数据为dict类型,然后放入通道中待存储
                else:
                    time.sleep(0.1)#延时休息
            except BaseException as e:
                time.sleep(0.1)#延时休息

    # 数据存储进程
    def store_proc(self, store_q):
        '''
        数据存储进程
        :param store_q: 数据提取进程将获取的数据提交给数据存储进程的通道,里面放的是HTML解析出来待存储的数据
        :return:
        '''
        # 数据存储实例化
        output = DataOutput()
        while True:
            # 判读队列中是否为空,前面加了not,不是空就返回True
            if not store_q.empty():
                # 取出数据
                data = store_q.get()
                # 如果里面的数据是字符串end,就代表结束存储进程
                if data=='end':
                    print('存储进程收到结束通知,立刻结束!')
                    # 写入HTML的结束标签内容
                    output.output_end(output.filepath)
                    return
                output.store_data(data) # 不是end,就一直向datas列表里面一直添加数据
            else:
                time.sleep(0.1)
        pass


# 进程运行逻辑顺序
if __name__=='__main__':

    #初始化4个队列
    url_q = Queue()
    result_q = Queue()
    store_q = Queue()
    conn_q = Queue()

    #创建分布式管理器
    node = NodeManager()
    manager = node.start_Manager(url_q, result_q)
    #创建URL管理进程、 数据提取进程和数据存储进程
    url_manager_proc = Process(target=node.url_manager_proc, args=(url_q, conn_q, 'http://baike.baidu.com/view/284853.htm',))
    result_solve_proc = Process(target=node.result_solve_proc, args=(result_q, conn_q, store_q,))
    store_proc = Process(target=node.store_proc, args=(store_q,))
    #启动3个进程和分布式管理器
    url_manager_proc.start()
    result_solve_proc.start()
    store_proc.start()
    manager.get_server().serve_forever()

URL管理器

# coding: utf-8

# 新的URL集合来自HTML解析器解析出的URL集合

# URL管理器思路:
# 第一步:判断是否有未爬取的URL,方法定义为has_new_url
# 第二步:添加新的URL到未爬取的URL集合中,方法定义为add_new_url(url)和add_new_urls(urls)
# 第三步:获取一个未爬取新的URL,方法定义为get_new_url()
# 第四步:获取未爬取URL集合的大小,方法为new_url_size(),获取已爬取URL结合的大小,方法为old_url_size()
# 第五步:

# 代码进行优化,对URL集合进行序列化操作,减少内存的消耗
# URL进行MD5处理,可以减少内存消耗
# Python2中交cPickle

import pickle as cPickle
import hashlib

class UrlManager(object):
    def __init__(self):
        self.new_urls = self.load_progress('new_urls.txt') # 未爬取的URL集合
        self.old_urls = self.load_progress('old_urls.txt') # 已爬取过的URL集合

    def has_new_url(self):
        '''判断是否有未爬取的URL'''
        return self.new_url_size() != 0

    def get_new_url(self):
        '''从new_urls集合中,获取一个未爬取的URL,用于爬取'''
        new_url = self.new_urls.pop() # 删除列表中最后一个URL
        m = hashlib.md5() # 创建一个MD5处理的实例
        m.update(new_url.encode('utf-8')) # 对new_url进行MD5处理,python处理后默认是256位,下面只取中间的128位
        self.old_urls.add(m.hexdigest()[8:-8]) # 将上面删除的URL加入到已爬的URL中
        return new_url

    def add_new_url(self, url):
        '''将HTML解析得到的新的URL添加到未爬取的URL集合中, 参数是一个url,来自下面的add_new_urls方法'''
        if url is None:
            return
        m = hashlib.md5()
        m.update(url.encode('utf-8'))
        url_md5 = m.hexdigest()[8:-8]
        if url not in self.new_urls and url_md5 not in self.old_urls:
            self.new_urls.add(url) # 将新的的url进行MD5处理后添加到未爬取的URL集合中

    def add_new_urls(self, urls):
        '''
        将HTML解析得到的新URL集合添加到未爬取的URL集合中,参数是一个URL集合或者列表之类
        注意,页面解析时候,获取的新的URL是一个列表,里面有一个或者多个新URL
        所以先得到新的URL列表,然后调用上面的add_new_url方法,将URL一个一个添加到未爬取的URL集合中
        '''
        if urls is None or len(urls) == 0:
            return
        for url in urls:
            self.add_new_url(url) # 将url传给add_new_url方法,调用add_new_url方法

    def new_url_size(self):
        '''获取未爬取的URL集合的大小,列表的长度,相当于URL的个数'''
        return len(self.new_urls)

    def old_url_size(self):
        '''获取已经爬取过URL集合的大小,列表的长度,相当于URL的个数'''
        return len(self.old_urls)

    def save_progerss(self, path, data):
        '''
        保存进度,将数据序列化写入文件
        :param path: 文件路径,看NodeManager.py,文件路径就是new_urls.txt和old_urls.txt
        :param data: 数据,就是new_urls和old_urls
        :return:
        '''
        with open(path, 'wb') as f:
            cPickle.dump(data, f) # 将data以二进制方式写入到f文件中

    def load_progress(self, path):
        '''
        从本地文件加载进度,读取文件,反序列化读取文件中的内容
        :param path: 文件路径
        :return:
        '''
        print("[+] 从文件加载进度:%s" % path)
        try:
            with open(path, 'rb') as f:
                tmp = cPickle.load(f)
                return tmp
        except:
            print("[!] 无进度文件,创建:%s" % path)
        return set()

数据存储器

# coding: utf-8

# 数据存储器,数据来自HTML解析出的数据
# 将获取到的数据写成HTML文件
# 生成的文件按照当前的时间进行命名,以避免重复,同时对文件进行缓存写入
# codecs一个编码转换模块

import codecs
import time

class DataOutput(object):

    def __init__(self):
        self.filepath = 'baike_%s.html' % (time.strftime("%Y_%m_%d_%H_%M_%S",
                                                         time.localtime()))
        self.output_head(self.filepath) # 调用store_data方法时候,需要初始化,初始化时最先向文件中写入头部信息
        self.datas = [] # 再向文件中写入data
    def store_data(self, data):
        if data is None:
            return
        self.datas.append(data) # data来自SpiderWorker
        if len(self.datas) > 10:
            self.output_html(self.filepath)

    def output_head(self, path):
        '''
        将HTML头部信息写进去写进去
        '''
        fout = codecs.open(path, 'w', encoding='utf-8')
        fout.write('<html>')
        fout.write('<head>')
        # 进行格式设置,不然浏览器打开会出现乱码
        fout.write('<meta http-equiv="content-type" content="text/html; charset=utf-8">')
        fout.write('</head>')
        fout.write('<body>')
        fout.write('<table>')
        fout.close()


    def output_html(self, path):
        '''
        将数据写入到HTML文件中
        :param path: 文件路径,就是代码开始的文件名称,
        :return:
        '''
        fout = codecs.open(path, 'a', encoding='utf-8') # 追加方式写入正文数据
        for data in self.datas:
            fout.write('<tr>')
            fout.write('<td>%s</td>' % data['url'])
            fout.write('<td>%s</td>' % data['title'])
            fout.write('<td>%s</td>' % data['summary'])
            fout.write('</tr>')
            self.datas.remove(data) # 写完了就删除数据
        fout.close()

    def output_end(self, path):
        '''
        写入HTML结尾信息
        :param path: 文件存储的路径
        :return:
        '''
        fout = codecs.open(path, 'a', encoding='utf-8')
        fout.write('</table>')
        fout.write('</body>')
        fout.write('</html>')
        fout.close()

爬虫程序

# coding: utf-8

# 爬虫调度器,管理下载器、解析器

# 首先导入所需的模块
from multiprocessing.managers import BaseManager
from SpiderNode.HtmlDownloader import HtmlDownloader
from SpiderNode.HtmlParser import HtmlParser


class SpiderWork(object):
    def __init__(self):
        #初始化分布式进程中的工作节点的连接工作
        # 实现第一步:使用BaseManager注册获取Queue的方法名称
        BaseManager.register('get_task_queue')
        BaseManager.register('get_result_queue')
        # 实现第二步:连接到服务器:
        server_addr = '127.0.0.1'
        print(('Connect to server %s...' % server_addr))
        # 端口和验证口令注意保持与服务进程设置的完全一致:
        self.m = BaseManager(address=(server_addr, 8001), authkey='baike'.encode('utf-8'))
        # 从网络连接:
        self.m.connect()
        # 实现第三步:获取Queue的对象:
        self.task = self.m.get_task_queue()
        self.result = self.m.get_result_queue()
        #初始化网页下载器和解析器
        self.downloader = HtmlDownloader()
        self.parser = HtmlParser()
        print('init finish')

    def crawl(self):
        while(True):
            try:
                if not self.task.empty():
                    url = self.task.get()

                    if url =='end':
                        print('控制节点通知爬虫节点停止工作...')
                        #接着通知其它节点停止工作
                        self.result.put({'new_urls': 'end','data': 'end'})
                        return
                    print('爬虫节点正在解析:%s' % url.encode('utf-8'))
                    content = self.downloader.download(url)
                    new_urls, data = self.parser.parser(url, content)
                    self.result.put({"new_urls": new_urls,"data": data})
            except EOFError as e:
                print("连接工作节点失败")
                return
            except Exception as e:
                print(e)
                print('Crawl  fali ')
if __name__=="__main__":
    spider = SpiderWork()
    spider.crawl()

HTML下载器

# coding: utf-8

# 网页下载器,下载获得网页的全部内容
# 最终返回网页的内容,提交给HTML解析器,解析出需要的东西

import requests

class HtmlDownloader(object):

    def download(self, url):
        if url is None:
            return None
        # 设置用户代理,伪装成浏览器访问
        user_agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0'
        headers = {'User-Agent': user_agent}
        # 请求获取网址
        r = requests.get(url, headers=headers)
        r.encoding = 'utf-8' # 解决中文乱码的问题
        if r.status_code == 200:
            r.encoding = 'utf-8'
            return r.text
        return None

HTML解析器

# coding: utf-8

# HTML解析器,提取网页中需要的URL和需要的数据
# 网页图片及原始代码,以及网页解析提取参考67_7_1

import re
from urllib.parse import urljoin
from bs4 import BeautifulSoup

class HtmlParser(object):

    def parser(self, page_url, html_cont):
        '''
        用于解析网页内容,抽取URL和数据
        参数page_url:下载下面的URL
        参数html_cont: 下载的网页内容
        return: 返回URL和数据
        '''
        if page_url is None or html_cont is None:
            return
        # 创建一个BS的实例
        soup = BeautifulSoup(html_cont, 'html.parser', from_encoding='utf-8') # 采用html.parser解析
        new_urls = self._get_new_urls(page_url, soup)
        new_data = self._get_new_data(page_url, soup)
        return new_urls, new_data

    def _get_new_urls(self, page_url, soup):
        '''
        获取新的URL集合
        参数page_url:下载页面的URL
        参数soup:上面的BS创建的BS实例
        返回新的URL集合
        '''
        new_urls = set()
        # 提取符合要求的a标记,提取的是a标签全部内容的一个列表
        # 提取词条概要中相关的部分词条,挑取其中几个,选定一个规则
        # 原书代码
        # links = soup.find_all('a',href=re.compile(r'/view/\d+\.htm'))
        # 2017-07-03 更新,原因百度词条的链接形式发生改变
        # r'/item/%E8%9',只匹配词条简介中的部分词条连接
        # links = soup.find_all('a', href=re.compile(r'/item/%E8%9'))
        # .匹配除换行符 \n 之外的任何单字符
        # *匹配前面的子表达式零次或多次
        # r'/item/.*'匹配到词条页面所有相关的词条,因为所有词条连接都是以/item/开头的
        links = soup.find_all('a', href=re.compile(r'/item/.*'))
        for link in links:
            # 提取href的属性,既URL地址
            new_url = link['href']
            # 拼接成完整的网址,目前的页面URL和提取得到的新的URL进行合并
            new_full_url = urljoin(page_url, new_url)
            new_urls.add(new_full_url)
        return new_urls

    def _get_new_data(self, page_url, soup):
        '''
        抽取有效数据
        :param page_url: 下载页面的URL
        :param soup: 上面创建的BS实例
        :return: 返回有效的数据
        '''
        data = {}
        data['url'] = page_url
        title = soup.find('dd', class_='lemmaWgt-lemmaTitle-title').find('h1')
        data['title'] = title.get_text() # 获取标签中的内容
        summary = soup.find('div', class_='lemma-summary')
        data['summary'] = summary.get_text()

        return data
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值