一, 背景
对于日常 Python 爬虫由于效率问题, 本次测试使用多线程和 Scrapy 框架来实现抓取斗图啦 http://www.doutula.com/ 表情.
由于 IO 操作不使用 CPU, 对于 IO 密集 (磁盘 IO / 网络 IO / 人机交互 IO) 型适合用多线程, 对于计算密集型: 建议用多进程.
进程:
优点: 充分利用多核 CPU(能够同时进行多个操作)
缺点: 系统资源消耗大, 重新开辟内存空间
线程:
优点: 共享内存, IO 操作可以创造出并发操作
缺点: 抢占资源, 请求上下文切换消耗时间
但是对于 python 这种解释性语言带有 GIL(全局解释器锁)解释器锁, 同一时刻只能有一个线程在运行, 遇到 IO 操作才会释放切换. 感觉没必要多线程, 但是经测试, 多线程还是在很大程度能够提升效率.
二, 代码
git 地址 https://github.com/redhatxl/my-python-code/tree/master/doutulai
2.1 多线程爬图
定义了 10 个线程去爬去每个页面的具体表情的 url 存放在类中的 img_url_list 内, 然后通过 10 个线程从这个列表内取 url 进行本地图片下载.
核心代码
- # 定义全局页面 url 列表
- page_url_list = []
- # 定义具体各表情图片 url 列表
- img_url_list = []
- # 定义 rlock 进程锁
- rlock = threading.RLock()
- def __init__(self,page_number=10,img_dir='imgdir',thread_number=5):
- """
- :param page_number: 抓去多少个页面, 默认 10
- :param img_dir: 定义图片目录
- :param thread_number: 默认 5 个线程
- """self.spider_url ='https://www.doutula.com/photo/list/?page='
- self.page_number = int(page_number)
- self.img_dir = img_dir
- self.thread_num = thread_number
- def __add_urllist(self):
- """
- 定义从 page_url_list 爬取具体的 image 的 url
- :return:
- """
- while True:
- DutuSpider.rlock.acquire()
- if len(DutuSpider.page_url_list) == 0:
- DutuSpider.rlock.release()
- break
- else:
- page_url = DutuSpider.page_url_list.pop()
- DutuSpider.rlock.release()
- response = requests.get(page_url, headers=self.__set_header())
- soup = BeautifulSoup(response.content,'lxml')
- sou_list = soup.find_all('img',attrs={'class':'img-responsive lazy image_dta'})
- # 将获取到的具体表情图标的 url 保存添加进 img_url_list 列表
- for url_content in sou_list:
- DutuSpider.rlock.acquire()
- DutuSpider.img_url_list.append(url_content['data-original'])
- DutuSpider.rlock.release()
- def __download_img(self):
- """
- 从 image_url_list 中来下载 image 到本地
- :return:
- """
- while True:
- DutuSpider.rlock.acquire()
- if len(DutuSpider.img_url_list) == 0:
- DutuSpider.rlock.release()
- continue
- else:
- img_url = DutuSpider.img_url_list.pop()
- DutuSpider.rlock.release()
- try:
- # 图片名称
- img_name = img_url.split('/')[-1]
- # 下载图片
- urllib.urlretrieve(img_url,os.path.join(self.img_dir,img_name))
- print('donload img %s' % img_name)
- except Exception as e:
- pass
- def run(self):
- # 启动 thread_num 个进程来爬去具体的 img url 链接
- for th in range(self.thread_num):
- add_pic_t = threading.Thread(target=self.__add_urllist)
- add_pic_t.start()
- # 启动 thread_num 个来下载图片
- for img_th in range(self.thread_num):
- download_t = threading.Thread(target=self.__download_img)
- download_t.start()
- 2.2 Scrapy 框架爬图
- 利用 Scrapy 框架来爬取表情, items 定义图片名称和每个图片的 url,scrapy 主文件来爬取每个图片的 url 来返回, piplines 来进行本地文件存储.
- 核心代码
- # items, 定义 img 的 url 和 name
- class ScrapyDoutulaiItem(scrapy.Item):
- # define the fields for your item here like:
- # name = scrapy.Field()
- # 定义图片 url 和 name
- img_url = scrapy.Field()
- img_name = scrapy.Field()
- # 爬虫文件
- class DoutulaiSpiderSpider(scrapy.Spider):
- name = 'doutulai_spider'
- allowed_domains = ['www.doutula.com']
- start_urls = ['https://www.doutula.com/photo/list/']
- page = 1
- def parse(self, response):
- content_items = ScrapyDoutulaiItem()
- # 解析 img_url 列表, 拿到图片的 url 和, 图片名称
- img_url_list = response.xpath('//img[@class="img-responsive lazy image_dta"]')
- # page_number = response.xpath('//*[@id="pic-detail"]/div/div[3]/div[3]/ul/li[12]/a/text()').extract_first()
- page_number = response.xpath('//a[@class="page-link"][last()]/text()').extract_first()
- for img_content in img_url_list:
- content_items['img_url'] = img_content.xpath('./@data-original').extract_first()
- content_items['img_name'] = img_content.xpath('./@data-original').extract_first().split('/')[-1]
- print(content_items)
- yield content_items
- # 不断爬取新页面
- if self.page <= page_number:
- self.page += 1
- next_url = self.start_urls[0] + '?page=' + str(self.page)
- yield scrapy.Request(next_url)
- #pipeline 下载图片
- from urllib import urlretrieve
- from scrapy_doutulai.settings import DOWNLOAD_DIR
- class ScrapyDoutulaiPipeline(object):
- def __init__(self):
- """
- 判断下载目录是否存在
- """
- if not os.path.exists(DOWNLOAD_DIR):
- os.makedirs(DOWNLOAD_DIR)
- def process_item(self, item, spider):
- """
- 下载图片
- :param item:
- :param spider:
- :return:
- """
- try:
- filename = os.path.join(DOWNLOAD_DIR,item['img_name'])
- print(filename)
- urlretrieve(item['img_url'],filename)
- except Exception as e:
- pass
三, 测试
测试使用 2C2G centos7.4,python2.7 版本, 启动线程 10 个, 爬去 1000 页的表情信息
3.1 多线程测试
启动爬虫
nohup doutulai/multithreading_spider/dutulai_spider.py &
查看系统负载
查看文件信息
3.2 Scrapy 框架爬图
启动爬虫
nohup doutulai/scrapy_doutulai/scrapy_doutulai/main.py &
查看系统负载
查看文件信息
爬取的图片
3.3 持久化存储在 OSS 上
最终配合阿里云 OSS 的 API 来将图片持久化存储在对象存储内.
整体 image 下载地址: 图片压缩包 https://doutulai.oss-cn-shanghai.aliyuncs.com/doutulai.tar.gz
四, 总结
经测试自己写的多线程爬图, CPU 使用率很高, 磁盘 IO 很大. Scrapy 默认也是 10 个线程, 但由于自己有磁盘 IO 操作, CPU 使用平稳.
虽然 Python 有 GIL, 但是在适当的场景下利用其多线程会很大程度的提升效率. 之前如果单线程 10 分钟, 利用多线程可以缩短 3/2 的 时间, 具体需要结合线程数, 磁盘与网络 IO 来判断.
来源: http://blog.51cto.com/kaliarch/2162411