SaltStack 使用总结

SaltStack是一个开源的、新的基础平台管理工具,使用Python语言开发,同时提供Rest API方便二次开发以及和其他运维管理系统进行集成。相对于出道比较早的Puppet,SaltStack先天的优势就是简单、易用,可以非常快速的在团队中推广和使用,而且运行多平台。

SaltStack目前拥有四大主要功能

  • 远程执行:就是在管理节点上实现在上百台、上千台机器上同时执行一个命令。
  • 配置管理:也可以称之为状态管理,你可以描述一个状态。例如,某台机器要安装Nginx软件包、Nginx必须是启动的状态、Nginx有一个配置文件(内容和某个地方的一样)。你用一种描述语法描述出来后交给SaltStack,SaltStack就可以帮你实现,而不用你手动进行Nginx软件包的安装、配置文件的修改、启动等。
  • 云管理:SaltStack有一个组件叫作salt-cloud,它可以帮你自动化的进行云主机的创建和管理,支持很多公有云或私有云,例如AWS、阿里云、HP云、OpenStack、CloudStack等。
  • 事件驱动:事件驱动基础设施是SaltStack最强大也是最神秘的功能,前面的远程执行和配置管理是最基础的的,事件驱动是指SaltStack在日常运行中可以产生和捕捉事件,并根据捕捉到的事件触发对应的操作。

SaltStack的四种运行方式

  • Local:在本地运行或者说单台使用SaltStack
  • Minion/Master: 传统的客户端/服务器端(C/S)架构
  • Syndic: 使用代理实现架构扩展,用于管理更多的节点
  • Salt SSH: 无须安装客户端,直接通过SSH通信

目标机器匹配方法

SaltStack有以下几种方式来选择目标机器,灵活而又强大。大的来讲分为两大类:

1. 基于Minion ID

Minion ID是客户端(minion)的唯一标识符。可以在minion配置文件里面使用ID选项进行配置,如果不指定,其默认是主机的FQDN名。Minion ID是不能变动的,因为在进行key认证的时候,生成的文件名是以Minion ID命名的。如果Minion发生变动,就需要使用salt-key -d删除老的Minion ID,然后重新加入新的Minion ID。

  • Globbing(通配符)
  • regex(正则表达式)
  • list(列表)

2. 不基于Minion ID

  • 子网/IP地址
  • Grains
  • Grains PCRE
  • Pillar
  • Compound matchers(复合匹配)
  • Node groups(节点组)
  • Batching execution(批处理执行)

演示

指定Minion ID是最直接的选择目标的方法

查看所有minion节点

[root@salt ~]# salt-key  -L
Accepted Keys:
Denied Keys:
Unaccepted Keys:
128.docker.itnotebooks.com
Rejected Keys:

授权minion节点

可以修改/etc/salt/master的配置auto_accept:True自动认证

[root@salt ~]# salt-key -a 128.docker.itnotebooks.com
The following keys are going to be accepted:
Unaccepted Keys:
128.docker.itnotebooks.com
Proceed? [n/Y] y
Key for minion 128.docker.itnotebooks.com accepted.
  • salt-key -L: 显示已经或未认证的minion节点,Accepted Keys为已认证清单
  • salt-key -D: 删除所有Minion节点的证书
  • salt-key -d id: 删除单个minion节点的证书
  • salt-key -A: 接受所有minion节点的请求
  • salt-key -a id: 接受单个minion节点的请求

Globbing是指在Minion ID的基础上,通过通配符来定位Minion。SaltStack默认使用Shell风格通配符(如“” “?” “[]”)来匹配Minion ID。不过需要注意的是,使用salt命令时必须将’’放在单引号中,或是用’’转义,用来避免shell解析。

匹配所有itnotebooks.com域的所有minion

[root@salt ~]# salt '*.itnotebooks.com' test.ping

匹配docker后面单个任意字符的Minion

[root@salt ~]# salt 'docker?.itnotebooks.com' test.ping

匹配docker节点1到节点3的Minion

[root@salt ~]# salt 'docker[1-3].itnotebooks.com' test.ping

匹配docker不是节点1和节点3的Minion

[root@salt ~]# salt 'docker[!13].itnotebooks.com' test.ping

list和直接Minion ID都是最基本的模式,可以列出每一个Minion ID来指定多个目标机器,使用选项’-L’

[root@salt ~]# salt -L 'docker1.itnotebooks.com,docker2.itnotebooks.com' test.ping

Salt可以使用Perl风格的正则表达式来匹配Minion ID,使用选项-E

[root@salt ~]# salt -E 'linux-(node1|node2)*' test.ping

规范的Minion ID可以很好的反映出该服务器运行的相关服务及所在位置

redis-node1-redis03-idc04-soa.itnotebooks.com
  • redis-node1:运行的服务是Redis,这是第一个节点
  • redis03:说明这个redis是Redis集群编号03里面的节点
  • idc04:这台服务器运行在编号04的IDC机房中
  • soa:这台服务器是给SOA服务使用的
  • itnotebooks.com:运行的服务器是itnotebooks.com业务

也可以使用IP地址或CIDR子网来指定目标,目前仅支持IPv4的地址

[root@salt ~]# salt -S '192.168.18.33' test.ping
[root@salt ~]# salt -S '192.168.18.0/24' test.ping

也可以使用Grains对Trageting进行匹配,使用选项’-G’

匹配所有CentOS系统的Minion

[root@salt ~]# salt -G 'os:CentOS' test.ping

通过Grain匹配非常灵活,如果你想进行更复杂的基于Grains的匹配,SaltStack提供了Grain PCRE,可以在Grains的基础上使用正则表达式

[root@salt ~]# salt --grain-pcre 'os_family:Red(Hat|Flag)' test.ping

Pillar的数据可以用来定位Minion,为定位Minions提供了灵活性和终极控制

[root@salt ~]# salt -I 'apache:httpd' test.ping

Compound matchers(混合匹配)可以使用布尔操作符连接多个目标条件。混合匹配可以用前面讨论的多种方式实现精确的匹配。混合配匹配默认使用Globbing,如果要使用其它匹配方式,需要加上类型前缀字母如下表所示。

未分类

复合匹配中也可以使用and、or、not操作符,例如要匹配主机名以及webserv开始且运行Debain系统的Minion,还能匹配主机名满足正则表达式web-dc1-srv.*的Minion

