网络爬虫之scrapy框架详解
twisted介绍
Twisted是用Python实现的基于事件驱动的网络引擎框架,scrapy正是依赖于twisted,
它是基于事件循环的异步非阻塞网络框架,可以实现爬虫的并发。
twisted是什么以及和requests的区别:
- request是一个python实现的可以伪造浏览器发送Http请求的模块,它封装了socket发送请求
- twisted是基于时间循环的异步非阻塞的网络框架,它也封装了socket发送请求,但是他可以单线程的完成并发请求。
twisted的特点是:
- 非阻塞:不等待
- 异步:回调
- 事件循环:一直循环去检查状态
scrapy的pipeline文件和items文件
这两个文件有什么作用
先看看我们上篇的示例:
在这个示例中,虽然我们已经通过chouti.py一个文件中的parse方法实现了爬去抽屉网的新闻并将之保存在文件中的功能,
但是我们会发现有两个问题:
1、在循环爬去每一页的时候,每次都需要重新打开然后再关闭文件,如果数据量庞大的话,这对性能有很大的影响。
2、我们将解析和数据持久化都放在了同一个文件的同一个方法中,没有做到分工明确
如果要解决这两个问题,则需要用到scrapy自动为我们生成的pipeline文件和items文件
这两个文件怎么用
如果我们要使用这两个文件从而解决问题,则需要有四部操作:
a.编写pipeline文件中的类,格式如下:
1
2
3
|
class
XXXPipeline(
object
):
def
process_item(
self
, item, spider):
return
item
|
b.编写items文件中的类,格式如下:
1
2
3
|
class
XXXItem(scrapy.Item):
href
=
scrapy.Field()
title
=
scrapy.Field()
|
c.配置settings文件
1
2
3
4
|
ITEM_PIPELINES
=
{
'xxx.pipelines.XXXPipeline'
:
300
,
# 'xxx.pipelines.XXXPipeline2': 600, # 后面的数字为优先级,数字越大,优先级月底
}
|
d.在parse方法中yield一个Item对象
1
2
3
4
5
|
from
xxx.items
import
XXXItem
def
parse(
self
, response):
...
yield
XXXItem(text
=
text,href
=
href)
|
执行流程为:
当我们在执行爬虫中的parse方法的时候,scrapy一旦解析到有yield XXXitem的语句,就会到配置文件中找
ITEM_PIPELINES的配置项,进而找到XXXPipeline类,然后执行其中的方法,我们就可以在方法中做很多操作
当然,pipeline中不止process_item一个方法。
Pipeline中的方法详解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
class
FilePipeline(
object
):
def
__init__(
self
,path):
self
.f
=
None
self
.path
=
path
@classmethod
def
from_crawler(
cls
, crawler):
"""
初始化时候,用于创建pipeline对象
:param crawler:
:return:
"""
# 从配置文件中获取配置好的文件存放目录
path
=
crawler.settings.get(
'HREF_FILE_PATH'
)
return
cls
(path)
def
open_spider(
self
,spider):
"""
爬虫开始执行时,调用
:param spider:
:return:
"""
self
.f
=
open
(
self
.path,
'a+'
)
def
process_item(
self
, item, spider):
# 在这里做持久化
self
.f.write(item[
'href'
]
+
'\n'
)
return
item
# 交给下一个pipeline的process_item方法
# raise DropItem()# 如果写上这一句,后续的 pipeline的process_item方法不再执行
def
close_spider(
self
,spider):
"""
爬虫关闭时,被调用
:param spider:
:return:
"""
self
.f.close()
|
去重
scrapy内部实现的去重
从上一篇的例子我们可以看出,其实scrapy内部在循环爬去页码的时候,已经帮我们做了去重功能的,
因为我们在首页可以看到1,2,3,4,5,6,7,8,9,10页的页码以及连接,当爬虫爬到第二页的时候,
还是可以看到这10个页面及连接,然后它并没有再重新把第一页爬一遍。
它内部实现去重的原理是,将已爬去的网址存入一个set集合里,每次爬取新页面的时候就先看一下是否在集合里面
如果在,就不再爬去,如果不在就爬取,然后再添加入到set里。当然,这个集合存放的不是原网址,
而是将链接通过request_fingerprint()方法将它变成一个类似于md5的值,这样可以节省存储空间
自定义去重
虽然scrapy已经帮我们实现了去重,但是有时候不足以满足我们的需求,这样就需要我们自定义去重了
自定义去重分两步
1、编写DupeFilter类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
from
scrapy.dupefilter
import
BaseDupeFilter
from
scrapy.utils.request
import
request_fingerprint
class
XXXDupeFilter(BaseDupeFilter):
def
__init__(
self
):
'''初始化一个集合,用来存放爬去过的网址'''
self
.visited_fd
=
set
()
@classmethod
def
from_settings(
cls
, settings):
'''
如果我们自定义了DupeFilter类并且重写了父类的该方法,
scrapy会首先执行该方法,获取DupeFilter对象,
如果没有定义,则会执行init方法来获取对象
'''
return
cls
()
def
request_seen(
self
, request):
'''在此方法中做操作,判断以及添加网址到set里'''
# 将request里的url转换下,然后判断是否在set里
fd
=
request_fingerprint(request
=
request)
# 循环set集合,如果已经在集合里,则返回True,爬虫将不会继续爬取该网址
if
fd
in
self
.visited_fd:
return
True
self
.visited_fd.add(fd)
def
open
(
self
):
# can return deferred
'''开始前执行此方法'''
print
(
'开始'
)
def
close(
self
, reason):
# can return a deferred
'''结束后执行此方法'''
print
(
'结束'
)
def
log(
self
, request, spider):
# log that a request has been filtered
'''在此方法中可以做日志操作'''
print
(
'日志'
)
|
2.配置settings文件
1
2
3
|
# 修改默认的去重规则
# DUPEFILTER_CLASS = 'scrapy.dupefilter.RFPDupeFilter'
DUPEFILTER_CLASS
=
'xxx.dupefilters.XXXDupeFilter'
|
深度
深度就是爬虫所要爬取的层级
限制深度只需要配置一下即可
1
2
|
# 限制深度
DEPTH_LIMIT
=
3
|
cookie
获取上一次请求之后获得的cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from
scrapy.http.cookies
import
CookieJar
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
parse(
self
, response):
# 去响应头中获取cookie,cookie保存在cookie_jar对象
cookie_jar
=
CookieJar()
cookie_jar.extract_cookies(response, response.request)
# 去对象中将cookie解析到字典
for
k, v
in
cookie_jar._cookies.items():
for
i, j
in
v.items():
for
m, n
in
j.items():
self
.cookie_dict[m]
=
n.value
|
再次请求的时候携带cookie
1
2
3
4
5
6
7
8
9
10
|
yield
Request(
url
=
'https://dig.chouti.com/login'
,
method
=
'POST'
,
body
=
"phone=861300000000&password=12345678&oneMonth=1"
,
#
cookies
=
self
.cookie_dict,
headers
=
{
'Content-Type'
:
'application/x-www-form-urlencoded; charset=UTF-8'
},
callback
=
self
.check_login
)
|
是不是感觉很麻烦?
那么,呵呵,其实,嘿嘿,
你只需要在Request对象的参数中加入 meta={'cookiejar': True} 即可!
网络爬虫之scrapy框架设置代理
前戏
os.environ()简介
os.environ()可以获取到当前进程的环境变量,注意,是当前进程。
如果我们在一个程序中设置了环境变量,另一个程序是无法获取设置的那个变量的。
环境变量是以一个字典的形式存在的,可以用字典的方法来取值或者设置值。
os.environ() key字段详解
windows:
1
2
3
4
5
6
|
os.environ[
'HOMEPATH'
]:当前用户主目录。
os.environ[
'TEMP'
]:临时目录路径。
os.environ[PATHEXT']:可执行文件。
os.environ[
'SYSTEMROOT'
]:系统主目录。
os.environ[
'LOGONSERVER'
]:机器名。
os.environ[
'PROMPT'
]:设置提示符。
|
linux:
1
2
3
4
5
|
os.environ[
'USER'
]:当前使用用户。
os.environ[
'LC_COLLATE'
]:路径扩展的结果排序时的字母顺序。
os.environ[
'SHELL'
]:使用shell的类型。
os.environ[
'LAN'
]:使用的语言。
os.environ[
'SSH_AUTH_SOCK'
]:ssh的执行路径。
|
内置的方式
原理
scrapy框架内部已经实现了设置代理的方法,它的原理是从环境变量中取出设置的代理,然后再使用,
所以我们只需要在程序执行前将代理以键值对的方式设置到环境变量中即可。
代码
第一种方式:直接添加键值对的方式
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
start_requests(
self
):
import
os
os.environ[
'HTTP_PROXY'
]
=
'19.11.2.32'
,
for
url
in
self
.start_urls:
yield
Request(url
=
url,callback
=
self
.parse)
|
第二种方式:设置meta参数的方式
1
2
3
4
5
6
7
8
9
|
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
start_requests(
self
):
for
url
in
self
.start_urls:
yield
Request(url
=
url,callback
=
self
.parse,meta
=
{
'proxy'
:
'"http://username:[email protected]:9999/"'
})
|
自定义方式
原理
我们可以根据内部实现的添加代理的类(中间件)的实现方法,来对它进行升级,比如内部的方式一次只能使用一个代理,
我们可以弄一个列表,装很多代理地址,然后随机选取一个代理,这样可以防止请求过多被封ip
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
start_requests(
self
):
for
url
in
self
.start_urls:
yield
Request(url
=
url,callback
=
self
.parse,meta
=
{
'proxy'
:
'"http://username:[email protected]:9999/"'
})
import
base64
import
random
from
six.moves.urllib.parse
import
unquote
try
:
from
urllib2
import
_parse_proxy
except
ImportError:
from
urllib.request
import
_parse_proxy
from
six.moves.urllib.parse
import
urlunparse
from
scrapy.utils.python
import
to_bytes
class
XXProxyMiddleware(
object
):
def
_basic_auth_header(
self
, username, password):
user_pass
=
to_bytes(
'%s:%s'
%
(unquote(username), unquote(password)),
encoding
=
'latin-1'
)
return
base64.b64encode(user_pass).strip()
def
process_request(
self
, request, spider):
PROXIES
=
[
]
url
=
random.choice(PROXIES)
orig_type
=
""
proxy_type, user, password, hostport
=
_parse_proxy(url)
proxy_url
=
urlunparse((proxy_type
or
orig_type, hostport, '
', '
', '
', '
'))
if
user:
creds
=
self
._basic_auth_header(user, password)
else
:
creds
=
None
request.meta[
'proxy'
]
=
proxy_url
if
creds:
request.headers[
'Proxy-Authorization'
]
=
b
'Basic '
+
creds
|
写完类之后需要在settings文件里配置一下:
1
2
3
|
DOWNLOADER_MIDDLEWARES
=
{
'spider.xxx.XXXProxyMiddleware'
:
543
,
}
|
twisted介绍
Twisted是用Python实现的基于事件驱动的网络引擎框架,scrapy正是依赖于twisted,
它是基于事件循环的异步非阻塞网络框架,可以实现爬虫的并发。
twisted是什么以及和requests的区别:
- request是一个python实现的可以伪造浏览器发送Http请求的模块,它封装了socket发送请求
- twisted是基于时间循环的异步非阻塞的网络框架,它也封装了socket发送请求,但是他可以单线程的完成并发请求。
twisted的特点是:
- 非阻塞:不等待
- 异步:回调
- 事件循环:一直循环去检查状态
scrapy的pipeline文件和items文件
这两个文件有什么作用
先看看我们上篇的示例:
在这个示例中,虽然我们已经通过chouti.py一个文件中的parse方法实现了爬去抽屉网的新闻并将之保存在文件中的功能,
但是我们会发现有两个问题:
1、在循环爬去每一页的时候,每次都需要重新打开然后再关闭文件,如果数据量庞大的话,这对性能有很大的影响。
2、我们将解析和数据持久化都放在了同一个文件的同一个方法中,没有做到分工明确
如果要解决这两个问题,则需要用到scrapy自动为我们生成的pipeline文件和items文件
这两个文件怎么用
如果我们要使用这两个文件从而解决问题,则需要有四部操作:
a.编写pipeline文件中的类,格式如下:
1
2
3
|
class
XXXPipeline(
object
):
def
process_item(
self
, item, spider):
return
item
|
b.编写items文件中的类,格式如下:
1
2
3
|
class
XXXItem(scrapy.Item):
href
=
scrapy.Field()
title
=
scrapy.Field()
|
c.配置settings文件
1
2
3
4
|
ITEM_PIPELINES
=
{
'xxx.pipelines.XXXPipeline'
:
300
,
# 'xxx.pipelines.XXXPipeline2': 600, # 后面的数字为优先级,数字越大,优先级月底
}
|
d.在parse方法中yield一个Item对象
1
2
3
4
5
|
from
xxx.items
import
XXXItem
def
parse(
self
, response):
...
yield
XXXItem(text
=
text,href
=
href)
|
执行流程为:
当我们在执行爬虫中的parse方法的时候,scrapy一旦解析到有yield XXXitem的语句,就会到配置文件中找
ITEM_PIPELINES的配置项,进而找到XXXPipeline类,然后执行其中的方法,我们就可以在方法中做很多操作
当然,pipeline中不止process_item一个方法。
Pipeline中的方法详解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
class
FilePipeline(
object
):
def
__init__(
self
,path):
self
.f
=
None
self
.path
=
path
@classmethod
def
from_crawler(
cls
, crawler):
"""
初始化时候,用于创建pipeline对象
:param crawler:
:return:
"""
# 从配置文件中获取配置好的文件存放目录
path
=
crawler.settings.get(
'HREF_FILE_PATH'
)
return
cls
(path)
def
open_spider(
self
,spider):
"""
爬虫开始执行时,调用
:param spider:
:return:
"""
self
.f
=
open
(
self
.path,
'a+'
)
def
process_item(
self
, item, spider):
# 在这里做持久化
self
.f.write(item[
'href'
]
+
'\n'
)
return
item
# 交给下一个pipeline的process_item方法
# raise DropItem()# 如果写上这一句,后续的 pipeline的process_item方法不再执行
def
close_spider(
self
,spider):
"""
爬虫关闭时,被调用
:param spider:
:return:
"""
self
.f.close()
|
去重
scrapy内部实现的去重
从上一篇的例子我们可以看出,其实scrapy内部在循环爬去页码的时候,已经帮我们做了去重功能的,
因为我们在首页可以看到1,2,3,4,5,6,7,8,9,10页的页码以及连接,当爬虫爬到第二页的时候,
还是可以看到这10个页面及连接,然后它并没有再重新把第一页爬一遍。
它内部实现去重的原理是,将已爬去的网址存入一个set集合里,每次爬取新页面的时候就先看一下是否在集合里面
如果在,就不再爬去,如果不在就爬取,然后再添加入到set里。当然,这个集合存放的不是原网址,
而是将链接通过request_fingerprint()方法将它变成一个类似于md5的值,这样可以节省存储空间
自定义去重
虽然scrapy已经帮我们实现了去重,但是有时候不足以满足我们的需求,这样就需要我们自定义去重了
自定义去重分两步
1、编写DupeFilter类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
from
scrapy.dupefilter
import
BaseDupeFilter
from
scrapy.utils.request
import
request_fingerprint
class
XXXDupeFilter(BaseDupeFilter):
def
__init__(
self
):
'''初始化一个集合,用来存放爬去过的网址'''
self
.visited_fd
=
set
()
@classmethod
def
from_settings(
cls
, settings):
'''
如果我们自定义了DupeFilter类并且重写了父类的该方法,
scrapy会首先执行该方法,获取DupeFilter对象,
如果没有定义,则会执行init方法来获取对象
'''
return
cls
()
def
request_seen(
self
, request):
'''在此方法中做操作,判断以及添加网址到set里'''
# 将request里的url转换下,然后判断是否在set里
fd
=
request_fingerprint(request
=
request)
# 循环set集合,如果已经在集合里,则返回True,爬虫将不会继续爬取该网址
if
fd
in
self
.visited_fd:
return
True
self
.visited_fd.add(fd)
def
open
(
self
):
# can return deferred
'''开始前执行此方法'''
print
(
'开始'
)
def
close(
self
, reason):
# can return a deferred
'''结束后执行此方法'''
print
(
'结束'
)
def
log(
self
, request, spider):
# log that a request has been filtered
'''在此方法中可以做日志操作'''
print
(
'日志'
)
|
2.配置settings文件
1
2
3
|
# 修改默认的去重规则
# DUPEFILTER_CLASS = 'scrapy.dupefilter.RFPDupeFilter'
DUPEFILTER_CLASS
=
'xxx.dupefilters.XXXDupeFilter'
|
深度
深度就是爬虫所要爬取的层级
限制深度只需要配置一下即可
1
2
|
# 限制深度
DEPTH_LIMIT
=
3
|
cookie
获取上一次请求之后获得的cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from
scrapy.http.cookies
import
CookieJar
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
parse(
self
, response):
# 去响应头中获取cookie,cookie保存在cookie_jar对象
cookie_jar
=
CookieJar()
cookie_jar.extract_cookies(response, response.request)
# 去对象中将cookie解析到字典
for
k, v
in
cookie_jar._cookies.items():
for
i, j
in
v.items():
for
m, n
in
j.items():
self
.cookie_dict[m]
=
n.value
|
再次请求的时候携带cookie
1
2
3
4
5
6
7
8
9
10
|
yield
Request(
url
=
'https://dig.chouti.com/login'
,
method
=
'POST'
,
body
=
"phone=861300000000&password=12345678&oneMonth=1"
,
#
cookies
=
self
.cookie_dict,
headers
=
{
'Content-Type'
:
'application/x-www-form-urlencoded; charset=UTF-8'
},
callback
=
self
.check_login
)
|
是不是感觉很麻烦?
那么,呵呵,其实,嘿嘿,
你只需要在Request对象的参数中加入 meta={'cookiejar': True} 即可!
前戏
os.environ()简介
os.environ()可以获取到当前进程的环境变量,注意,是当前进程。
如果我们在一个程序中设置了环境变量,另一个程序是无法获取设置的那个变量的。
环境变量是以一个字典的形式存在的,可以用字典的方法来取值或者设置值。
os.environ() key字段详解
windows:
1
2
3
4
5
6
|
os.environ[
'HOMEPATH'
]:当前用户主目录。
os.environ[
'TEMP'
]:临时目录路径。
os.environ[PATHEXT']:可执行文件。
os.environ[
'SYSTEMROOT'
]:系统主目录。
os.environ[
'LOGONSERVER'
]:机器名。
os.environ[
'PROMPT'
]:设置提示符。
|
linux:
1
2
3
4
5
|
os.environ[
'USER'
]:当前使用用户。
os.environ[
'LC_COLLATE'
]:路径扩展的结果排序时的字母顺序。
os.environ[
'SHELL'
]:使用shell的类型。
os.environ[
'LAN'
]:使用的语言。
os.environ[
'SSH_AUTH_SOCK'
]:ssh的执行路径。
|
内置的方式
原理
scrapy框架内部已经实现了设置代理的方法,它的原理是从环境变量中取出设置的代理,然后再使用,
所以我们只需要在程序执行前将代理以键值对的方式设置到环境变量中即可。
代码
第一种方式:直接添加键值对的方式
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
start_requests(
self
):
import
os
os.environ[
'HTTP_PROXY'
]
=
'19.11.2.32'
,
for
url
in
self
.start_urls:
yield
Request(url
=
url,callback
=
self
.parse)
|
第二种方式:设置meta参数的方式
1
2
3
4
5
6
7
8
9
|
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
start_requests(
self
):
for
url
in
self
.start_urls:
yield
Request(url
=
url,callback
=
self
.parse,meta
=
{
'proxy'
:
'"http://username:[email protected]:9999/"'
})
|
自定义方式
原理
我们可以根据内部实现的添加代理的类(中间件)的实现方法,来对它进行升级,比如内部的方式一次只能使用一个代理,
我们可以弄一个列表,装很多代理地址,然后随机选取一个代理,这样可以防止请求过多被封ip
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
class
ChoutiSpider(scrapy.Spider):
name
=
'chouti'
allowed_domains
=
[
'chouti.com'
]
start_urls
=
[
'https://dig.chouti.com/'
]
cookie_dict
=
{}
def
start_requests(
self
):
for
url
in
self
.start_urls:
yield
Request(url
=
url,callback
=
self
.parse,meta
=
{
'proxy'
:
'"http://username:[email protected]:9999/"'
})
import
base64
import
random
from
six.moves.urllib.parse
import
unquote
try
:
from
urllib2
import
_parse_proxy
except
ImportError:
from
urllib.request
import
_parse_proxy
from
six.moves.urllib.parse
import
urlunparse
from
scrapy.utils.python
import
to_bytes
class
XXProxyMiddleware(
object
):
def
_basic_auth_header(
self
, username, password):
user_pass
=
to_bytes(
'%s:%s'
%
(unquote(username), unquote(password)),
encoding
=
'latin-1'
)
return
base64.b64encode(user_pass).strip()
def
process_request(
self
, request, spider):
PROXIES
=
[
]
url
=
random.choice(PROXIES)
orig_type
=
""
proxy_type, user, password, hostport
=
_parse_proxy(url)
proxy_url
=
urlunparse((proxy_type
or
orig_type, hostport, '
', '
', '
', '
'))
if
user:
creds
=
self
._basic_auth_header(user, password)
else
:
creds
=
None
request.meta[
'proxy'
]
=
proxy_url
if
creds:
request.headers[
'Proxy-Authorization'
]
=
b
'Basic '
+
creds
|
写完类之后需要在settings文件里配置一下:
1
2
3
|
DOWNLOADER_MIDDLEWARES
=
{
'spider.xxx.XXXProxyMiddleware'
:
543
,
}
|