├── .gitignore ├── README ├── README.md ├── setup.py ├── test └── test.py └── weixin.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.pyc 3 | wxtest.py 4 | newtest.py 5 | access_token 6 | Makefile 7 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Python client SDK for Micro Message Public Platform API. 2 | 3 | You can download the source code at: 4 | 5 | https://github.com/kun945/weixinpy 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # weixinpy 2 | ---- 3 | 这是一个简单的python版本的腾讯微信公共平台的sdk。
4 | 5 | ## 更新日志 6 | ---- 7 | 8 | ### 当前版本 version 0.5.0 9 | ---- 10 | 1、HTTP GET方法关键字由'_get'改为'dget'(原来的'_get'仍然可用)。
11 | 2、修改完善媒体文件的上传(支持"mpeg4/jpeg/jpg/png/gif/bmp/mp3/wma/wav/amr"), 具体看usage。
12 | 13 | ### version 0.4.9 14 | ---- 15 | 1、修复bug。
16 | 2、默认采用FileCache方式保存access_token。
17 | 3、增加对“微信智能接口”、“微信摇一摇周边”、“网页授权”、“数据统计”、“微信小店”的支持。
18 | 19 | ## Usage 20 | ---- 21 | 初始化过程: 22 | ``` 23 | from weixin import WeixinClient, APIError, AccessTokenError 24 | # 如果你有使用python-memcache 可以使用fc=False,path='ip:port'来启用memcache 25 | # 默认使用FileCache, 保存在/tmp/access_token, 可以通过path='path'来设置保存目录 26 | wc = WeiXinClient('您的AppID', '您的AppSecret') 27 | # 使用其他api前必须先获取token 28 | wc.request_access_token() 29 | ``` 30 | 31 | 请求用户列表: 32 | API URL:https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID 33 | ``` 34 | # 我们这里是第一次调用所以next_openid=None 35 | rjson = wc.user.get.dget(next_openid=None) 36 | count = rjson.count 37 | id_list = rjson.data.openid 38 | while count < rjson.total: 39 | rjson = wc.user.get.dget(next_openid=rjson.next_openid) 40 | count += rjson.count 41 | id_list.extend(rjson.openid) 42 | # 最后看看都有哪些用户 43 | print id_list 44 | ``` 45 | 46 | 发送文字信息: 47 | API URL:https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN 48 | ``` 49 | for uid in id_list: 50 | content = '{"touser":"%s", "msgtype":"text", "text":{ "content":"hello!"}}' %uid 51 | #print 可以看有没有发送成功, 可以捕获api错误异常 52 | try: 53 | print wc.message.custom.send.post(body=content) 54 | except APIError, e: 55 | print e, uid 56 | ``` 57 | 58 | 上传并发送媒体: 59 | API URL:http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE 60 | ``` 61 | rjson = wc.media.upload.file(type='image', png=open('/home/pi/ocr_pi.png', 'rb')) 62 | print rjson 63 | # 把上传的图片发出去 64 | for uid in id_list: 65 | content = '{"touser":"%s", "msgtype":"image", ' \ 66 | '"image":{ "media_id":"%s"}}' % (uid, rjson.media_id) 67 | print wc.message.custom.send.post(body=content) 68 | ``` 69 | 70 | 下载媒体文件,如何判断token失效: 71 | API URL:http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID 72 | ``` 73 | # 这里演示了如何捕获token失效的异常,产生这个异常就要更新token了 74 | try: 75 | rm = wc.media.get.file(media_id=rjson.media_id) 76 | print rm 77 | open('./test.png', 'rb').write(rm.read()) 78 | rm.close() 79 | except AccessTokenError, e: 80 | print e 81 | #更新token 82 | wc.refurbish_access_token() 83 | 84 | # 另外我们可以主动判断token是否失效, 这里只是在时间上验证,大多数的情况下是能正常工作的 85 | if wc.is_expires(): 86 | wc.refurbish_access_token() 87 | ``` 88 | 89 | 微信智能接口 90 | ``` 91 | q = '{"query":"查一下鹰潭的天气", "city":"鹰潭", "category":"weather", "appid":"%s"}' %(your_pid) 92 | print wc.semantic.semproxy.search.post(body=q) 93 | ``` 94 | 95 | 媒体文件上传 96 | ``` 97 | print wc.media.upload.file(type='video', mpeg4=open('./a.mp4', 'rb')) 98 | print wc.media.upload.file(type='image', jpeg=open('./a.jpg', 'rb')) 99 | print wc.media.upload.file(type='image', gif=open('./a.gif', 'rb')) 100 | print wc.media.upload.file(type='voice', amr=open('./a.amr', 'rb')) 101 | print wc.media.upload.file(type='voice', wma=open('./a.wma', 'rb')) 102 | ``` 103 | 104 | Have Fun! 105 | 106 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import sys 3 | 4 | import weixin 5 | 6 | kw = dict( 7 | name = 'weixinpy', 8 | version = weixin.__version__, 9 | description = 'Python client SDK for Micro Message Public Platform API', 10 | long_description = open('README', 'r').read(), 11 | author = 'Liang Cha', 12 | author_email = 'ckmx945@gmail.com', 13 | url = 'https://github.com/kun945/weixinpy', 14 | download_url = 'https://github.com/kun945/weixinpy', 15 | py_modules = ['weixin'], 16 | classifiers = [ 17 | 'Development Status :: 5 - Production/Stable', 18 | 'Environment :: Web Environment', 19 | 'Intended Audience :: Developers', 20 | 'License :: OSI Approved :: Apache Software License', 21 | 'Operating System :: OS Independent', 22 | 'Programming Language :: Python', 23 | 'Topic :: Internet', 24 | 'Topic :: Software Development :: Libraries :: Python Modules', 25 | ]) 26 | 27 | setup(**kw) 28 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from weixin import WeiXinClient 5 | 6 | if __name__ == '__main__': 7 | wc = WeiXinClient('your_appid', 'your_secret', fc = True, path='/tmp') 8 | #"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET" 9 | wc.request_access_token() 10 | 11 | data = '{"touser":"obMnLt3bf7t65jyEsa7vOtXphdu4", "msgtype":"text", "text":{ "content":"hello!"}}' 12 | #data = '{"touser":"obMnLt9Qx5ZfPdElO3DQblM7ksl0", "msgtype":"image", ' \ 13 | # '"image":{ "media_id":"OaPSe4DP-HF4s_ABWHEVDgMKOPCUoViID8x-yPUvwCfqTEA0whZOza4hGODiHs93"}}' 14 | key = '{"button":[{"type":"click","name":"test","key":"V1001_TEST"}]}' 15 | #"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN" 16 | print wc.message.custom.send.post(body=data) 17 | #"https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN" 18 | print wc.user.info.dget(openid='obMnLt43lgfeeC8Ljn4-cLixEW6Q', lang='zh_CN') 19 | #"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN" 20 | print wc.message.custom.send.post(body=data) 21 | #"https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID" 22 | print wc.user.get.dget(next_openid=None) 23 | #"http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE" 24 | print wc.media.upload.file(type='image', jpeg = open('./test.jpg', 'rb')) 25 | #"https://api.weixin.qq.com/cgi-bin/groups/create?access_token=ACCESS_TOKEN" 26 | print wc.groups.create.post(body='{"group":{"name":"test_group_01"}}') 27 | #"https://api.weixin.qq.com/cgi-bin/groups/update?access_token=ACCESS_TOKEN" 28 | print wc.groups.update.post(body='{"group":{"id":100,"name":"test"}}') 29 | #"https://api.weixin.qq.com/cgi-bin/groups/members/update?access_token=ACCESS_TOKEN" 30 | print wc.groups.members.update.post(body='{"openid":"obMnLt9Qx5ZfPdElO3DQblM7ksl0","to_groupid":100}') 31 | #"https://api.weixin.qq.com/cgi-bin/groups/getid?access_token=ACCESS_TOKEN" 32 | print wc.groups.getid.post(body='{"openid":"obMnLt9Qx5ZfPdElO3DQblM7ksl0"}') 33 | #"https://api.weixin.qq.com/cgi-bin/groups/get?access_token=ACCESS_TOKEN" 34 | print wc.groups.get.dget() 35 | #"https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN" 36 | print wc.menu.create.post(body=key) 37 | #"https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN" 38 | print wc.menu.get.dget() 39 | #"http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID" 40 | print wc.media.get.file(media_id='OaPSe4DP-HF4s_ABWHEVDgMKOPCUoViID8x-yPUvwCfqTEA0whZOza4hGODiHs93') 41 | -------------------------------------------------------------------------------- /weixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import json 6 | import urllib 7 | import urllib2 8 | 9 | __version__ = '0.5.0' 10 | __author__ = 'Liang Cha (ckmx945@gmail.com)' 11 | 12 | 13 | ''' 14 | Python client SDK for Micro Message Public Platform API. 15 | ''' 16 | 17 | 18 | try: 19 | import memcache 20 | except Exception, e: 21 | print '\033[95mWrining: %s. use local FileCache.\033[0m' %e 22 | 23 | 24 | (_HTTP_GET, _HTTP_POST, _HTTP_FILE) = range(3) 25 | 26 | _CONTENT_TYPE_MEDIA = ('image/jpeg', 'audio/amr', 'video/mpeg4') 27 | 28 | _MEIDA_TYPE = ('mpeg4', 'jpeg', 'jpg', 'gif', 'png', 'bmp', 'mp3', 'wav', 'wma', 'amr') 29 | 30 | _CONTENT_TYPE_JSON= ( 31 | 'application/json; encoding=utf-8', 32 | 'application/json', 33 | 'text/plain', 34 | ) 35 | 36 | _API_URLS = ( 37 | 'https://api.weixin.qq.com/', 38 | 'https://api.weixin.qq.com/cgi-bin/', 39 | ) 40 | 41 | _OTHER_FEATURES = ( 42 | 'semantic', #微信智能接口 43 | 'shackearound', #微信摇一摇周边 44 | 'sns', #网页授权相关 45 | 'datacube', #数据统计 46 | 'merchant', #微信小店 47 | ) 48 | 49 | 50 | class APIError(StandardError): 51 | ''' 52 | raise APIError if reciving json message indicating failure. 53 | ''' 54 | 55 | def __init__(self, error_code, error_msg): 56 | self.error_code = error_code 57 | self.error_msg = error_msg 58 | StandardError.__init__(self, error_msg) 59 | 60 | def __str__(self): 61 | return '%d:%s' %(self.error_code, self.error_msg) 62 | 63 | 64 | class AccessTokenError(APIError): 65 | ''' 66 | raise AccessTokenError if reciving json message indicating failure. 67 | ''' 68 | 69 | def __init__(self, error_code, error_msg): 70 | APIError.__init__(self, error_code, error_msg) 71 | 72 | def __str__(self): 73 | return APIError.__str__(self) 74 | 75 | 76 | class JsonDict(dict): 77 | ''' 78 | general json object that allows attributes to bound to and also behaves like a dict 79 | ''' 80 | 81 | def __getattr__(self, attr): 82 | try: 83 | return self[attr] 84 | except KeyError: 85 | raise AttributeError(r"'JsonDict' object has no attribute '%s'" %(attr)) 86 | 87 | def __setattr__(self, attr, value): 88 | self[attr] = value 89 | 90 | 91 | def _parse_json(s): 92 | ''' parse str into JsonDict ''' 93 | 94 | def _obj_hook(pairs): 95 | o = JsonDict() 96 | for k, v in pairs.iteritems(): 97 | o[str(k)] = v 98 | return o 99 | return json.loads(s, object_hook = _obj_hook) 100 | 101 | 102 | def _encode_params(**kw): 103 | ''' 104 | do url-encode parmeters 105 | 106 | >>> _encode_params(a=1, b='R&D') 107 | 'a=1&b=R%26D' 108 | ''' 109 | args = [] 110 | body = None 111 | path = None 112 | for k, v in kw.iteritems(): 113 | if k == 'body': 114 | body = v 115 | continue 116 | if k in _MEIDA_TYPE: 117 | continue 118 | if isinstance(v, basestring): 119 | qv = v.encode('utf-8') if isinstance(v, unicode) else v 120 | args.append('%s=%s' %(k, urllib.quote(qv))) 121 | else: 122 | if v is None: 123 | args.append('%s=' %(k)) 124 | else: 125 | qv = str(v) 126 | args.append('%s=%s' %(k, urllib.quote(qv))) 127 | return ('&'.join(args), body) 128 | 129 | 130 | def _encode_multipart(**kw): 131 | ''' 132 | build a multipart/form-data body with randomly generated boundary 133 | ''' 134 | data = [] 135 | boundary = '----------%s' % hex(int(time.time()) * 1000) 136 | media_key = [key for key in _MEIDA_TYPE if key in kw.keys()] 137 | media_key = media_key[0] if media_key else 'null' 138 | fd = kw.get(media_key) 139 | media_type = kw.get('type') if kw.has_key('type') else 'null' 140 | content = fd.read() if hasattr(fd, 'read') else 'null' 141 | filename = getattr(fd, 'name', None) 142 | if filename is None: filename = '/tmp/fake.%s' %(media_key) 143 | data.append('--%s' % boundary) 144 | data.append('Content-Disposition: form-data; name="%s"; filename="%s"' %(media_key, filename)) 145 | data.append('Content-Length: %d' % len(content)) 146 | data.append('Content-Type: %s/%s' %(media_type, media_key)) 147 | data.append('Content-Transfer-Encoding: binary\r\n') 148 | data.append(content) 149 | data.append('--%s--\r\n' % boundary) 150 | if hasattr(fd, 'close'): fd.close() 151 | return '\r\n'.join(data), boundary 152 | 153 | 154 | class WeiXinResponse(object): 155 | ''' To deal with response of the base class ''' 156 | 157 | def __init__(self, resp): 158 | self._resp = resp 159 | 160 | def read(self): 161 | return self._resp.read() 162 | 163 | def close(self): 164 | self._resp.close() 165 | 166 | def __str__(self): 167 | return self._resp.headers['Content-Type'] 168 | 169 | 170 | class WeiXinJson(WeiXinResponse): 171 | ''' Json string ''' 172 | 173 | def __init__(self, resp): 174 | WeiXinResponse.__init__(self, resp) 175 | 176 | def read(self): 177 | '''Check api or token error and return json''' 178 | rjson = _parse_json(self._resp.read()) 179 | self._resp.close() 180 | if hasattr(rjson, 'errcode') and rjson.errcode != 0: 181 | if rjson.errcode in (40001, 40014, 41001, 42001): 182 | raise AccessTokenError(rjson.errcode, rjson.errmsg) 183 | raise APIError(rjson.errcode, rjson.errmsg) 184 | return rjson 185 | 186 | #response object close in read() 187 | def close(self): 188 | pass 189 | 190 | 191 | class WeiXinMedia(WeiXinResponse): 192 | ''' Audio, Image, Video etc... ''' 193 | 194 | def __init__(self, resp): 195 | WeiXinResponse.__init__(self, resp) 196 | 197 | 198 | def _http_call(the_url, method, token, **kw): 199 | ''' 200 | send an http request and return a json object if no error occurred. 201 | ''' 202 | body = None 203 | params = None 204 | boundary = None 205 | (params, body) = _encode_params(**kw) 206 | if method == _HTTP_FILE: 207 | the_url = the_url.replace('https://api.', 'http://file.api.') 208 | body, boundary = _encode_multipart(**kw) 209 | if token == None: 210 | http_url = '%s?%s' %(the_url, params) 211 | else: 212 | the_url = '%s?access_token=%s' %(the_url, token) 213 | http_url = '%s&%s' %(the_url, params) if (method == _HTTP_GET or method == _HTTP_FILE) else the_url 214 | http_body = str(body) if (method == _HTTP_POST) else body 215 | req = urllib2.Request(http_url, data = http_body) 216 | if boundary: req.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boundary) 217 | try: 218 | resp = urllib2.urlopen(req, timeout = 8) 219 | except urllib2.HTTPError, e: 220 | if resp.headers['Content-Type'] == _CONTENT_TYPE_JSON: 221 | json = WeiXinJson(resp) 222 | return json.read() 223 | except Exception, e: 224 | raise e 225 | try: 226 | content_type = resp.headers['Content-Type'] 227 | except KeyError, e: 228 | content_type = '??' 229 | resp.headers['Content_Type'] = content_type 230 | #print content_type 231 | if content_type in _CONTENT_TYPE_MEDIA: 232 | return WeiXinMedia(resp) 233 | elif content_type in _CONTENT_TYPE_JSON: 234 | json = WeiXinJson(resp) 235 | return json.read() 236 | else: 237 | return WeiXinResponse(resp) 238 | 239 | 240 | class FileCache(object): 241 | ''' 242 | the information is temporarily saved to the file. 243 | ''' 244 | 245 | def __init__(self, path): 246 | self.path = path 247 | try: 248 | fd = open(self.path, 'rb'); data = fd.read(); fd.close() 249 | self.dict_data = json.loads(data) 250 | except Exception, e: 251 | self.dict_data = dict() 252 | 253 | def get(self, key): 254 | return self.dict_data.get(key) 255 | 256 | def set(self, key, value, time = 0): 257 | self.dict_data[key] = value 258 | 259 | def delete(self, key, time = 0): 260 | if self.dict_data.has_key(key): del self.dict_data[key] 261 | 262 | def save(self): 263 | data = (repr(self.dict_data)).replace('\'', '\"') #json must to use double quotation marks 264 | fd = open(self.path, 'wb'); fd.write(data); fd.close() 265 | 266 | def remove(self): 267 | import os 268 | try: 269 | os.remove(self.path) 270 | except Exception, e: 271 | pass 272 | 273 | def __str__(self): 274 | return repr(self.dict_data) 275 | 276 | 277 | class WeiXinClient(object): 278 | ''' 279 | API clinet using synchronized invocation. 280 | 281 | >>> fc = True 282 | 'use memcache save access_token, otherwise use FileCache, path=[file_path | ip_addr]' 283 | ''' 284 | def __init__(self, appID, appsecret, fc = True, path = '/tmp'): 285 | self.api_url= '' 286 | self.app_id = appID 287 | self.app_secret = appsecret 288 | self.access_token = None 289 | self.expires = 0 290 | self.fc = fc 291 | self.mc = FileCache('%s/access_token' %(path)) \ 292 | if fc else memcache.Client([path], debug=0) 293 | 294 | def request_access_token(self): 295 | token_key = 'access_token_%s' %(self.app_id) 296 | expires_key = 'expires_%s' %(self.app_id) 297 | access_token = self.mc.get(token_key) 298 | expires = self.mc.get(expires_key) 299 | if not access_token or not expires or expires < int(time.time()): 300 | rjson =_http_call(_API_URLS[1] + 'token', _HTTP_GET, 301 | None, grant_type = 'client_credential', 302 | appid = self.app_id, secret = self.app_secret) 303 | self.access_token = str(rjson.access_token) 304 | self.expires = int(time.time()) + rjson.expires_in 305 | self.mc.set(token_key, self.access_token, time = rjson.expires_in) 306 | self.mc.set(expires_key, self.expires, time = rjson.expires_in) 307 | if self.fc: self.mc.save() 308 | else: 309 | self.access_token = str(access_token) 310 | self.expires = expires 311 | 312 | def del_access_token(self): 313 | token_key = 'access_token_%s' %(self.app_id) 314 | expires_key = 'expires_%s' %(self.app_id) 315 | self.access_token = None 316 | self.expires = 0 317 | self.mc.delete(token_key) 318 | self.mc.delete(expires_key) 319 | 320 | def refurbish_access_token(self): 321 | self.del_access_token() 322 | self.request_access_token() 323 | 324 | def set_access_token(self, token, expires_in, persistence=False): 325 | self.access_token = token 326 | self.expires = expires_in + int(time.time()) 327 | if persistence: 328 | token_key = 'access_token_%s' %(self.app_id) 329 | expires_key = 'expires_%s' %(self.app_id) 330 | self.mc.set(token_key, token, time = expires_in) 331 | self.mc.set(expires_key, self.expires, time = expires_in) 332 | if self.fc: self.mc.save() 333 | 334 | def is_expires(self): 335 | return not self.access_token or int(time.time()) >= (self.expires - 10) 336 | 337 | def __getattr__(self, attr): 338 | self.api_url = _API_URLS[0] if attr in _OTHER_FEATURES else _API_URLS[1] 339 | return _Callable(self, attr) 340 | 341 | def __str__(self): 342 | return 'url=%s\napp_id=%s\napp_secret=%s\naccess_token=%s\nexpires=%d' \ 343 | %(self.api_url, self.app_id, self.app_secret, self.access_token, self.expires) 344 | 345 | 346 | class _Executable(object): 347 | 348 | def __init__(self, client, method, path): 349 | self._client = client 350 | self._method = method 351 | self._path = path 352 | 353 | def __call__(self, **kw): 354 | return _http_call('%s%s' %(self._client.api_url, self._path), \ 355 | self._method, self._client.access_token, **kw) 356 | 357 | def __str__(self): 358 | return '_Executable (%s)' %(self._path) 359 | 360 | __repr__ = __str__ 361 | 362 | 363 | 364 | class _Callable(object): 365 | 366 | def __init__(self, client, name): 367 | self._client = client 368 | self._name = name 369 | 370 | def __getattr__(self, attr): 371 | if attr in ('dget', '_get'): 372 | return _Executable(self._client, _HTTP_GET, self._name) 373 | if attr == 'post': 374 | return _Executable(self._client, _HTTP_POST, self._name) 375 | if attr == 'file': 376 | return _Executable(self._client, _HTTP_FILE, self._name) 377 | name = '%s/%s' %(self._name, attr) 378 | return _Callable(self._client, name) 379 | 380 | def __str__(self): 381 | return '_Callable (%s)' %(self._name) 382 | 383 | def test(): 384 | ''' test the API ''' 385 | pass 386 | 387 | if __name__ == '__main__': 388 | test() 389 | --------------------------------------------------------------------------------