一、简介

  为了提高爬虫程序效率,由于python解释器GIL,导致同一进程中即使有多个线程,实际上也只会有一个线程在运行,但通过request.get发送请求获取响应时有阻塞,所以采用了多线程依然可以提高爬虫效率。

多线程爬虫注意点
1.解耦

  整个程序分为4部分,url list模块、发送请求,获取响应模块、数据提取模块、保存模块,如果某一模块出现问题,互相之间不会影响。

2. 资源竞争

  由于使用了多线程,不同线程在共享数据时,容易产生资源竞争,假设共享数据放入列表中,那么同一时刻有可能2个线程去列表中取同一个数据,重复使用。解决办法是使用队列,使得某一线程get数据时,其他线程无法get同一数据,真正起到保护作用,类似互斥锁。

队列常用方法介绍

from queue import Queue

q = Queue()
q.put(url)
q.get() # 当队列为空时,阻塞
q.empty() # 判断队列是否为空,True/False

注意:

  • get和get_nowait两者的区别是当队列取完了即队列为空时,get()会阻塞,等待着新数据继续取,而get_nowait()会报错;
  • put和put_nowait 两者的区别是当队列为满时,put_nowait()会报错;

队列其他方法join task_done setDaemon

  • 在python3中,join()会等待子线程、子进程结束之后,主线程、主进程才会结束.
  • 队列中put队列计数会+1,get时计数不会减1,但当get+task_done时,队列计数才会减1,如果没有task_done则程序跑到最后不会终止。task_done()的位置,应该放在方法的最后以保证所有任务全部完成.
  • setDaemon方法把子线程设置为守护线程,即认为该方法不是很重要,记住主线程结束,则该子线程结束
  • join方法和setDaemon方法搭配使用。主线程进行到join()处,join的效果是让主线程阻塞,等待子线程中队列任务完成之后再解阻塞,等子线程结束,join效果失效,之后主线程结束,由于使用了setDaemon(True),所以子线程跟着结束,此时整个程序结束。

 

线程模块

from threading import Thread

# 使用流程
t = Thread(target=函数名) # 创建线程对象
t.start() # 创建并启动线程
t.join() # 阻塞等待回收线程

应用场景

  • 多进程 :CPU密集程序
  • 多线程 :爬虫(网络I/O)、本地磁盘I/O

二、案例

1. 小米应用商店抓取

目标

  1. 网址 :百度搜 - 小米应用商店,进入官网,应用分类 - 聊天社交
  2. 目标 :爬取应用名称和应用链接

实现步骤

1、确认是否为动态加载:页面局部刷新,查看网页源代码,搜索关键字未搜到,因此此网站为动态加载网站,需要抓取网络数据包分析

2、抓取网络数据包

  • 抓取返回json数据的URL地址(Headers中的Request URL)http://app.mi.com/categotyAllListApi?page={}&categoryId=2&pageSize=30
  • 查看并分析查询参数(headers中的Query String Parameters)只有page在变,0 1 2 3 ... ... ,这样我们就可以通过控制page的值拼接多个返回json数据的URL地址
page: 1
categoryId: 2
pageSize: 30

3、将抓取数据保存到csv文件。注意多线程写入的线程锁问题

from threading import Lock

lock = Lock()
lock.acquire()
lock.release()

整体实现思路

  1. 在 __init__(self) 中创建文件对象,多线程操作此对象进行文件写入;
  2. 每个线程抓取数据后将数据进行文件写入,写入文件时需要加锁;
  3. 所有数据抓取完成关闭文件;
