憨憨呆呆的IT之旅

我见,我思,我行

我们做后端开发,权限控制是绕不开的话题。几乎所有后台系统都需要权限控制模块。权限控制涉及的概念不多,但容易混淆,这里尝试简单梳理一下。为了便于大家理解,我们结合案例来加以说明。

什么是权限控制?

某个主体(subject)对某个客体(object)需要实施某种操作(operation),系统对这种操作的限制就是权限控制。
权限控制涉及三个概念:

  • **主体(subject)**:可以拥有权限的概念对象,比如 用户、角色、岗位、部门等。
  • **客体(object)**:可以被用来进行权限控制的概念对象,比如 文件、菜单、功能、操作等。
  • **操作(operation)**:主体对客体进行的访问动作,比如 读、写、执行、查询、修改、新增、删除等。

主体能够做什么,就是权限。权限可以细分为不同的能力。比如,在Linux文件系统中,文件具备读、写、执行三种能力。

要充分理解权限控制,需要仔细分辨以下几组概念。

  • 认证 和 授权
  • 访问 和 验证
  • 操作权限 和 数据权限

认证(Authentication) VS 授权(Authorization)

认证是识别操作者是谁的过程,解决Who am I的问题。
授权是识别操作者能够做什么事,解决What can I do的问题。

认证是进入系统的第一步,通常就是指系统中的“登录”这一步。认证的常见实现方式有:表单验证、Http Basic验证、OAuth2等。在做授权之前,需要先完成认证。举个例子,我要在Github中新建一个project,第一步要做的就是登录Github,完成用户鉴权,让Github知道我是谁,然后Github才能判断我是否有权限创建一个project。

认证只关注我是谁,不管后面的业务权限如何分配。通过认证进入系统的用户,能够做什么事,则交给授权来处理。

访问(Access) VS 验证(Validation)

访问和验证可以看做是授权的两个步骤。访问解决Can I call this operation的问题,验证解决Can I access the data behind this operation的问题。对于接口来说,我能不能调用这个接口,是访问问题,它与数据无关,可以在网关层拦截。而我能够调用这个接口,但能不能操作接口后面的数据,则需要验证。再拿Github举例,假定我要删除一个project。首先,我作为登陆用户,可以访问删除project接口,但我只能删除属于我的项目,不能删除别人的项目,这就是验证要做的工作。验证一般需要在接口的业务逻辑层实现。

操作权限 VS 数据权限

操作权限 就是控制用户可以访问的操作。数据权限则控制被访问数据的可见范围。
实际上,访问 和 验证 就分别对应了 操作权限 和 数据权限。

我们一般的权限管理系统,都是针对操作权限给出的解决方案。目前比较成熟的有 Apache Shiro、Spring Security等基于RBAC模型的框架。而对于数据权限,由于跟业务结合过于紧密,目前尚无统一的成熟框架。

总结

本文介绍了权限控制相关的核心概念,并对认证和授权、访问和验证、操作权限和数据权限这几组容易混淆的概念做了澄清。希望能够帮助大家在做权限控制系统设计时少走弯路。

幂等性是编程中一个很重要的概念。特别是我们在设计编程接口时,往往需要考虑接口是否满足幂等性。那么,什么是幂等性呢?幂等性具体应用于哪些场景?实现幂等性有哪些可行的方案?本文一一为您揭秘。

什么是幂等性

幂等原本是一个数学概念,用数学公式表示为:

1
f(f(x)) = f(x)

引入到计算机编程领域,幂等性是指客户端对服务端接口调用一次和调用多次,对服务端状态的影响是相同的。对于HTTP请求,一次请求和多次请求,如果对被请求资源本身的影响完全相同,则可认为请求是幂等的。同样地,对于一个微服务接口,如果多次调用和一次调用的效果一样,则认为这个接口是幂等的。
我们用幂等性的上述定义来检查数据库的增删改查。显然,查询和删除是幂等的。新增和更新操作,则不是幂等的。举反例如下:

  • 新增客户记录时,每执行一次insert操作,都会在数据库表中添加一条记录。
  • 更新文章浏览量时,需要在原有浏览量数值上执行+1操作,每执行一次,浏览量数值都会随之改变。