[root@salt ~]# salt -C 'webserv* and G@os:Debian or E@web-dc1-srv.*' test.ping
  • G表示用shell通配符匹配Grains
  • E表示用正则表达式匹配Minion ID

需要注意的是not不能用于第一个条件,需要用时可以像下面这样写:

[root@salt ~]# salt -C '* and not G@kernel:Darwin' test.ping

Node group是在Master中nodegroup用复合条件字义的一组Minion

[root@salt ~]# vim /etc/salt/master
nodegroups:
  group1: '[email protected],linux-node2.itnotebooks.com'
[root@salt ~]# systemctl restart salt-master
[root@salt ~]# salt -N group1 test.ping
linux-node2.itnotebooks.com
    True
linux-node1.itnotebooks.com
    True

远程执行命令-查看内存使用

[root@salt ~]# salt 'linux-node1.itnotebooks.com' cmd.run 'free -m'
              total        used        free      shared  buff/cache   available
Mem:           1838         749         119           0         969         887
Swap:             0           0           0

远程执行命令-查看操作系统类型

[root@salt ~]# salt 'linux-node1.itnotebooks.com' grains.item osfullname
linux-node1.itnotebooks.com:
  osfullname: CentOS

远程执行命令-查看指定发行版本号为6.4的主机的python版本

[root@salt ~]# salt -G 'osrelease:6.4' cmd.run 'python -V'
linux-node1.itnotebooks.com:
  Python 2.6.6

远程执行命令-解压文件

[root@salt ~]# salt 'linux-node1.itnotebooks.com' archive.gunzip /tmp/jdk-8.1.0.gz

远程执行命令-压缩文件

[root@salt ~]# salt 'linux-node1.itnotebooks.com' archive.gzip /tmp/test.txt

除了上面所提到的外,其它类似的模块还有很多很多
如cp、cron、file、iptables、network、dnsuti、service、pkg等等,详细的自己用过就知道了,当然你也可以在cmd.run里面去完成这一切

saltstack的jinja模

[root@master ~]# cd /srv/salt/base/
[root@master base]# ll
总用量 12
-rw-r--r-- 1 root root 172 11月 14 21:26 apache.sls
-rw-r--r-- 1 root root 128 11月 16 00:12 dns.sls
drwxr-xr-x 2 root root  25 11月 16 00:23 files
-rw-r--r-- 1 root root  28 11月 16 00:22 top.sls
[root@master base]# vim dns.sls 
[root@master base]# cat dns.sls 
/etc/resolv.conf:
  file.managed:
     - source: salt://files/resolv.conf
     - user: root
     - group: root
     - mode: 777
     - template: jinja
     - defaults:    #定义变量
       DNS_SERVER: 192.168.43.118

[root@master base]# vim files/resolv.conf 
[root@master base]# cat files/resolv.conf
#jjjjjjjjjjjjjjjjjj
nameserver {{ DNS_SERVER }}   #2个大括号表示变量

[root@master base]# salt '*'  state.highstate
192.168.43.118:
----------
          ID: /etc/resolv.conf
    Function: file.managed
      Result: True
     Comment: File /etc/resolv.conf updated
     Started: 00:43:27.222821
    Duration: 21.809 ms
     Changes:   
              ----------
              diff:
                  --- 
                  +++ 
                  @@ -1,2 +1,3 @@
                   #jjjjjjjjjjjjjjjjjj
                  -nameserver 192.168.43.1
                  +nameserver 192.168.43.118
                  +

Summary
------------
Succeeded: 1 (changed=1)
Failed:    0

[root@master base]# vim files/resolv.conf 
[root@master base]# cat files/resolv.conf
#jjjjjjjjjjjjjjjjjj
#  {{ grains['fqdn_ip4'] }}
nameserver {{ DNS_SERVER }}
[root@master base]# salt '*'  state.highstate
192.168.43.118:
----------
          ID: /etc/resolv.conf
    Function: file.managed
      Result: True
     Comment: File /etc/resolv.conf updated
     Started: 00:47:03.799795
    Duration: 32.514 ms
     Changes:   
              ----------
              diff:
                  --- 
                  +++ 
                  @@ -1,3 +1,3 @@
                   #jjjjjjjjjjjjjjjjjj
                  +#  ['192.168.43.118']
                   nameserver 192.168.43.118
                  -

Summary
------------
Succeeded: 1 (changed=1)
Failed:    0

系统初始化配置:

[root@master base]# pwd
/srv/salt/base
[root@master base]# mkdir init
[root@master base]# mv apache.sls dns.sls files/ /tmp/
[root@master base]# tree
.
├── init
└── top.sls

1 directory, 1 file

[root@master base]# cp /tmp/dns.sls init/
[root@master base]# ll
总用量 4
drwxr-xr-x 2 root root 21 11月 16 00:55 init
-rw-r--r-- 1 root root 28 11月 16 00:22 top.sls
[root@master base]# cd init/
[root@master init]# ls
dns.sls

[root@master init]# vim dns.sls 
[root@master init]# cat dns.sls
/etc/resolv.conf:
  file.managed:
     - source: salt://init/files/resolv.conf
     - user: root
     - group: root
     - mode: 777

[root@master init]# mkdir files
[root@master init]# cp /etc/resolv.conf files/
[root@master init]# cp /etc/resolv.conf files/
[root@master init]# vim history.sls
[root@master init]# cat history.sls
/etc/profile:
  file.append:  #file模块的追加方法
    - text:
      - export HISTTIMEFORMAT="%F %T 'whoami'"

