python实现的json数据以HTTP GET,POST,PUT,DELETE方式页面请求

【转载】文章转载自光阴过客

一、JSON简介

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。
它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition – December 1999的一个子集。
JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。
这些特性使JSON成为理想的数据交换语言。

二、HTTP的请求方法

HTTP/1.1协议中共定义了八种方法(有时也叫“动作”)来表明Request-URI指定的资源的不同操作方式:
. OPTIONS – 返回服务器针对特定资源所支持的HTTP请求方法。
也可以利用向Web服务器发送’*’的请求来测试服务器的功能性。
. HEAD    – 向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。
这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
. GET     – 向特定的资源发出请求。
注意:GET方法不应当被用于产生“副作用”的操作中,例如在web app.中。
其中一个原因是GET可能会被网络蜘蛛等随意访问。
. POST    – 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。
数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
. PUT     – 向指定资源位置上传其最新内容。
. DELETE  – 请求服务器删除Request-URI所标识的资源。
. TRACE   – 回显服务器收到的请求,主要用于测试或诊断。
. CONNECT – HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
. PATCH   – 用来将局部修改应用于某一资源,添加于规范RFC5789。

其中,GET,POST, PUT, DELETE常用于RESTful API的实现,所以下面做的代码实现

三、Python实现的json数据以HTTP GET,POST,PUT,DELETE方式进行页面请求

闲言少述,直接上代码.

1. GET方法

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# File: http_get.py

import urllib2

def http_get():
url=’http://192.168.1.13:9999/test’   #页面的地址
response = urllib2.urlopen(url)         #调用urllib2向服务器发送get请求
return response.read()                     #获取服务器返回的页面信息

ret = http_get()
print(“RET %r” % (ret))

2. POST方法

#!/usr/bin/env python
#  -*- coding:utf-8 -*-
# File http_post.py

import urllib
import urllib2
import json

def http_post():
url=’http://192.168.1.13:9999/test’
values ={‘user’:’Smith’,’passwd’:’123456}

jdata = json.dumps(values)             # 对数据进行JSON格式化编码
req = urllib2.Request(url, jdata)       # 生成页面请求的完整数据
response = urllib2.urlopen(req)       # 发送页面请求
return response.read()                    # 获取服务器返回的页面信息

resp = http_post()
print resp

3. PUT方法

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# File: http_put.py

import urllib2
import json

def http_put():
url=’http://192.168.1.13:9999/test’
values={”:”}

jdata = json.dumps(values)                  # 对数据进行JSON格式化编码
request = urllib2.Request(url, jdata)
request.add_header(‘Content-Type’, ‘your/conntenttype’)
request.get_method = lambda:’PUT’           # 设置HTTP的访问方式
request = urllib2.urlopen(request)
return request.read()

resp = http_put()
print resp

4. DELETE方法

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# File: http_delete.py

import urllib2
import json

def http_delete():
url=’http://192.168.1.13:9999/test’
values={‘user’:’Smith’}

jdata = json.dumps(values)
request = urllib2.Request(url, jdata)
request.add_header(‘Content-Type’, ‘your/conntenttype’)
request.get_method = lambda:’DELETE’        # 设置HTTP的访问方式
request = urllib2.urlopen(request)
return request.read()

resp = http_delete()
print resp

Python Flask Restful API

【转载】文章转载自bestallen

目前Web应用这块,restufl API用得非常普遍,因为,你手上的前端设备五花八门,各种系统的手机,pad等等,而且网站和手机APP经常会有需要资源共享的时候。

如果网站做个app,手机端再独立一个app,要保持2者同步,估计做开发的要做死了。

所以,RESTFUL API提出了一个概念,就是资源为上,通俗地讲:就是,我有一个资源A,可以作为接口来提供出来,那么,前端设备的B,C,D 都可以通过申请这个API接口的方式,来进行获取资源,而最后显示画面如何渲染,让客户看到是怎么样的一个样子,那是前端的事情了。

学习API之前,有两个前置技能需要去了解下,一个是JSON数据格式,另外一个是curl或者HTTPie这样的http调试工具,我这里用的是HTTPie