幂等是服务端对外的一种承诺,承诺只要调用服务端成功,无论客户端调用多少次,都不用担心因重复调用打乱服务端的状态。

什么场景下需要幂等性

  1. 不允许重复提交时。 比如电商系统下订单时,用户重复点击下单按钮,后台应避免重复下单、重复扣款。
  2. 客户端引入失败重试机制时。在某些情况下,服务端执行操作成功而未来得及把操作结果返回给客户端,客户端误认为服务端操作失败,自动发起重试。

如何实现幂等性

实现幂等性,总体思路就是先判断再插入/更新。也就是check if exist then insertcheck if not updated then update类似的操作。这类操作往往不是原子的,需要加锁。具体有以下几种方案:

  1. 使用唯一索引
    适用于业务上要求只存在一条记录的数据表,比如用手机号来作为客户的唯一标识,则针对手机号建立唯一索引,这样在插入相同手机号的客户记录时,数据库就会报错并回滚插入操作,从而保证幂等性。

  2. token校验
    借助外部系统存储的token来判断是否重复提交。具体做法是,服务端先生成一个token保存下来,然后发给客户端使用。客户端在提交时,请求中必须带上该token。服务端收到客户端请求后,在执行具体操作前,先校验客户端传入的token,看是否和服务端已存储的token一致。若一致则表明是第一次请求,可以开绿灯放行,并在操作完成后删除或更新token。若不一致或未找到,则认为是重复提交,不予放行。我们常见的csrf_token就是为防止表单重复提交而使用的。

  3. 悲观锁
    悲观锁一般配合事务一起使用,用于记录更新频繁的场景。在更新某条记录前,需要先使用select ... where id=111 for update语法来对记录加锁,然后再执行更新操作。

  4. 乐观锁
    乐观锁只需要在更新数据的瞬间锁表,往往比悲观锁更高效。一般用增加记录版本号的方式来实现乐观锁。比如下面这个微博计数表:

weibo_idrepost_countversion
1001374

每次转发,都需要将转发数+1。这里我们用version字段来保存记录版本号。更新时,为保证幂等性,需执行如下SQL:

1
update t_weibo_count set repost_count=repost_count+1, version=version+1 where id=1001 and version=4

这样,如果是第一次请求,version匹配,更新成功,version+1。而后续的请求,无论执行多少次,因为version在第一次请求后已经+1,和SQL中的version对不上,因此更新不会被执行。

  1. 分布式锁
    分布式锁可以变相地认为是一种token校验机制。业务系统在插入或更新数据前,需要先获得锁。操作完成后,需要释放锁。在锁定期间,其他客户端只能等待。分布式锁一般用redis和zookeeper实现。

  2. 有限状态机
    有些领域对象比如订单,它的状态较为固定,转化途径也比较确定。我们在构造订单更新SQL时,可以带上订单状态字段,类似上面乐观锁的情况。若订单状态不匹配,则认为是重复操作,不予执行。

总结

幂等性规定多次操作和一次操作对系统状态的影响是想同的。幂等性同时也是接口对外部调用方的一种承诺。在接口和系统设计中,我们需要充分衡量某个接口是否需要实现成幂等性的。实现幂等性的主要思路就是先判断再插入/更新,可通过判断唯一索引、单次有效token、是否有锁、状态是否匹配等方式来识别并无视重复操作,达到幂等性的要求。

普通dict实现计数统计

我们常常用Python的dict来做简单的计数统计。先看下面一段代码:

1
2
3
4
5
6
7
8
9
names = ['alice','bob','cindy','ocre','alice','ocre','ocre','cindy','tom']
name_counts = {}
for name in names:
if name not in counts:
name_counts[name] = 1
else:
name_counts[name] += 1