[root@master init]# export  PROMPT_COMMAND=' { msg=$(history 1 | { read x y; echo $y; });logger "[euid=$(whoami)]":$(who am i):['pwd']"$msg";} '
[root@master init]# who
root     pts/0        2018-11-16 00:29 (desktop-4a0ohej)
root     pts/1        2018-11-16 00:38 (desktop-4a0ohej)
[root@master init]# tail -f /var/log/messages
Nov 16 01:01:01 master systemd: Starting Session 34 of user root.
Nov 16 01:10:01 master systemd: Started Session 35 of user root.
Nov 16 01:10:01 master systemd: Starting Session 35 of user root.
Nov 16 01:14:00 master root: [euid=root]:root pts/0 2018-11-16 00:29 (desktop-4a0ohej):[pwd]export PROMPT_COMMAND=' { msg=$(history 1 | { read x y; echo $y; });logger "[euid=$(whoami)]":$(who am i):['pwd']"$msg";} '
Nov 16 01:14:04 master root: [euid=root]:root pts/0 2018-11-16 00:29 (desktop-4a0ohej):[pwd]uptime
Nov 16 01:14:39 master root: [euid=root]:root pts/0 2018-11-16 00:29 (desktop-4a0ohej):[pwd]tail -f /var/log/messages
Nov 16 01:14:42 master root: [euid=root]:root pts/0 2018-11-16 00:29 (desktop-4a0ohej):[pwd]uptime
Nov 16 01:15:06 master root: [euid=root]:root pts/0 2018-11-16 00:29 (desktop-4a0ohej):[pwd]tail -f /var/log/messages
Nov 16 01:15:12 master root: [euid=root]:root pts/0 2018-11-16 00:29 (desktop-4a0ohej):[pwd]tail -f /var/log/messages
Nov 16 01:15:17 master root: [euid=root]:root pts/0 2018-11-16 00:29 (desktop-4a0ohej):[pwd]who

[root@master init]# ll
总用量 12
-rw-r--r-- 1 root root 172 11月 16 01:17 audit.sls
-rw-r--r-- 1 root root 137 11月 16 00:57 dns.sls
drwxr-xr-x 2 root root  25 11月 16 00:58 files
-rw-r--r-- 1 root root  88 11月 16 01:03 history.sls
[root@master init]# cat audit.sls 
/etc/bashrc:
  file.append:
    - text:
      - export  PROMPT_COMMAND=' { msg=$(history 1 | { read x y; echo $y; });logger "[euid=$(whoami)]":$(who am i):['pwd']"$msg";} '

[root@master init]# cat sysctl.sls
vm.swappinese:   #尽量不适用swap分区
  sysctl.present:
    - value: 0

net.ipv4.ip_local_port_range:
  sysctl.present:
    - value: 10000 65000

fs.file-max:    #最大打开文件数
  sysctl.present:
    - value: 100000

[root@master init]# vim env_init.sls
[root@master init]# cat env_init.sls
include:
  - init.dns
  - init.history
  - init.audit
  - init.sysctl

[root@master init]# cd ..
[root@master base]# ls
init  top.sls
[root@master base]# vim top.sls 
[root@master base]# cat top.sls
base:
  '*':
    - init.env_init  #在base路径下去init目录找env_init文件并执行

[root@master base]# salt '*' state.highstate test=True  测试不执行
[root@master base]# salt '*' state.highstate

saltstack配置管理

状态模块:

https://docs.saltstack.com/en/latest/ref/states/all/salt.states.file.html#module-salt.states.file

[root@master ~]# grep -v '^$' /etc/salt/master |grep -v '#'
client_acl:
  luo:
    - test.ping
    - network.*
file_roots:     #top.sls路径,可以写多个
  base:         #但是只读取base路径
    - /srv/salt/base
  test:
    - /srv/salt/test
  prod:
    - /srv/salt/prod
pillar_roots:
  base:
    - /srv/pillar
mysql.host: '192.168.43.118'
mysql.user: 'salt'
mysql.pass: 'Myq1231!'
mysql.db: 'salt'
mysql.port: 3306

[root@master ~]# systemctl restart salt-master

[root@master ~]# mkdir /srv/salt/{base,test,prod}

[root@master salt]# ls
apache.sls  base  prod  test  top.sls
[root@master salt]# mv apache.sls top.sls base/

编写基本文件管理:

[root@master ~]# mkdir /srv/salt/base/files
[root@master base]# pwd
/srv/salt/base
[root@master base]# vim dns.sls
[root@master base]# cat dns.sls
/etc/resolv.conf:
  file.managed:     #file模块的managed方法
     - source: salt://files/resolv.conf  #路径,salt://=/srv/salt/base ,files新建的文件。
     - user: root   #用户
     - group: root  #用户组
     - mode: 777    #权限

[root@master base]# cp /etc/resolv.conf ./files/
[root@master base]# vim files/resolv.conf 
[root@master base]# cat files/resolv.conf
nameserver 192.168.43.1

[root@master base]# salt '*' state.sls dns   state状态模块的sls方法,dns为状态名,可以任意写。
192.168.43.118:
----------
          ID: /etc/resolv.conf
    Function: file.managed
      Result: True
     Comment: File /etc/resolv.conf updated
     Started: 00:18:45.928344
    Duration: 18.082 ms
     Changes:   
              ----------
              diff:
                  --- 
                  +++ 
                  @@ -1,3 +1 @@
                  -# Generated by NetworkManager
                  -search com
                   nameserver 192.168.43.1
              mode:
                  0777

Summary
------------
Succeeded: 1 (changed=1)

[root@master base]# pwd
/srv/salt/base
[root@master base]# vim top.sls 
[root@master base]# cat top.sls
base:
  '*':
    - dns

[root@master files]# pwd
/srv/salt/base/files
[root@master files]# vim resolv.conf 
[root@master files]# cat resolv.conf 
#jjjjjjjjjjjjjjjjjj
nameserver 192.168.43.1

[root@master base]# salt '*' state.sls dns  state状态模块的sls方法,dns为状态名,可以任意写。
192.168.43.118:
----------
          ID: /etc/resolv.conf
    Function: file.managed
      Result: True                
     Comment: File /etc/resolv.conf updated
     Started: 00:26:07.017848
    Duration: 24.144 ms
     Changes:   
              ----------
              diff:      
                  --- 
                  +++ 
                  @@ -1 +1,2 @@
                  +#jjjjjjjjjjjjjjjjjj
                   nameserver 192.168.43.1

Summary
------------
Succeeded: 1 (changed=1)
Failed:    0

[root@master base]# cat /etc/resolv.conf 
#jjjjjjjjjjjjjjjjjj
nameserver 192.168.43.1

这个Python资源在GitHub上标星超8000,现在被翻译成了中文

未分类

最近,GitHub上一个关于Python的工程完工了。

一个名为“暮晨”的贡献者,把一个非常有趣的Python项目,翻译成了中文版。

这个项目是《What the f*ck Python!》,专门介绍 Python 里面那些奇奇怪怪的语言坑。

未分类

关于项目

项目的主体构成部分就是示例,一共分为5个部分,分别是:

Strain your brain!/大脑运动!

未分类

Appearances are deceptive!/外表是靠不住的!

未分类

