python-基础分布式爬虫

简单分布式爬虫
简单分布式爬虫接口

本次采用主从模式。主从模式是指由一台主机作为控制节点,负责管理所有运行网络爬虫的主机,爬虫只需要从控制节点那里接受任务,
并把新生成任务提交给控制节点就可以了,这个过程中不必与其他爬虫通信,这种方式实现简单,利于管理。

控制节点

控制节点(ControlNode)主要分为URL管理器,数据存储器和控制调度器。
控制调度器通过三个进程来协调URL管理器和数据存储器的工作:
一个是URL管理进程,负责URL的管理和URL传递给爬虫节点;
一个是数据提取进程,负责读取爬虫节点返回的数据,将返回数据中的URL交给URL管理进程,将标题和摘要等数据交给数据存储进程;
一个是数据存储进程,负责将数据提取进程中提交的数据进行本地存储
在这里插入图片描述

URL管理器

参考基础爬虫的代码,对URL管理器优化一下。
采用set内存去重的方式,如果直接存储大量的URL连接,尤其链接很长的时候,很容易造成内存溢出,所以可以将URL进行MD5处理。字符串经过MD5处理后的信息摘要长度为128位。
不过Python中的MD5算法生成的是256位,取中间的128位即可。同时添加了save_progress 和 load_progress方法,进行序列化操作。

import pickle
import hashlib


class UrlManager(object):
    def __init__(self):
        # 未爬取URL集合 已爬取URl集合。
        self.new_urls = set()
        self.old_urls = set()

    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(str(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(str(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集合中
        :urls: url集合
        :return: u
        """
        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.old_urls)

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

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

数据存储器

数据存储器的内容和基础爬虫一致,生成的文件按照当前时间命名,避免重复

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('<head>')
        fout.write('<meta charset="UTF-8">')
        fout.write('</head>')
        fout.write('<body>')
        fout.write('<table>')
        fout.close()

    def output_html(self, path):
        """
        将数据写入HTML文件中
        :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()

控制调度器

产生并启动URL管理进程,数据提取进程和数据存储进程,同时维护4个队列保持进程间的通信,分别为url_queue, result_queue, conn_q, store_q。
url_q:队列是URL管理进程将URL传递给爬虫节点的通道。
result_q:队列是爬虫节点将数据返回给数据提取进程的通道。
conn_q:队列是数据提取进程将新的URL数据提交给URL管理进程的通道。
store_q:队列是数据提取进程将获取到的数据交给数据存储进程的通道。

import random, time
from URLManager import UrlManager
from DataOutput import DataOutput
from multiprocessing.managers import BaseManager

from multiprocessing import Process, Queue, freeze_support


class NodeManager(object):

    def start_Manager(self, url_q, result_q):
        """
        创建一个分布式管理器
        :param url_q: url队列
        :param result_q: 结果队列
        :return:
        """
        # 把创建的两个队列注册在网络上,利用register方法,callable参数关联了Queue对象
        # 将Queue对象在网络中暴露
        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', 8002), authkey=b'baike')
        # 返回manager对象
        return manager

    def url_manager_proc(self, url_q, conn_q, root_url):
        time.sleep(1)
        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('新的URL发给工作节点:' + new_url)
                print('old_url=', url_manager.old_url_size())
                # 加一个判断条件,当爬取2000个链接后就关闭,并保存进度
                if url_manager.old_url_size() > 100:
                    # 通知爬行节点工作结束
                    url_q.put('end')
                    print('控制节点发起结束通知')
                    # 关闭管理节点,同时存储set状态
                    url_manager.save_progress('new_urls.txt', url_manager.new_urls)
                    url_manager.save_progress('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 as e:
                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')

                    conn_q.put(content['new_urls'])
                    store_q.put(content['data'])
                else:
                    time.sleep(1)
            except BaseException as e:
                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__':
    url = "xxx"
    # 初始化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, url,))
    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()

爬虫节点

HTML下载器,HTML解析器和爬虫调度器
爬虫调度器从控制节点中url_q队列读取URL
爬虫调度器调用HTML下载器,HTML解析器获取网页中新的URL和标题
爬虫调度器将新的URL和标题摘要传入result_q队列交给控制节点。

下载器
import requests


class HtmlDownloader(object):
    def download(self, url):
        if url is None:
            return None
        user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.42"
        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


解析器
import re
from urllib import parse
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标记
        # soupClass = .select('#lemma-summary > #para')
        # links = soup.find_all('a', href=re.compile(r'/item/.+'))
        links = soup.select('.lemma-summary .para > a')
        for link in links:
            # 提取href属性
            new_url = link['href']
            # 凭借成完整的网址
            new_full_url = parse.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中的内容.
        data['summary'] = summary.get_text()
        return data


爬虫调度器

爬虫调度器需要用到分布式进程中工作进程的代码。
爬虫调度器需要先连接上控制节点,然后从url_q队列中获取URL,下载并解析网页,接着将获取的数据交给result_q队列并返回给控制节点。

import random, time
from URLManager import UrlManager
from DataOutput import DataOutput
from HtmlDownloader import HtmlDownloader
from HtmlParser import HtmlParser
from multiprocessing.managers import BaseManager
from multiprocessing import Process, Queue
from 爬虫伪装 import ProxiesIP

# proxies_ip = ProxiesIP.getIp()
#
# print(proxies_ip)


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, 8002), authkey=b'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()

    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)
                    time.sleep(1)
                    self.result.put({'new_urls': new_urls, 'data': data})
            except EOFError as e:
                print('连接工作节点失败')
                return
            except Exception as e:
                print(e)
                print('Crawl fail ')


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值