最简单的Web API

先来看看通过Python Flask做一个非常简单的Web API接口范例

[python] view plain copy

  1. from flask import Flask,jsonify
  2. app = Flask(__name__)
  3. tasks =[
  4.     {
  5.         ‘id’:1,
  6.         ‘title’: u‘Buy groceries’,
  7.         ‘description’: u‘Milk, Cheese, Pizza, Fruit, Tylenol’,
  8.         ‘done’False
  9.     },
  10.     {
  11.         ‘id’2,
  12.         ‘title’: u‘Learn Python’,
  13.         ‘description’: u‘Need to find a good Python tutorial on the web’,
  14.         ‘done’False
  15.     }]
  16. @app.route(‘/’)
  17. def index():
  18.     return ‘Hello,world!’
  19. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘GET’])
  20. def get_tasks():
  21.     return jsonify({‘tasks’:tasks})

如上面代码,当你请求 /todo/api/v1.0/tasks 的时候,通过路由get_tasks,就能返回当前所有的数据信息

他是通过jsonify来进行返回的,这里需要注意的是,有2种方法可以返回json数据,一个是jsonify,还有一个是json.dumps()

那么2者有什么区别呢?可以看下图

jsonify直接返回的是Content-type:application/json的响应对象(Response对象)

json.dumps返回的,则是Content-type:text/html,charset=utf-8的HTML格式

我们可以看下jsonfiy的官方解释


来看下效果图及两者的区别

返回单个数据

刚才是获取了所有的任务,那如果我需要其中某一个数据的话,如何做呢?

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks/<int:task_id>’,methods=[‘GET’])
  2. def get_task(task_id):
  3.     task = list(filter(lambda t:t[‘id’]==task_id,tasks))   #检查tasks内部元素,是否有元素的id的值和参数id相匹配
  4.     if len(task)==0:                                       #有的话,就返回列表形式包裹的这个元素,如果没有,则报错404
  5.         abort(404)
  6.     return jsonify({‘tasks’:task[0]})                      #否则,将这个task以JSON的响应形式返回

在URL里加入变量,通过检查tasks内部每个元素的id对应的值,是否有和输入的id匹配的,如果有匹配的,那就返回成一个list给task变量

比如下面,他就获取了id为2的这个数据的内容,当然,如果你在URL后面添加的是3,那他就会报错了,因为暂时还没有id为3的这个数据

上面的报错画面有些难看,而且,这样的报错格式,不太利于别人接口的使用。

一般像正规的接口,他都会以JSON格式返回error内容或者代码,所以,我们也要优化一下这样的错误信息。

[python] view plain copy

  1. @app.errorhandler(404)
  2. def not_found(error):
  3.     return jsonify({‘error’:‘Not found’}),404

 

这样,返回的格式,也是以JSON的格式,那样,如果别人调用你的接口,如果发生错误,别人就可以通过error信息,来进行渲染了。

POST方法添加信息

接下来,既然有数据,那肯定会涉及到添加数据,那我们就要用到POST方法了,就和上传FORM表单的性质是一样的

HTTPie的好处是,他默认的POST格式是JSON的,所以,你不必特意指定格式,只要写一个POST,就ok,如下

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘POST’])
  2. def create_task():
  3.     if not request.json or not ‘title’ in request.json:   #如果请求里面没有JSON数据,或者在JSON数据里面,title的内容是空的
  4.         abort(404)                                    #返回404报错
  5.     task = {
  6.         ‘id’:tasks[-1][‘id’]+1,                       #取末尾task的id号,并加一作为新的数据的id号
  7.         ‘title’:request.json[‘title’],                #title必须要设置,不能为空
  8.         ‘description’:request.json.get(‘description’,””),     #描述可以添加,但是也可以不写,默认为空
  9.         ‘done’:False
  10.     }
  11.     tasks.append(task)                                    #完了以后,添加这个task进tasks列表
  12.     return jsonify({‘task’:task}),201                     #并返回这个添加的task内容,和状态码

 

后面加入的信息,你写title=test或者title=”test”都可以


