Python基于进/线程池实现大数据量爬虫项目

如今计算机已经进入多核CPU的时代了,使用多线程或多进程能够充分利用CPU多核性能来提高程序的执行效率。

Python多任务的解决方案主要有以下三种:
1.启动多进程,每个进程只有一个线程,通过多进程执行多任务;
2.启动单进程(即多线程),在进程内启动多线程,通过多线程执行多任务;
3.启动多进程,在每个进程内再启动多个线程,同时执行更多的任务;

我们都知道,由于Cpython解释器存在全局GIL锁原因,无论是单核还是多核CPU,任意特定时刻只有一个线程会被Python解释器执行。但是创建一个线程操作系统花费的开销要比创建一个进程少很多。
从这两个方面我们可以得出python多线程和多进程的选择的原则:

多进程:高CPU利用型(计算密集型)
多线程:低CPU利用型(I/O密集型)

计算密集型特点
计算密集型任务的特点是需要进行大量的计算,在整个时间片内始终消耗CPU的资源。由于GIL机制的原因多线程中无法利用多核参与计算,但多线程之间切换的开销时间仍然存在,因此多线程比单一线程需要更多的执行时间。而多进程中有各自独立的GIL锁互不影响,可以充分利用多核参与计算,加快了执行速度。

I/O密集型特点
I/O密集型任务的特点是CPU消耗很少,任务大部分时间都在等待I/O操作的完成(I/O速度远低于CPU和内存速度)

python全局GIL锁
Python代码的执行由Python解释器进行控制。目前Python的解释器有多种,如CPython、PyPy、Jython等,其中CPython为最广泛使用的Python解释器。理论上CPU是多核时支持多个线程同时执行,但在Python设计之初考虑到在Python解释器的主循环中执行Python代码,于是CPython中设计了全局解释器锁GIL(Global Interpreter Lock)机制用于管理解释器的访问,Python线程的执行必须先竞争到GIL权限才能执行。

全局解释器锁GIL机制流程
  a、设置 GIL;
  b、切换到一个线程去运行;
  c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
  d、把线程设置为睡眠状态;
  e、解锁 GIL;
  d、再次重复以上所有步骤。

池的概念:
  创建进程或线程都需要消耗时间,销毁进程/线程也需要消耗时间。即便开启了成千上万的进程/线程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程/线程。
所以就有了池的概念。
  定义一个池子,在里面放上固定数量的进程/线程,有需求来了,就拿一个池中的进程/线程来处理任务,等到处理完毕,并不关闭,而是将进程/线程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程/线程数量不够,任务就要等待之前的进程/线程执行任务完毕归来,拿到空闲进程/线程才能继续执行。
  减少了进程/线程创建/销毁带来的消耗,同时又可以最大化的利用CPU。

进程池/线程池数量的确定
知道了池的概念,那进程/线程开启的数量该怎么确定呢?多少个进程/线程才能最大化利用CPU,并且不会给操作系统带来额外的消耗呢?

进程数:CPU数量 < 进程数 < CPU数量*2
线程数: CPU数量 * 5

实际案例实现:
http://db.pharmcube.com/database/cfda/detail/cfda_cn_instrument/135999
爬取药监局13万条医疗器材名录

方式一:python进程池实现13万+数据的爬取
  进程池的创建很简单,直接用multiprocessing.Pool类
  爬取的数据写入redis数据库中。

###进程池

import requests
from lxml import etree
import redis
from multiprocessing import Pool

redis = redis.Redis()

def get_msg(url,i):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
    }
    ret_html = requests.get(url=url, headers=headers)
    ret_text = ret_html.text

    tree = etree.HTML(ret_text)
    tr_list = tree.xpath("/html/body/div/table/tbody/tr")
    tr_list.remove(tr_list[7])
    dic = {}
    for tr in tr_list:
        name = tr.xpath("./td[1]/text()")[0] if tr.xpath("./td[1]/text()") else "kong"
        value = tr.xpath("./td[2]/text()")[0] if tr.xpath("./td[2]/text()") else "kong"
        dic[name] = value

    redis.hset("equipment","%s"%i,dic)
    print("已完成%s条信息"%i)

if __name__ == '__main__':
    p = Pool(8)

    for i in range(1,136000):
        url = "http://db.pharmcube.com/database/cfda/detail/cfda_cn_instrument/%s"%i
        # 异步运行,根据进程池中有的进程数,每次最多3个子进程在异步执行
        # 返回结果之后,将结果放入列表,归还进程,之后再执行新的任务
        # 需要注意的是,进程池中的三个进程不会同时开启或者同时结束
        # 而是执行完一个就释放一个进程,这个进程就去接收新的任务。 
        res = p.apply_async(get_msg,args=(url,i))

    # 异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,
    # 等待进程池内任务都处理完,然后可以用get收集结果
    # 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
    #调用join之前,先调用close函数,否则会出错。执行完close后不会有
    # 新的进程加入到pool,join函数等待所有子进程结束
    p.close()
    p.join()

