NO.34——多线程爬虫实战

以糗事百科为例,参考上一个单进程项目

python多线程的基础知识

Queue是python中的标准库,可以直接import Queue引用,这里要注意,Python2要从Queue引入,Python要从queue引入。;队列是线程间最常用的交换数据的形式

对于资源,加锁是个重要的环节,避免多个线程同时调用一个资源,产生意想不到的后果。因为python原生的list,dict等,都是not thread safe的。而Queue,是线程安全的,因此在满足使用条件下,建议使用队列。

Python Queue模块有三种队列及构造函数:

1、Python Queue模块的FIFO队列先进先出。 class Queue.Queue(maxsize)

2、LIFO类似于堆,即后进先出。 class Queue.LifoQueue(maxsize)

3、还有一种是优先级队列级别越低越先出来。 class Queue.PriorityQueue(maxsize)

Queue(队列对象)

  • 初始化: class Queue.Queue(maxsize) FIFO 先进先出

  • 包中的常用方法:

    Queue.qsize() 返回队列的大小

    Queue.empty() 如果队列为空,返回True,反之False

    Queue.full() 如果队列满了,返回True,反之False

    Queue.full 与 maxsize 大小对应

    Queue.get([block[, timeout]])获取队列,timeout等待时间

      调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。
    
      如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。
    
      如果队列为空且block为False,队列将引发Empty异常。
    
  • 创建一个“队列”对象

    import Queue myqueue = Queue.Queue(maxsize = 10)

  • 将一个值放入队列中

    myqueue.put(10)

  • 将一个值从队列中取出

    myqueue.get()

糗事百科多线程思路

代码实战

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Oct 13 21:33:09 2018

@author: Macx
"""

import requests
from lxml import etree
from queue import Queue
import threading
import json


class thread_crawl(threading.Thread):
    '''
    抓取线程类,注意需要继承线程类Thread
    '''

    def __init__(self, threadID, q):
        #调用Thread的_init_方法,完成对一个线程的初始化创建工作
        threading.Thread.__init__(self)
        self.threadID = threadID
        #页码队列
        self.q = q
    '''
    线程在调用过程中就会调用对应的run方法
    :return:
    '''

    def run(self):
        print("启动线程Starting: " + self.threadID)
        self.qiushi_spider()
        print ("退出线程Exiting: " + self.threadID)

    def qiushi_spider(self):
 
        while True:
            if self.q.empty():   #如果队列为空,则跳出
                break
            else:
                page = self.q.get()
                print("当前工作的线程:"+self.threadID + ",正在采集:" + str(page))
                url = 'http://www.qiushibaike.com/hot/page/' + str(page) + '/'
                headers = {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36',
                    'Accept-Language': 'zh-CN,zh;q=0.8'}
                # 多次尝试失败结束、防止死循环
                timeout = 4
                while timeout > 0:
                    timeout -= 1
                    try:
                        content = requests.get(url, headers=headers)
                        data_queue.put(content.text)  #将采集的结果放入data_queue队列中
                        break
                    except Exception as e:
                        print ("采集线程错误")
                if timeout < 0:
                    print("timeout"+url)


class Thread_Parser(threading.Thread):
    '''
    解析网页的类,就是对采集结果进行解析,也是多线程方式进行解析
    '''

    def __init__(self, threadID, queue, lock, f):
        threading.Thread.__init__(self)
        self.threadID = threadID
        #网页源码队列
        self.queue = queue
        self.lock = lock
        #文档输出
        self.f = f

    def run(self):
        print("starting:" + self.threadID)
        global total, exitFlag_Parser
        while not exitFlag_Parser:
            try:
                '''
                调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。
                如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。
                如果队列为空且block为False,队列将引发Empty异常。
                '''
                item = self.queue.get(False)  #get()参数为False时,队列为空,抛出异常
                if not item:
                    pass
                self.parse_data(item)
                self.queue.task_done()  #每当发生一次get操作,就会提示是否堵塞
                print("Thread_Parser="+self.threadID+",total="+total)
            except:
                pass
        print ("Exiting: "+ self.threadID)

    def parse_data(self, item):
        '''
        解析网页函数
        :param item: 网页内容
        :return:
        '''
        global total
        try:
            html = etree.HTML(item)
            result = html.xpath('//div[contains(@id,"qiushi_tag")]')
            for site in result:
                try:
                    imgUrl = site.xpath('.//img/@src')[0]
                    title = site.xpath('.//h2')[0].text
                    content = site.xpath('.//div[@class="content"]')[0].text.strip()
                    vote = None
                    comments = None
                    try:
                        vote = site.xpath('.//i')[0].text
                        comments = site.xpath('.//i')[1].text
                    except:
                        pass
                    result = {
                        'imgUrl': imgUrl,
                        'title': title,
                        'content': content,
                        'vote': vote,
                        'comments': comments,
                    }

                    with self.lock:
                        #print ("write %s" % json.dumps(result))
                        with open('qiushibaike.json','a',encoding='utf-8') as file:
                            self.f.write(json.dumps(result, ensure_ascii=False) + "\n")

                except Exception as e:
                    print("site in result")
        except Exception as e:
            print("parse_data")
        with self.lock:
            total += 1

data_queue = Queue()   # 存放解析数据的queue
exitFlag_Parser = False
lock = threading.Lock()
total = 0

def main():
    output = open('qiushibaike.json', 'a')  # 将结果保存到一个json文件中

    #初始化网页页码page从1-10个页面
    page_queue = Queue(50)  #任务队列,存放网页的队列
    for page in range(1, 11):
        page_queue.put(page)  #构造任务队列

    #初始化采集线程
    crawlthreads = []
    crawlList = ["crawl-1", "crawl-2", "crawl-3"]  #总共构造三个爬虫线程

    for threadID in crawlList:
        thread = thread_crawl(threadID, page_queue)  #启动爬虫线程
        thread.start()   #启动线程
        crawlthreads.append(thread)

    #初始化解析线程parserList
    parserthreads = []
    parserList = ["parser-1", "parser-2", "parser-3"]
    #分别启动parserList
    for threadID in parserList:
        thread = Thread_Parser(threadID, data_queue, lock, output)
        thread.start()
        parserthreads.append(thread)

    # 第一步,等待页码队列清空,先进行网页的抓取
    while not page_queue.empty():
        pass   #不为空,则继续堵塞,占据该线程

    # 等待所有爬取网页线程完成
    for t in crawlthreads:
        t.join()
    # 第二步,等待网页源码队列清空,对采集的页面队列中的页面进行解析,等待所有页面解析完成
    while not data_queue.empty():
        pass
    # 通知线程是时候退出
    global exitFlag_Parser
    exitFlag_Parser = True
    #等待所有解析网页线程完成
    for t in parserthreads:
        t.join()  # 等待所有线程执行到此处再继续往下执行
    print("Exiting Main Thread")
    with lock:
        output.close()


if __name__ == '__main__':
    main()

猜你喜欢

转载自blog.csdn.net/ghl1390490928/article/details/83044624