[R.M.C] 用python实现微信控制电脑端虾米音乐(3)

接上篇 用python实现微信控制电脑端虾米音乐(2)

上篇里介绍了如何接受用户发送到微信的消息,做了个简单的echo程序,这次我们来讲SAE中完整的代码写完。

一、指令的处理以及储存

也就是图上画红圈的部分:

我们给weixinInterface.py里的POST方法中加上处理指令的代码(weixinInterface.py改后完整的代码见文章末尾):

# 检测是否为命令(是否为"cmd "开头)
if content.startswith("cmd "):
    command = content[4:] # 截取指令的内容
    mc = pylibmc.Client() # 获取Memcache实例
    old = mc.get(fromUser + "_cmd") # 获取该用户缓存的指令
    if not old: # 如果没有缓存的指令,设置成空列表
        old = []
    
    # 将之前缓存的指令与新的指令形成的列表
    # 存放在"用户名_cmd"的键下
    mc.set(fromUser + "_cmd", old + [command])
    content = "命令[{}]已接收".format(command) # 修改回复的内容
Memcache在这里的作用:

先说明一下Memcache在这里的作用,SAE是我们的我们的网络应用,每次我们访问一次我们网络应用的URL就会运行一遍我们的代码,所以如果我们只是单纯的创建一个变量将我们的数据储存在内存中,下一次访问的时候,这些数据都会重置,不能储存下来,所以我们需要Memcache这个缓存服务来讲我们需要储存的数据储存下来,以便之后使用。还有我们创建的这个网络应用时选的是共享环境,所以每次运行我们代码的服务器可能都不一样,所以需要Memcache来缓存我们的数据。Memcache是以键值对的形式来管理数据的,就像Python中的字典一样。

在Memcache中储存命令的形式:

将用户的所有命令已字符串的形式存在列表中,并将该列表储存在Memcache中以”用户名_cmd”的键下

处理命令的具体步骤如下:
  1. 判断用户发送的消息是否为”cmd空格”开头
  2. 截取指令部分即cmd空格之后的内容
  3. 获取pylibmc实例(pylibmc库是一个让Python管理Memcache的库)
  4. 从Memcache中获取用户的旧指令
  5. 将新指令连接到旧指令列表中,储存进Memcache

二、客户端指令的接收

思路

这里让客户端使用GET方法来获取用户储存在SAE的指令。指令以json的形式传递,接收到的将是json形式的列表,列表内以字符串的形式存放着所有未获得的指令。这里为了简单起见,定义:如果使用GET方法访问我们SAE的URL时,有usercmd这个参数表明要获取以这个参数为名称的用户所存放的所有指令。当然这里存在一点安全隐患,因为任何人都可以随意的访问我们这个URL来读取用户的指令列表,可以采取一些简单的加密来提高安全性,这里简单起见就不做这些处理了。

SAE端的设置

首先在weixinInterface.py的GET方法中添加(weixinInterface.py改后完整的代码见文章末尾):

try: # 来自电脑客户端的请求
    user = params.usercmd # 获取usercmd参数
    mc = pylibmc.Client() # 获取Memcache实例
    cmdlist = mc.get(user + "_cmd") # 获取用户对应的所有指令
    if not cmdlist:
        cmdlist = []
    mc.set(user + "_cmd", []) # 将用户对应的指令列表清空
    return json.dumps(cmdlist) # 将指令列表以json的格式作为响应返回

except: # 错误请求
    pass

 

客户端的设置

再在电脑本地创建一个client.py文件,作为我们的客户端程序,之后将使用它来访问我们的SAE来获取指令,我们先写个简单的测试程序:

import requests
import json
import time


if __name__ == "__main__":
    while True:
        user = "LeiZiTing" # 这里填入你企业微信中用户的名称,可以在企业微信的成员管理中找到
        # 访问SAE,并添加参数usercmd
        r = requests.get("http://timpcfanwx01.applinzi.com/weixin?usercmd=" + user)
        cmdlist = json.loads(r.text) # 读取获得的json数据,得到指令列表
        for command in cmdlist: # 处理指令列表中每一条指令
            print(time.strftime("%D %H:%M:%S ") + command)
        time.sleep(1) # 休息一秒钟~

运行我们的测试程序,然后在微信上发以”cmd空格”开头的指令试试看~

效果图:

我们成功将微信上发的内容传递到电脑上了,接下来我们就可以做任何我们想做的事了。

随时随地使用微信给自己的电脑发送指令,通过python完成一些简单的任务,是不是很棒棒xD

以下附上修改后完整的weixinInterface.py:(修改的部分使用注释标明)

# -*- coding: utf-8 -*-
import hashlib
import web
import lxml
import time
import os
import urllib2, json, urllib
from lxml import etree
from WXBizMsgCrypt import WXBizMsgCrypt
import pylibmc # 需要在SAE中申请创建Memcached
import json # 处理json

sCorpID = 'YOUR_CORPID' # 企业微信的CorpID
AGENTID_RMC = 'YOUR_AGENTID' # 应用的AgentId
SECRET_RMC = 'YOUR_SECRET' # 应用的Secret
sToken = 'Token00000'
sEncodingAESKey = 'EncodingAESKey00000000000000000000000000000'