import requests
from threading import Thread
from queue import Queue
import time
from lxml import etree
import csv
from threading import Lock class XiaomiSpider(object):
def __init__(self):
self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId={}&pageSize=30'
self.ua = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1'}
self.q = Queue() # 存放所有URL地址的队列
self.i = 0
self.id_list = [] # 存放所有类型id的空列表
# 打开文件
self.f = open('xiaomi.csv', 'a', newline="")
self.writer = csv.writer(self.f)
self.lock = Lock() # 创建锁 def get_cateid(self):
url = 'http://app.mi.com/'
html = requests.get(url=url, headers=self.ua).text parse_html = etree.HTML(html)
li_list = parse_html.xpath('//ul[@class="category-list"]/li')
for li in li_list:
typ_name = li.xpath('./a/text()')[0]
typ_id = li.xpath('./a/@href')[0].split('/')[-1]
pages = self.get_pages(typ_id) # 计算每个类型的页数
self.id_list.append((typ_id, pages)) self.url_in() # 入队列 def get_pages(self, typ_id):
# 每页返回的json数据中,都有count这个key
url = self.url.format(0, typ_id)
html = requests.get(url=url, headers=self.ua).json()
count = html['count'] # 类别中的数据总数
pages = int(count) // 30 + 1 # 每页30个,看有多少页 return pages # url入队列
def url_in(self):
for id in self.id_list:
# id为元组,(typ_id, pages)-->('2',pages)
for page in range(2):
url = self.url.format(page, id[0])
print(url)
# 把URL地址入队列
self.q.put(url) # 线程事件函数: get() - 请求 - 解析 - 处理数据
def get_data(self):
while True:
# 当队列不为空时,获取url地址
if not self.q.empty():
url = self.q.get()
html = requests.get(url=url, headers=self.ua).json()
self.parse_html(html)
else:
break # 解析函数
def parse_html(self, html):
# 存放1页的数据 - 写入到csv文件
app_list = []
for app in html['data']:
# 应用名称 + 链接 + 分类
name = app['displayName']
link = 'http://app.mi.com/details?id=' + app['packageName']
typ_name = app['level1CategoryName']
# 把每一条数据放到app_list中,目的为了 writerows()
app_list.append([name, typ_name, link])
print(name, typ_name)
self.i += 1 # 开始写入1页数据 - app_list
self.lock.acquire()
self.writer.writerows(app_list)
self.lock.release()
# 主函数
def main(self):
self.get_cateid() # URL入队列
t_list = [] # 创建多个线程
for i in range(1):
t = Thread(target=self.get_data)
t_list.append(t)
t.start() # 统一回收线程
for t in t_list:
t.join() # 关闭文件
self.f.close()
print('数量:', self.i) if __name__ == '__main__':
start = time.time()
spider = XiaomiSpider()
spider.main()
end = time.time()
print('执行时间:%.2f' % (end - start))

2.腾讯招聘数据抓取(Ajax)

确定URL地址及目标

要求与分析

  1. 通过查看网页源码,得知所需数据均为 Ajax 动态加载
  2. 通过F12抓取网络数据包,进行分析
  3. 一级页面抓取数据: 职位名称
  4. 二级页面抓取数据: 工作职责、岗位要求

一级页面json地址(pageIndex在变,timestamp未检查)

https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn

二级页面地址(postId在变,在一级页面中可拿到)

https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn

useragents.py文件

ua_list = [
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1',
'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0',
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)',
]

非多线程爬取