方式二:python线程池爬取13w+数据
  线程池的创建有两种方式:
    第一种:multiprocessing.dummy.Pool
    第二种:基于queue队列来创建
  下面这个示例采用第一种方式创建。

import requests
from lxml import etree
import redis
from multiprocessing.dummy import Pool as Threadpool

# 创建redis链接
redis = redis.Redis()

def get_msg(i):
    url = "http://db.pharmcube.com/database/cfda/detail/cfda_cn_instrument/%s" % i
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
    }
    ret_html = requests.get(url=url, headers=headers)
    ret_text = ret_html.text

    tree = etree.HTML(ret_text)
    tr_list = tree.xpath("/html/body/div/table/tbody/tr")
    tr_list.remove(tr_list[7])
    dic = {}
    for tr in tr_list:
        name = tr.xpath("./td[1]/text()")[0] if tr.xpath("./td[1]/text()") else "kong"
        value = tr.xpath("./td[2]/text()")[0] if tr.xpath("./td[2]/text()") else "kong"
        dic[name] = value

    redis.hset("equipment", "%s" % i, dic)
    print("已完成%s条信息" % i)

def main():
    th = Threadpool(20)
    th.map(get_msg, [i for i in range(1, 136000)])

if __name__ == '__main__':
    main()

方式三:python进程中开线程实现爬取数据:

import requests
from lxml import etree
import redis
from multiprocessing.dummy import Pool as Threadpool
from multiprocessing import Pool
import time

# 创建redis链接
redis = redis.Redis()

def get_msg(i):
    url = "http://db.pharmcube.com/database/cfda/detail/cfda_cn_instrument/%s" % i
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
    }
    ret_html = requests.get(url=url, headers=headers)
    ret_text = ret_html.text

    tree = etree.HTML(ret_text)
    tr_list = tree.xpath("/html/body/div/table/tbody/tr")
    tr_list.remove(tr_list[7])
    dic = {}
    for tr in tr_list:
        name = tr.xpath("./td[1]/text()")[0] if tr.xpath("./td[1]/text()") else "kong"
        value = tr.xpath("./td[2]/text()")[0] if tr.xpath("./td[2]/text()") else "kong"
        dic[name] = value

    redis.hset("Hequipment", "%s" % i, dic)
    print("已完成第%s条信息" % i)

def main():
    th = Threadpool(10)
    th.map(get_msg, [i for i in range(1, 136000)])

if __name__ == '__main__':
    start_time = time.time()
    p = Pool(5)
    for i in range(5):
        p.apply_async(main)
    p.close()
    p.join()
    stop_time= time.time()
    print("136000条数据总共用时%s s"%(start_time- stop_time))

附:基于queue创建线程池的另一个例子:(6000+数据)
  http://125.35.6.84:81/xk/
  国家药品监督管理总局

import requests
import threading
import redis
import queue

redis = redis.Redis()

def get_msg(th, i):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) '
                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
    }
    url = "http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsList"

    data = {
        'on': 'true',
        'page': i,
        'pageSize': 15,
        'productName': '',
        'conditionType': 1
    }
    proxies = {
        "http": "122.226.73.12:80",
    }
    req = requests.post(url=url, headers=headers, data=data, proxies=proxies)

    date_dict = req.json()
    print("正在下载第%s页" % i)

    for d in date_dict['list']:
        ID = d["ID"]

        n_data = {
            "id": ID
        }
        ret_url = "http://125.35.6.84:81/xk/itownet/portalAction.do?method=getXkzsById"
        text = requests.post(url=ret_url, data=n_data, headers=headers)
        redis.hset("business", ID, text.text)

    th.add_thread()  # 向队列中添加线程,保证线程的数量

# 定义一个线程类
class ThreadPool(object):
    def __init__(self, max_num):
        # 基于队列来设定线程的总个数
        self.queue = queue.Queue(max_num)
        for i in range(max_num):
            self.queue.put(threading.Thread)

    # 定义方法从队列中得到线程
    @property
    def get_thread(self):
        return self.queue.get()

    # 定义方法线程结束后向队列中添加线程,保证线程总数量
    def add_thread(self):
        self.queue.put(threading.Thread)

if __name__ == '__main__':

    # 创建线程池类,执行类的init方法
    th = ThreadPool(20)

    for i in range(1, 314):
        # 从队列中获得线程,执行操作
        thread = th.get_thread
        cur_th = thread(target=get_msg, args=(th, i))
        cur_th.start()  # 开启线程

以上两个网站的数据爬去都是合法的,放心!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容