print(name_counts)

这段代码能够统计出每个名字出现的次数。这里我们使用了普通的dict结构names_counts来保存名字和出现次数的映射关系。我们知道,如果直接访问字典中不存在的键,会抛出KeyError异常。比如:

1
2
3
4
5
6
>>> name_counts = {}
>>> name_counts['ocre'] += 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'ocre'
>>>

为了避免KeyError异常,通常需要使用一个if name in name_counts: ... else ...的代码段来检查词典中是否存在特定的键。如果键已经存在,则直接累加操作,如果不存在,则先赋一个初始值。

引入defaultdict来简化代码

对于有代码洁癖的猿来说,这样的代码仍然有一股坏味道。我们可以用defaultdict来简化一下。简化后的代码如下:

1
2
3
4
5
6
7
8
from collections import defaultdict

names = ['alice','bob','cindy','ocre','alice','ocre','ocre','cindy','tom']
name_counts = defaultdict(int)
for name in names:
name_counts[name] +=1

print(name_counts)

可见,在引入defaultdict后,烦人的if-else被干掉了。(BTW, 统计代码还可以利用其他方法进一步简化,参考我的另一篇博文:Python中实现计数统计的4种方法

引申:如何构造defaultdict?

这里我们使用了defaultdict来代替普通dict,来给name_counts中不存在的键设置默认值,默认值由defaultdict()的第一个参数确定。

构造defaultdict,需传递一个工厂函数作为参数:

1
mydict = collections.defaultdict( factory_function )

这里的工厂函数,可以是普通类型,比如 list,set,str,int等,当访问一个不存在键不存在时,返回这些类型的默认值,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> from collections import defaultdict
>>>
>>> d1 = defaultdict(int)
>>> d2 = defaultdict(list)
>>> d3 = defaultdict(set)
>>> d4 = defaultdict(str)
>>> d1
defaultdict(<class 'int'>, {})
>>> d1['key']
0
>>> d2['key']
[]
>>> d3['key']
set()
>>> d4['key']
''
>>>

工厂函数也可以是普通的无参函数,比如:

1
2
3
4
5
6
7
8
9
10
11
>>> from collections import defaultdict
>>>
>>> def factory_func():
... return []
...
>>> d5 = defaultdict(factory_func)
>>> d5
defaultdict(<function factory_func at 0x0000027817B3F378>, {})
>>> d5['ocre']
[]
>>>

工厂函数还可以用lambda表达式(匿名函数)来代替,比如:

1
2
3
4
5
6
7
8
>>> from collections import defaultdict
>>>
>>> d6 = defaultdict(lambda : 0)
>>> d6
defaultdict(<function <lambda> at 0x0000027817C2E268>, {})
>>> d6['ocre']
0
>>>

我们在日常开发过程中经常会遇到这样的需求,统计某个IP的访问次数,统计某个商户的订单数 等等。这类统计统称为计数统计。
作为一门优秀的高级语言,Python提供了至少4种方式来方便地实现计数统计。下面我们举例说明。
首先给出问题: 给定下面的输入和输出,实现name_count方法。

1
2
3
4
5
6
7
8
9
# Input:
names = ['alice','bob','cindy','ocre','alice','ocre','ocre','cindy','tom']

# Inteface
result = name_count(names)

# Output:
{'alice': 2, 'bob': 1, 'cindy': 2, 'ocre': 3, 'tom': 1}

1. 使用dict字典实现计数统计

1
2
3
4
5
6
7
8
def name_count(names):
name_counts = {}
for name in names:
if name not in counts:
name_counts[name] = 1
else:
name_counts[name] += 1
return name_counts

2. 使用defaultdict实现计数统计

1
2
3
4
5
6
7
from collections import defaultdict

def name_count(names):
name_counts = defaultdict(lambda: 0)
for name in names:
name_counts[name] += 1
return name_counts

3. 使用set和list实现计数统计

1
2
3
4
5
6
def name_count(names):
name_set = set(names)
name_list = []
for name in name_set:
name_list.append(name, names.count(name))
return name_list

4.使用Counter实现计数统计

1
2
3
4
from collections import Counter

def name_count(names):
return dict(Counter(names))

可见,使用Counter类的第4种方法代码最简洁。
有了以上4种方法,平时面对小规模数据集的计数统计问题时就能够手到擒来了。

更多统计实现方式等待发现!

看到这么一段Flask代码:

1
2
3
4
5
6
view_data = dict(
rows = posts[start:end],
page = page,
has_next_page = has_next_page,
)
return render_template('posts.html', **view_data)

代码中最后一行时,发现里边有**这样两个星号连在一起用的语法,不解其意。查阅资料后,才发现星号运算符还是真是神通广大。具体来说,有4种不同用途。

1. 算数运算符

* 表示乘法运算, **表示乘方运算。

1
2
3
4
>>> 3 * 2
6
>>> 3 ** 2
9

2. 序列解包

序列解包是Python的一种语法糖,可以用来简化代码。
普通的序列解包,要求赋值运算符=左侧的变量个数和右侧的变量值个数相等。示例如下:

1
2
3
4
5
>>> lat, lon = 108, 33
>>> lat
108
>>> lon
33

=左边的变量个数和右侧的变量值个数不相等,则解包失败报错:

1
2
3
4
5
>>> a, b = 1, 2, 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)
>>>
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> a, b, c = 1, 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)
>>>