import time
import json
import random
import requests
from useragents import ua_list class TencentSpider(object):
def __init__(self):
self.one_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn'
self.two_url = 'https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn'
self.f = open('tencent.json', 'a') # 打开文件
self.item_list = [] # 存放抓取的item字典数据 # 获取响应内容函数
def get_page(self, url):
headers = {'User-Agent': random.choice(ua_list)}
html = requests.get(url=url, headers=headers).text
html = json.loads(html) # json格式字符串转为Python数据类型 return html # 主线函数: 获取所有数据
def parse_page(self, one_url):
html = self.get_page(one_url)
item = {}
for job in html['Data']['Posts']:
item['name'] = job['RecruitPostName'] # 名称
post_id = job['PostId'] # postId,拿postid为了拼接二级页面地址
# 拼接二级地址,获取职责和要求
two_url = self.two_url.format(post_id)
item['duty'], item['require'] = self.parse_two_page(two_url)
print(item)
self.item_list.append(item) # 添加到大列表中 # 解析二级页面函数
def parse_two_page(self, two_url):
html = self.get_page(two_url)
duty = html['Data']['Responsibility'] # 工作责任
duty = duty.replace('\r\n', '').replace('\n', '') # 去掉换行
require = html['Data']['Requirement'] # 工作要求
require = require.replace('\r\n', '').replace('\n', '') # 去掉换行 return duty, require # 获取总页数
def get_numbers(self):
url = self.one_url.format(1)
html = self.get_page(url)
numbers = int(html['Data']['Count']) // 10 + 1 # 每页有10个推荐 return numbers def main(self):
number = self.get_numbers()
for page in range(1, 3):
one_url = self.one_url.format(page)
self.parse_page(one_url) # 保存到本地json文件:json.dump
json.dump(self.item_list, self.f, ensure_ascii=False)
self.f.close() if __name__ == '__main__':
start = time.time()
spider = TencentSpider()
spider.main()
end = time.time()
print('执行时间:%.2f' % (end - start))

多线程爬取

多线程即把所有一级页面链接提交到队列,进行多线程数据抓取

import requests
import json
import time
import random
from useragents import ua_list
from threading import Thread
from queue import Queue class TencentSpider(object):
def __init__(self):
self.one_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn'
self.two_url = 'https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn'
self.q = Queue()
self.i = 0 # 计数 # 获取响应内容函数
def get_page(self, url):
headers = {'User-Agent': random.choice(ua_list)}
html = requests.get(url=url, headers=headers).text
# json.loads()把json格式的字符串转为python数据类型
html = json.loads(html) return html # 主线函数: 获取所有数据
def parse_page(self):
while True:
if not self.q.empty():
one_url = self.q.get()
html = self.get_page(one_url)
item = {}
for job in html['Data']['Posts']:
item['name'] = job['RecruitPostName'] # 名称
post_id = job['PostId'] # 拿postid为了拼接二级页面地址
# 拼接二级地址,获取职责和要求
two_url = self.two_url.format(post_id)
item['duty'], item['require'] = self.parse_two_page(two_url)
print(item)
# 每爬取按完成1页随机休眠
time.sleep(random.uniform(0, 1))
else:
break # 解析二级页面函数
def parse_two_page(self, two_url):
html = self.get_page(two_url)
# 用replace处理一下特殊字符
duty = html['Data']['Responsibility']
duty = duty.replace('\r\n', '').replace('\n', '')
# 处理要求
require = html['Data']['Requirement']
require = require.replace('\r\n', '').replace('\n', '') return duty, require # 获取总页数
def get_numbers(self):
url = self.one_url.format(1)
html = self.get_page(url)
numbers = int(html['Data']['Count']) // 10 + 1 return numbers def main(self):
# one_url入队列
number = self.get_numbers()
for page in range(1, number + 1):
one_url = self.one_url.format(page)
self.q.put(one_url) t_list = []
for i in range(5):
t = Thread(target=self.parse_page)
t_list.append(t)
t.start() for t in t_list:
t.join() print('数量:', self.i) if __name__ == '__main__':
start = time.time()
spider = TencentSpider()
spider.main()
end = time.time()
print('执行时间:%.2f' % (end - start))

多进程实现

