爬虫多线程

定义多线程类, 爬虫类 爬取 m.sohu.com的内容中的带有href属性的a链接地址


import logging
from enum import unique, Enum
from queue import Queue
from random import random
from threading import current_thread, Thread
from time import sleep
from urllib.parse import urlparse

import requests
from bs4 import BeautifulSoup

visited_urls = set()


@unique
class SpiderStatus(Enum):
    """
    定义一个爬虫的状态类, unique意思是类里面定义的值是不能重复的, 继承Enum类这是一个枚举类
    """
    IDEL = 0
    WORKING = 1


def decode_page(page_bytes, charsets=('utf-8',)):
    """
    定义一个专门用来解码的函数, 传入的charsets默认有多个值
    只写了一个默认值, 元祖里面传入参数,多个值不写完后面加逗号
    :param page_bytes:  page_bytes是传入的未解码的页面,
    :param charsers: 编码种类
    :return: 返回解码后的页面
    """
    page_html = None
    for charset in charsets:
        try:
            # 解码成功 程序才会执行 break
            page_html = page_bytes.decode(charset)
            break
        except UnicodeDecodeError as e:
            pass
            logging.error('Decode:', e)

    return page_html


class Retry(object):
    """
    包装器
    """

    def __init__(self, *, retry_times=3, wait_secs=5, error=(Exception, )):
        """
        给对象定义属性
        :param retry_times: 爬虫当爬取内容为空或者失败时 尝试爬取的次数
        :param wait_secs:   爬取  两次相隔的时间
        :param error:  错误 传入的是个元祖意思是可以传入多个, 不过给了一个默认
        """
        self.retry_times = retry_times
        self.wait_secs = wait_secs
        self.error = error

    def __call__(self, fn):
        """
        call的作用是把包装器的对象变成函数  装饰器都是调用的函数
        :param fn:  无论什么函数 都用fn代表
        :return: 返回包装器
        """

        def wrapper(*args, **kwargs):
            """
            函数尝试的次数 当 成功返回函数自己程序就不往下执行了,如果 没有成功接着循环,
            有异常时进行异常处理 当for循环结束时 还没有返回fn 就返回None

            返回wrapper 是返回给call函数的
            包装器好像就是在操作 fn 函数 传入一些参数用来限制 fn
            """
            for _ in range(self.retry_times):
                try:
                    return fn(*args, **kwargs)
                except self.error as e:
                    logging.error(e)
                    logging.info('[Retry]')
                    sleep((random()+1)* self.wait_secs)
            return None
        return wrapper


class Spider(object):
    """
    爬虫类
    """

    def __init__(self, task_queue):
        """

        :param task_queue: 给爬虫传入任务队列,爬虫是要干活的
        """
        self.status = SpiderStatus.IDEL

    # 这是一个装饰器 里面是可以传入参数的 默认是三次 间隔时间5 可以传入retry_times=5 wait_secs=10 给对象的属性传入参数
    # 不是直接传入值就行了嘛, 为什么还要加属性的名字呢, 是因为 * 后面的为命名参数 意思是 * 号后面的参数 传入值时需要加上参数名字
    @Retry()
    def fetch(self, current_url, *, charsets=('utf-8', ), user_agent=None, proxies=None):
        """
        爬取页面的方法
        :param current_url: url地址
        :param charsets: 采用的编解码方式
        :param user_agent: 用户代理,冒充有名的爬虫爬取网站, 网站一般会禁止不知名的爬虫爬取
        :param proxies:代理
        :return: 返回爬取的页面
        """
        # 打印 current_url  利用current_thread函数获取当前线程的名字
        logging.info('[Fetch]:' + current_url)
        thread_name = current_thread().name

        # 打印出进程的名字 和url f是格式化的意思,代替了 %s %d形式
        print(f'[{thread_name}]:{current_url}')
        # 冒充用户
        headers = {'user-agent': user_agent}\
            if user_agent else {}
        # 获取页面
        resp = requests.get(current_url, headers=headers,
                            proxies=proxies)
        # 解码并返回页面  调用解码函数
        return decode_page(resp.content, charsets) if resp.status_code == 200 else None


    def parse(self, html_page, *, domain='m.sohu.com'):
        """
        解析页面
        :param html_page: 传入要解析的页面
        :param domain: 传入域名
        :return: 返回从页面中提取的url
        """
        # 解析页面 soup是一个完整的html页面
        soup = BeautifulSoup(html_page, 'lxml')
        url_links = []
        # 取出页面中 带有href属性的a标签
        for a_tag in soup.body.select('a[href]'):
            # 使用urlparse函数解析url地址, 该函数是分段解析
            # 要解析的原因是url可能不全
            parser= urlparse(a_tag.attrs['href'])
            # 标题的文本协议 可能url中没有则拿取可能为空  空了把后面的 http 文本协议赋给scheme
            scheme = parser.scheme or 'http'
            # 域名  parser.netloc拿取 域名 www.baidu.com
            netloc = parser.netloc or 'domain'
            # 拿取的url的文本协议中有javascript的, 这样的我不要
            # 想要爬的是sohu 所以先让 netloc等于默认域名souhu的 这是第一次拿
            if scheme != 'javascript' and netloc == domain:
                # 拿取url中的绝对路径部分, 像/user/user/类型
                path = parser.path
                # parser.query 取到的是url的?后面传入的参数
                query = '?' + parser.query if parser.query else ''
                # 格式化url, 用上面取到的分段内容拼接成一个完整的url
                full_url = f'{scheme}://{netloc}{path}{query}'
                # 如果 url没有访问过
                if full_url not in visited_urls:
                    # 就把它放到列表中
                    url_links.append(full_url)
        # 返回解析出来的完整的没有访问过的url
        return url_links

    def extract(self, html_page):
        """
        从页面中摘取内容
        :param html_page: 页面
        :return:
        """
        pass

    def store(self, data_dict):
        """
        存储数据
        :param data_dict:
        :return:
        """
        pass

