一 什么是爬虫
爬虫能做什么
● 政治角逐
2016年这场美国总统竞选被媒体称作“第一次数字化竞选”,希阿姨和川大大都组建了庞大的技术团队,将大量资金花在获取和使用投票者的信息上。民意调查结果,一直是总统大选时最倚重的数据来源。在长达半年的总统竞选活动中,会有许多组织或机构通过不同方式进行大量调查,并将结果汇总整理加工成民意调查数据。在更大的数据规模上,总统候选人们也采用了同样的策略,所依赖的数据来源也不仅仅是民意调查结果,还涵盖了诸多的如facebook这类的社交网站和公开及私有的数据库。
及时准确的收集这些数据,并且帮助制定策略以获得更多的选民支持。将美国超过2亿的选民资料,与大型网站与社交网络上的个人账号相互匹配起来,将网络行为对应到具体的个体,再和已经构成的、庞大的用户个人数据相结合,最终完全由准确数据来驱动竞选策略。
而以上这些就使得大数据分析技术成了两党候选人的重要武器。
● 电商分析
抓取天猫、京东、淘宝等电商网的评论及销量数据,对各种商品(颗粒度可到款式)沿时间序列的销量以及用户的消费场景进行分析。
甚至还可以根据用户评价做情感分析,实时监控产品在消费者心目中的形象,对新发布的产品及时监控,以便调整策略。
● 投资理财
雪球等财经类网站通过抓取雪球KOL或者高回报用户的行为,找出推荐股票。
● 消费习惯分析
在大众点评、美团网等餐饮及消费类网站抓取各种店面的开业情况以及用户消费和评价,了解周边变化的口味,所谓是“舌尖上的爬虫”。
以及各种变化的口味,比如:啤酒在衰退,重庆小面在崛起。
● 内部数据利用
企业在运营过程中产生的大量数据,其实是蕴含着巨大的价值,对企业未来的发展和创新商业模式都有着很大的帮助。充分的挖掘数据潜在价值,能帮助企业更好的细分市场,以助于公司能有针对性的为企业日后的发展提供数据支撑。更好的掌握市场动向,更好的对市场反应产生新的决策。
数据背后所隐藏的巨大商业价值正开始被越来越多的企业所重视,越来越多的企业开始进入大数据市场,建立各种大数据入口,以获得更多更大的海量数据。那么问题来了,数据从何而来?
什么是网络爬虫
所谓爬虫,其本质是一种计算机程序,它的行为看起来就像是蜘蛛在网上面爬行一样,顺着互联网这个“网”,一条线一条线地“爬行”。所以爬虫在英文中又叫作“Spider”,正是蜘蛛这个单词。
由于传统低效率的数据收集手段越来越不能满足当今日益增长的数据需求,但是面对互联网这样一个由数据构建而成的海洋,如何有效获取数据,如何获取有效数据都是极其劳神费力、浪费成本、制约效率的事情。很多时候,按照传统手段完成一个项目可能80%~90%的时间用于获取和处理数据。这样的矛盾冲突,搁在以往,搁在普通的人和普通的公司身上,除了用金钱去填补(直接购买数据)之外,似乎只有默默认命了。
然而现在,终于有了扭转之机,那就是驾驭爬虫技术。
二 爬虫工具
urllib库是爬虫常用的一个库,通过这个库的学习,能够了解一些爬虫的基础技术。
下面以爬取某网站首页源码的示例代码介绍urilib库中常用的request()方法:
导入urllib库
import urllib.request
urllib爬取某网站首页的步骤:
(1) 定义一个url 即目标地址
url = 'http://www.xxx.com'
(2) 模拟浏览器向服务器发送请求
response = urllib.request.urlopen(url)
(3) 获取响应中的页面的源码
这里read()函数可以获取响应,但是响应的格式是二进制的,需要解码
解码:decode('编码格式') 编码格式在 <head><meta chaset ></head>中显示
content = response.read().decode('utf-8')
(4) 打印数据
print(content)
● urllib库下载资源
下面介绍一下用urllib.request()方法如何下载文件:
urllib下载文件的操作
(1) 下载网页
url_page = 'http://www.baidu.com'
使用urillib.request.urlretrieve() 函数,
传参分别是url(网页的地址路径)、filename(网页文件的名字)
urllib.request.urlretrieve(url_page,'baidu.html')
(2) 下载图片
url_img = 'https://xxx'
urllib.request.urlretrieve(url_img,'xxx.jpg')
(3) 下载视频
url_video = 'https://xxx'
urllib.request.urlretrieve(url_video,'xxx.mov')
● urllib库之GET请求
get请求的quote()方法:单个参数的情况下常用该方法进行编解码
需求:用get请求的quote()方法获取源码
找到网页地址url:此时复制的汉字会自动转成unicode编码,如下,即这段编码就是周杰伦三个汉字的编码
url = 'https://www.baidu.com/s?wd=%E5%91%A8%E6%9D%B0%E4%BC%A6'
寻找UA
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
定制请求对象
request = urllib.request.Request(url = url,headers = headers)
模拟浏览器向服务器发起请求
response = urllib.request.urlopen(request)
获取响应内容
content = response.read().decode('utf-8')
打印内容
print(content)
● urllib库之POST请求
post请求:
import urllib.request
url = 'https://xxx'
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
data = {
'data':'data'
}
import urllib.parse
post请求的参数 必须 进行编码
data = urllib.parse.urlencode(data).encode('utf-8')
post请求的参数是不会拼接在url 的后面的,而是需要放置在请求对象定制的地方
request = urllib.request.Request(url = url,data = data,headers = headers)
模拟浏览器向服务器发送请求
response = urllib.request.urlopen(request)
获取响应的数据
content = response.read().decode('utf-8')
import json
字符串 - - - > json 对象
obj = json.loads(content)
print(obj)
● urllib库之handler处理器
首先简单介绍一下handler处理器:handler处理器是urllib库中继urlopen()方法之后又一种模拟浏览器向服务器发起请求的方法或技术。
它的意义在于使用handler处理器,能够携带代理ip,这为对抗反爬机制提供了一种策略(很多的网站会封掉短时间多次访问的ip地址)。
下面是handler处理器的具体使用方法,其中不含代理ip的部分,代理ip这部分将在下一篇笔记中介绍。
handler处理器的基础使用
需求:使用handler访问百度 获取网页源码
import urllib.request
url = 'http://www.baidu.com'
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
request = urllib.request.Request(url = url,headers = headers)
handler build_opener open
第一步:获取handler对象
handler = urllib.request.HTTPHandler()
第二步:通过handler获取opener对象
opener = urllib.request.build_opener(handler)
第三步:调用open()函数
response = opener.open(request)
content = response.read().decode('utf-8')
print(content)
● urllib库之IP池
urllib代理
import urllib.request
url = 'https://www.xxx'
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
proxies = {
'http' : '40.83.102.86:80'
}
request = urllib.request.Request(url = url, headers = headers)
handler = urllib.request.ProxyHandler(proxies = proxies)
opener = urllib.request.build_opener(handler)
response = opener.open(request)
content = response.read().decode('utf-8')
with open('daili.html','w',encoding = 'utf-8') as fp:
fp.write(content)
import requests
url = 'http://www.xxx.com'
response = requests.get(url = url)
● requests库的六种属性:
1️⃣ text属性:字符串形式返回网页源码(由于此时编码格式是gbk,中文部分可能会乱码,稍后解决)
print(response.text) // 由于没有设置编码格式,中文会乱码
2️⃣ encoding属性:设置相应的编码格式
response.encoding = ‘utf-8’
这之后的response就不会出现中文乱码现象了。
3️⃣ url属性:返回url地址
url = response.url
4️⃣ content属性:返回二进制的数据
content_binary = response.content
5️⃣ status_code属性:返回状态码 200是正常
status_code = response.status_code
6️⃣ headers属性:返回响应头
headers = response.headers
● requests库的get请求
import requests
url = 'https://www.xxx.com'
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
data = {
'data' : 'data'
}
response = requests.get(url = url,params = data,headers = headers)
response.encoding = 'utf-8'
content = response.text
print(content)
● requests库的post请求
requests_post请求
import requests
url = 'https://www.com'
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
data = {
'data' : 'xxx'
}
response = requests.post(url = url,data = data,headers = headers)
content = response.text
import json
obj = json.loads(content.encode('utf-8'))
print(obj)
● requests库的代理ip方法
requests_ip代理
import requests
url = 'http://www.baidu.com/s'
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
data = {
'wd' : 'ip'
}
proxy = {
'http:' : '218.14.108.53'
}
response = requests.get(url = url, params = data,headers = headers,proxies = proxy)
content = response.text
bs4的基本操作语法:
解析本地文件:bs4基础语法的学习
soup = BeautifulSoup(open(‘soup.html’,encoding = ‘utf-8’),‘lxml’)
bs4的基础操作:
(1) 根据标签名查找节点,找到的是第一个符合条件的节点:
print(soup.a) // 返回的是soupDemo而不是soupDemo2
(2) 获取标签的属性:
print(soup.a.attrs)
response.read().decode('utf-8') ***** etree.HTML()
xpath解析本地文件
tree = etree.parse('new.html')
查找ul下面的li
li_list = tree.xpath('//body//li')
判断列表的长度:length
print(li_list)
print(len(li_list))
查找带有id属性的li标签
li_list = tree.xpath('//ul/li[@id]')
print(li_list)
print(len(li_list))
获取标签的内容:text()
li_list = tree.xpath('//ul/li[@id]/text()')
print(li_list)
print(len(li_list))
获取指定id的标签,属性值id要加引号
li_list = tree.xpath('//ul/li[@id = "l1"]/text()')
print(li_list)
查找指定id的标签的class属性值
li_list = tree.xpath('//ul/li[@id = "l1"]/@class')
print(li_list)
模糊查询:
(1) id中含有l的li标签
li_list = tree.xpath('//ul/li[contains(@id,"l")]/text()')
print(li_list)
(2) id的值以l开头的li标签
li_list = tree.xpath('//ul/li[starts-with(@id,"c")]/text()')
print(li_list)
逻辑运算:
(1) 查询id为l1和class为c1的标签:
li_list = tree.xpath('//ul/li[@id = "l1" and @ class = "c1"]/text()')
print(li_list)
(2) 查询id为l1或l2的标签:
li_list = tree.xpath('//ul/li[@id = "l1"]/text() | //ul/li[@id = "l2"]/text()')
print(li_list)
xpath解析服务器响应文件:从某网站html文件中提取内容
(1) 获取源码
import urllib.request
url = "https://xxx.com"
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
request = urllib.request.Request(url = url,headers = headers)
response = urllib.request.urlopen(request)
content = response.read().decode('utf-8')
print(content)
(2) xpath解析服务器响应的文件
from lxml import etree
解析服务器响应的文件的核心操作:
tree = etree.HTML(content)
注意xpath的返回数据类型是列表,我们可以用索引值:
result = tree.xpath('//input[@id = "su"]/@value')[0]
print(result)
import json
import jsonpath
// 注意,默认打开文件的格式是gbk,但json.load()返回的的对象格式要求编码为utf-8,
// 因此我们要强制编码为utf-8
obj = json.load(open('store.json','r',encoding = 'utf-8'))
// 解析书店所有书的作者
book_author_list = jsonpath.jsonpath(obj,'$.store.book[*].author')
print(book_author_list)
// 可以用索引值标注第几本书:
author = jsonpath.jsonpath(obj,'$.store.book[1].author')
print(author)
// 所有的作者,包括自行车
author_list = jsonpath.jsonpath(obj,'$..author')
print(author_list)
// store下面所有的元素
tag_list = jsonpath.jsonpath(obj,'$.store.*')
print(tag_list)
// store下面所有的price
price_list = jsonpath.jsonpath(obj,'$.store..price')
print(price_list)
// 第三个书
book = jsonpath.jsonpath(obj,'$.store.book[2]') // 也可以写作 $..book[2]
print(book)
// 最后一本书
// @相当于this,指代当前的每一个对象
// @.length表示当前的json的字典长度
last_book = jsonpath.jsonpath(obj,'$..book[(@.length-1)]')
print(last_book)
// 前两本书
// 用切片思维:
book_list = jsonpath.jsonpath(obj,'$..book[0,1]')
// 另一种写法:
book_list = jsonpath.jsonpath(obj,'$..book[:2]')
// 过滤包含版本号isbn的书:
// 条件过滤需要在圆括号前面添加一个问号
book_list = jsonpath.jsonpath(obj,'$..book[?(@.isbn)]')
print(book_list)
// 过滤超过十元的书
book_list = jsonpath.jsonpath(obj,'$..book[?(@.price > 10)]')
print(book_list)
● selenium库的基本语法
导入selenium库,并初始化浏览器操作对象:
from selenium import webdriver
path = 'chromedriver.exe'
browser = webdriver.Chrome(path)
上面的部分一共干了两件事:导入selenium库,初始化了浏览器操作对象。导入时格式是 from selenium import webdriver,导入后,我们可以创建一个字符串变量path,path的值是我们之前安装浏览器工具的路径,如果安装在与此python文件同级目录下,则直接输入其名称即可,否则要使用绝对路径!
最后用webdriver.Chrome()函数,传入路径,创建一个浏览器操作对象browser(名字可以自定义),这个对象会作为我们模拟真人操作浏览器的帮手!
模拟真人,自动打开浏览器,并获取网页源码:
from selenium import webdriver
path = 'chromedriver.exe'
browser = webdriver.Chrome(path)
url = 'https://www.baidu.com'
browser.get(url)
content = browser.page_source
这一步,首先我们定义需要打开的网页的地址,之后使用get()函数,模拟真人打开浏览器并传入url,与此同时,我们的browser对象也与这个url建立了绑定,后续获取源码或者节点的信息都需要通过这个browser对象。最后,通过page_source函数,获取当前url的网页的源码。
定位元素的几种方法:
(1) 根据id属性的属性值找到对象_重要:
button = browser.find_element_by_id('su')
print(button)
(2) 根据name属性的属性值找到对象:
button = browser.find_element_by_name('wd')
print(button)
(3) 根据xpath的语句找到对象_重要:
button = browser.find_element_by_xpath('//input[@id = "su"]')
print(button)
(4) 根据标签的名称找到对象
button = browser.find_element_by_tag_name('input')
print(button)
(5) 根据CSS选择器找到对象,相当于bs4的语法_重要:
button = browser.find_element_by_css_selector('//su')
(6) 根据链接元素查找对象:
button = browser.find_element_by_link_text('新闻')
所谓的定位元素,就是指我们通过一些方法把页面上的元素与实际的代码中的对象(变量)进行绑定,以便于后续通过操作这些对象来获取元素信息、实际控制或操作页面上的元素(如果学过前端js、安卓的朋友可能比较理解这样的模式)。这些上面展示了六种定位元素的办法,其中比较重要的是前三种和第五种,即id、name、xpath语句、CSS选择器这四种方式,其他两种仅作为了解即可。
元素信息的获取:
首先,拿到页面中id值是su的input输入框元素,与变量input建立绑定关系
input = browser.find_element_by_id('su')
(1) get_attribute()函数获取标签的指定属性的属性值
传参是属性的名称,例如class、id等,返回这些属性的属性值
print(input.get_attribute('class'))
(2) tag_name函数获取元素对应的标签的名称,例如元素是input标签,返回值就是input
print(input.tag_name)
(3) text函数获取标签的文本,文本指的是标签尖括号的内容:
例如:<div> xxx </div> 于是获取的结果是xxx
print(input.text)
定位到id值是su的input表单元素之后,我们把这个元素与变量input进行绑定,而后通过操作input,我们能够获取关于这个表单元素的信息,其中重要的信息有两个:一个是元素的属性值,则可以通过get_attribute()函数获取,这个函数的传参是属性的名称,比如class、id等等,返回的是该属性的属性值;另一个是标签内的文本,这可以通过text属性获取。
selenium交互学习:
// (1) 点击按钮:
button.click()
// (2) 文本框输入指定内容:
input.send_keys('content')
// (3) 滑到底部:
js_bottom = 'document.documentElement.scrollTop = 100000'
browser.execute_script(js_bottom)
// (4) 回到上一页:
browser.back()
// (5) 回到下一页:
browser.forward()
// (6) 关闭浏览器:
browser.quit()
注意,上面的代码的前提是定义了一个button对象,与页面中的某个按钮对象进行了绑定;定义了一个input对象,与页面中的某个文本框对象进行了绑定;browser是定义的浏览器操作对象。
● selenium爬虫实战案例:获取网页源码
学习了基础之后,我们先做一个简单的爬虫案例:获取网页的源码。
首先解释一下我们为什么要用selenium来做这个实战:当我们使用urllib库的urlopen()函数获取服务器的响应时,由于服务器识别了我们是模拟服务器而非真实服务器,因此返回的数据有大量的缺失,这等价于我们不能使用urllib库获取完整的响应。
于是我们使用下面的代码实现我们想要的效果:
from selenium import webdriver
// 创建浏览器操作对象
path = 'chromedriver.exe'
browser = webdriver.Chrome(path)
// (1) 访问网站,即模拟人的操作,打开浏览器并访问链接,用get()函数:
url = 'https://xxx.com'
browser.get(url)
// (2) page_source获取网页源码:(此时的url是上一步传入的url)
content = browser.page_source
print(content)
三. scrapy框架
Scrapy简介
● Scrapy是什么:Scrapy是适用于Python的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试
● ScrapyEngine:引擎。负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件。 此组件相当于爬虫的“大脑”,是 整个爬虫的调度中心。
● Schedule:调度器。接收从引擎发过来的requests,并将他们入队。初始爬取url和后续在页面里爬到的待爬取url放入调度器中,等待被爬取。调度器会自动去掉重复的url。
● Downloader:下载器。负责获取页面数据,并提供给引擎,而后提供给spider。
● Spider:爬虫。用户编些用于分析response并提取item和额外跟进的url。将额外跟进的url提交给ScrapyEngine,加入到Schedule中。将每个spider负责处理一个特定(或 一些)网站。
● ItemPipeline:负责处理被spider提取出来的item。当页面被爬虫解析所需的数据存入Item后,将被发送到Pipeline,并经过设置好次序
● DownloaderMiddlewares:下载中间件。是在引擎和下载器之间的特定钩子(specific hook),处理它们之间的请求(request)和响应(response)。提供了一个简单的机制,通过插入自定义代码来扩展Scrapy功能。通过设置DownloaderMiddlewares来实现爬虫自动更换user-agent,IP等。
● SpiderMiddlewares:Spider中间件。是在引擎和Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items或requests)。提供了同样简单机制,通过插入自定义代码来扩展Scrapy功能。
Scrapy项目结构
在开始爬取之前,您必须创建一个新的Scrapy项目。 进入您打算存储代码的目录中,运行下列命令:
scrapy startproject tutorial
该命令将会创建包含下列内容的 tutorial 目录:
tutorial/
scrapy.cfg
tutorial/
init.py
items.py
pipelines.py
settings.py
spiders/
init.py
…
这些文件分别是:
scrapy.cfg: 项目的配置文件
tutorial/: 该项目的python模块。之后您将在此加入代码。
tutorial/items.py: 项目中的item文件.
tutorial/pipelines.py: 项目中的pipelines文件.
tutorial/settings.py: 项目的设置文件.
tutorial/spiders/: 放置spider代码的目录.
Scrapy工作流程
Scrapy中的数据流由执行引擎控制,如下所示:
1引擎从Spider获取爬行的初始请求。
2引擎在调度器中调度请求,并请求下一个要爬网的请求。
3调度器将下一个请求返回给引擎。
4引擎通过下载器中间件向下载器发送请求。
5一旦页面完成下载,Downloader将生成一个响应(带有该页面)并将其发送到引擎,通过Downloader中间件。
6引擎从下载器接收响应,并通过Spider中间件将其发送给Spider进行处理。
7Spider处理响应,并通过Spider中间件将回收的项目和新的请求返回给引擎。
8引擎将处理的项目发送到项目管道,然后将处理的请求发送到调度器,并请求可能的下一个请求进行爬网。
9该过程重复(从步骤3开始),直到调度器不再发出请求。
Scrapy下载中间件
UserAgentMiddleware
classscrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware
用于覆盖spider的默认user agent的中间件。
随机的user-agent,模拟各种终端
要使得spider能覆盖默认的user agent,其 user_agent 属性必须被设置。
CookiesMiddleware
classscrapy.contrib.downloadermiddleware.cookies.CookiesMiddleware
该中间件可以爬取需要cookie的网站
DefaultHeadersMiddleware
classscrapy.contrib.downloadermiddleware.defaultheaders.DefaultHeadersMiddleware
该中间件设置可以设置request header。
Scrapy项目管道
在一个项目被蜘蛛抓取之后,它被发送到项目管道,该管道通过几个按顺序执行的组件来处理它。每个项管道组件(有时称为“项管道”)都是一个实现简单方法的Python类。它们接收一个项目并对其执行操作,还决定该项目是否应继续通过管道,或者是否应删除并不再处理。
作用:
清理HTML数据
验证抓取的数据(检查项目是否包含某些字段)
检查重复项(并删除它们)
将爬取的项目存储在数据库中
process_item(self, item, spider)
对每个项管道组件调用此方法。将数据进行处理,写入文件,数据库等
open_spider(self, spider)
当spider打开时调用此方法。如打开文件流,打开数据库连接等
close_spider(self, spider)
当spider关闭时调用此方法。如关闭文件流,关闭数据库连接等
四. 提升爬虫速度
多进程爬虫
多进程爬虫可以利用CPU的多核,进程数取决于计算机CPU的处理器个数。由于运行在不同的核上,各个进程的运行是并行的。在Python中,如果我们要用多进程,需要用multiprocessing这个库。
使用multiprocessing的两种方法:
Process+Queue
Pool+Queue
当进程数量大于CPU的内核数量时,等待运行的进程会等到其他进程运行完毕让出内核为止。所以,单核CPU是无法进行多进程并行的。
Process+Queue
例子1:使用3个进程,抓取1000个网页:
from multiprocessing import Process,Queue
import time
import requests
link_list = []
with open('alexa.txt', 'r') as file:
file_list = file.readlines()
for eachone in file_list:
link = eachone.split('\t')[1]
link = link.replace('\n', '')
link_list.append(link)
start = time.time()
class MyProcess(Process):
def __init__(self, q):
super(MyProcess, self).__init__()
self.q = q
def run(self):
print('Starting ', self.pid)
while not self.q.empty():
crawler(self.q)
print('Exiting ', self.pid)
def crawler(q):
url = q.get(timeout=2)
try:
r = requests.get(url, timeout=2)
print(q.qsize(), r.status_code, url)
except Exception as e:
print(q.qsize(), url, 'Error: ', e)
if __name__ == '__main__':
ProcessNames = ['Process-1', 'Process-2', 'Process-3']
workQueue = Queue(1000)
for url in link_list:
workQueue.put(url)
for i in range(0, 3):
p = MyProcess(workQueue)
p.daemon = True
p.start()
p.join()
end = time.time()
print('Process + Queue :', end-start)
print('Main process Ended!')
上述代码中,p.daemon = True,每个进程都可以单独设置它的属性,设置为True时,当父进程结束后,子进程就会自动被终止。
Pool+Queue
当被操作对象数目不大时,可以直接利用上述方法进行动态生成多个进程,但是如果进程数量很多,手动设置进程数量太麻烦,使用pool进程池可以提高效率。
pool可以提供指定数量的进程供用户调用。
阻塞和非堵塞关注的是程序在等待调用结果时返回的状态。堵塞要等到回调结果出来,在有结果之前,当前进程会被挂起。非堵塞为添加进程后,不一定非要等到结果出来就可以添加其他进程运行。
例子2:使用pool+process的方式,抓取1000个网页:
from multiprocessing import Pool, Manager
import time
import requests
link_list = []
with open('alexa.txt', 'r') as file:
file_list = file.readlines()
for eachone in file_list:
link = eachone.split('\t')[1]
link = link.replace('\n', '')
link_list.append(link)
start = time.time()
def crawler(q, index):
Process_id = 'Process-' + str(index)
while not q.empty():
url = q.get(timeout=2)
try:
r = requests.get(url, timeout=20)
print(Process_id, q.qsize(), r.status_code, url)
except Exception as e:
print(Process_id, q.qsize(), url, 'Errpr', e)
if __name__ == '__main__':
manager = Manager()
workQueue = manager.Queue(1000)
for url in link_list:
workQueue.put(url)
pool = Pool(processes=3)
for i in range(4):
pool.apply_async(crawler, args=(workQueue, i))
print('Started process')
pool.close()
pool.join()
end = time.time()
print('Pool + Queue :', end-start)
print('Main process Ended!')
Queue的使用方式就需要改变,这里用到multiprocessing中的Manager,使用manager=Manager()和workQueue=manager.Queue(1000)来创建队列。这个队列对象可以在父进程与子进程间通信。
使用pool.apply_async(target=func,args=(args))实现。
多线程爬虫
多线程爬虫是以并发的方式执行的。也就是并不能真正的同时执行,而是通过进程的快速切换加快网络爬虫的速度。
Python中的GIL(Global Interpreter Lock,全局解释器锁),一个线程的执行过程包括获取GIL、执行代码直到挂起和释放GIL。并且Python进程中,只有一个GIL,拿不到GIL的线程就能允许进入CPU执行。
每次释放GIL时,线程之间会进行锁竞争,而切换线程会消耗资源。由于GIL的存在,Python中一个进程永远只能同时执行一个线程(拿到GIL的线程),这就是多核CPU上Python的多线程效率不高的原因。
Python的多线程对于IO密集型代码比较友好,网络爬虫能够在获取网页的过程中使用多线程,从而加快速度。
例子,多线程的方式抓取1000个网页,并开启5个线程:
import threading
import requests
import time
import queue as Queue
link_list = []
with open('alexa.txt', 'r',) as file:
file_list = file.readlines()
for eachone in file_list:
link = eachone.split('\t')[1]
link = link.replace('\n', "")
link_list.append(link)
start = time.time()
class myThread(threading.Thread):
def __init__(self, name, q):
super(myThread, self).__init__()
self.name = name
self.q = q
def run(self):
print('Starting ' + self.name)
while True:
try:
crawler(self.name, self.q)
except:
break
print('Exiting ' + self.name)
def crawler(threadName, q):
url = q.get(timeout=2)
try:
r = requests.get(url, timeout=20)
print(threadName, r.status_code)
except Exception as e:
print(threadName, 'Error', e)
threadList = ['Thread-1', 'Thread-2', 'Thread-3', 'Thread-4', 'Thread-5']
workQueue = Queue.Queue(1000)
threads = []
for tName in threadList:
thread = myThread(tName, workQueue)
thread.start()
threads.append(thread)
for url in link_list:
workQueue.put(url)
for t in threads:
t.join()
end = time.time()
print('简单多线程爬虫的总时间为:', end-start)
print('Exiting Main Thread')
使用队列的方法,可以加快线程的利用率。
多协程爬虫
协程是一种用户态的轻量级线程,使用协程有许多好处:
协程像一种在程序级别模拟系统级别的进程,由于是单线程并且少了上下文切换,因此相对来说系统消耗很少。
协程方便切换控制流,简化了编程模型。协程能保留上一次调用时的状态,每次进程重入时,就相当于进入了上一次调用的状态。
协程的高扩展性和高并发性,一个CPU支持上万协程都不是问题,所以很适合用于高并发处理。
协程也有缺点:
协程的本质是一个单线程,不能同时使用单个CPU的多核,需要和进程配合才能运行在多核CPU上。
长时间的阻塞的IO操作时不要用协程,因为可能会阻塞整个程序。
Python协程使用gevent库
例子:使用多协程抓取1000个网页:
import gevent
from gevent.queue import Queue, Empty
import time
import requests
from gevent import monkey
monkey.patch_all()
link_list = []
with open('alexa.txt', 'r') as file:
file_list = file.readlines()
for eachone in file_list:
link = eachone.split('\t')[1]
link = link.replace('\n', '')
link_list.append(link)
start = time.time()
def crawler(index):
Process_id = 'Process-' + str(index)
while not workQueue.empty():
url = workQueue.get(timeout=2)
try:
r = requests.get(url, timeout=20)
print(Process_id, workQueue.qsize(), r.status_code, url)
except Exception as e:
print(Process_id, workQueue.qsize(), url, 'Error:', e)
def boss():
for url in link_list:
workQueue.put_nowait(url)
if __name__ == '__main__':
workQueue = Queue(1000)
gevent.spawn(boss).join()
jobs = []
for i in range(10):
jobs.append(gevent.spawn(crawler, i))
gevent.joinall(jobs)
end = time.time()
print('gevent + Queue :', end-start)
print('Main Ended!')
Python协程使用asyncio库
例子:使用多协程抓取1000个网页:
import asyncio
import arrow
def current_time():
'''
获取当前时间
:return:
'''
cur_time = arrow.now().to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss')
return cur_time
async def func(sleep_time):
func_name_suffix = sleep_time // 使用 sleep_time(函数 I/O 等待时长)作为函数名后缀,以区分任务对象
print(f"[{current_time()}] 执行异步函数 {func.__name__}-{func_name_suffix}")
await asyncio.sleep(sleep_time)
print(f"[{current_time()}] 函数 {func.__name__}-{func_name_suffix} 执行完毕")
return f"【[{current_time()}] 得到函数 {func.__name__}-{func_name_suffix} 执行结果】"
async def run():
task_list = []
for i in range(5):
task = asyncio.create_task(func(i))
task_list.append(task)
results = await asyncio.gather(*task_list)
for result in results:
print((f"[{current_time()}] 得到执行结果 {result}"))
def main():
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
if __name__ == '__main__':
main()
五. 反爬虫问题
反爬虫方式
一般而言,抓取稍微正规一点的网站,都会有反爬虫的制约。反爬虫主要有以下几种方式:
通过UA判断:这是最低级的判断,一般反爬虫不会用这个做唯一判断,因为反反爬虫非常容易,直接随机UA即可解决。
通过单IP频繁访问判断:这个判断简单,而且反反爬虫比较费力,反爬虫绝佳方案。需采用多IP抓取。
通过Cookie判断:例如通过会员制账号密码登陆,判断单账号短时间抓取次数判断。这个反反爬虫也很费力。需采用多账号抓取。
动态页面加载:这个考验前端工程师的功底,如果前端写的好,各种JS判断,各种逻辑,像百度、淘宝一样,post登录很难。较好的方法,但是对于大牛,还是防不胜防。反反爬虫多采用渲染浏览器抓取,效率低下。
采用验证码:这里要么是登录的时候有验证码,要么是判断是爬虫时,不封IP,而是采用验证码验证,例如链家网。验证码是反爬虫性价比较高的方案。反反爬虫一般接入OCR验证码识别平台或者人工打码平台,亦或者利用TesseractOCR识别,亦或者采用神经网络训练识别验证码等。
如何反反爬虫
修改请求头:除了处理网站表单,requests 模块还是一个设置请求头的利器。HTTP 的请求头是在你每次向网络服务器发送请求时,传递的一组属性和配置信息。HTTP 定义了十几种古怪的请求头类型,不过大多数都不常用。只有下面的七个字段被大多数浏览器用来初始化所有网络请求(表中信息是我自己浏览器的数据)。
修改爬虫的间隔时间:修改爬虫的间隔时间,使用随机间隔时间,模拟访问操作。合理控制速度是你不应该破坏的规则。所以请控制采集速度!
使用代理:上网者可以通过正向代理的方法隐藏自己的 ip。正向代理是一个位于客户端和目标服务器之间的代理服务器(中间服务器)。为了从目标服务器取得内容,客户端向代理服务器发送一个请求,并且指定目标服务器,之后代理向目标服务器转发请求,将获得的内容返回给客户端。正向代理的情况下,客户端必须要进行一些特殊的设置才能使用。
登录获取数据:在采集一些网站时 cookie 是不可或缺的。要在一个网站上持续保持登录状态,需要在多个页面中保存一个 cookie。有些网站不要求在每次登录时都获得一个新 cookie,只要保存一个旧的“已登录”的 cookie 就可以访问。