这时,就可以使用 `*`星号运算符来处理了。`*`运算符将右侧多余的变量值**按顺序合并**成一个序列赋值给`*`标记的变量,用法示例如下:
​```python
>>> a, *b = 1, 2, 3
>>> a
1
>>> b
[2, 3]
1
2
3
4
5
>>> *a, b = 1, 2, 3
>>> a
[1, 2]
>>> b
3
1
2
3
4
5
>>> a, *b = 1, 2
>>> a
1
>>> b
[2]
1
2
3
4
5
>>> a, *b = 1
>>> a
1
>>> b
[]

3. 函数形参

***都可以用作函数的形参,用来将不定数量的参数传递给一个函数。一个星号用来传递元组tuple,两个星号用来传递字典dict,通常用*args*kwargs来分别表示。
示例如下:

1
2
3
4
5
6
>>> def func(a, *args):
... print(a, args)
...
>>> func(1, 2, 3)
1 (2, 3)
>>>
1
2
3
4
5
>>> def func(a, **kwargs):
... print(a, kwargs)
...
>>> func(1, lat=108, lon=33)
1 {'lat': 108, 'lon': 33}
1
2
3
4
5
>>> def func(*args, **kwargs):
... print(args, kwargs)
...
>>> func(1, 2, 3, name='ocre', sex='male')
(1, 2, 3) {'name': 'ocre', 'sex': 'male'}

4. 函数实参

为避免实参过长,可以用***对多个参数进行打包,示例如下:

1
2
3
4
5
6
7
8
>>> def func(a, **kwargs):
... print(a, kwargs)
...
>>> func(1, name='ocre', sex='male')
1 {'name': 'ocre', 'sex': 'male'}
>>> data = {'name':'ocre', 'sex':'male'}
>>> func(1, **data)
1 {'name': 'ocre', 'sex': 'male'}

本文开头我碰到的情况,就属于最后这种用法。

问题描述

写Blog时,想展示时序图和流程图,网友给推荐了流程图插件hexo-filter-flowchart和时序图插件hexo-filter-sequence。装上两个插件之后,根据github上的示例测试了一下,发现在Hexo中时序图无法显示。

问题处理

根据这篇博文的指示,顺利解决了这个问题。修改过程记录如下:

  1. 修改node_modules\hexo-filter-sequence\index.js文件,flowchart改为sequence,另外去掉snap.svg.js添加rahpael.js,具体改动部分如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    hexo.config.sequence = assign({
    webfont: 'https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.27/webfontloader.js',
    // snap: 'https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js',
    raphael: 'https://cdn.bootcss.com/raphael/2.3.0/raphael.min.js',
    underscore: 'https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js',
    sequence: 'https://cdnjs.cloudflare.com/ajax/libs/js-sequence-diagrams/1.0.6/sequence-diagram-min.js',
    css: '',
    options: {
    theme: 'simple'
    }
    }, hexo.config.sequence);
  2. 修改node_modules\hexo-filter-sequence\lib\renderer.js文件第25行左右,flowchart改为sequenceconfig.snap改为config.rahpael,改动后的内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    if (sequences.length) {
    var config = this.config.sequence;
    // resources
    data.content += '<script src="' + config.webfont + '"></script>';
    // data.content += '<script src="' + config.snap + '"></script>';
    data.content += '<script src="' + config.raphael + '"></script>';
    data.content += '<script src="' + config.underscore + '"></script>';
    data.content += '<script src="' + config.sequence + '"></script>';
  3. hexo的_config.yml配置文件中启用国内CDN(默认的cdnjs.cloudflare.com国内可能无法访问),修改如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # sequence config
    sequence:
    webfont: 'https://cdn.bootcss.com/webfont/1.6.28/webfontloader.js'
    raphael: 'https://cdn.bootcss.com/raphael/2.3.0/raphael.min.js'
    # snap: 'https://cdn.bootcss.com/snap.svg/0.5.1/snap.svg-min.js'
    underscore: 'https://cdn.bootcss.com/underscore.js/1.9.1/underscore-min.js'
    sequence: 'https://cdn.bootcss.com/js-sequence-diagrams/1.0.6/sequence-diagram-min.js'
    css: # optional, the url for css, such as hand drawn theme
    options:
    theme:
    css_class:
    1
    2
    3
    4
    5
    6
    # flowchart config
    flowchart:
    raphael: 'https://cdn.bootcss.com/raphael/2.3.0/raphael.min.js'
    flowchart: 'https://cdn.bootcss.com/flowchart/1.12.2/flowchart.min.js'
    options: # options used for `drawSVG`

修改完成后,依次执行hexo clean,hexo g,hexo d重新部署,问题解决。

附测试代码:

1
2
3
4
5
6
Client->Server: request
Note right of Server: WSGI
Server->Application: app_callable(environ, start_response)
Application->Server: start_response(status, response_headers, exc_info=None)
Application->Server: return iterator
Server->Client: response

展示效果如下:

1
2
3
4
5
6
Client->Server: request
Note right of Server: WSGI
Server->Application: app_callable(environ, start_response)
Application->Server: start_response(status, response_headers, exc_info=None)
Application->Server: return iterator
Server->Client: response

什么是WSGI?

WSGI的全称是Web Server Gateway Interface,即Web服务器网管接口。注意,它不是一个服务器、不是Python模块、不是框架、也不是API程序,它不是任何一种软件,而仅仅是Python语言针对Web服务器和Web应用程序之间通用接口的规范(PEP 3333)。符合WSGI规范的应用程序可以运行在任何符合该规范的Web服务器上。

WSGI规范

WSGI规范十分简单。下面这张时序图展示了WSGI所处的位置,以及调用规则。

1
2
3
4
5
6
Client->Server: request
Note right of Server: WSGI
Server->Application: app_callable(environ, start_response)
Application->Server: start_response(status, response_headers, exc_info=None)
Application->Server: return iterator
Server->Client: response

从上图可见,WSGI处于ServerApplication之间。Server端负责实现start_response这个callbackApplication端负责实现app_callable这个callable对象。
其中,app_callable接受两个参数:

  • environ: 包含有Server提供的所有请求信息的一个dict对象。
  • start_response: Server端提供的回调方法,Application端可以通过它发送HTTP状态码和HTTP头部信息。
    app_callable最后返回一个封装成可迭代对象的响应体字符串。

以下是一个简单的app_callable实现:

1
2
3
4
5
6
7
8
def app_callable(environ, start_response):
response_body = 'Request method: %s' % environ['REQUEST_METHOD']
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]

start_response(status, response_headers)

return [response_body]

WSGIApplication端可以支持堆栈式调用。调用栈中间的Application又被称为MiddlewareMiddleware同时扮演ServerApplication两种角色,因此需要同时实现WSGI两端的接口。
下图是对Middleware所处位置的一个简单表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Note right of Server: WSGI
Server->Application1:
Note right of Application1: WSGi
Middleware1->Application1:
Note right of Middleware1: WSGI
Application1->Middleware2:
Note right of Middleware2: WSGI
Middleware2->Middleware3:
Note right of Middleware3: WSGI
Middleware3->Application2:
Application2->Middleware3:
Middleware3->Middleware2:
Middleware2->Middleware1:
Middleware1->Application1:

WSGI实现示例

在生产环境,一般用Apache+mod_wsgi来作为Server端的标准实现。这里我们使用Python内置的WSGI服务器wsgiref来实现一个简单示例。
编写一个wsgi_test.py文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""test wsgi
"""

# wsgi_test.py

from wsgiref.simple_server import make_server

def application (environ, start_response):
status = '200 OK'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)

return [f'Request {environ["REQUEST_METHOD"]}'
f' {environ["PATH_INFO"]} has been'
f' processed\r\n'.encode('utf-8')]

server = make_server('localhost', 8000, application)

# Wait for a single request, serve it and quit
print('Serving HTTP on port 8000...')
server.serve_forever()

接着在命令行运行python wsgi_test.py,启动WSGI服务器。然后打开浏览器,输入http://localhost:8000/ 就可以看到效果了。

什么是JSON Lines

JSON Lines是一种类似JSON的文本格式。它采用\\n作为分隔符存储多个JSON对象的列表,每个JSON对象仍然使用JSON格式表示。它有三个硬性约束:
1.使用UTF-8编码。
2.每一行都是一个有效的JSON对象。
3.行分隔符为\\n

JSON Line文件的建议扩展名是.jsonl。一般建议使用gzip或者bzip2进行压缩,生成.jsonl.gz.jsonl.bz2文件,进一步节省存储空间。

为什么使用JSON Lines

我们平时习惯用CSVJSON文本格式来存储列表类对象。同样的数据,CSV相比JSON更简洁,更节省空间,但是可读性很差(数据列分隔符随便定义没有统一标准,数据列定义只在第一行体现,不够直观),并且不支持嵌套数据。而JSON格式的可读性更好,能够支持嵌套数据,但是在存储数据列表时,需要识别数组首尾的[],涉及到读取、解析、存储整个文件,不适合大文件的高效存取。因此,JSON Lines应运而生。采用 JSON Lines 保存数据集,则文件中每一行就是一条JSON数据,可以一边读取一边解析、操作。而在添加数据时,只需要 append 数据到文件尾部即可。

示例

一个简单的JSON Lines格式文件内容如下:

1
2
3
4
{"name": "Gilbert", "wins": [["straight", "7♣"], ["one pair", "10♥"]]}
{"name": "Alexa", "wins": [["two pair", "4♠"], ["two pair", "9♠"]]}
{"name": "May", "wins": []}
{"name": "Deloise", "wins": [["three of a kind", "5♣"]]}

应用场景

JSON Lines文本格式由于同时具备可读性和便于流式处理的特点,常常被用于以下场景:
1.日志记录
2.流数据API
3.爬虫数据存储

什么是Conda?

Conda是一种通用包管理系统,旨在构建和管理任何语言和任何类型的软件。Conda主要提供包管理和环境管理两种能力。包管理与pip的使用类似,环境管理则允许用户同时安装多个版本的python,并支持快速切换。

Conda常用命令

Conda包管理

1.查看已安装的所有包: conda list
2.查看已安装的特定包:conda list scrapy
3.搜索云上的包: conda search scrapy
4.安装包: conda install scrapy
5.一次安装多个包: conda install numpy pandas scipy
6.安装固定版本的包: conda install numpy=1.10
7.从指定频道安装包: conda install scrapy -c conda-forge
8.升级一个包: conda update scrapy
9.升级全部包: conda upgrade –all

Conda环境管理

1.查看所有虚拟环境: conda env list
2.激活特定环境:activate env_name
3.退出环境:deactivate env_name
4.创建虚拟环境:conda create -n env_name
5.创建虚拟环境并指定特定Python版本:conda create -n env_name python2=2.7
6.删除虚拟环境:conda env remove -n env_name

准备Python3运行环境

Scrapy基于Python语言编写,因此运行Scrapy需要先安装Python运行环境。所谓Python运行环境指可以编译执行Python代码的软件集合。已经安装了运行环境的请直接跳过。
目前常见的Python运行环境有三种方式:
1.直接安装Python3
2.安装Anaconda
3.安装Miniconda

我选择安装Anaconda,因为相对于直接安装Python3,Anaconda提供了更简便强大的包管理和环境管理功能。
Anaconda是Python的一个科学计算发行版,支持 Linux, Mac和Windows系统,可以很方便地解决多版本python并存、切换以及各种第三方包安装问题。Anaconda内置了数百个Python标准库,装上Anaconda,就相当于把Python和一些如Numpy、Pandas、Scrip、Matplotlib等常用的库自动安装好了,使得安装比常规python安装要容易。因而建议直接安装Anaconda。直接从官网下载最新版Anaconda社区版安装包安装即可。

当然,喜欢掌控一切的同学可以选择安装Miniconda。Miniconda可以看做是Anaconda的一个精简版。如果拿房子来比喻,Anaconda是精装房,Miniconda是毛坯房。Miniconda去掉了Anaconda内置的大量常用工具包,只提供一个较为纯净的Python运行环境和Conda(Conda是一种通用包管理系统,旨在构建和管理任何语言和任何类型的软件。Conda简介及基本用法)。需要什么包就自己装什么包。

装好Anaconda后,就可以继续安装Scrapy了。

安装Scrapy

按惯例,参照Scrapy官网的建议进行安装。官网提供了conda和pip两种安装方式,一条命令即可搞定。

1
2
# 选择conda方式安装
conda install scrapy -c conda-forge
1
2
# 选择pip方式安装
pip install scrapy

安装好Scrapy后,就可以创建Scrapy项目了。

创建Scrapy项目

首先进入将要存放Scrapy工程代码的目录,执行如下命令即可创建一个名为tutorial的工程。

1
scrapy startproject tutorial

我在执行上面命令的时候系统报错了,提示Fatal error in launcher: Unable to create process using '"d:\bld\scrapy_1572360424769\_h_env\python.exe"。改为执行以下命令即可:

1
python -m scrapy startproject tutorial

执行成功后,将在当前目录下创建一个新的tutorial目录,目录结构如下:

1
2
3
4
5
6
7
8
9
10
tutorial/
scrapy.cfg # Scrapy的部署配置文件
tutorial/ # Scrapy项目的Python模块
__init__.py
items.py # items definition file
middlewares.py # project middlewares file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # spider子模块
__init__.py

接下来就可以编写自己的spider类了。请参考Scrapy官方的Tutorial

Scrapy常见命令

  • 创建spider(基于basic模板):
    1
    python -m scrapy genspider quotes "quotes.toscrape.com"
  • 运行spider:
    1
    python -m scrapy crawl quotes
    或者
    1
    python -m scrapy runspider quotes.py
  • 测试提取数据:
    1
    python -m scrapy shell 'http://quotes.toscrape.com/page/1/'
  • 导出数据到JSON文档:
    1
    python -m scrapy crawl quotes -o quotes.json
0%