Watch out for the landmines!/小心地雷!

未分类

The Hidden treasures!/隐藏的宝藏!

未分类

Miscellaneous/杂项

未分类

以上,总计51个示例。

每一个示例的结构都是一样的,以“Mutating the immutable!/强人所难”为例:

首先,会给出代码:

some_tuple = ("A", "tuple", "with", "values")
another_tuple = ([1, 2], [3, 4], [5, 6])

然后,给出Output( Python version):

>>> some_tuple[2] = "change this"
TypeError: 'tuple' object does not support item assignment
>>> another_tuple[2].append(1000) # 这里不出现错误
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000])
>>> another_tuple[2] += [99, 999]
TypeError: 'tuple' object does not support item assignment
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000, 99, 999])

然后,对意外输出的结果进行简短的描述,在这个示例中,就是:

我还以为元组是不可变的呢…

接下来,就会对示例进行说明,简要叙述发生了什么以及为什么会发生。如有必要, 也会举例说明。

在这个示例中是这样的:

  • 引用 https://docs.python.org/2/reference/datamodel.html
不可变序列 不可变序列的对象一旦创建就不能再改变。(如果对象包含对其他对象的引用,则这些其他对象可能是可变的并且可能会被修改; 但是,由不可变对象直接引用的对象集合不能更改。)
  • += 操作符在原地修改了列表. 元素赋值操作并不工作, 但是当异常抛出时, 元素已经在原地被修改了。

有些地方,贡献者还会给出译注,比如整个示例中就是:

对于不可变对象, 这里指tuple, +=并不是原子操作, 而是extend和=两个动作, 这里=操作虽然会抛出异常, 但 extend 操作已经修改成功了。

其他还有50个示例,等你来看~

怎么使用?

当然,要学习一下怎么使用这个资源。项目贡献者在用法部分表示,最好依次阅读下面的示例。

然后,在阅读每一个示例的时候,这样做:

仔细阅读设置例子最开始的代码。阅读输出结果。确认结果是否如你所料。确认你是否知道这背后的原理。如果不知道, 深呼吸然后阅读说明 (如果你还是看不明白, 别沉默!可以提问题)。如果知道, 给自己点奖励, 然后去看下一个示例。

此外,还可以在命令行阅读 WTFpython,有 pypi 包 和 npm 包(支持代码高亮),不过都是英文版的。

关于作者

这个项目的原作者,是一个名为Satwik Kansal的印度小哥。

未分类

GitHub上的介绍称,在深度学习和去中心化应用方面是一个“老司机”。

目前,英文版资源,标星已经8.3k了。

未分类

传送门

中文版:

https://github.com/leisurelicht/wtfpython-cn

英文原版:

https://github.com/satwikkansal/wtfpython

— 完 —

python玩转街机游戏,操作亲民!

未分类

这是一个允许你在几乎任何街机游戏中训练你的强化学习算法的Python库,它目前在Linux系统上可用。通过这个工具包,你可以定制算法逐步完成游戏过程,同时接收每一帧的数据和内部存储器地址值以跟踪游戏状态,以及发送与游戏交互的动作。

安装

GitHub地址:github.com/M-J-Murray/MAMEToolkit/blob/master/README.md

你可以用pip安装这个库,只需运行以下命令:

pip install MAMEToolkit

演示:街霸

未分类

街霸是史上最经典的游戏之一。现在工具包内包含的街霸版本是街头霸王3:三度冲击(Japan 990608, NO CD),我们以此为例,用以下代码写一个随机智能体:

import random

from MAMEToolkit.sf_environment import Environment

roms_path = "roms/"

env = Environment("env1", roms_path)

env.start()

while True:

   move_action = random.randint(0, 8)

   attack_action = random.randint(0, 9)

   frames, reward, round_done, stage_done, game_done = env.step(move_action, attack_action)

   if game_done:

       env.new_game()

   elif stage_done:

       env.next_stage()

   elif round_done:

       env.next_round()

支持hogwild!

hogwild!? Niu等人引入了一个叫做 Hogwild! 的更新策略,可以使 SGD 可以在多 CPU 上并行更新。处理器在无需对参数加锁的情况下就可以访问共享内存。但仅在输入的是稀疏数据时才有效,因为每次更新仅修改所有参数的一小部分。他们展示了在这种情况下,更新策略几乎可以达到一个最优的收敛率,因为处理器不太可能覆盖掉有用的信息。

from threading import Thread

import random

from MAMEToolkit.sf_environment import Environment

def run_env(env):

   env.start()

   while True:

       move_action = random.randint(0, 8)

       attack_action = random.randint(0, 9)

       frames, reward, round_done, stage_done, game_done = env.step(move_action, attack_action)

       if game_done:

           env.new_game()

       elif stage_done:

           env.next_stage()

       elif round_done:

           env.next_round()

def main():

   workers = 8

   # Environments must be created outside of the threads

   roms_path = "roms/"

   envs = [Environment(f"env{i}", roms_path) for i in range(workers)]

   threads = [Thread(target=run_env, args=(envs[i], )) for i in range(workers)]

   [thread.start() for thread in threads]

建立自己的游戏环境

这个工具包之所以易于上手,是因为它和模拟器本身不需要太多交互,只需注意两点——一是查找你关注的内部状态相关联的内存地址值,二是用选取的环境跟踪状态。你可以用MAME Cheat Debugger,它会反馈游戏的内存地址值如何随时间变化。如果要创建游戏模拟,你得先获得正在模拟的游戏的ROM,并知道MAME使用的游戏ID,比如街霸的ID是’sfiii3n’。

游戏ID

你可以通过运行以下代码找到游戏的ID

from MAMEToolkit.emulator import Emulator

emulator = Emulator("env1", "", "", memory_addresses)

这个命令会打开MAME仿真器。你可以搜索游戏列表以找到想要的游戏,游戏的ID位于游戏标题末尾的括号中。

内存地址

如果获得了ID,也有了想要跟踪的内存地址,你可以开始模拟:

from MAMEToolkit.emulator import Emulator

from MAMEToolkit.emulator import Address

roms_path = "roms/"

game_id = "sfiii3n"

memory_addresses = {

       "fighting": Address('0x0200EE44', 'u8'),

       "winsP1": Address('0x02011383', 'u8'),

       "winsP2": Address('0x02011385', 'u8'),

       "healthP1": Address('0x02068D0B', 's8'),

       "healthP2": Address('0x020691A3', 's8')

   }