class WeixinInterface:
    def __init__(self):
        self.app_root = os.path.dirname(__file__)
        self.templates_root = os.path.join(self.app_root, 'templates')
        self.render = web.template.render(self.templates_root)
        self.wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID)

    def GET(self):
        # 获取URL参数
        params = web.input()

        try: # 来自微信的请求
            signature = params.msg_signature
            timestamp = params.timestamp
            nonce = params.nonce
            echostr = params.echostr
            ret, echostr = self.wxcpt.VerifyURL(signature, timestamp, nonce, echostr)
            if ret == 0:
                return echostr
           	
        except:
            try: # 来自电脑客户端的请求
                user = str(params.usercmd) # 获取usercmd参数,并转化为str类型(原本是unicode类型)
                mc = pylibmc.Client() # 获取Memcache实例
                cmdlist = mc.get(user + "_cmd") # 获取用户对应的所有指令
                if not cmdlist:
                    cmdlist = []
                mc.set(user + "_cmd", []) # 将用户对应的指令列表清空
                print cmdlist
                return json.dumps(cmdlist) # 将指令列表以json的格式作为响应返回
            
            except: # 错误请求
                pass
                
        
    def POST(self):
        # 获取URL参数
        params = web.input()

        # 获取POST的数据
        data = web.data()

        signature = params.msg_signature
        timestamp = params.timestamp
        nonce = params.nonce

        # 解密消息
        ret, msg = self.wxcpt.DecryptMsg(data, signature, timestamp, nonce)
        if ret != 0:
            print "ERR: DecryptMsg ret: " + str(ret)
            return ''

        xml = etree.fromstring(msg)
        content = xml.find("Content").text
        msgType = xml.find("MsgType").text
        fromUser = xml.find("FromUserName").text
        toUser = xml.find("ToUserName").text
        msgid = xml.find("MsgId").text
        
        # 检测是否为命令(是否为"cmd "开头)
        if content.startswith("cmd "):
            command = content[4:] # 截取指令的内容
            mc = pylibmc.Client() # 获取Memcache实例
            old = mc.get(fromUser + "_cmd") # 获取该用户缓存的指令
            if not old: # 如果没有缓存的指令,设置成空列表
                old = []
            # 将之前缓存的指令与新的指令形成的列表
            # 存放在"用户名_cmd"的键下
            mc.set(fromUser + "_cmd", old + [command])
            content = "命令[{}]已接收".format(command) # 修改回复的内容

        msg_xml = self.render.reply_text(fromUser, toUser, timestamp, content, msgid, AGENTID_RMC)
        # 将产生的数据加密
        ret, encryptmsg = self.wxcpt.EncryptMsg(str(msg_xml), nonce, timestamp)
        if ret != 0:
            print "ERR: EncryptMsg ret: " + str(ret)
            return ''
        # 将加密的数据返回给用户
        return encryptmsg

 

待续未完~

 

[R.M.C] 用python实现微信控制电脑端虾米音乐(2)

接上篇 用python实现微信控制电脑端虾米音乐(1)

上一篇介绍了准备工作,以及企业微信的连接。这次我们来试试让SAE接收微信用户发送的信息,做一个简单的echo程序,即回复用户发送的内容。

先简单说一下流程,当企业微信收到用户发送的消息以后,会将消息本身进行AES加密再POST到我们设置好的SAE应用的URL上。

我们首先在SAE应用的根目录下创建一个templates的文件夹,并在里面创建一个reply_text.xml

$def with (toUser,fromUser,createTime,content,msgid)
<xml>
   <ToUserName><![CDATA[$toUser]]></ToUserName>
   <FromUserName><![CDATA[$fromUser]]></FromUserName> 
   <CreateTime>$createTime</CreateTime>
   <MsgType><![CDATA[text]]></MsgType>
   <Content>$content</Content>
   <MsgId>$msgid</MsgId>
   <AgentID>1000005</AgentID>
</xml>

注意,把最后的AgentID改成你的AgentID。这个文件是回复信息时的模板。

我们来继续编辑SAE上我们之前创建的weixinInterface.py这个文件。上一次,我们只给它写了个GET方法,这个方法是当我们对SAE应用URL使用GET方法访问时会自动调用的方法。这次由于消息接收是使用POST方法传递的,所以我们这里定义一个POST方法。

