爬虫 数据库中存取内容

使用redis 、 mongodb数据库中 存取 爬取页面的内容


import pickle
import zlib
from enum import Enum, unique
from hashlib import sha1
from random import random
from threading import Thread, current_thread
from time import sleep
from urllib.parse import urlparse

import pymongo
import redis
import requests
from bs4 import BeautifulSoup
from bson import Binary


@unique
class SpiderStatus(Enum):
    """
    定义一个枚举类, 继承了Enum, unique的意思是里面列举的对象的值是不能重复的
    0 代表空闲状态  1 带表处于工作状态
    """
    IDLE = 0
    WORKING = 1


def decode_page(page_bytes, charsets=('utf-8',)):
    """
    解码专用
    :param page_bytes: 要解码的页面
    :param charsets: 解码的方式
    :return: 返回解析后的页面
    """
    # 当所有的解码方式都解不出来时, 返回一个空页面
    page_html = None
    # 尝试 用不同的方式解码 解出来停止循环 程序往下执行 返回解码后页面
    for charset in charsets:
        try:
            page_html = page_bytes.decode(charset)
            break
        except UnicodeDecodeError:
            pass
    return page_html


class Retry(object):
    """
    定义一个包装器 用来装饰爬虫爬取页面 失败时尝试的次数 和 两次之间间隔的时间
    """

    def __init__(self, *, retry_times=3,
                 wait_secs=5, errors=(Exception, )):
        """
        把传入的参数赋给对象的属性,初始化赋值
        :param retry_times: 尝试的次数
        :param wait_secs: 间隔对的时间
        :param errors: 异常种类
        """
        self.retry_times = retry_times
        self.wait_secs = wait_secs
        self.errors = errors

    def __call__(self, fn):
        """
        call的作用是把对象变成函数, 装饰器都是函数, 这里可以用Retry() 里面可以传入参数的用来修改
        尝试的次数和间隔的时间
        :param fn: 所有的装饰器装饰的函数,在包装器中都用fn代替
        :return: 返回包装器
        """

        def wrapper(*args, **kwargs):
            """
            包装器
            :param args:可变参数
            :param kwargs: 多个参数
            :return: 返回函数
            """
            # 尝试几次 成功返回fn失败打印出错误 然后执行下一次循环
            # 如果循环结束了还不能爬取成功 就是 爬虫的fetch行为还没有返回内容
            # 就返回空
            for _ in range(self.retry_times):
                try:
                    return fn(*args, **kwargs)
                except self.errors as e:
                    print(e)
                    sleep((random() + 1) * self.wait_secs)
            return None

        return wrapper