import requests
import json
import time
import random
from useragents import ua_list
from multiprocessing import Process
from queue import Queue class TencentSpider(object):
def __init__(self):
self.one_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn'
self.two_url = 'https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn'
self.q = Queue() # 获取响应内容函数
def get_page(self, url):
headers = {'User-Agent': random.choice(ua_list)}
html = requests.get(url=url, headers=headers).text
# json格式字符串 -> Python
html = json.loads(html) return html # 主线函数: 获取所有数据
def parse_page(self):
while True:
if not self.q.empty():
one_url = self.q.get()
html = self.get_page(one_url)
item = {}
for job in html['Data']['Posts']:
# 名称
item['name'] = job['RecruitPostName']
# postId
post_id = job['PostId']
# 拼接二级地址,获取职责和要求
two_url = self.two_url.format(post_id)
item['duty'], item['require'] = self.parse_two_page(two_url) print(item)
else:
break # 解析二级页面函数
def parse_two_page(self, two_url):
html = self.get_page(two_url)
# 用replace处理一下特殊字符
duty = html['Data']['Responsibility']
duty = duty.replace('\r\n', '').replace('\n', '')
# 处理要求
require = html['Data']['Requirement']
require = require.replace('\r\n', '').replace('\n', '') return duty, require # 获取总页数
def get_numbers(self):
url = self.one_url.format(1)
html = self.get_page(url)
numbers = int(html['Data']['Count']) // 10 + 1 return numbers def main(self):
# url入队列
number = self.get_numbers()
for page in range(1, number + 1):
one_url = self.one_url.format(page)
self.q.put(one_url) t_list = []
for i in range(4):
t = Process(target=self.parse_page)
t_list.append(t)
t.start() for t in t_list:
t.join() if __name__ == '__main__':
start = time.time()
spider = TencentSpider()
spider.main()
end = time.time()
print('执行时间:%.2f' % (end - start))

基于multiprocessing.dummy线程池的数据爬取

案例:爬取梨视频数据。在爬取和持久化存储方面比较耗时,所以两个都需要多线程

import requests
import re
from lxml import etree
from multiprocessing.dummy import Pool
import random pool = Pool(5) # 实例化一个线程池对象 url = 'https://www.pearvideo.com/category_1'
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
}
page_text = requests.get(url=url,headers=headers).text
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@id="listvideoList"]/ul/li') video_url_list = []
for li in li_list:
detail_url = 'https://www.pearvideo.com/'+li.xpath('./div/a/@href')[0]
detail_page = requests.get(url=detail_url,headers=headers).text
video_url = re.findall('srcUrl="(.*?)",vdoUrl',detail_page,re.S)[0]
video_url_list.append(video_url) # pool.map(回调函数,可迭代对象)函数依次执行对象
video_data_list = pool.map(getVideoData,video_url_list) # 获取视频 pool.map(saveVideo,video_data_list) # 持久化存储 def getVideoData(url):
return requests.get(url=url,headers=headers).content def saveVideo(data):
fileName = str(random.randint(0,5000))+'.mp4' # 因回调函数只能传一个参数,所以没办法再传名字了,只能自己取名
with open(fileName,'wb') as fp:
fp.write(data) pool.close()
pool.join()