def POST(self):
    # 获取URL参数
    params = web.input()

    # 获取POST的数据
    data = web.data()


    signature = params.msg_signature
    timestamp = params.timestamp
    nonce = params.nonce

    # 解密消息
    ret, msg = self.wxcpt.DecryptMsg(data, signature, timestamp, nonce)
    if ret != 0:
        print "ERR: DecryptMsg ret: " + str(ret)
        return ''
    # 这里使用print可以用来当做记录日志,也能用来Debug
    # 在SAE的日志中心的错误日志中可以看到print的内容

    xml = etree.fromstring(msg)  # 进行XML解析
    content = xml.find("Content").text  # 获得用户所输入的内容
    msgType = xml.find("MsgType").text
    fromUser = xml.find("FromUserName").text
    toUser = xml.find("ToUserName").text
    msgid = xml.find("MsgId").text

    # 使用模版产生response的内容,我们这里将toUser和fromUser反过来填写,将消息回复给发送的用户
    msg_xml = self.render.reply_text(fromUser, toUser, timestamp, content, msgid, AGENTID_RMC)
    # 将产生的数据加密
    ret, encryptmsg = self.wxcpt.EncryptMsg(str(msg_xml), nonce, timestamp)
    if ret != 0:
        print "ERR: EncryptMsg ret: " + str(ret)
        return ''
    # 将加密的数据返回给用户
    return encryptmsg

在微信上给应用发送一个消息来测试一下吧~

PS:再说一下调试的技巧,如果发送消息过去,并没有消息回复回来不要着急,很可能是代码哪里错了。我们可以在SAE的日志中心里查看错误日志,我们每发一次消息,代码就会运行一次,如果有错误产生,可以在错误日志中看到,所以每改一次代码,保存后,在微信上发送一条消息,然后刷新一下错误日志,可以帮助我们debug喔,是不是很棒棒(๑•̀ㅂ•́)و✧

错误日志如下:

用python实现微信控制电脑端虾米音乐(3)

[R.M.C] 用python实现微信控制电脑端虾米音乐(1)

前言

这个计划的代号叫RMC,至于什么意思呢,你猜嘿嘿。。

嘛,用微信控制虾米这个想法的起源是:有一天,我躺在宿舍的床上,我们宿舍是标准的上床下桌,我的电脑在下面,插着扬声器在外放音乐,当时随机到一首特别嘈杂的歌,听得很难受,我想切歌,又不想下床orz,实在没办法只好自己下床换了一首歌。。

