简单分布式爬虫的实现(单机)

实现之前要先了解分布式爬虫的结构:
在这里插入图片描述

了解完之后我们可以分为两部分编写代码:
1.控制节点部分
2.爬虫节点部分
可以根据这两部分,创建代码架构如下
在这里插入图片描述

代码如下
一.控制节点部分
1.控制调度器

# coding:utf-8

from multiprocessing.managers import BaseManager
from multiprocessing import Process,Queue
import time

from 爬虫代码.分布式爬虫.控制节点.Url管理器 import UrlManager
from 爬虫代码.分布式爬虫.控制节点.数据存储器 import DataOutput

# 分布式管理器
class NodeManager():

    # def get_url(self,url_q):
    #     return url_q
    #
    # def get_result(self,result_q):
    #     return result_q
    url_q=Queue(10)
    result_q = Queue(10)
    def get_task(self):
        return url_q
    def get_result(self):
        return result_q
    def start_Manager(self,url_q,result_q):
        """
        创建一个分布式管理器
        :param url_q: 队列
        :param result_q: 结果队列
        :return:
        """
        # 把创建的两个队列注册在网络上,利用register方法,callable参数关联了Queue
        # 将Queue对象在网络中暴露
        BaseManager.register('get_task_queue',callable=self.get_task)
        BaseManager.register('get_result_queue',callable=self.get_result)
        # 绑定端口8001,设置验证口令“baike”,这相当于对象的初始化
        manager=BaseManager(address=('127.0.0.1',8789),authkey='baike')
        # 返回manager对象
        return manager


    def url_manager_proc(self,url_q,conn_q,root_url):
        url_manager=UrlManager()
        url_manager.add_new_url(root_url)
        while True:
            while(url_manager.has_new_url()):
                # 从URL管理器获取新的URL
                new_url=url_manager.get_new_url()
                # 将新的URL发给工作节点
                url_q.put(new_url)
                print('old_url=',url_manager.old_url_size())
                # 加一个判断条件,当爬取2000个链接后就关闭,并保存进度
                if(url_manager.old_url_size()>2000):
                    # 通过爬行节点工作结束
                    url_q.put('end')
                    print('爬行节点发起结束通知')
                    # 关闭管理节点,同时存储set状态
                    url_manager.save_process('new_urls.txt',url_manager.new_urls)
                    url_manager.save_process('old_urls.txt', url_manager.old_urls)
                    return
                    # 将从result_solve_proc获取到URL添加到URL管理器
            try:
                if not conn_q.empty():
                    urls=conn_q.get()
                    url_manager.add_new_urls(urls)
            except BaseException :
                time.sleep(0.1)#延时休息

    def result_solve_proc(self,result_q,conn_q,store_q):
        while True:
            try:
                if not result_q.empty():
                    content=result_q.get(True)
                    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:
                time.sleep(0.1)#延时休息

    def store_proc(self,store_q):
        output=DataOutput()
        while True:
            if not store_q.empty():
                data=store_q.get()
                if data=='end':
                    print('存储进程接受通知然后结束')
                    output.output_end(output.filepath)
                    return
                output.store_data(data)
            else:
                time.sleep(0.1)


if __name__=='__main__':
#     初始化4个队列
    url_q=Queue(10)
    result_q=Queue(10)
    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.html',))
    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,))
# 启动三个进程和分布式管理器
    url_manager_proc.start()
    result_solve_proc.start()
    store_proc.start()
    manager.get_server().serve_forever()



2.URL管理器

# coding:utf-8
import _compat_pickle
import hashlib


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

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

    def get_new_url(self):
        """
        获取一个未爬取的URL
        :return:
        """
        new_url=self.new_urls.pop()
        m=hashlib.md5()
        m.update(new_url.encode("utf-8"))
        self.old_urls.add(m.hexdigest()[8:-8])
        return new_url

    def add_new_url(self,url):
        """
        将新的URL添加到未爬取的URL集合中
        :param url: 单个URL
        :return:
        """
        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)

    def add_new_urls(self,urls):
        """
        将新的URL添加到未爬取的URL集合中
        :param urls: url集合
        :return:
        """
        if urls is None or len(urls)==0:
            return
        for url in urls:
            self.add_new_url(url)

    def new_url_size(self):
        """
        获取未爬取的URL集合的大小
        :return:
        """
        return len(self.new_urls)
    def old_url_size(self):
        """
        获取已爬取的URL集合的大小
        :return:
        """
        return len(self.new_urls)

    def save_progress(self,path,data):
        """
        保存进度
        :param path:文件路径
        :param data: 数据
        :return:
        """
        with open(path,'wb') as f:
            _compat_pickle.dump(data,f)

    def load_progress(self,path):
        """
        从本地文件加载进度
        :param path: 文件路径
        :return: 返回set集合
        """
        print('[+]从文件加载进度:%s'%path)
        try:
            with open(path,'rb')as f:
                tmp=_compat_pickle.load(f)
                return tmp
        except:
            print('[1]无进度文件,创建%s'%path)
        return set()