PUT方法修改和DELETE方法删除

既然已经有了添加的功能,那么修改和删除的也必不可少,如下

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks/<int:task_id>’,methods=[‘PUT’])
  2. def update_task(task_id):
  3.     task = list(filter(lambda t:t[‘id’]==task_id,tasks))     #检查是否有这个id的数据
  4.     if len(task)==0:
  5.         abort(404)
  6.     if not request.json:                                     #如果请求中没有附带json数据,则报错400
  7.         abort(400)
  8.     if ‘title’ in request.json and not isinstance(request.json[‘title’],str):   #如果title对应的值,不是字符串类型,则报错400
  9.         abort(400)
  10.     if ‘description’ in request.json and not isinstance(request.json[‘description’],str):  #同上,检查description对应的值是否为字符串
  11.         abort(400)
  12.     if ‘done’ in request.json and not isinstance(request.json[‘done’],bool):    #检查done对应的值是否是布尔值
  13.         abort(400)
  14.     task[0][‘title’] = request.json.get(‘title’,task[0][‘title’])          #如果上述条件全部通过的话,更新title的值,同时要设置默认值
  15.     task[0][‘description’] = request.json.get(‘description’, task[0][‘description’])  #修改description值
  16.     task[0][‘done’] = request.json.get(‘done’, task[0][‘done’])            #修改done的值
  17.     return jsonify({‘task’:task[0]})                                       #最后,返回修改后的数据
  18. @app.route(‘/todo/api/v1.0/tasks/<int:task_id>’,methods=[‘DELETE’])
  19. def delete_task(task_id):
  20.     task = list(filter(lambda t: t[‘id’]==task_id,tasks))     #检查是否有这个数据
  21.     if len(task) ==0:
  22.         abort(404)
  23.     tasks.remove(task[0])                                     #从tasks列表中删除这个值
  24.     return jsonify({‘result’True})                          #返回结果状态,自定义的result

优化接口

这样做接口系统,功能是达到了,但是你不见得让用户去背你的URL吧?这样不现实,像我们平时用的话,最好是可以自动生成URL

所以,这里我们需要写一个辅助函数,来返回一个完整的URL,这样可以就可以直接拿着URL用了

[python] view plain copy

  1. def make_public_task(task):
  2.     new_task={}             #新建一个对象,字典类型
  3.     for key in task:        #遍历字典内部的KEY
  4.         if key == ‘id’#当遍历到id的时候,为新对象增加uri的key,对应的值为完整的uri
  5.             new_task[‘uri’] = url_for(‘get_task’,task_id=task[‘id’],_external=True)
  6.         else:
  7.             new_task[key] = task[key]  #其他的key,分别一一对应加入新对象
  8.     return new_task                            #最后返回新对象数据

这样的话,你获取数据合集的路由,也需要修改了

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘GET’])
  2. def get_tasks():
  3.     return jsonify({‘tasks’: list(map(make_public_task,tasks))})  #使用map函数,罗列出所有的数据,返回的数据信息,是经过辅助函数处理的

也就是说,经过辅助函数处理,你就可以直接获取整体URI了,而不是单个id号

接口安全性

接口的应用功能上,已经做得差不多了,但是现在的接口外圈是对外开放的,如果有人恶意破坏,数据就有可能有危险

所以,这里引入了认证机制,其实简单来说就是要求客户端的用户有使用使用权限,比如有账号和密码。

这里引入HTTPBasicAuth

auth=HTTPBasicAuth()

[python] view plain copy

  1. @auth.get_password
  2. def get_password(username):
  3.     if username == “allen”:
  4.         return “python”
  5.     return None
  6. @auth.error_handler
  7. def unauthorized():
  8.     return make_response(jsonify({‘error’:‘Unauthorized access’}),401)

通过客户端发送的用户名和密码,匹配get_password函数内部这个用户名和密码是否匹配

如果没有用户名密码,或者说是错误的,则返回401错误

于是,像查看资料啊这些的路由,都要给他挂一个login_required的装饰器了

