多进程面向对象爬虫程序

爬虫基本步骤:

1.定义全局变量
redis_client = redis.Redis(host='*******',
                           port=***, password='*****') # 链接redis数据库,存放任务队列和已完成任务
mongo_client = pymongo.MongoClient(host='*****', port=27017) # 连接mongdb数据库
db = mongo_client.msohu 
sohu_data_coll = db.webpages 
hasher_proto = sha1() # 引入哈希摘要
2.定义一个常量来判断爬虫是否在工作
@unique # 装饰器,限定惟一值
class SpiderStatus(Enum):
    '''
    枚举,定义常量,作用是用包装器定义状态是唯一的
    '''
    IDLE = 0
    WORKING = 1
这里 0表示爬虫没有工作了
3.写一个通用的解码方法,注意不是每个网页都是utf8编码格式,所以这里传参要传一个元组
def decode_page(page_bytes, charsets=('utf-8',)):
    page_html = None
    for charset in charsets:
        try:
            page_html = page_bytes.decode(charset)
            break
        except UnicodeDecodeError:
            pass
    return page_html
4.定义一个爬虫类,并写他的行为和属性
class Spider(object):

    def __init__(self):
        self.status = SpiderStatus.IDLE  # 定义初识状态

    @Retry() # 这里用到类装饰器,当爬虫获取数据失败时有可能是网络故障,所以多给几次尝试的机会,但是又不想更改爬虫自身的属性,所以我定义一个装饰器来装饰爬虫的行为
    def fetch(self, current_url, *, charsets=('utf-8', ),
              user_agent=None, proxies=None): # 定义爬虫抓取当前网页的方法
        thread_name = current_thread().name # 拿到当前线程的名字,current_url是thread里面的一个方法,.name就是获取name
        print(f'[{thread_name}]: {current_url}') # f'{}'是python新版的粘结字符串的方法,和以前的 '%S%S' % (a,b)一个意思

        headers = {'user-agent': user_agent} if user_agent else {}# 伪装成别的被承认的spider,比如Baiduspider
        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'):  
    # domain表示需要传入的网址参数,更好的是将这个参数提出来,降低耦合
        soup = BeautifulSoup(html_page, 'lxml') 
        # BeatifulSoup解析页面,并按照指定的格式输出,lxml是我们目前很好的选择
        for a_tag in soup.body.select('a[href]'): # 拿到所有有href的a标签
            parse = urlparse(a_tag.attrs['href']) # urlparse可以将拿到的url分段
            netloc = parse.netloc or domain    # 拿到关键域名的分段,这里就是 m.sohu.com
            scheme = parse.scheme or 'http'  # 拿出url的协议的内容
            if scheme != 'javascript' and netloc == domain: # 因为之前拿出来有的参数带有javascript,所以在这里将包含这个的数据去掉
                path = parse.path  # 拿出url的的路径
                query = '?' + parse.query if parse.query else ''  
                # 拿出url的的传入的参数
                full_url = f'{scheme}://{netloc}{path}{query}' # 完成网页的拼接,那么为什么要这样做呢,因为这个网站的有的网页是隐藏了协议的,有的网页传入了参数等等,总之要拿到含有netloc的网址
                if not redis_client.sismember('visited_urls', full_url): 
                # 我们将爬取的任务队列放在非关系型数据库redis中,因为我们在爬取数据时如果需要对进程中断,那就下次就要重新爬取已经获取的数据,把任务队列放在redis中,下次操作时如果redis里面保存有将要爬取的数据的信息就不会再次爬取。这里判断如果拼接的网址不在已经爬取的数据库内才进行爬取。
                    redis_client.rpush('m_sohu_task', full_url) 
                    # rpush是redis中添加数据的一种方法,redis采用先进先出的数据结构,从右边进就要从左边拿,rpush对应lpop,lpush对应rpop
5.定义完爬虫之后还要定义爬虫类里面的retry类装饰器

有关装饰器在另一篇有详细介绍

class Retry(object):

    def __init__(self, *, retry_times=3,
                 wait_secs=5, errors=(Exception, )):
        self.retry_times = retry_times
        self.wait_secs = wait_secs
        self.errors = errors

    def __call__(self, fn): # 类装饰器的魔法方法,调用这个类装饰器其实就是调用这个方法

        def wrapper(*args, **kwargs):
            for _ in range(self.retry_times):
                try:
                    return fn(*args, **kwargs)
                except self.errors as e:
                    logging.info('[Retry]')
                    sleep((random() + 1) * self.wait_secs + 1)
                    # 这里如果失败就停歇一段时间再次调用
            return None

        return wrapper # 返回的也是一个函数
6.定义多线程

class SpiderThread(Thread):
    """
    定义线程类
    """
    def __init__(self, name, spider):
        super().__init__(name=name, daemon=True)  # deamon = True将程序设为守护线程,主程序结束后也跟着结束,name是线程名,也可以不写,这里是为了方便查看程序开始时是否启用了多线程
        self.spider = spider

    def run(self):
        while True:
            current_url = redis_client.lpop('m_sohu_task')  
            # 拿到第一个,锁机制(一次只能有一个操作),用put在最后加一个
            while not current_url: 
                current_url = redis_client.lpop('m_sohu_task')
                # 这里这样写是确保必须拿到'm_sohu_task'里面的第一个元素,否则程序就不往下执行
            self.spider.status = SpiderStatus.WORKING # 程序启起来后爬虫的状态改变
            current_url = current_url.decode('utf-8') # 解码
            if not redis_client.sismember('visited_urls', current_url):
            # 这个语句是判断当前网页不在已经访问过的库中
                redis_client.sadd('visited_urls', current_url) # 如果当前网页不在已经访问的库中就加进去,这里的sadd是redis语句
                html_page = self.spider.fetch(current_url) # 抓取页面
                if html_page not in [None, '']: # 如果页面不为空
                    hasher = hasher_proto.copy() 
                    # 这里是引入生成哈希摘要,为什么不在这里直接生成呢?因为复制比生成快得多,节约计算机性能
                    hasher.update(current_url.encode('utf-8'))
                    # 传入urlbong生成哈希摘要
                    doc_id = hasher.hexdigest() 
                    # 生成一个doc_id的摘要数据
                    if not sohu_data_coll.find_one({'_id': doc_id}):
                    # 如果刚才生成的数据不在MongoDB数据库中那么就进行插入数据行为
                        sohu_data_coll.insert_one({
                            '_id': doc_id,
                            'url': current_url,
                            'page': Binary(zlib.compress(pickle.dumps(html_page))) # 这个操作时对网页的数据进行压缩
                        })
                    self.spider.parse(html_page) 
                    # 如果页面不在redis中保存的库中就继续执行解析操作,因为这是一个while Ttue循环
                self.spider.status = SpiderStatus.IDLE 
                # 更改爬虫状态
7.判断状态
def is_any_alive(spider_threads):
    return any([spider_thread.spider.status == SpiderStatus.WORKING
                for spider_thread in spider_threads])


visited_urls = set()
8.执行主程序

def main():
    if not redis_client.exists('m_sohu_task'):
        redis_client.rpush('m_sohu_task', 'http://m.sohu.com/') # 如果任务队列里面没有目标网址就将其加入到任务队列中
    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/qq_41768400/article/details/80564111