Python爬虫进阶 | 多线程的更多相关文章

  1. Python爬虫进阶五之多线程的用法

    前言 我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理. 首先声明一点! 多线程和多进程是不一样的!一个是 thread ...

  2. Python爬虫进阶四之PySpider的用法

    审时度势 PySpider 是一个我个人认为非常方便并且功能强大的爬虫框架,支持多线程爬取.JS动态解析,提供了可操作界面.出错重试.定时爬取等等的功能,使用非常人性化. 本篇内容通过跟我做一个好玩的 ...

  3. Python爬虫进阶一之爬虫框架概述

    综述 爬虫入门之后,我们有两条路可以走. 一个是继续深入学习,以及关于设计模式的一些知识,强化Python相关知识,自己动手造轮子,继续为自己的爬虫增加分布式,多线程等功能扩展.另一条路便是学习一些优 ...

  4. Python爬虫进阶三之Scrapy框架安装配置

    初级的爬虫我们利用urllib和urllib2库以及正则表达式就可以完成了,不过还有更加强大的工具,爬虫框架Scrapy,这安装过程也是煞费苦心哪,在此整理如下. Windows 平台: 我的系统是 ...

  5. Python爬虫进阶之Scrapy框架安装配置

    Python爬虫进阶之Scrapy框架安装配置 初级的爬虫我们利用urllib和urllib2库以及正则表达式就可以完成了,不过还有更加强大的工具,爬虫框架Scrapy,这安装过程也是煞费苦心哪,在此 ...

  6. python爬虫之多线程、多进程+代码示例

    python爬虫之多线程.多进程 使用多进程.多线程编写爬虫的代码能有效的提高爬虫爬取目标网站的效率. 一.什么是进程和线程 引用廖雪峰的官方网站关于进程和线程的讲解: 进程:对于操作系统来说,一个任 ...

  7. 芝麻软件: Python爬虫进阶之爬虫框架概述

    综述 爬虫入门之后,我们有两条路可以走. 一个是继续深入学习,以及关于设计模式的一些知识,强化Python相关知识,自己动手造轮子,继续为自己的爬虫增加分布式,多线程等功能扩展.另一条路便是学习一些优 ...

  8. Python爬虫之多线程下载豆瓣Top250电影图片

    爬虫项目介绍   本次爬虫项目将爬取豆瓣Top250电影的图片,其网址为:https://movie.douban.com/top250, 具体页面如下图所示:   本次爬虫项目将分别不使用多线程和使 ...

  9. Python爬虫之多线程下载程序类电子书

      近段时间,笔者发现一个神奇的网站:http://www.allitebooks.com/ ,该网站提供了大量免费的编程方面的电子书,是技术爱好者们的福音.其页面如下:   那么我们是否可以通过Py ...

随机推荐

  1. C++错题记录

    D. 通俗讲 , 前置++ : 先自增,再赋值    后置++: 先赋值,再自增 从反汇编中,可以看出: 前置++效率比后置++高 前置++: 自增后返回引用   后置++: 拷贝一份临时变量,再自增 ...

  2. 【LEETCODE】66、字符串分类,hard级别,题目:32,72,76

    package y2019.Algorithm.str.hard; import java.util.Stack; /** * @ProjectName: cutter-point * @Packag ...

  3. Latex中如何设置字体颜色(三种方式)

    1.直接使用定义好的颜色 \usepackage{color} \textcolor{red/blue/green/black/white/cyan/magenta/yellow}{text} 其中t ...

  4. 牛客CSP-S提高组赛前集训营2 T2沙漠点列

    原题链接 算法不难,比赛的时候就和cyc大佬一起yy了正解,不过因为交的时候比较急(要回寝室惹),我有两数组开错大小直接爆到50,cyc大佬则只把文件输入关了一半,直接爆零(╯ ̄Д ̄)╯┻━┻ 要尽量 ...

  5. python ---升级所有安装过的package

    # -*- coding:utf8 -*- import pip from subprocess import call from pip._internal.utils.misc import ge ...

  6. centos7 挂载未分配的硬盘空间

    =============================================== 2019/7/28_第1次修改                       ccb_warlock == ...

  7. Java自学-数组 二维数组

    Java 如何使用二维数组 这是一个一维数组, 里面的每一个元素,都是一个基本类型int int a[] =new int[]{1,2,3,4,5}; 这是一个二维数组,里面的每一个元素,都是一个一维 ...

  8. Matlab建造者模式

    构建者模式又叫建造者模式(Builder),是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示.创建者模式隐藏了复杂对象的创建过程,它把复杂对象的创建过程加以抽象,通过子类继 ...

  9. Oracle队列实现

    Oracle队列实现 -- 核心技术点:for update 创建测试表 create table t ( id       number primary key, processed_flag va ...

  10. 如何去除有道云笔记广告(windows)

    一.适用于6.0之前版本 你只需要:找到有道云笔记的安装路径,*\Youdao\YoudaoNote\theme\build.xml 用笔记本打开这个文件,找到'左下角广告'这几个字,把下面的代码删掉 ...