[python] view plain copy

  1. @app.route(‘/todo/api/v1.0/tasks’,methods=[‘GET’])
  2. @auth.login_required
  3. def get_tasks():
  4.     return jsonify({‘tasks’: list(map(make_public_task,tasks))})

如果你没有附带用户名密码,那么,就会禁止访问了

附带了用户名和密码的话,就可以有权限进行操作

python_socket

python下tcp服务也分为服务端和客户端

实测服务端在ubuntu下,客户端在windows下正常运行

*客户端运行在ubuntu下会提示input报错,替换为固定str正常数据传输,正在查找原因……

(问题找到了,是输入用input未加“双引号”引起的输入不合法,可通过raw_input函数解决,点击链接到原文查看详细解决办法)

继续阅读python_socket

python笔记函数

Python内置了很多有用的函数,我们可以直接调用。

要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs
调用函数就是向函数里传入相应的参数(按照顺序一个或多个参数)并得到结果的过程

调用abs函数:

>>> abs(100)
100
>>> abs(-20)
20
>>> abs(12.34)
12.34

 

>>> abs(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: abs() takes exactly one argument (2 given)

如果传入的参数数量是对的,但参数类型不能被函数所接受,也会报TypeError的错误,并且给出错误信息:str是错误的参数类型:

>>> abs('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'

数据类型转换

Python内置的常用函数还包括数据类型转换函数,比如int()函数可以把其他数据类型转换为整数:

>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False

函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

定义函数

在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。

我们以自定义一个求绝对值的my_abs函数为例:

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

如果你已经把my_abs()的函数定义保存为abstest.py文件了,那么,可以在该文件的当前目录下启动Python解释器,用from abstest import my_abs来导入my_abs()函数,注意abstest是文件名(不含.py扩展名)

递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:

fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x n

所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

于是,fact(n)用递归的方式写出来就是:

def fact(n):
    if n==1:
        return 1
    return n * fact(n - 1)

 

 

python笔记

list

Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。

>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']

变量classmates就是一个list。用len()函数可以获得list元素的个数:

>>> len(classmates)
3

tuple

另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:

>>> classmates = ('Michael', 'Bob', 'Tracy')

 

条件判断

计算机之所以能做很多自动化的任务,因为它可以自己做条件判断。

比如,输入用户年龄,根据年龄打印不同的内容,在Python程序中,用if语句实现:

age = 20
if age >= 18:
    print('your age is', age)
    print('adult')

根据Python的缩进规则,如果if语句判断是True,就把缩进的两行print语句执行了,否则,什么也不做。

也可以给if添加一个else语句,意思是,如果if判断是False,不要执行if的内容,去把else执行了:

age = 3
if age >= 18:
    print('your age is', age)
    print('adult')
else:
    print('your age is', age)
    print('teenager')

注意不要少写了冒号:

再议 input

最后看一个有问题的条件判断。很多同学会用input()读取用户的输入,这样可以自己输入,程序运行得更有意思:

birth = input('birth: ')
if birth < 2000:
    print('00前')
else:
    print('00后')

输入1982,结果报错:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() > int()

这是因为input()返回的数据类型是strstr不能直接和整数比较,必须先把str转换成整数。Python提供了int()函数来完成这件事情:

s = input('birth: ')
birth = int(s)
if birth < 2000:
    print('00前')
else:
    print('00后')

循环

为了让计算机能计算成千上万次的重复运算,我们就需要循环语句。

Python的循环有两种

  • 一种是for…in循环,依次把list或tuple中的每个元素迭代出来,看例子:
names = ['Michael', 'Bob', 'Tracy']
for name in names:
    print(name)

执行这段代码,会依次打印names的每一个元素:

Michael
Bob
Tracy

所以for x in ...循环就是把每个元素代入变量x,然后执行缩进块的语句。

再比如我们想计算1-10的整数之和,可以用一个sum变量做累加:

sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
    sum = sum + x
print(sum)
  • 第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:
sum = 0
n = 99
while n > 0:
    sum = sum + n
    n = n - 2
print(sum)

在循环内部变量n不断自减,直到变为-1时,不再满足while条件,循环退出。

break

在循环中,break语句可以提前退出循环。例如,本来要循环打印1~100的数字:

n = 1
while n <= 100:
    print(n)
    n = n + 1
print('END')

上面的代码可以打印出1~100。

如果要提前结束循环,可以用break语句:

n = 1
while n <= 100:
    if n > 10: # 当n = 11时,条件满足,执行break语句
        break # break语句会结束当前循环
    print(n)
    n = n + 1
print('END')

执行上面的代码可以看到,打印出1~10后,紧接着打印END,程序结束。

可见break的作用是提前结束循环。

dict

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。

举个例子,假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list:

names = ['Michael', 'Bob', 'Tracy']
scores = [95, 75, 85]

给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,list越长,耗时越长。

如果用dict实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:

>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95

要避免key不存在的错误,有两种办法,一是通过in判断key是否存在:

>>> 'Thomas' in d
False

二是通过dict提供的get方法,如果key不存在,可以返回None,或者自己指定的value:

>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1

注意:返回None的时候Python的交互式命令行不显示结果。

和list比较,dict有以下几个特点:

  1. 查找和插入的速度极快,不会随着key的增加而变慢;
  2. 需要占用大量的内存,内存浪费多。

而list相反:

  1. 查找和插入的时间随着元素的增加而增加;
  2. 占用空间小,浪费内存很少。

所以,dict是用空间来换取时间的一种方法。

set

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

要创建一个set,需要提供一个list作为输入集合:

>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。

重复元素在set中自动被过滤:

>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:

>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

通过remove(key)方法可以删除元素:

>>> s.remove(4)
>>> s
{1, 2, 3}

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}

set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。

Python学习笔记

python产生:Python是著名的“龟叔”Guido van Rossum在1989年圣诞节期间,为了打发无聊的圣诞节而编写的一个编程语言。

编程语言排行榜:TIOBE排行榜

C语言是可以用来编写操作系统的贴近硬件的语言,所以,C语言适合开发那些追求运行速度、充分发挥硬件性能的程序。

而Python是用来编写应用程序的高级编程语言。

高级编程语言通常都会提供一个比较完善的基础代码库,让你能直接调用,比如,针对电子邮件协议的SMTP库,针对桌面环境的GUI库,在这些已有的代码库的基础上开发。

数字运算直接出结果

例如

>>>2+2

4

输入输出

print(‘Hello Word’)

name=input(‘引号里边放提示符’)

回车后变为输入状态,再次调用那么得到输入的值

python采用空格缩紧的方式,四个空格标准

#开头的为注释,不被执行

当语句以“:”结尾时缩进的语句视为代码块

python对大小写敏感

 

数据类型(python廖雪峰)

    1. 整数
    2. 浮点数
    3. 字符串
    4. 布尔值
    5. 空值
    6. 变量
    7. 常量

注意:如果字符串内部既包含'又包含"怎么办?可以用转义字符\来标识,比如:

'I\'m \"OK\"!'

表示的字符串内容是:

I'm "OK"!

所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。在Python中,通常用全部大写的变量名表示常量

PI = 3.14159265359

但事实上PI仍然是一个变量,Python根本没有任何机制保证PI不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量PI的值,也没人能拦住你。

最后解释一下整数的除法为什么也是精确的。在Python中,有两种除法,一种除法是/

>>> 10 / 3
3.3333333333333335

/除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:

>>> 9 / 3
3.0

还有一种除法是//,称为地板除,两个整数的除法仍然是整数:

>>> 10 // 3
3

 

PM工具箱

工欲善必先利其器,好的工具能使工作事半功倍。

以下为PM必备工具箱列表:

1、Axure

原型工具,可视化demo

2、XMind

思维导图工具,归纳抽象的思维想法

3、Project

任务计划排期

4、Visio

windows下的强大的流程图软件,没有之一

5、Omnigraffle

mac下流程图软件,媲美Visio

6、Word

写方案、写PRD神器

7、PPT

产品大多数时候都是看着一张幻灯片对着开发或领导狂喷

8、Excel

任务细分、版本记录、竟品分析惯用利器