class SpiderThread(Thread):
    """
    定义一个多线程类  用来启动爬虫 继承了Thread类,自带的
    该类就是一个线程类
    单个单个线程的写法:
        Thread(target=foo, agrs=( , )).start()
        foo是目标函数,线程要启动的函数, args 是给目标函数传入的参数
    这是一个线程类,调用该类时自动调用run函数, run函数中肯定要使用蜘蛛实现爬取页面
    解析页面 把解析出来的东西 做下处理, 一个线程类里面定义的就是一个线程, 一个蜘蛛
    一个蜘蛛类里面定义的就是一个蜘蛛该干的事情, 想要启动多线程 多个蜘蛛 就在main函数中
    通过for循环 实现 , 现在调用线程类就执行run函数,而run函数中,使用蜘蛛爬去和解析了页面
    蜘蛛是传进来的,所以定义一个蜘蛛类, 蜘蛛的行为,是在类中定义好的, 该蜘蛛的某个行为干某件事情
    直接利用蜘蛛点一下就可以了, 不得不赞叹爬虫太伟大了, 教我们一个人就干一件事情,一个人就做好
    自己眼前的事情
    线程类的区别是, 你调用一次等于启一个线程,大家是同时执行的 只不过共用计算机内存, 公用资源而已
    而普通类,你调用一次一次,他们是排队等候的,当上一个程序执行完成,才会执行下一个
    线程太伟大了
    多进程 运作方式,一样, 只不过是不共用资源,启一个自动复制一个资源,占用一份内存
    但是效率更高

    一个类定义的就是一个对象,一个方法,一个对象的属性,一个对象的行为
    """

    def __init__(self, spider, task_queue):
        """
        只是参数而已, 传入时是灵活的
        daemon=True 是把线程设为守护线程, 当主线程挂了,守护线程也挂
        :param spider: 传入爬虫函数
        :param task_queue: 任务队列  给爬虫函数的参数
        """
        super().__init__(daemon=True)
        self.spider = spider
        self.task_queue = task_queue

    def run(self):
        """
        这是个回调函数 前面有 'O+向上的箭头' 代表实回调函数 Thread是有这个函数的 是方法的重写
        当利用这个类创建对象时,会自动调用该函数
        :return:
        """
        # 这是一个死循环, 启动一个线程 时 线程 不死 一直执行 所以应该可以设定一个线程存活的时间
        while True:
            # 从队列中取出url
            current_url = self.task_queue.get()
            # 把url添加到访问过的url的集合中 把他放进访问过的url集合的
            # 原因是 放置其他进程爬取, 10个进程是公用队列的, 不过有
            # 上面的判断,爬取过了,就不会再爬了
            visited_urls.add(current_url)
            # 标记爬虫为工作状态
            self.spider.status = SpiderStatus.WORKING
            # 爬虫 爬取页面  这个spider是上面传入的spider,这只是个参数, 当真正的爬虫传进来时所有的spider参数都会自动换成真正的参数
            # 相当与数学中的 参数 X X可以代表任意数
            html_page = self.spider.fetch(current_url)
            # 如果页面存在 不是没有 或者 获取到的页面是一个空字符串
            if html_page not in [None, '']:
                # 调用爬虫下的 解析函数 解析页面 解析函数解析出来的是url_links
                url_links = self.spider.parse(html_page)
                # 把url放到队列中
                for url_link in url_links:
                    self.task_queue.put(url_link)
            # 爬完一次之后把爬虫状态 标记为空闲状态
            self.spider.status = SpiderStatus.IDEL


def is_any_alive(spider_threads):
    """
    判断进程是否活着
    :param spider_threads: 进程
    :return:有一个为 True 返回True 全都死掉返回false
    """
    return any([spider_thread.spider.status == SpiderStatus.WORKING\
                for spider_thread in spider_threads])


def main():
    """
    主进程,当它停止运行时, 守护线程就卵了
    主进程里主要就是定义类的参数
    :return:
    """
    # Queue队列的意思, 可以往里面存取东西  定义任务队列
    task_queue = Queue()
    # 存东西用put 拿东西用get  先放入sohu的url
    task_queue.put('http://m.sohu.com/')
    # 调用线程对象传入参数,传入爬虫对象 和 任务队列两个参数
    # spider()这是个爬虫对象 创建线程对象 会自动调用回调函数run
    spider_threads = [SpiderThread(Spider(task_queue), task_queue) for _ in range(10)]
    # 取出每一个线程 启动线程 因为这是一个线程类 要是普通类就不需要这一步了
    for spider_thread in spider_threads:
        spider_thread.start()
    # 为了不让程序执行完了之后主线程结束 要不主线程结束 守护线程就挂了 当没有任务了 为false时 while循环停止 任务随着爬虫的执行源源不断的加入队列之中
    # 当 没有时 意味着爬完了 或者当线程全死了 也停止循环  其他pass 意思是什么也不执行过,那就进行第二次循环
    # 这个循环其实就是为了保证 守护线程不死 写的不好, 不过暂时先这么用
    # 什么时候结束了 程序往下执行输入 'Over'
    while not task_queue or is_any_alive(spider_threads):
        pass

    print('Over!')


if __name__ == '__main__':
    main()

猜你喜欢

转载自blog.csdn.net/hello_syt_2018/article/details/80537729