emulator = Emulator("env1", roms_path, "sfiii3n", memory_addresses)

这会启动仿真器,并在工具包连接到模拟器进程时暂停。

分步运行仿真器

连接工具箱后,你可以分步运行仿真器:

data = emulator.step([])

frame = data["frame"]

is_fighting = data["fighting"]

player1_wins = data["winsP1"]

player2_wins = data["winsP2"]

player1_health = data["healthP1"]

player2_health = data["healthP2"]

step函数会把帧数据作为NumPy矩阵返回,同时,它也会返回该时间步长的所有内存地址整数值。

如果要向仿真器输入动作,你还需要确定游戏支持的输入端口和字段。比如玩街霸需要先投币,这个代码是:

from MAMEToolkit.emulator import Action

insert_coin = Action(':INPUTS', 'Coin 1')

data = emulator.step([insert_coin])

要确定哪些端口可用,请使用list actions命令:

from MAMEToolkit.emulator import list_actions

roms_path = "roms/"

game_id = "sfiii3n"

print(list_actions(roms_path, game_id))

下面这个返回的列表就包含街霸环境中可用于向步骤函数发送动作的所有端口和字段:

[

   {'port': ':scsi:1:cdrom:SCSI_ID', 'field': 'SCSI ID'},

   {'port': ':INPUTS', 'field': 'P2 Jab Punch'},

   {'port': ':INPUTS', 'field': 'P1 Left'},

   {'port': ':INPUTS', 'field': 'P2 Fierce Punch'},

   {'port': ':INPUTS', 'field': 'P1 Down'},

   {'port': ':INPUTS', 'field': 'P2 Down'},

   {'port': ':INPUTS', 'field': 'P2 Roundhouse Kick'},

   {'port': ':INPUTS', 'field': 'P2 Strong Punch'},

   {'port': ':INPUTS', 'field': 'P1 Strong Punch'},

   {'port': ':INPUTS', 'field': '2 Players Start'},

   {'port': ':INPUTS', 'field': 'Coin 1'},

   {'port': ':INPUTS', 'field': '1 Player Start'},

   {'port': ':INPUTS', 'field': 'P2 Right'},

   {'port': ':INPUTS', 'field': 'Service 1'},

   {'port': ':INPUTS', 'field': 'Coin 2'},

   {'port': ':INPUTS', 'field': 'P1 Jab Punch'},

   {'port': ':INPUTS', 'field': 'P2 Up'},

   {'port': ':INPUTS', 'field': 'P1 Up'},

   {'port': ':INPUTS', 'field': 'P1 Right'},

   {'port': ':INPUTS', 'field': 'Service Mode'},

   {'port': ':INPUTS', 'field': 'P1 Fierce Punch'},

   {'port': ':INPUTS', 'field': 'P2 Left'},

   {'port': ':EXTRA', 'field': 'P2 Short Kick'},

   {'port': ':EXTRA', 'field': 'P2 Forward Kick'},

   {'port': ':EXTRA', 'field': 'P1 Forward Kick'},

   {'port': ':EXTRA', 'field': 'P1 Roundhouse Kick'},

   {'port': ':EXTRA', 'field': 'P1 Short Kick'}

]

仿真器类还有一个frame_ratio参数,可用于调整算法所见的帧速率。默认情况下,MAME以每秒60帧的速度生成帧,如果你觉得这太多了,想把它改成每秒20帧,可以输入以下代码:

from MAMEToolkit.emulator import Emulator

emulator = Emulator(roms_path, game_id, memory_addresses, frame_ratio=3)

未分类

MAME性能基准测试

目前这个工具包的开发和测试已在8核AMD FX-8300 3.3GHz CPU以及3GB GeForce GTX 1060 GPU上完成。在使用单个随机智能体的情况下,街头霸王环境可以以正常游戏速度的600%+运行。而如果是用8个随机智能体进行hogwild!训练,环境可以以正常游戏速度的300%+运行。

ConvNet智能体

为了确保工具包能够训练算法,作者还设置了一个简单的5层ConvNet,只需少量调整,你就可以用它进行测试。在街霸实验中,这个算法能够成功学习到游戏的一些简单技巧,比如连击(combo)和格挡(blocking)。街霸本身的游戏机制是分成10个关卡(难度递增),玩家在每个关卡都要迎战不同的对手。刚开始的时候,这个智能体平均只能打到第2关。但在经过2200次训练后,它平均能打到第5关。

至于智能体的学习率,它是用每一局智能体所造成的净伤害和所承受的伤害来计算的。

python安装教程(Windows系统,python3.7为例)

1. 在python的官网下载python对应版本:https://www.python.org/downloads/windows/

64位下载Windows x86-64 executable installer 版本

32位下载Windows x86 executable installer 版本

打开链接如下图,版本会一直更新,选择任意一个适合自己电脑的版本就好

未分类

2. 勾选 Add python to PATH 添加路径

安装界面点击Customize installation 自定义安装

未分类

3. 不改变默认进行Next下一步

未分类

4. 选择一个自己喜欢的安装位置

点击Install开始安装

未分类

5. 等待进度条加载完毕

未分类

6. 安装完毕,点击Close关闭

若方框位置出现管理员字样则点击授权再关闭

未分类

7. 验证:运行cmd

进入到自己的安装目录下,运行语句:python -V

若显示出Python对应版本则表示安装成功

未分类

让你事半功倍的小众Python库

Python 是世界上发展最快的编程语言之一。它一次又一次地证明了自己在开发人员和跨行业的数据科学中的实用性。Python 及其机器学习库的整个生态系统使全世界的用户(无论新手或老手)都愿意选择它。Python 成功和受欢迎的原因之一是存在强大的库,这些库使 Python 极具创造力且运行快速。然而,使用 Pandas、Scikit-learn、Matplotlib 等常见库在解决一些特殊的数据问题时可能并不实用,本文介绍的这些非常见库可能更有帮助。

WGET

提取数据,特别是从网络中提取数据是数据科学家的重要任务之一。Wget 是一个免费的工具,用于以非交互式方式从 Web 上下载文件。它支持 HTTP、HTTPS 和 FTP 协议,通过 HTTP 代理进行检索。由于它是非交互式的,即使用户没有登录,它也可以在后台工作。所以,如果你想下载一个网站或一个页面上的所有图片,wget 会帮助你。