嗯。。于是呢,我就想,有没有什么方法可以用手机控制我电脑换歌的。当然可以用各种vnc远程控制桌面,但感觉太小题大做了,而且要专门控制鼠标去点击播放键实在有点麻烦,一点都不优雅。我就想如果手机端的虾米能控制电脑端的虾米就好了。。当然,用过电脑版虾米的人都知道,电脑版虾米就是个x(笑。这种功能靠虾米官方肯定是不存在的。。

就在这时,我看到了一篇文章,是使用python+微信来实现运维报警的。他是用微信企业号,使用python向企业号api发送消息,来实现运维报警的。我于是想到,我能不能也弄个微信企业号或公众号什么的,通过给它发消息,来控制我的电脑。

想了想感觉还是可以实现的,于是去学习了一下微信公众号的开发,通过这篇文章,简单了解了微信公众号的开发方法。

先来看看整体的思路:

使用微信发送消息至企业号,企业号转发给我的服务器(这里用新浪SAE),服务器管理着用户发的指令,电脑端程序访问新浪SAE来请求新的指令,然后执行相应操作来控制虾米音乐。

下面是详细的流程图

接下来,我来说下实现微信控制电脑端虾米的具体步骤了。

准备

  1.  申请一个微信企业号(订阅号和服务号理论上都可以,企业号的话比较安全,个人申请的话选组织-》没有组织机构代码证,继续注册
  2.  申请一个新浪SAE云应用
  3.  电脑端需要安装python(我这里使用的是python3.6但SAE上使用的是python2.7
  4.  电脑端需要安装python模块pyautogui用来控制虾米(可以直接在命令行中使用pip install pyautogui命令来安装)

具体步骤

第一步、SAE的基本配置

1、在SAE上创建一个python云应用,这里我给它起名叫wx01

(因为我已经创建过了,所以提示名称已占用)

2、打开应用的管理界面,点数据库与缓存服务-》Memcached-》创建memcached,申请一个最小的空间即可,这个是为接下来做准备,在这一节中暂时不会用到。

3、打开应用的管理界面,点应用-》代码管理-》创建版本

SAE是个好东西,相当于小型的web服务器,我们可以通过网页版的代码编辑器来编辑SAE的代码,也可以使用git或svn来进行代码管理。这里我们为了方便直接使用网页版编辑器。

首先编写config.yaml配置文件,打开网页版编辑器,新建一个文件config.yaml,并将下面的代码复制进去,保存。该配置文件指定了SAE上python使用的模块的版本,如果提示python找不到某某模块一般是这个配置文件设置错了。

name: trystanlei
version: 1

libraries:
- name: webpy 
  version: "0.36"

- name: lxml
  version: "2.3.4"

- name: PyCrypto
  version: "2.6"

...

第二步、微信的对接

登录企业微信后台,企业应用-》自建应用-》创建应用,填写好应用名称,在可见范围中选好自己

创建完后,就可以在应用管理这个界面中看到应用的AgentIdSecret,这两个就是这个应用的标识了。

然后我们点 接收消息设置API接收 

按照根据自己情况设置:

这时候点保存会报错:回调URL测试失败。 别紧张,是因为我们SAE上面还没设置完呢~

我们再打开SAE的在线代码编辑器,我们来写ndex.wsgi这个文件:

# encoding: UTF-8

import sae
import web
import os

from weixinInterface import WeixinInterface # 我们即将创建,不要担心

urls = (
'/weixin','WeixinInterface'
) # 将 "你的域名/weixin" 这个地址指向WeixinInterface这个应用


app_root = os.path.dirname(__file__)
templates_root = os.path.join(app_root, 'templates')
render = web.template.render(templates_root)

app = web.application(urls, globals()).wsgifunc()
application = sae.create_wsgi_app(app)

这里使用的是webpy框架,想了解的话就百度一下吧,这里我们不需要详细的理解它就能实现我们的应用。

现在我们下载企业微信的python加密库,可以在这个连接中找到。将下载的python.zip直接上传到SAE代码库。可以直接在SAE应用的代码管理界面上传。

上传完以后是这样的:

其中WXBizMsgCrypt.py就是企业微信的加密库 Readme.txt和Sample.py是介绍和演示可以右键删除掉。

我们再来创建一个weixinInterface.py文件,在里面写上

# -*- coding: utf-8 -*-
import hashlib
import web
import lxml
import time
import os
import urllib2, json, urllib
from lxml import etree
from WXBizMsgCrypt import WXBizMsgCrypt

sCorpID = 'YOUR_CORPID' # 企业微信的CorpID在 我的企业-》企业信息 中可以找到
AGENTID_RMC = 'YOUR_AGENTID' # 应用的AgentId
SECRET_RMC = 'YOUR_SECRET' # 应用的Secret
sToken = 'Token00000' # 设置接收消息时的Token
sEncodingAESKey = 'EncodingAESKey00000000000000000000000000000' # 设置接收消息时的EncodingASEKey


class WeixinInterface:
    def __init__(self):
        self.app_root = os.path.dirname(__file__)
        self.templates_root = os.path.join(self.app_root, 'templates')
        self.render = web.template.render(self.templates_root)
        self.wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID)

    def GET(self):
        # 获取URL参数
        params = web.input()

        try: # 来自微信的请求
            signature = params.msg_signature
            timestamp = params.timestamp
            nonce = params.nonce
            echostr = params.echostr
            ret, echostr = self.wxcpt.VerifyURL(signature, timestamp, nonce, echostr)

            # 如果是来自微信的请求,则回复echostr
            if ret == 0:
                return echostr
            
        except:
            pass

之后点击全部保存

我们再切换到微信设置接收消息的页面,点击保存试试能不能保存成功吧~

如果保存成功,说明前面的设置都没问题啦 🙂

最后可以看到,接收消息这里显示已启用API接收

好了,现在我们准备工作就做完了。我们下一篇见~

用python实现微信控制电脑端虾米音乐(2)

Python 将对象序列化成json格式

之前,介绍过如何将python的对象序列化成python用的pickle格式,但是,pickle格式不方便跨编程语言使用。这里我们使用json格式,方便跨语言使用。

json跟pickle的区别:

  • json是使用文本形式(而且是unicode编码的文本形式,默认utf-8)来保存数据的,具有人类可读性,而pickle使用二进制形式
  • json被设计成跨语言使用的,而pickle是专门针对python的
  • python内建的json模块默认情况下只能序列化python的基本数据类型(dict/list/tuple(但会变成list)/str/int/float/bool/None,不能序列化bytes、set),而pickle可以序列化所有python的基础数据类型且可以序列化用户自定义类

python内建的json模块,有以下几个基本方法:

  • json.dump(obj, f)  #将对象序列化存入文件
  • json.dumps(obj)  #将对象序列化,并返回序列化的字符串
  • json.load(f)   #从文件中读取对象并返回
  • json.loads(str)   #从字符串读取对象并返回

使用方法和pickle模块基本一致,但由于json是文本形式,所以写入或读取文件的时候使用w或r模式:

>>> a_list = [1,2,3,4,5,6]
>>> a_dict = {1:'one', 2:'two', 3:'three'}
>>> a_tuple = ('a', 'b', 'c')
>>> import json
>>> with open('everything.json', 'w', encoding='utf8') as f:
        # 使用pickle.dump方法将对象按照pickle协议序列化,并存到文件everything.json中
        json.dump(a_list, f)  
        json.dump(a_dict, f)
        json.dump(a_tuple, f)
  
>>> with open('everything.json', 'r', encoding='utf8') as f:
        # 使用pickle.load方法,将对象从文件中读取出来
        b_list = json.load(f)
        b_dict = json.load(f)
        b_tuple = json.load(f)
  
>>> # 检测读取的对象跟原对象是否相同
>>> a_list == b_list and a_dict == b_dict
True
>>> a_tuple == b_tuple
False
>>> b_tuple  #tuple对象经过序列化反序列化后变成了list对象!!
['a', 'b', 'c']
>>> a_list
[1, 2, 3, 4, 5, 6, '线性代数']
>>> a_list_json = pickle.dumps(a_list) # 将a_list序列化成josn形式的str对象,并储存在a_list_json中
>>> a_list_json # 序列化后的a_list对象
'[1, 2, 3, 4, 5, 6, "\\u7ebf\\u6027\\u4ee3\\u6570"]'
>>> new_list = pickle.loads(a_list_json) # 将a_list_json反序列化,并赋值给new_list
>>> new_list # json_list和a_list完全一样
[1, 2, 3, 4, 5, 6, '线性代数']

 

虽然说json默认情况下不能保存自定义类型,但实际上是可以通过自己定义转换的函数来实现保存自定义类型的:

import json

class Person:  #自定义类型
    def __init__(self, name):
        self.name = name

    def say(self):
        print(self.name)

    def __eq__(self, other):
        return self.name == other.name


def to_json(python_object):
    """将json不支持的set和Person类型序列化"""
    if isinstance(python_object, set): #set类型
        return {
            '__class__':'set', #创建一个'__class__'键储存类型信息
            '__value__':list(python_object) #创建一个'__value__'存储原本的数据,并使用list类型保存原本set的数据
        }

    if isinstance(python_object, Person): #Person类型
        return {
            '__class__' : 'Person',
            '__value__' : {
                'name':python_object.name
            }
        }
    raise TypeError(repr(python_object) + ' is not JSON serializable')


def from_json(json_object):
    if '__class__' in json_object:
        if json_object['__class__'] == 'set': #处理set类型数据
            return set(json_object['__value__'])
        elif json_object['__class__'] == 'Person': #处理Person类型数据
            return Person(json_object['__value__']['name'])
    return json_object


a_mix = {
    'list' : [1,2,3,4],
    'set' : set([1,2,3,4]),
    'Person' : Person('tim')
}

json_data = json.dumps(a_mix, default=to_json)

print(json_data)

new_mix = json.loads(json_data, object_hook=from_json)

print(a_mix)
print(new_mix)
print(a_mix==new_mix)

输出:

{"list": [1, 2, 3, 4], "set": {"__class__": "set", "__value__": [1, 2, 3, 4]}, "Person": {"__class__": "Person", "__value__": {"name": "tim"}}}
{'list': [1, 2, 3, 4], 'set': {1, 2, 3, 4}, 'Person': <__main__.Person object at 0x00A683D0>}
{'list': [1, 2, 3, 4], 'set': {1, 2, 3, 4}, 'Person': <__main__.Person object at 0x00BEC470>}
True

可以给dump/dumps方法的default参数传递一个处理对象序列化的函数,使其支持自定义类型的序列化。

这里的原理是,将json不支持的类型,以特定的方式储存成json支持的类型,像这里将json不支持的类型set转换成一个包含’__class__’键和’__value__’的dict,在’__class__’键上用字符串储存了类型信息,在’__value__’键上使用list储存了原本set中的信息,而字符串、dict和list都是json支持的类型。dump/dumps在遇到json不支持的类型的时候,就会调用default函数来处理。

可以给load/loads方法的object_hook参数传递一个处理对象反序列化的函数,使其支持自定义类型的序列化。

在这里,我们判断是否存在’__class__’键,如果存在说明是自定义类型,就特殊处理,否则就直接返回就行了。

另外,需要注意的是,使用这种方法还是会让tuple对象变成list对象。

 

参考:http://www.diveintopython3.net/serializing.html#json-unknown-types

Python pickle模块的使用方法

Python内置的pickle模块可以完成python对象的序列化,实现永久储存。使用起来非常之方便。

以下是使用的例子:

>>> a_list = [1,2,3,4,5,6]
>>> a_set = {'one', 'two', 'three'}
>>> a_dict = {1:'one', 2:'two', 3:'three'}
>>> a_tuple = ('a', 'b', 'c')
>>> a_mix = {1:['one', {1.0, 1}], 2:['two', {1.0, 1}]}
>>> import pickle
>>> with open('everything.pickle', 'wb') as f:
        # 使用pickle.dump方法将对象按照pickle协议序列化,并存到文件everything.pickle中
        pickle.dump(a_list, f)  
        pickle.dump(a_set, f)
        pickle.dump(a_dict, f)
        pickle.dump(a_tuple, f)
        pickle.dump(a_mix, f)

  
>>> with open('everything.pickle', 'rb') as f:
        # 使用pickle.load方法,将对象从文件中读取出来
        b_list = pickle.load(f)
        b_set = pickle.load(f)
        b_dict = pickle.load(f)
        b_tuple = pickle.load(f)
        b_mix = pickle.load(f)

  
>>> # 检测读取的对象跟原对象是否相同
>>> a_list == b_list and a_set == b_set and a_dict == b_dict and a_tuple == b_tuple and a_mix == b_mix
True

pickle模块中有dump方法和dumps方法,用于对象序列化,他们的区别是,dump方法将序列化的对象存入文件中,而dumps方法将序列化的对象返回成bytes对象。

与之相对应的有load方法和loads方法,用于对象反序列化,区别是load是从文件中读取,而loads是从bytes对象中读取。

>>> a_list
[1, 2, 3, 4, 5, 6]
>>> a_list_bytes = pickle.dumps(a_list) # 将a_list序列化成bytes对象,并储存在a_list_bytes中
>>> a_list_bytes # 序列化后的a_list对象
b'\x80\x03]q\x00(K\x01K\x02K\x03K\x04K\x05K\x06e.'
>>> b_list = pickle.loads(a_list_bytes) # 将a_list_bytes反序列化,并赋值给b_list
>>> b_list # b_list和a_list完全一样
[1, 2, 3, 4, 5, 6]

 

pickle模块可以序列化用户自定义类:

>>> class Person:
        def __init__(self, name):
            self.name = name
        def say(self):
            print(self.name)
  
>>> a_person = Person('tim')
>>> data = pickle.dumps(a_person)
>>> data
b'\x80\x03c__main__\nPerson\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00timq\x04sb.'
>>> new_person = pickle.loads(data)
>>> new_person.say()
tim

可以看到被序列化内容中有__main__和Person的字眼,也就是__name__和类名,可以看出pickle是通过__name__和类名来识别对象的,所以当跨程序复用pickle序列化的用户自定义类的时候,一定要有相应的类的声明。

 

注意事项:

  • pickle使用二进制形式来序列化,所以写入或读取文件时要用二进制模式
  • pickle是专门针对python设计的,不支持跨语言使用
  • 因为是二进制形式,序列化的内容不具有可读性
  • pickle协议有很多个版本,新版本会兼容老版本,但老版本不支持新版本,因为老版本没有新版本的数据结构。关于pickle协议版本的信息参见这个连接

Python with语句的用法

with块定义了一个runtime context(运行时上下文),当执行了with的语句时进入这个context,当执行完with块中最后一个语句时离开这个context。
当进入with块时,python会自动调用你with语句的__enter__()方法,离开with块时,python会自动调用__exit__(exc_type, exc_value, traceback)方法
with x: # python will calls x.__enter__() automatically
    pass # do something
# python will calls x.__exit__(exc_type, exc_value, traceback) automatically here

 

可以在with块中使用的类,需要拥有以下几个条件
  • 定义了__enter__()方法
  • 定义了__exit__(exc_type, exc_value, traceblack)方法
如下:
class TestCls:
    def __enter__(self):
        print('enter!')
        return self  #返回自己,用于as

    def __exit__(self, *args):
        print('exit!')

    def say(self):
        print('hello there!')

 

>>> x = TestCls()
>>> with x as obj:
        obj.say()

enter!
hello there!
exit!

 

大多数情况下__enter__()方法都会return self,在进入with块的时候会将__enter__返回的值绑定到as指示的变量,这样方便在with块中调用。
在上面的例子中,当进入with块时调用了__enter__方法,并返回self绑定到变量obj上,在with块中访问了obj.say()方法,离开with块时又自动调用了__exit__方法。
特别的,不管以何种方式(异常、错误、正常结束)导致with块结束,都会自动调用__exit__方法,所以使用with块可以比较安全的使用管理资源。

[水] 为了控制自己玩游戏时间写了个小程序hhh

em……..昨天发现网易代理的minecraft国服居然出来了,我就赶紧下下来试一试。里面有Hypixel中国服,Hypixel服务器是一个非常有名的mc小游戏服务器。然而,因为服务器在海外,国内玩的话ping值特别感人,而且大部分游戏还是pvp,在高延迟的情况下玩真的就是找虐额。。现在出了Hypixel中国服,一进去都被感动了,完全没有延迟的感觉,有种活在梦里的感觉(笑。

于是呢,我就玩了一天。。

到晚上,突然感觉不对劲,怎么我又开始沉迷游戏了。。。由于已经成年了,游戏的防沉迷系统已经管不了我了OAO,我就想自己写一个程序来控制一下我玩游戏的时间hhh

于是写了个很简陋的记录游戏时间的程序:

from tkinter import *
import checkproc
import os
import time

def time2str(seconds):
    """将秒钟转换为h:mm:ss的形式的字符串"""
    seconds = int(seconds)
    h, m, s = 0, 0, 0
    while seconds >= 3600:
        h += 1
        seconds -= 3600
    while seconds >= 60:
        m += 1
        seconds -= 60
    s = seconds
    return '{h:}:{m:02}:{s:02}'.format(**locals())


def save(app):
    """储存程序数据到文件"""
    #将数据储存在%appdata%\mytmp\antiaddiction_mc文件夹下
    savepath = os.path.join(os.environ['APPDATA'],'mytmp\\antiaddiction_mc') 
    if not os.path.exists(savepath):
        os.makedirs(savepath)
    filename = os.path.join(savepath, 'user.dat')
    with open(filename, 'w') as f:
        f.write(time.strftime('%y%m%d')+'\n') #储存日期,作为是否为新一天的判定条件
        f.write(str(app.timer_today) + '\n')
        f.write(str(app.timer_total) + '\n')


def load(app):
    """读取文件数据到程序"""
    filename = os.path.join(os.environ['APPDATA'],'mytmp\\antiaddiction_mc\\user.dat')
    if not os.path.exists(filename): #若不存在数据,表明是第一次开启该程序,不载入数据
        return
    with open(filename, 'rU') as f:
        ftime = f.readline().strip()
        today = f.readline().strip()
        total = f.readline().strip()
    if ftime != time.strftime('%y%m%d'):
        app.timer_today = 0 #如果现在的日期跟文件中储存的日期不一样,重置当天计时器
    else:
        app.timer_today = int(today) #日期一样,则载入计时器
    app.timer_total = int(total) #载入总时间


class App(object):
    def __init__(self, master):
        #初始化变量
        self.timer_today = 0 #当天计时器,秒
        self.timer_total = 0 #总时间计时器,秒
        self.time_limit = 3600 * 2 #设置每天的游戏时间限制,秒
        self.is_game_run = None #指示游戏是否运行
        self.is_game_run_str = StringVar()
        self.total_time_str = StringVar()
        self.today_time_left_str = StringVar()
        self.penalty_time_str = StringVar()

        #界面初始化
        frame = Frame(master=master)
        frame.pack()
        self.status_label = Label(frame, textvariable=self.is_game_run_str, font=('微软雅黑', 10))
        self.status_label.grid(row=2, column=0, rowspan=2)
        Label(frame, text='累计游戏时间', font=('微软雅黑', 10)).grid(row=0, column=1)
        Label(frame, textvariable=self.total_time_str, font=('微软雅黑', 10)).grid(row=1, column=1)
        Label(frame, text='今日剩余游戏时间', font=('微软雅黑', 10)).grid(row=0,column=0)
        Label(frame, textvariable=self.today_time_left_str, font=('微软雅黑', 10)).grid(row=1,column=0)
        Label(frame, text='超出的时间', font=('微软雅黑', 10)).grid(row=2, column=1)
        Label(frame, textvariable=self.penalty_time_str, font=('微软雅黑', 10)).grid(row=3, column=1)


def updata(root, app):

    #检查游戏是否在运行(检查javaw.exe这个进程有没有运行)
    if checkproc.CheckProcExistByPN('javaw.exe'): app.is_game_run = True
    else: app.is_game_run = False

    if app.is_game_run: #如果游戏在运行的话,计时器加1秒
        app.timer_today += 1
        app.timer_total += 1

    #更新显示
    if app.is_game_run: #开始计时
        app.is_game_run_str.set('游戏正在运行')
        app.status_label.configure(fg='green')
    else: #停止计时
        app.is_game_run_str.set('游戏未运行')
        app.status_label.configure(fg='red')
    app.today_time_left_str.set(time2str(max(0, app.time_limit - app.timer_today))) #当天剩余游戏时间
    app.total_time_str.set(time2str(app.timer_total)) #总游戏时间
    app.penalty_time_str.set(time2str(max(0, app.timer_today - app.time_limit))) #超出的时间

    #1000毫秒后再调用一遍updata(root,app)
    root.after(1000, updata, root, app)


def main():
    root = Tk()
    root.wm_title('AntiAddiction for me')
    app = App(root)
    load(app) #载入数据
    updata(root, app) #调用updata,并让root,1000毫秒后再次调用updata
    root.mainloop() #进入界面主循环
    save(app) #退出程序时,保存数据


if __name__ == '__main__':
    main()

 

其中,检查进程的函数是使用http://www.cnblogs.com/samren/archive/2012/09/12/2682501.html文章上的。

# -*- checkproc.py -*-

import win32com.client

def CheckProcExistByPN(process_name):
    try:
        WMI = win32com.client.GetObject('winmgmts:')
        processCodeCov = WMI.ExecQuery('select * from Win32_Process where Name="%s"' % process_name)
    except Exception as e:
        print(process_name + "error : ", e)
    if len(processCodeCov) > 0:
        #print(process_name + " exist")
        return True
    else:
        #print(process_name + " is not exist")
        return False

 

最后,程序是长这样的啦hhh

Python str.format()的使用方法

str.format()方法是用来格式化字符串的,可以将变量插入给定字符串中,也可以控制格式。

占位符{}

format使用大括号{}来当占位符,大括号中包含三个字段,其标准形式是
{ [field_name] ["!" conversion] [":" format_spec] }
中括号表示可选部分
field_name:表示这个占位符引用的变量。
conversion:表示在格式化之前对变量进行转换的方法,可以用’!s’表示先使用str()方法,’!r’表示先使用repr()方法,’!a’表示先使用ascii()方法处理变量。
format_spec:就是格式化字段啦,在下面会详细讲。

format引用变量的方式

>>> name = 'tim'
>>> age = 19

>>> 'My name is {}, and i am {}'.format(name, age)  # {}用来做占位符,返回的字符串中,会将给format的参数插入字符串内,如果大括号内什么都不写,默认就是从左到右按序号0,1,2...排列
'My name is tim, and i am 19'

>>> 'My name is {1}, and i am {0}'.format(age, name)  # 可以在大括号中加入数字,表示编号,这里{0}对应的是age,{1}对应的是name
'My name is tim, and i am 19'

>>> 'My name is {name}, and i am {age}'.format(name=name, age=age)  # 也可以给使用字符串给占位符命名,在给format参数的时候使用"占位符名=变量"的形式
'My name is tim, and i am 19'

>>> kv = {'name' : 'tim', 'age' : 19}
>>> 'My name is {name}, and i am {age}'.format(**kv)  # 既然有上面的方式,就可以使用**解字典的方式给format提供参数
'My name is tim, and i am 19'

format还可以使用一些类似python语法的方式访问一些数据结构,如:

>>> fruitlist = [('apple', '$1.99'), ('banana', '$2.99')]
>>> '{0[0][0]} is {0[0][1]}, while {0[1][0]} is {0[1][1]}'.format(fruitlist)
'apple is $1.99, while banana is $2.99'

其中,0[0][0]的第一个’0’表示format的第一个参数,也就是’fruitlist’,0[0][0]就表示 fruitlist[0][0] 也就是’apple’了。

访问字典:

>>> fruitdict = {'apple':'$1.99', 'banana': '$2.99'}
>>> 'apple is {0[apple]}, while banana is {0[banana]}'.format(fruitdict)
'apple is $1.99, while banana is $2.99'

注意,字典中的字符串在引用时不需要加单引号或双引号,如 0[apple] 表示 fruitdict['apple'],这就导致数字和数字字符串难以区分,如下:

>>> numberdict = {'0' : 'string zero', 0 : 'number zero'}
>>> 'It is {0[0]}'.format(numberdict)
'It is number zero'

还可以访问对象的属性:

>>> class person(object):
        def __init__(self, name, age):
            self.name = name
            self.age = age

>>> a_person = person('tim', 19)

>>> 'my name is {0.name}, i am {0.age}'.format(a_person)  # '0'代表'a_person'
'my name is tim, i am 19'

甚至还可以访问模块:

>>> import sys
>>> 'My python verson is {0.version}'.format(sys) # '0.version' 表示 'sys.version'
'My python verson is 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)]'

 

format的格式控制

format_spec字段的完整定义是:[[fill]align][sign][#][0][width][,][.precision][type] 中括号部分可以省略

fill:填充符,可以使用除’}’外的所有字符,必须要有对齐标识符,填充符才有效
align:对齐标识符,有:

align
Meaning
‘<‘
左对齐
‘>’
右对齐
‘=’
强制符号位在占位符前面,而不是在数字前面
‘^’
居中对齐

sign:符号标识符,可以使用”+”:正数也显示符号,”-“:只有负数才显示符号(默认)  ” “:空格表示正数前有一个空格,负数前有负号
#:只对整数有效,显示前缀’0b’, ‘0o’, ‘0x’
width:表示字符串的宽度,在前面加0表示使用0填充
,:使用千位分隔符
.precision:表示小数点后应该保留几位
type:表示类型

对于字符串来说,类型有:

type
Meaning
‘s’
字符串输出
None
同’s’

对于整型来说,类型有:

type
Meaning
‘b’
使用二进制形式输出
‘c’
以单字符的形式输出(将整数转换成对应的unicode字符)
‘d’
使用十进制形式输出
‘o’
使用八进制形式输出
‘x’
使用十六进制形式输出,字母使用小写
‘X’
使用十六进制形式输出,字母使用大写
‘n’
表示数字,使用本地的设置在数字中适当插入分隔符
None
同’d’

对于浮点数来说,类型有:

type
Meaning
‘e’
使用科学计数法来打印浮点数,用e来表示指数
‘E’
同’e’,但用E来表示指数
‘f’
定点表示
‘F’
同’f’,但用INF替代inf,NAN替代nan
‘g’
通用模式,根据情况选择使用’e’或’f’的形式表示
‘G’
通用模式,根据情况选择使用’E’或’F’的形式表示
‘n’
表示数字,使用本地的设置在数字中适当插入分隔符
‘%’
表示百分数,将浮点数乘100,使用’f’的形式显示,并再末尾加百分号%
None
跟’g’相似

 

使用对齐:

>>> '{:<30}'.format('left')  # <表示左对齐,30为宽度
'left                          '
>>> '{:>30}'.format('right')  # >表示右对齐
'                         right'  
>>> '{:^30}'.format('center')  # ^表示居中对齐
'            center            '
>>> '{:#^30}'.format('center')  #  其中的井号#表示填充字符
'############center############'

使用不同进制:

>>> '{0:#d} {0:#b} {0:#o} {0:#x}'.format(24)  # 其中的井号#表示显示前缀
'24 0b11000 0o30 0x18'

format的参数非常灵活,可以嵌套使用,如:

>>> for w in range(1, 10):
        print('|{0:{fill}>{width}}|'.format(w, fill=w, width=w))

 
|1|
|22|
|333|
|4444|
|55555|
|666666|
|7777777|
|88888888|
|999999999|

在每轮循环里,相当于先用format把format_spec字段先构造了一遍,构造成’|{0:w>w}|’这个字符串(这里的w为确定的数),再调用一次format完成格式化。