3.数据存储器

# coding:utf-8
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)
        self.datas=[]

    def store_data(self,data):
        if data is None:
            return
        self.datas.append(data)
        if len(self.datas)>10:
            self.output_html(self.filepath)

    def output_head(self,path):
        """
        将HTML头写进去
        :param path:
        :return:
        """
        fout=codecs.open(path,'w',encoding='utf-8')
        fout.write("<html>")
        fout.write("<body>")
        fout.write("<table>")
        fout.close()

    def output_html(self,path):
        """
        将文件写入HTML文件中
        :param path: 文件路径
        :return:
        """
        fount=codecs.open(path,'a',encoding='utf-8')
        for data in self.datas:
            fount.write("<tr>")
            fount.write("<td>%s</td>"%data['url'])
            fount.write("<td>%s</td>"%data['title'])
            fount.write("<td>%s</td>"%data['summary'])
            fount.write("</tr>")
            self.datas.remove(data)
        fount.close()


    def output_end(self,path):
        """
        输出HTML结束
        :param path:文件存储路径
        :return:
        """
        fount=codecs.open(path,'a',encoding='utf-8')
        fount.write("</table>")
        fount.write("</body>")
        fount.write("</html>")
        fount.close()

二.爬虫节点部分
1.爬虫调度器

from multiprocessing.managers import BaseManager
from multiprocessing import Queue

from 爬虫代码.分布式爬虫.爬虫节点.HTML下载器 import HtmlDownloader
from 爬虫代码.分布式爬虫.爬虫节点.HTML解析器 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,8789),authkey='baike')
#         从网络连接
        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:
                print('连接工作节点失败')
                return
            except Exception :
                # print(e)
                print('Crawl fail')


if __name__=='__main__':
    spider=SpiderWork()
    spider.crawl()

2.HTML下载器

#coding:utf-8
import requests


class HtmlDownloader(object):
    def download(self,url):
        if url is None:
            return None
        user_agent="Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36"
        headers={
            'User-Agent':user_agent
        }
        r=requests.get(url,headers=headers)
        if r.status_code==200:
            r.encoding='utf-8'
            return r.text
        return None

3.HTML解析器

#coding:utf-8
import re
from urllib.parse import urljoin
from bs4 import BeautifulSoup


class HtmlParser(object):
    def parser(self,page_url,html_cont):
        """
        用于解析网页内容,抽取URL和数据
        :param page_url: 下载网页的URL
        :param html_cont: 下载的页面内容
        :return: 返回URL和数据
        """
        if page_url is None or html_cont is None:
            return
        soup=BeautifulSoup(html_cont,"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集合
        :param page_url: 下载页面的URL
        :param soup: soup
        :return: 返回新的URL集合
        """
        new_urls=set()
        # 抽取符合要求的a标记
        links=soup.find_all('a',href=re.compile(r'^/item/[\%\w{2}]+/\d+'))
        for link in links:
            # 提取href属性
            new_url=link['href']
#             拼接成完整网址
            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: soup
        :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")
#         获取tag中包含的所有文本内容,包括子孙tag中的内容,并将结果作为Unicode字符串返回
        data['summary']=summary.get_text()
        return data

出现的bug,及解决办法如下:

bug1
selenium.common.exceptions.WebDriverException: Message: Service C:\用户\Justin\AppData\Local\Google\Chrome\Application\chromedriver.exe unexpectedly exited. Status code was: 1

解决方案:
更换chromedriver.exe的位置并相应的更换executable_path的值

bug2
File “C:\Users\Justin\AppData\Local\Programs\Python\Python37-32\lib\multiprocessing\managers.py”, line 505, in init
self._authkey = process.AuthenticationString(authkey)
TypeError: string argument without an encoding

解决方案
修改
self._authkey = process.AuthenticationString(authkey)

if isinstance(authkey,str):
self._authkey = process.AuthenticationString(authkey,‘utf-8’)
else:
self._authkey = process.AuthenticationString(authkey)

bug3
File “E:\pythoncode\爬虫代码\分布式爬虫\Url管理器.py”, line 25, in get_new_url
m.update(new_url)
TypeError: Unicode-objects must be encoded before hashing

解决方案:
m.update(new_url)
改为
m.update(new_url.encode(“utf-8”))

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值