安装:

$ pip install wget

示例:

import wget
url = 'http://www.futurecrew.com/skaven/song_files/mp3/razorback.mp3'
filename = wget.download(url)
100% [................................................] 3841532 / 3841532
filename
'razorback.mp3'

对于那些在 python 中被处理datetimes困扰的人来说,Pendulum 是个好选择。它是一个 Python 包,用于简化 datetimes 操作。它是 Python「本机」类(native class)的代替。更多内容,请参阅文档:https://um.eustace.io/docs/# installation。

安装:

$ pip install pendulum

示例:

import pendulum
dt_toronto = pendulum.datetime(2012, 1, 1, tz='America/Toronto')
dt_vancouver = pendulum.datetime(2012, 1, 1, tz='America/Vancouver')
print(dt_vancouver.diff(dt_toronto).in_hours())
3

可以看出,当每个类的样本数量相等即平衡时,大多数分类算法的工作效果最好。但现实生活中充满了不平衡的数据集,这些数据集对机器学习的学习阶段和后续预测都有影响。创建这个库是为了解决这个问题。它与 scikit-learn 兼容,并且是 scikit-learn-contrib 项目的一部分。下次遇到不平衡的数据集时,可以尝试一下。

安装:

pip install -U imbalanced-learn

# or

conda install -c conda-forge imbalanced-learn

示例:

有关用法和示例,请参考:http://imbalancedlearn.org/en/stable/api.html。

FLASHTEXT

在 NLP 任务中,清理文本数据通常需要替换句子中的关键词或从句子中提取关键词。通常,这样的操作可以用正则表达式来完成,但是如果要搜索的词汇量过大,操作就会变得麻烦。Python 中基于 FlashText 算法的 FlashText 模块,为这种情况提供了一个合适的替代方案。FlashText 最大的优点是搜索词数量不影响运行时长。更多相关信息请见:https://flashtext.readthedocs.io/en/latest/#。

安装:

$ pip install flashtext

示例

提取关键词:

from flashtext import KeywordProcessor
keyword_processor = KeywordProcessor()
# keyword_processor.add_keyword(<unclean name>, <standardised name>)
keyword_processor.add_keyword('Big Apple', 'New York')
keyword_processor.add_keyword('Bay Area')
keywords_found = keyword_processor.extract_keywords('I love Big Apple and Bay Area.')
keywords_found
['New York', 'Bay Area']

替换关键词:

keyword_processor.add_keyword('New Delhi', 'NCR region')
new_sentence = keyword_processor.replace_keywords('I love Big Apple and new delhi.')
new_sentence
'I love New York and NCR region.'

更多使用示例,请参阅官方文档。

FUZZYWUZZY

虽然名字听起来很奇怪,但涉及到字符串匹配时,fuzzywuzzy 是一个非常有用的库,可以很容易地实现诸如字符串比较比率、token 比率等操作。对于匹配不同数据库中的记录也很方便。

安装:

$ pip install fuzzywuzzy

示例:

from fuzzywuzzy import fuzz
from fuzzywuzzy import process
# Simple Ratio
fuzz.ratio("this is a test", "this is a test!")
97
# Partial Ratio
fuzz.partial_ratio("this is a test", "this is a test!")
 100

更多有趣的例子可以在 GitHub 上找到:https://github.com/seatgeek/fuzzywuzzy。

PYFLUX

时间序列分析是机器学习领域最常见的问题之一。PyFlux 是 Python 中为处理时间序列问题而创建的开源库。该库有一系列极好的时间序列模型,包括但不限于 ARIMA、 GARCH 和 VAR 模型。简而言之,PyFlux 提供了一个时间序列建模的概率方法。值得尝试。

安装:

pip install pyflux

示例:

有关用法和示例,请参考:https://pyflux.readthedocs.io/en/latest/index.html。

IPYVOLUME

交流结果是数据科学的一个基本方面。能够将结果可视化是一个很大的优势。IPyvolume 是一个用于在 Jupyter notebook 中可视化 3d 体积和字形(如 3d 散点图)的 Python 库,只需少量配置即可。然而,它目前还处于前 1.0 版。IPyvolume 的 volshow 之于 3d 数组,就像 matplotlib 的 imshow 之于 2d 数组一样。更多相关信息请见:https://ipyvolume.readthedocs.io/en/latest/?badge=latest。

安装:

Using pip
$ pip install ipyvolume
Conda/Anaconda
$ conda install -c conda-forge ipyvolume

DASH

Dash 是一个用于构建 web 应用程序的高效 Python 框架。它写在 Flask、Plotly.js 和 React.js 之上,将下拉列表、滑块和图形等 UI 元素与你的分析性 Python 代码直接相连,无需 javascript。Dash 非常适合构建数据可视化应用程序。然后这些应用程序可以在 web 浏览器中进行渲染。用户指南请见:https://dash.plot.ly/。

安装:

pip install dash==0.29.0 # The core dash backend

pip install dash-html-components==0.13.2 # HTML components

pip install dash-core-components==0.36.0 # Supercharged components

pip install dash-table==3.1.3 # Interactive DataTable component (new!)

示例:

下图示例显示了具有下拉功能的高度交互图。当用户在下拉列表中选择一个值时,应用程序代码会动态地将 Google Finance 的数据导出为 Pandas DataFrame。资源:https://gist.github.com/chriddyp/3d2454905d8f01886d651f207e2419f0。

未分类

GYM

来自 OpenAI 的 Gym 是一个开发和对比强化学习算法的工具包。它兼容于任何数值计算库,如 TensorFlow 或 Theano。Gym 库是一个测试问题的集合,也被称为环境——可以用它来计算你的强化学习算法。这些环境有一个共享的接口,允许你写通用算法。

安装:

pip install gym

示例:

运行环境 CartPole-v0 的 1000 个时间步骤实例,在每个步骤渲染环境。

未分类

阅读其他环境请见:https://gym.openai.com/。

结论

这些是作者为数据科学挑选的实用 python 库,而非常见的 numpy、panda 等。值得一试。

Python的闭包

Python代码中,可以定义嵌套函数,即在函数中再定义一个函数。嵌套函数有什么特别之处吗?因为闭包。

简单说,闭包就是嵌套的内层函数能够记住外层函数的namespace,在外层函数返回之后,内层函数还可以正常访问外层的这些变量。看如下代码:

def outer():
    msg = 'hello world'
    def inner():
        print(msg)
    return inner