class Spider(object):
    """
    定义一个爬虫类
    """

    def __init__(self):
        """
        爬虫的初始状态为空闲
        """
        self.status = SpiderStatus.IDLE

    # 装饰器里面是可以传入参数的
    @Retry()
    def fetch(self, current_url, *, charsets=('utf-8', ),
              user_agent=None, proxies=None):
        """
        抓取页面
        :param current_url: 当前的url 要进行爬取的url
        :param charsets:编解码的方式
        :param user_agent:用户代理, 需要冒充有名的爬虫才可以爬取, 大多数网站是禁用不知名的爬虫的
        :param proxies:
        :return:返回爬取出来的页面
        """
        # 利用threading模块中的current_thread对象,获取当前进程的名字
        thread_name = current_thread().name
        print(f'[{thread_name}]: {current_url}')
        # 用于代理
        headers = {'user-agent': user_agent} if user_agent else {}
        # 访问请求, 获得响应
        resp = requests.get(current_url,
                            headers=headers, proxies=proxies)
        # resp.content 二进制编码的页面内容  解码
        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: 域名 像www.baidu.com 就是一个域名
        :return: 返回解析出来的东西
        """
        # 解析页面 soup 就是一个完整的页面
        soup = BeautifulSoup(html_page, 'lxml')
        # 选出中带有href属性的a标签
        for a_tag in soup.body.select('a[href]'):
            # urlparse函数解析href的内容
            parser = urlparse(a_tag.attrs['href'])
            # url中的文本协议
            scheme = parser.scheme or 'http'
            # 域名
            netloc = parser.netloc or domain
            if scheme != 'javascript' and netloc == domain:
                # 绝对路径
                path = parser.path
                # ?后面出入的参数
                query = '?' + parser.query if parser.query else ''
                # 拼接url 格式化的新形势 代替了%s
                full_url = f'{scheme}://{netloc}{path}{query}'
                # 如果redis中的visiteds_urls数据库中没有 full_url
                if not redis_client.sismember('visited_urls', full_url):
                    # 就把full_url 放进 数据库 m_sohu_task 中 这就是任务队列
                    redis_client.rpush('m_sohu_task', full_url)

    def extract(self, html_page):
        pass

    def store(self, data_dict):
        pass


class SpiderThread(Thread):
    """
    多线程类
    """

    def __init__(self, name, spider):
        """
        daemon设为守护线程 name=name 是为了在爬虫中通过current_thread对象获取当前线程的名字
        :param name: 线程的名字
        :param spider: 线程要执行的爬虫
        """
        super().__init__(name=name, daemon=True)
        # 传入的参数赋给类的属性
        self.spider = spider

    def run(self):
        while True:
            # 从任务队列中 获取一个url 放的顺序是从右往左 取的时候从左往右取最后一个
            current_url = redis_client.lpop('m_sohu_task')
            # 当任务对列中没有 url 时 即没有任务可以执行时
            while not current_url:
                current_url = redis_client.lpop('m_sohu_task')
            # 标记爬虫的状态为工作状态
            self.spider.status = SpiderStatus.WORKING
            # url采用utf-8解码, 从redis数据库中取出来的数据是二进制的
            current_url = current_url.decode('utf-8')
            # 验证下从任务队列中取出来的url是否已访问过 如果没有添加到访问数据库中
            if not redis_client.sismember('visited_urls', current_url):
                redis_client.sadd('visited_urls', current_url)
                # 调用爬虫 爬取页面
                html_page = self.spider.fetch(current_url)
                # 如果爬取出来的有内容并且内容不为空
                if html_page not in [None, '']:
                    # 生成哈希摘要 使用的是sha1  为了防止每次循环时清除掉上次的 内容, 复制一个新的
                    hasher = hasher_proto.copy()
                    hasher.update(current_url.encode('utf-8'))
                    # 摘要
                    doc_id = hasher.hexdigest()

                    # 如果在mongodb数库中找不到 就把新的数据插入 数据库中 sohu_data_coll是mongo创建的,在连接数据的地方
                    # 定义的
                    if not sohu_data_coll.find_one({'_id': doc_id}):
                        sohu_data_coll.insert_one({
                            '_id': doc_id,
                            'url': current_url,
                            'page': Binary(zlib.compress(pickle.dumps(html_page)))
                        })
                    # 上面定义的是爬虫的抓取页面行为, 抓取到页面之后就要进行页面解析了
                    self.spider.parse(html_page)
            # 解析过页面之后爬虫行为结束了, 一个进程中是循环进行页面爬取和解析的 在下一次循环之前
            # 把爬虫状态标记为 空闲转态
            self.spider.status = SpiderStatus.IDLE


def is_any_alive(spider_threads):
    """
    判断进程中的爬虫状态是否活着
    :param spider_threads:
    :return: 如果有一个活着, 就返回真
    """
    return any([spider_thread.spider.status == SpiderStatus.WORKING
                for spider_thread in spider_threads])

# 连接redis服务器
redis_client = redis.Redis(host='112.74.171.100',
                           port=6379, password='940211')
# 连接mongodb服务器
mongo_client = pymongo.MongoClient(host='112.74.171.100', port=27017)
db = mongo_client.msohu
# 创建一个名为 sohu_data_coll的数据库
sohu_data_coll = db.webpages
# 创建生成摘要的对象
hasher_proto = sha1()


def main():
    """
    上面写的那么欢快,都是铺垫, 只有在这里面调用了才会执行
    :return:
    """
    # 检查下redis数据库中有没有
    if not redis_client.exists('m_sohu_task'):
        redis_client.rpush('m_sohu_task', 'http://m.sohu.com/')
    # 生成10个进程
    spider_threads = [SpiderThread('thread-%d' % i, Spider())
                      for i in range(10)]
    # 启动进程
    for spider_thread in spider_threads:
        spider_thread.start()
    # 当有任务 和 线程活着时 一直循环 什么也不执行就进行下一次循环
    while redis_client.exists('m_sohu_task') or is_any_alive(spider_threads):
        pass

    print('Over!')


if __name__ == '__main__':
    main()

猜你喜欢

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