>>> func = outer()
>>> func()
hello world

闭包有2个特点:

  1. inner能访问outer及其祖先函数的命名空间内的变量(局部变量,函数参数)。
  2. 调用outer已经返回了,但是它的命名空间被返回的inner对象引用,所以还不会被回收。

这部分想深入可以去了解Python的LEGB规则。

Python有了闭包,就可以创建函数的装饰器了。

Python中fnmatch模块的使用

fnmatch()函数匹配能力介于简单的字符串方法和强大的正则表达式之间,如果在数据处理操作中只需要简单的通配符就能完成的时候,这通常是一个比较合理的方案。此模块的主要作用是文件名称的匹配,并且匹配的模式使用的Unix shell风格。源码很简单:

"""Filename matching with shell patterns.

fnmatch(FILENAME, PATTERN) matches according to the local convention.
fnmatchcase(FILENAME, PATTERN) always takes case in account.

The functions operate by translating the pattern into a regular
expression.  They cache the compiled regular expressions for speed.

The function translate(PATTERN) returns a regular expression
corresponding to PATTERN.  (It does not compile it.)
"""
import os
import posixpath
import re
import functools

__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"]

def fnmatch(name, pat):
    """Test whether FILENAME matches PATTERN.

    Patterns are Unix shell style:

    *       matches everything
    ?       matches any single character
    [seq]   matches any character in seq
    [!seq]  matches any char not in seq

    An initial period in FILENAME is not special.
    Both FILENAME and PATTERN are first case-normalized
    if the operating system requires it.
    If you don't want this, use fnmatchcase(FILENAME, PATTERN).
    """
    name = os.path.normcase(name)
    pat = os.path.normcase(pat)
    return fnmatchcase(name, pat)

@functools.lru_cache(maxsize=256, typed=True)
def _compile_pattern(pat):
    if isinstance(pat, bytes):
        pat_str = str(pat, 'ISO-8859-1')
        res_str = translate(pat_str)
        res = bytes(res_str, 'ISO-8859-1')
    else:
        res = translate(pat)
    return re.compile(res).match

def filter(names, pat):
    """Return the subset of the list NAMES that match PAT."""
    result = []
    pat = os.path.normcase(pat)
    match = _compile_pattern(pat)
    if os.path is posixpath:
        # normcase on posix is NOP. Optimize it away from the loop.
        for name in names:
            if match(name):
                result.append(name)
    else:
        for name in names:
            if match(os.path.normcase(name)):
                result.append(name)
    return result

def fnmatchcase(name, pat):
    """Test whether FILENAME matches PATTERN, including case.

    This is a version of fnmatch() which doesn't case-normalize
    its arguments.
    """
    match = _compile_pattern(pat)
    return match(name) is not None


def translate(pat):
    """Translate a shell PATTERN to a regular expression.

    There is no way to quote meta-characters.
    """

    i, n = 0, len(pat)
    res = ''
    while i < n:
        c = pat[i]
        i = i+1
        if c == '*':
            res = res + '.*'
        elif c == '?':
            res = res + '.'
        elif c == '[':
            j = i
            if j < n and pat[j] == '!':
                j = j+1
            if j < n and pat[j] == ']':
                j = j+1
            while j < n and pat[j] != ']':
                j = j+1
            if j >= n:
                res = res + '\['
            else:
                stuff = pat[i:j].replace('\','\\')
                i = j+1
                if stuff[0] == '!':
                    stuff = '^' + stuff[1:]
                elif stuff[0] == '^':
                    stuff = '\' + stuff
                res = '%s[%s]' % (res, stuff)
        else:
            res = res + re.escape(c)
    return r'(?s:%s)Z' % res

fnmatch的中的5个函数["filter", "fnmatch", "fnmatchcase", "translate"]

  • filter 返回列表形式的结果
def gen_find(filepat, top):
    """
    查找符合Shell正则匹配的目录树下的所有文件名
    :param filepat: shell正则
    :param top: 目录路径
    :return: 文件绝对路径生成器
    """
    for path, _, filenames in os.walk(top):
        for file in fnmatch.filter(filenames, filepat):
            yield os.path.join(path, file)
  • fnmatch
# 列出元组中所有的python文件
pyfiles = [py for py in ('restart.py', 'index.php', 'file.txt') if fnmatch(py, '*.py')]
# 字符串的 startswith() 和 endswith() 方法对于过滤一个目录的内容也是很有用的
  • fnmatchcase 区分大小写的文件匹配
# 这两个函数通常会被忽略的一个特性是在处理非文件名的字符串时候它们也是很有用的。 比如,假设你有一个街道地址的列表数据
address = [
    '5412 N CLARK ST',
    '1060 W ADDISON ST',
    '1039 W GRANVILLE AVE',
    '2122 N CLARK ST',
    '4802 N BROADWAY',
]
print([addr for addr in address if fnmatchcase(addr, '* ST')])
  • translate 这个似乎很少有人用到,前面说了fnmatch是Unix shell匹配风格,可以使用translate将其转换为正则表达式,举个栗子
shell_match = 'Celery_?*.py'
print(translate(shell_match))
# 输出结果:(?s:Celery_..*.py)Z

Celery_..*.py就是正则表达式的写法。

[译] 鲜为人知的数据科学 Python 库

Python 是一个很棒的语言。它是世界上发展最快的编程语言之一。它一次又一次地证明了在开发人员职位中和跨行业的数据科学职位中的实用性。整个 Python 及其库的生态系统使它成为全世界用户(初学者和高级用户)的合适选择。它的成功和流行的原因之一是它强大的第三方库的集合,这些库使它可以保持活力和高效。

在本文中,我们会研究一些用于数据科学任务的 Python 库,而不是常见的比如 panda、scikit-learn 和 matplotlib 等的库。尽管像 panda 和 scikit-learn 这样的库,是在机器学习任务中经常出现的,但是了解这个领域中的其它 Python 产品总是很有好处的。

Wget

从网络上提取数据是数据科学家的重要任务之一。Wget 是一个免费的实用程序,可以用于从网络上下载非交互式的文件。它支持 HTTP、HTTPS 和 FTP 协议,以及通过 HTTP 的代理进行文件检索。由于它是非交互式的,即使用户没有登录,它也可以在后台工作。所以下次当你想要下载一个网站或者一个页面上的所有图片时,wget 可以帮助你。

安装:

$ pip install wget

例子:

import wget
url = 'http://www.futurecrew.com/skaven/song_files/mp3/razorback.mp3'

filename = wget.download(url)
100% [................................................] 3841532 / 3841532

filename
'razorback.mp3'

Pendulum

对于那些在 python 中处理日期时间时会感到沮丧的人来说,Pendulum 很适合你。它是一个简化日期时间操作的 Python 包。它是 Python 原生类的简易替代。请参阅文档深入学习。

安装:

$ pip install pendulum

例子:

import pendulum

dt_toronto = pendulum.datetime(2012, 1, 1, tz='America/Toronto')
dt_vancouver = pendulum.datetime(2012, 1, 1, tz='America/Vancouver')

print(dt_vancouver.diff(dt_toronto).in_hours())

3

imbalanced-learn

可以看出,当每个类的样本数量基本相同时,大多数分类算法的效果是最好的,即需要保持数据平衡。但现实案例中大多是不平衡的数据集,这些数据集对机器学习算法的学习阶段和后续预测都有很大影响。幸运的是,这个库就是用来解决此问题的。它与 scikit-learn 兼容,是 scikit-lear-contrib 项目的一部分。下次当你遇到不平衡的数据集时,请尝试使用它。

安装:

pip install -U imbalanced-learn

# 或者

conda install -c conda-forge imbalanced-learn

例子:

使用方法和例子请参考文档 http://imbalanced-learn.org/en/stable/api.html

FlashText

在 NLP 任务中,清理文本数据往往需要替换句子中的关键字或从句子中提取关键字。通常,这种操作可以使用正则表达式来完成,但是如果要搜索的术语数量达到数千个,这就会变得很麻烦。Python 的 FlashText 模块是基于 FlashText 算法为这种情况提供了一个合适的替代方案。FlashText 最棒的一点是,不管搜索词的数量如何,运行时间都是相同的。你可以在这里了解更多内容。

安装:

$ pip install flashtext

例子:

提取关键字

from flashtext import KeywordProcessor
keyword_processor = KeywordProcessor()

# keyword_processor.add_keyword(<unclean name>, <standardised name>)

keyword_processor.add_keyword('Big Apple', 'New York')
keyword_processor.add_keyword('Bay Area')
keywords_found = keyword_processor.extract_keywords('I love Big Apple and Bay Area.')

keywords_found
['New York', 'Bay Area']

替换关键字

keyword_processor.add_keyword('New Delhi', 'NCR region')

new_sentence = keyword_processor.replace_keywords('I love Big Apple and new delhi.')

new_sentence
'I love New York and NCR region.'

更多实用案例,请参考官方文档。

Fuzzywuzzy

这个库的名字听起来很奇怪,但是在字符串匹配方面,fuzzywuzzy 是一个非常有用的库。可以很方便地实现计算字符串匹配度、令牌匹配度等操作,也可以很方便地匹配保存在不同数据库中的记录。

安装:

$ pip install fuzzywuzzy

例子:

from fuzzywuzzy import fuzz
from fuzzywuzzy import process

# 简单匹配度

fuzz.ratio("this is a test", "this is a test!")
97

# 模糊匹配度
fuzz.partial_ratio("this is a test", "this is a test!")
 100

更多有趣例子可以在 https://github.com/seatgeek/fuzzywuzzy 找到。

PyFlux

时间序列分析是机器学习领域中最常见的问题之一。PyFlux 是 Python 中的一个开源库,它是为处理时间序列问题而构建的。该库拥有一系列优秀的现代时间序列模型,包括但不限于 ARIMA、GARCH 和 VAR 模型。简而言之,PyFlux 为时间序列建模提供了一种概率方法。值得尝试一下。

安装

pip install pyflux

例子

详细用法和例子请参考官方文档 https://pyflux.readthedocs.io/en/latest/index.html

Ipyvolume

结果展示也是数据科学中的一个重要方面。能够将结果进行可视化将具有很大优势。IPyvolume 是一个可以在 Jupyter notebook 中可视化三维体和图形(例如三维散点图等)的 Python 库,并且只需要少量配置。但它目前还是 1.0 之前的版本阶段。用一个比较恰当的比喻来解释就是:IPyvolume 的 volshow 对于三维数组就像 matplotlib 的 imshow 对于二维数组一样好用。可以在这里获取更多。

使用 pip
$ pip install ipyvolume

使用 Conda/Anaconda
$ conda install -c conda-forge ipyvolume

例子

  • 动画

未分类

未分类

  • 体绘制

未分类

Dash

Dash 是一个高效的用于构建 web 应用程序的 Python 框架。它是在 Flask、Plotly.js 和 React.js 基础上设计而成的,绑定了很多比如下拉框、滑动条和图表的现代 UI 元素,你可以直接使用 Python 代码来写相关分析,而无需再使用 javascript。Dash 非常适合构建数据可视化应用程序。然后,这些应用程序可以在 web 浏览器中呈现。用户指南可以在这里获取。

安装

pip install dash==0.29.0  # 核心 dash 后端
pip install dash-html-components==0.13.2  # HTML 组件
pip install dash-core-components==0.36.0  # 增强组件
pip install dash-table==3.1.3  # 交互式 DataTable 组件(最新!)

例子

下面的例子展示了一个具有下拉功能的高度交互式图表。当用户在下拉菜单中选择一个值时,应用程序代码将动态地将数据从 Google Finance 导出到 panda DataFrame。源码在这里 https://gist.github.com/chriddyp/3d2454905d8f01886d651f207e2419f0

未分类

Gym

OpenAI 的 Gym 是一款用于增强学习算法的开发和比较工具包。它兼容任何数值计算库,如 TensorFlow 或 Theano。Gym 库是测试问题集合的必备工具,这个集合也称为环境 —— 你可以用它来开发你的强化学习算法。这些环境有一个共享接口,允许你进行通用算法的编写。

安装

pip install gym

例子

这个例子会运行 CartPole-v0 环境中的一个实例,它的时间步数为 1000,每一步都会渲染整个场景。

未分类

你可以在 https://gym.openai.com/ 获取其它环境的相关资料

总结

以上这些有用的数据科学 Python 库都是我精心挑选出来的,不是常见的如 numpy 和 pandas 等库。如果你知道其它库,可以添加到列表中来,请在下面的评论中提一下。另外别忘了先尝试运行一下它们。