├── img ├── qq-qun.png ├── step1.jpg ├── step2.jpg ├── step3.png ├── step4.png ├── step5.PNG ├── QA-06-01.png ├── QA-06-02.png ├── QA-07-01.png └── QA-07-02.png ├── requirements.txt ├── __pycache__ ├── browser.cpython-38.pyc ├── kbhit.cpython-38.pyc ├── imessage.cpython-38.pyc ├── qpython3.cpython-38.pyc └── idcard_information.cpython-38.pyc ├── qpython3_run.py ├── ChangeLog.md ├── browser.py ├── config.yaml ├── tips.md ├── qpython3.py ├── kbhit.py ├── imessage.py ├── README.md ├── doc.md ├── bjguahao.py ├── LICENSE └── lib └── prettytable.py /img/qq-qun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/qq-qun.png -------------------------------------------------------------------------------- /img/step1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/step1.jpg -------------------------------------------------------------------------------- /img/step2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/step2.jpg -------------------------------------------------------------------------------- /img/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/step3.png -------------------------------------------------------------------------------- /img/step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/step4.png -------------------------------------------------------------------------------- /img/step5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/step5.PNG -------------------------------------------------------------------------------- /img/QA-06-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/QA-06-01.png -------------------------------------------------------------------------------- /img/QA-06-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/QA-06-02.png -------------------------------------------------------------------------------- /img/QA-07-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/QA-07-01.png -------------------------------------------------------------------------------- /img/QA-07-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/img/QA-07-02.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.20.0 2 | pyyaml>=4.2b1 3 | pycryptodome>=3.8.2 4 | tqdm>=4.7.0 5 | -------------------------------------------------------------------------------- /__pycache__/browser.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/__pycache__/browser.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/kbhit.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/__pycache__/kbhit.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/imessage.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/__pycache__/imessage.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/qpython3.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/__pycache__/qpython3.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/idcard_information.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndyXiaoyu/beijingguahao/HEAD/__pycache__/idcard_information.cpython-38.pyc -------------------------------------------------------------------------------- /qpython3_run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 - 3 | 4 | # 这里需修改 config_name 为你的配置文件名称,如 5 | config_name = "config.yaml" 6 | 7 | # 以脚本地址作为配置文件地址 8 | import sys, os 9 | config_path = os.path.join(os.path.dirname(sys.argv[0]), config_name) 10 | 11 | try: 12 | import requests 13 | except: 14 | import pip 15 | pip.main(['install', 'requests']) 16 | 17 | try: 18 | import yaml 19 | except: 20 | import pip 21 | pip.main(['install', 'PyYAML']) 22 | 23 | if __name__ == "__main__": 24 | from bjguahao import Guahao 25 | guahao = Guahao(config_path) 26 | guahao.run() 27 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## bjguahao 1.0.4 2 | * realse date: 2019-04-01 3 | * 支持儿研所挂号,其他儿童号自测 4 | * 更新域名为114yygh.com 5 | * 配置文件增加相关儿童挂号参数 6 | 7 | 8 | ## bjguahao 1.0.3 9 | * realse date: 2018-11-05 10 | * issue64修复 11 | 12 | 13 | ## bjguahao 1.0.2 14 | * realse date: 2018-10-18 15 | * 优化挂号逻辑 16 | * 支持安卓挂号 17 | * 修改网站验证方式改变导致的登录不上的问题 18 | 19 | 20 | ## bjguahao 1.0.1 21 | * realse date: 2018-03-01 22 | * 添加requirements.txt文件 23 | * 支持iphone - MAC验证码自动获取 24 | 25 | ## bjguahao 1.0.0 26 | * realse date: 2018-01-01 27 | * 从python2环境迁移到python3 28 | * 配置文件修改为yaml,支持注释 29 | * 修改了一些bug 30 | 31 | ## bjguahao 0.1.4 32 | * realse date: 2017-11-20 33 | * 超时重新登录功能 34 | * Add LICENSE 35 | * 支持挂专家号 36 | * 添加挂号攻略 37 | * 添加挂号技巧 38 | * 修复一些bug 39 | 40 | ## bjguahao 0.1.3 41 | * realse date: 2017-07-13 42 | * 自动挂最后一日的号码 43 | * 指定配置文件地址 44 | 45 | ## bjguahao 0.1.2 46 | * realse date: 2017-04-01 47 | * 优化挂号流程 48 | * 修正中文乱码的问题 49 | 50 | ## bjguahao 0.1.1 51 | * realse date: 2017-03-20 52 | * 支持其他就诊人 53 | * 获取放号时间 54 | 55 | ## bjguahao 0.1.0 56 | * realse date: 2017-03-19 57 | * 完成基本功能,可以正常挂号 58 | -------------------------------------------------------------------------------- /browser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 3 | 4 | 5 | import pickle 6 | import requests 7 | 8 | 9 | class Browser(object): 10 | """ 11 | 浏览器 12 | """ 13 | 14 | def __init__(self): 15 | self.session = requests.Session() 16 | self.session.headers = { 17 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36', 18 | 'Content-Type': 'application/json; charset=UTF-8', 19 | } 20 | 21 | def load_cookies(self, path): 22 | with open(path, 'rb') as f: 23 | self.session.cookies = requests.utils.cookiejar_from_dict(pickle.load(f)) 24 | 25 | def save_cookies(self, path): 26 | with open(path, 'wb') as f: 27 | cookies_dic = requests.utils.dict_from_cookiejar(self.session.cookies) 28 | pickle.dump(cookies_dic, f) 29 | 30 | def get(self, url, data): 31 | """ 32 | http get 33 | """ 34 | pass 35 | response = self.session.get(url) 36 | if response.status_code == 200: 37 | self.session.headers['Referer'] = response.url 38 | return response 39 | 40 | def post(self, url, data): 41 | """ 42 | http post 43 | """ 44 | response = self.session.post(url, json=data) 45 | if response.status_code == 200: 46 | self.session.headers['Referer'] = response.url 47 | return response 48 | 49 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | 2 | # username: 您的的用户名(一般是手机号码) 3 | username: "17710338875" 4 | 5 | ## password: 密码 6 | #password: "guanyu666" 7 | 8 | # date: 挂号日期,如填写latest自动挂最新一天 9 | date: "2020-05-11" 10 | 11 | 12 | # hospitalId: 医院id 13 | hospitalId: "122" 14 | 15 | # departmentId: 科室id 16 | departmentId: "200001004" 17 | 18 | # 关于如何获取 hospitalId 和 departmentId 19 | # 1. 打开挂号页面 20 | # 2. 假设地址栏中地址是 http://www.114yygh.com/dpt/appoint/162-200002248.htm 21 | # 3. 其中 162 是 hospitalId 22 | # 4. 其中 200002248 是 departmentId 23 | 24 | 25 | # 需要挂早上的号请填写1 需要挂下午的号请填写2 上下午均可填写0 26 | dutyCode: "0" 27 | 28 | # patientName: 患者姓名 29 | # 若是自己挂号可为空 30 | patientName: "关宇" 31 | 32 | # 就诊卡号 33 | hospitalCardId: "" 34 | 35 | # 医保卡号 36 | medicareCardId: "232324198905056737" 37 | 38 | # 保险类型 39 | # 1:医保 40 | # 10:自费 41 | reimbursementType: "1" 42 | 43 | # doctorName: 医生姓名 44 | # 不填写的话默认选最好的医生 45 | # 填写后若这个医生没有号,会自动选其余号中最好的医生 46 | doctorName: 47 | - "黄石玺" 48 | 49 | # 指定医生 50 | # false:默认不指定 51 | # true:只挂指定医生的号 52 | assign: "true" 53 | 54 | #true:检索每天余票 55 | remaining: "false" 56 | 57 | #remaining=true时,默认检索工作日,周末:6,7 58 | week: "1,2,3,4,5,6,7" 59 | 60 | #挂号类型是否为儿童号 61 | # 可选项为"true" 或者 "false" 62 | children: "false" 63 | 64 | #患儿名字 如果儿童挂号必须填写 65 | childrenName: "" 66 | 67 | #患儿证件号 如果儿童挂号必须填写 68 | childrenIdNo: "" 69 | 70 | #患儿证件 如果儿童挂号必须填写 71 | #1:身份证 72 | #2:其他 73 | cidType: "1" 74 | 75 | # chooseBest: 选择模式 76 | # 不填写的默认从最好的医生开始选择 77 | # 可选项为"yes" 或者 "no" 78 | chooseBest: "yes" 79 | 80 | # DebugLevel: 调试等级 81 | # 支持的调试等级有 debug/info/warning/error/critical 82 | DebugLevel: "debug" 83 | 84 | #使用ios短信和mac电脑接收验证码 85 | # 可选项为"true" 或者 "false" 86 | useIMessage: "true" 87 | 88 | # 是否使用 QPython3.6 运行本脚本 89 | # 可选项为"true" 或者 "false" 90 | useQPython3: "false" 91 | -------------------------------------------------------------------------------- /tips.md: -------------------------------------------------------------------------------- 1 | **敲黑板!!!敲黑板!!敲黑板** 2 | 3 | 大家都知道,在大北京挂号看病是如此的艰难。所以!!!我就结合自己的亲身经历以及自己收集信息的技能,总结一下几种挂号方式。废话不多说,上干货! 4 | 北医三院是我最常去的医院,也是最熟悉的,可以通过很多种方式挂到号:服务号,114,电话,自己去排队..... 5 | 6 | ## 挂号方式 7 | 8 | ### 1.自己去医院排队,挂当天的号 9 | 这个可以参考这个:[拔牙攻略](https://bbs.byr.cn/#!article/Health/200261) 10 | 这个主要是写的关于拔牙的攻略。不过个人觉得挂号流程可以适用于其它科。*(还没咨询小姐姐or小哥哥盗用了链接,要是觉得有问题,联系我我撤掉哦!)* 11 | 12 | ### 2.北医三院服务号挂号 13 | 微信号:**bysy_001** 14 | 这个服务号**预约挂号** 是没有牙科、运动医学科,骨科的。**但是!!** 它的当天挂号就会有运动医学、骨科(还是木有牙科)! 15 | 服务号的预约挂号是九点半放号,但是当天挂号是**早上七点十分!!** ,可以参考这个小哥哥or小姐姐的文章:[当天挂号](https://bbs.byr.cn/#!article/Health/202896) *(有问题联系我我撤掉哦!)* 16 | ***attention: 17 | 放号之前最好先把自己的个人信息神马的都填了!!做好一切准备工作。*** 18 | 19 | ### 3.挂号脚本挂号(只针对114网站,9点半放号) 20 | 这个是之前我为了挂号,看到我邮大神写的一个脚本,目前有python2,python3两个版本。 21 | [python2:](https://github.com/iBreaker/bjguahao) 22 | [python3:](https://github.com/wzhvictor/bjguahao) 23 | 这是大佬们写的挂号脚本,只能针对北京统一挂号网站(114网站),上面可以挂很多北京医院的号!!!只需要配置文件就可以了!再次膜拜大佬~ 24 | 25 | ### 4.北京统一挂号平台(114挂号)其它人工方式 26 | 114也有自己的微信公众号: **beijing114guahao** ,这个上面也可以挂号。 27 | 114还可以电话挂号,电话是 **010-114/116114**,放号时间同步,最好是提前两分钟左右打进去。 28 | 29 | ### 5.其它各种商业软件 30 | 比如 好大夫 之类的,这个得估计医生来了,要是之前你看过某个医生,可以试试在上面预约试试,不过不保证有号! 31 | 32 | 33 | 34 | ## QA(根据QA部分编写者个人使用情况陆续更新) 35 | 36 | ### 1. 无法使用密码登录?(待确认) 37 | 38 | 114网站更新,现阶段只能用手机号加验证码方式登录,所以现在最好的方式是mac配合iPhone通过iMessage自动登录。 39 | 40 | ### 2. 发送验证码失败? 41 | 42 | 短时间内发送验证码次数过多,等会儿再试 43 | 44 | ### 3. 您的请求过于频繁,请稍后再试 45 | 46 | 短时间内发送请求次数过多,等会儿再试 47 | 48 | ### 4. 请选择有效就诊人 49 | 50 | 请现在114网站中增加就诊人信息 51 | 52 | ### 5. 获取患者id失败 53 | 54 | 暂时不清楚原因,重新跑脚本应该就可以解决 55 | 56 | ### 6. 使用Mac+iPhone方式时, iPhone信息设置中没有信息转发? 57 | 58 | 进入信息——发送与接收,勾选AppleID邮箱 59 | 60 | ![QA-02-01](/img/QA-06-01.png) 61 | 62 | ![QA-02-01](/img/QA-06-02.png) 63 | 64 | ### 7. 找不到短信数据库 65 | 66 | 新版本mac OS权限收紧,终端默认没有访问 `~/Library/Messages/chat.db` 路径的权限 67 | 68 | 需要在系统偏好设置——安全性和隐私——文件保险箱中给终端完整的磁盘访问权限,不给这个权限`sudo`也没用 69 | 70 | ![QA-02-01](/img/QA-07-01.png) 71 | 72 | ![QA-02-01](/img/QA-07-02.png) 73 | 74 | 75 | 76 | 77 | ## 致谢 78 | - 这里尤其需要感谢一下写脚本的几位大佬 79 | 80 | [大佬1号:iBreaker](https://github.com/iBreaker) 81 | 82 | [大佬2号:wzhvictor](https://github.com/wzhvictor) 83 | 84 | [大佬3号:以上写攻略的小哥哥小姐姐们](https://bbs.byr.cn/) 85 | 86 | - 若有疑问,欢迎联系我这个身经挂号百战的小战士(在上述网址中的交流群@lily 或者私信) 87 | - 上述部分网址是北京邮电大学论坛网址,可能需要注册才能进入 88 | 89 | ## last 90 | 生病谁都不想,但是吧,这个也不全是你能控制的。所以生病不可怕,只要抓紧看病就好!愿更少的人能看到这篇文章! 91 | -------------------------------------------------------------------------------- /qpython3.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | import logging 4 | import time 5 | 6 | from androidhelper import Android 7 | 8 | # 主 Class 9 | class QPython3(object): 10 | # 初始化 11 | def __init__(self): 12 | self.regex = re.compile('证码为.*【(\d+)】') # regex 13 | self.start_time = datetime.datetime.now() 14 | # Android QPython3 15 | self.droid = Android() 16 | logging.debug("QPython3 实例初始化完成") 17 | # 读取验证码短信 18 | def _get_sms_verify_code(self): 19 | # init 20 | self.start_time = datetime.datetime.now() 21 | code = '000000' 22 | retry = 600 23 | # loop 24 | logging.debug("监控短信中……") 25 | while retry > 0: 26 | retry -= 1 27 | # 检查 SMS 28 | code = self._check_sms_verify_code() 29 | # 有效验证码? 30 | if code != '000000': 31 | logging.debug("取得有效验证码……"+code) 32 | break 33 | else: 34 | logging.debug("未找到有效验证码……重试中, retry = {}".format(retry)) 35 | time.sleep(0.05) 36 | else: 37 | logging.debug("未找到有效验证码……"+code) 38 | return code 39 | # 读取验证码短信 40 | def _check_sms_verify_code(self): 41 | # init 42 | code = '000000' 43 | # 获取当前的全部未读短信 44 | smsMessageIds = self.droid.smsGetMessageIds(True) 45 | # 无短信退出 46 | if len(smsMessageIds.result) == 0: 47 | logging.debug("无短信退出……") 48 | return code 49 | # loop 50 | for smsId in smsMessageIds.result: 51 | # get message 52 | smsMessage = self.droid.smsGetMessageById(smsId) 53 | # 时间筛选 54 | smsTimestamp = datetime.datetime.fromtimestamp(int(smsMessage.result['date'])/1e3) 55 | # 跳过开始时间点前 SMS 56 | if smsTimestamp < self.start_time: 57 | # print("时间跳过:", smsMessage) 58 | continue 59 | # 文字匹配 60 | smsContent = smsMessage.result['body'] 61 | res = self.regex.search(smsContent) 62 | if res is None: 63 | # print("匹配跳过:", smsMessage) 64 | continue 65 | else: 66 | # print("发现短信:", smsMessage) 67 | code = res.group(1).strip() 68 | break 69 | return code 70 | # 获取验证码的外部调用 71 | def get_verify_code(self): 72 | logging.debug("获取验证码中……") 73 | # 读取验证码短信 74 | return self._get_sms_verify_code() 75 | 76 | 77 | 78 | ## 测试使用 79 | def main(): 80 | logging.basicConfig(level=logging.DEBUG, 81 | format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', 82 | datefmt='%a, %d %b %Y %H:%M:%S') 83 | droid = QPython3() 84 | code = droid.get_verify_code() 85 | print(code) 86 | 87 | if __name__ == '__main__': 88 | main() 89 | -------------------------------------------------------------------------------- /kbhit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.5 2 | # !/usr/bin/env python 3 | """ 4 | A Python class implementing KBHIT, the standard keyboard-interrupt poller. 5 | Works transparently on Windows and Posix (Linux, Mac OS X). Doesn't work 6 | with IDLE. 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU Lesser General Public License as 10 | published by the Free Software Foundation, either version 3 of the 11 | License, or (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | """ 18 | 19 | import os 20 | 21 | # Windows 22 | if os.name == 'nt': 23 | import msvcrt 24 | 25 | # Posix (Linux, OS X) 26 | else: 27 | import sys 28 | import termios 29 | import atexit 30 | from select import select 31 | 32 | 33 | class KBHit: 34 | 35 | def __init__(self): 36 | """Creates a KBHit object that you can call to do various keyboard things. 37 | """ 38 | 39 | if os.name == 'nt': 40 | pass 41 | 42 | else: 43 | 44 | # Save the terminal settings 45 | self.fd = sys.stdin.fileno() 46 | self.new_term = termios.tcgetattr(self.fd) 47 | self.old_term = termios.tcgetattr(self.fd) 48 | 49 | # New terminal setting unbuffered 50 | self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO) 51 | termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term) 52 | 53 | # Support normal-terminal reset at exit 54 | atexit.register(self.set_normal_term) 55 | 56 | def set_normal_term(self): 57 | """ Resets to normal terminal. On Windows this is a no-op. 58 | """ 59 | 60 | if os.name == 'nt': 61 | pass 62 | 63 | else: 64 | termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term) 65 | 66 | def getch(self): 67 | """ Returns a keyboard character after kbhit() has been called. 68 | Should not be called in the same program as getarrow(). 69 | """ 70 | 71 | s = '' 72 | 73 | if os.name == 'nt': 74 | return msvcrt.getch().decode('utf-8') 75 | 76 | else: 77 | return sys.stdin.read(1) 78 | 79 | def getarrow(self): 80 | """ Returns an arrow-key code after kbhit() has been called. Codes are 81 | 0 : up 82 | 1 : right 83 | 2 : down 84 | 3 : left 85 | Should not be called in the same program as getch(). 86 | """ 87 | 88 | if os.name == 'nt': 89 | msvcrt.getch() # skip 0xE0 90 | c = msvcrt.getch() 91 | vals = [72, 77, 80, 75] 92 | 93 | else: 94 | c = sys.stdin.read(3)[2] 95 | vals = [65, 67, 66, 68] 96 | 97 | return vals.index(ord(c.decode('utf-8'))) 98 | 99 | def kbhit(self): 100 | """ Returns True if keyboard character was hit, False otherwise. 101 | """ 102 | if os.name == 'nt': 103 | return msvcrt.kbhit() 104 | 105 | else: 106 | dr, dw, de = select([sys.stdin], [], [], 0) 107 | return dr != [] 108 | 109 | 110 | # Test 111 | if __name__ == "__main__": 112 | kb = KBHit() 113 | print('Hit any key, or ESC to exit') 114 | while True: 115 | if kb.kbhit(): 116 | c = kb.getch() 117 | if ord(c) == 27: # ESC 118 | break 119 | print(c) 120 | kb.set_normal_term() 121 | -------------------------------------------------------------------------------- /imessage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.5 2 | # -*- coding: utf-8 -*- 3 | from os.path import expanduser 4 | import sqlite3 5 | import datetime 6 | import re 7 | import time 8 | import kbhit 9 | import sys 10 | from concurrent.futures import ThreadPoolExecutor 11 | from concurrent.futures import wait 12 | from concurrent.futures import FIRST_COMPLETED 13 | 14 | OSX_EPOCH = 978307200 15 | 16 | 17 | class IMessage(object): 18 | def __init__(self): 19 | self.regex = re.compile('证码为.*【(\d+)】') 20 | self.pool = ThreadPoolExecutor(2) 21 | self.done = False 22 | 23 | @staticmethod 24 | def _new_connection(): 25 | # The current logged-in user's Messages sqlite database is found at: 26 | # ~/Library/Messages/chat.db 27 | db_path = expanduser("~") + '/Library/Messages/chat.db' 28 | try: 29 | conn = sqlite3.connect(db_path) 30 | return conn 31 | except Exception as e: 32 | print('找不到短信数据库', file=sys.stderr, flush=True) 33 | # sys.exit(-1) 34 | return None 35 | 36 | def _get_keyboard_verify_code(self): 37 | print('验证码: ', end='', flush=True) 38 | kb = kbhit.KBHit() 39 | code = '' 40 | while not self.done: 41 | if kb.kbhit(): 42 | c = kb.getch() 43 | code += c 44 | print(c, end='', flush=True) 45 | if c == '\n': 46 | break 47 | return code 48 | 49 | def _get_sms_verify_code(self): 50 | now = datetime.datetime.now() 51 | connection = self._new_connection() 52 | if connection is None: 53 | time.sleep(30) 54 | return '000000' 55 | c = connection.cursor() 56 | retry = 600 57 | code = '000000' 58 | while retry > 0 and not self.done: 59 | retry -= 1 60 | # The `message` table stores all exchanged iMessages. 61 | c.execute("SELECT text, date FROM `message` ORDER BY date DESC limit 1") 62 | for row in c: 63 | text = row[0] 64 | tm = row[1] 65 | if text is None: 66 | continue 67 | res = self.regex.search(text) 68 | if res is None: 69 | continue 70 | try: 71 | #osx 10.11.6,直接相加即可得正确时间。 72 | rec_time = datetime.datetime.fromtimestamp(tm + OSX_EPOCH) 73 | #print('find msg: rec_time=', rec_time) 74 | except Exception: 75 | rec_time = datetime.datetime.fromtimestamp((tm / 1e9) + OSX_EPOCH) 76 | if rec_time < now: 77 | continue 78 | code = res.group(1).strip() 79 | break 80 | if code != '000000': 81 | break 82 | time.sleep(0.05) 83 | connection.close() 84 | return code 85 | 86 | def get_verify_code(self): 87 | self.done = False 88 | # keyboard = self.pool.submit(self._get_keyboard_verify_code) 89 | sms = self.pool.submit(self._get_sms_verify_code) 90 | done, not_done = wait([sms], timeout=30, return_when=FIRST_COMPLETED) 91 | self.done = True 92 | code = '000000' 93 | for ft in done: 94 | code = ft.result() 95 | return code 96 | 97 | def __del__(self): 98 | try: 99 | self.pool.shutdown(False) 100 | except Exception as e: 101 | pass 102 | 103 | 104 | def main(): 105 | imsg = IMessage() 106 | code = imsg.get_verify_code() 107 | print(code) 108 | 109 | 110 | if __name__ == '__main__': 111 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 北京市预约挂号统一平台脚本 2 | 3 | ![](https://img.shields.io/badge/Language-Python-007fc0.svg) 4 | ![](https://img.shields.io/badge/license-GPLv3-000000.svg) 5 | ![](https://img.shields.io/badge/made%20with-%3C3-red.svg) 6 | 7 | Copyright (C) 2017-2019 8 | 9 | **声明: 本软件只是方便大家挂号,请勿用于非法用途,否则后果自负** 10 | 11 | **目前稳定版已经发布,欢迎吐槽和试用** 12 | 13 | * 本程序用于 [北京市预约挂号统一平台](http://www.bjguahao.gov.cn/) 的挂号,只支持北京地区医院的挂号。 14 | * 挂号是刚需。帝都有些医院号源紧张,放号瞬间被秒杀一空,遂产生了撸一脚本挂号的念头。说干就干,简单的分析和调试后于 16 年 8 月份左右产出第一版,顺利挂上了XXX院运动医学科的号。很开心。 15 | * 17 年 2 月底的时候,朋友也需要挂一个号,脚本给他改了改,貌似删了重写的?没有仔细看。经过精心的分析和调试,挂了一个专家号。很开心。 16 | * 17 年 3 月 8 号,两位热心网友github上发起issues,提出反馈,让我很意外。本来想着这脚本自己写着用就可以了。接到反馈后觉得可以写成一个成熟的软件了。两位热心网友也主动提出改进代码的愿望。很开心。 17 | * __还看什么看,来贡献代码__ ;-) 18 | 19 | `2017-03-08 17:12:20 breaker` 20 | 21 | ## 环境 22 | 23 | ### 正式版已经不支持python2环境,请使用python3运行本程序 24 | - Python3 25 | 26 | ## 使用方法 27 | 28 | 1. 安装依赖库,例如:``` pip install --user -r requirements.txt ``` 29 | 2. 修改配置文件 30 | 3. 运行命令: 31 | - 默认用法: ```python bjguahao.py``` 32 | - 指定配置: ```python bjguahao.py -c your-conf.yaml``` 33 | 34 | **Android QPython3 使用方法** 35 | 1. 安装 [QPython3](https://play.google.com/store/apps/details?id=org.qpython.qpy3) 和 [QPython](https://play.google.com/store/apps/details?id=org.qpython.qpy) 36 | 2. 安装 [QPy3.6](https://play.google.com/store/apps/details?id=org.qpython.qpy36) 并运行(会安装 Python 3.6) 37 | 3. 在 QPython3 中将版本切为 Python 3.6(默认为 Python 3.2) 38 | 4. 修改配置文件(```config.yaml```或自定义) 39 | 5. 由于 QPython3 不支持传参,如需指定配置文件,需手动修改```qpython3_run.py```中的```config_name```配置文件名 40 | 6. 将整个项目复制到你的 Android 41 | 7. 在 QPython3 中运行```qpython3_run.py``` 42 | 43 | *备注:* 44 | - 若配置文件不在项目目录,也可修改```qpython3_run.py```中的```config_path```为配置文件的**绝对**地址 45 | - 如需以项目的形式直接运行脚本,可以将```qpython3_run.py```改名为```main.py```,并将文件夹放置在```qpython/projects3/```下 46 | - 也可将文件夹放置在```qpython/scripts3/```下,而后为```qpython3_run.py```建立桌面快捷方式。 47 | 48 | 49 | **Windows 环境使用方法** 50 | 1. 新增了windows版本的exe文件 51 | 2. 把配置文件放在exe文件同目录 52 | 3. 修改配置文件 53 | 4. 双击exe开始挂号,成功后程序自动退出 54 | 55 | 56 | ## 配置文件 57 | 58 | 默认配置文件 `config.yaml` 59 | 60 | ```yaml 61 | 62 | # username: 您的的用户名(一般是手机号码) 63 | username: "13888888888" 64 | 65 | # date: 挂号日期 66 | date: "2018-01-01" 67 | 68 | 69 | # hospitalId: 医院id 70 | hospitalId: "162" 71 | 72 | # departmentId: 科室id 73 | departmentId: "200002248" 74 | 75 | # 关于如何获取 hospitalId 和 departmentId 76 | # 1. 打开挂号页面 77 | # 2. 假设地址栏中地址是 http://www.bjguahao.gov.cn/dpt/appoint/162-200002248.htm 78 | # 3. 其中 162 是 hospitalId 79 | # 4. 其中 200002248 是 departmentId 80 | 81 | 82 | # 需要挂早上的号请填写1 需要挂下午的号请填写2 83 | dutyCode: "1" 84 | 85 | # patientName: 患者姓名 86 | # 若是自己挂号可为空 87 | patientName: "曹操" 88 | 89 | # doctorName: 医生姓名 90 | # 不填写的话默认选最好的医生 91 | # 填写后若这个医生没有号,会自动选其余号中最好的医生 92 | doctorName: 93 | - "扁鹊" 94 | - "华佗" 95 | - "杨永信" 96 | 97 | # 指定医生 98 | # false:默认不指定 99 | # true:只挂指定医生的号 100 | assign: "false" 101 | 102 | #true:检索每天余票 103 | remaining: "false" 104 | 105 | #remaining=true时,默认检索工作日,周末:6,7 106 | week: "1,2,3,4,5" 107 | 108 | #挂号类型是否为儿童号 109 | children: "false" 110 | 111 | #患儿名字 如果儿童挂号必须填写 112 | childrenName: "" 113 | 114 | #患儿证件号 如果儿童挂号必须填写 115 | childrenIdNo: "" 116 | 117 | #患儿证件 118 | #1:身份证 119 | #2:其他 120 | cidType: "1" 121 | 122 | # chooseBest: 选择模式 123 | # 不填写的默认从最好的医生开始选择 124 | # 可选项为"yes" 或者 "no" 125 | chooseBest: "yes" 126 | 127 | 128 | # DebugLevel: 调试等级 129 | # 支持的调试等级有 debug/info/warning/error/critical 130 | DebugLevel: "info" 131 | 132 | #使用ios短信和mac电脑接收验证码 133 | useIMessage: "false" 134 | 135 | # 是否使用 QPython3.6 运行本脚本 136 | useQPython3: "false" 137 | ``` 138 | 139 | ## 文档 140 | 141 | [文档](doc.md) 中有比较详细的接口分析和装包。 142 | 143 | [ChangeLog](ChangeLog.md) release版本更新内容 144 | 145 | ## 挂号攻略 146 | 147 | [攻略](tips.md) 中有详细的挂号攻略, 感谢[@lily0101](https://github.com/lily0101)、[@Ryan-Shang](https://github.com/Ryan-Shang)提供 148 | 149 | ## 调试 150 | 151 | 开发者请将`config.yaml`配置文件中的`DebugLevel`参数设置为`debug` 152 | 153 | ## 加入我们 154 | 155 | 在使用过程中有任何问题建议,或者贡献代码,请加入交流群 156 | 157 | ![image](https://github.com/iBreaker/bjguahao/raw/master/img/qq-qun.png) 158 | 159 | ## 致谢 160 | 161 | 感谢 [yiqian987](https://github.com/yiqian987) 修改 [issues#27](https://github.com/iBreaker/bjguahao/issues/27) 162 | 163 | 感谢 [coeusite](https://github.com/coeusite) 支持android挂号 [pull#56](https://github.com/iBreaker/bjguahao/pull/56) 164 | 165 | 感谢 [cuteapi](https://github.com/cuteapi) 添加 iphone mac 验证码自动获取的功能,抢号神器哦 166 | 167 | 感谢 [cxl008](https://github.com/cxl008) 更新登录以及获取验证码代码 168 | 169 | 感谢 [haoming06](https://github.com/haoming06) 支持挂指定大夫号、可监测每天余号,捡漏等功能 [pull#96](https://github.com/iBreaker/bjguahao/pull/96)、[pull#99](https://github.com/iBreaker/bjguahao/pull/99) 170 | 171 | 感谢 [lann-zh](https://github.com/lann-zh) 修改totalFee获取方式、修改接口调用 [pull#110](https://github.com/iBreaker/bjguahao/pull/110) 172 | 173 | 若遗漏了您,请发邮件通知我 <791628659#qq.com> 174 | 175 | ## 协议 176 | 177 | ![](https://www.gnu.org/graphics/gplv3-127x51.png) 178 | 179 | bjguahao 基于 GPL-3.0 协议进行分发和使用,更多信息参见协议文件。 180 | 181 | 182 | ## Stargazers over time 183 | 184 | ![Stargazers over time](https://starchart.cc/iBreaker/bjguahao.svg) 185 | 186 | 187 | -------------------------------------------------------------------------------- /doc.md: -------------------------------------------------------------------------------- 1 | ## 北京挂号网站协议分析 2 | 3 | ### 登陆 4 | 5 | #### 登陆请求 6 | 7 | |参数名|含义|举个栗子| 8 | |------|----|----| 9 | |mobileNo|手机号|181\*\*\*\*\*\*\*\*| 10 | |password|密码|123456| 11 | |yzm|是个谜,不影响|空| 12 | |isAjax|是个谜,不影响|true| 13 | 14 | 抓包 15 | ``` 16 | POST /quicklogin.htm HTTP/1.1 17 | Host: www.bjguahao.gov.cn 18 | Connection: keep-alive 19 | Content-Length: 55 20 | Accept: application/json, text/javascript, */*; q=0.01 21 | Origin: http://www.bjguahao.gov.cn 22 | X-Requested-With: XMLHttpRequest 23 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36 24 | Content-Type: application/x-www-form-urlencoded; charset=UTF-8 25 | Referer: http://www.bjguahao.gov.cn/logout.htm 26 | Accept-Encoding: gzip, deflate 27 | Accept-Language: zh-CN,zh;q=0.8 28 | Cookie: SESSION_COOKIE=3cab1829cea36adbceb47f7e; Hm_lvt_bc7eaca5ef5a22b54dd6ca44a23988fa=1488332034,1488961795,1488964531,1489046102; Hm_lpvt_bc7eaca5ef5a22b54dd6ca44a23988fa=1489051044; JSESSIONID=72682F948A035CA8B7AE4FCA180EF92E 29 | 30 | mobileNo=185********&password=********&yzm=&isAjax=true 31 | ``` 32 | 33 | 34 | #### 登陆回应 35 | |参数名|含义|举个栗子| 36 | |------|----|----| 37 | |data||一般为空| 38 | |hasError|是否有错误|false| 39 | |code||200| 40 | |msg|是否登陆成功(重要)|OK| 41 | 42 | 抓包(登陆成功) 43 | ``` 44 | HTTP/1.1 200 OK 45 | Set-Cookie: JSESSIONID=8154A5F2CFA1A140155CCAD2A13480B2; Path=/; HttpOnly 46 | Content-Disposition: inline;filename=f.txt 47 | Accept-Charset: big5, big5-hkscs, cesu-8, euc-jp, euc-kr, gb18030, gb2312, gbk, ibm-thai, ibm00858, ibm01140, ibm01141, ibm01142, ibm01143, ibm01144, ibm01145, ibm01146, ibm01147, ibm01148, ibm01149, ibm037, ibm1026, ibm1047, ibm273, ibm277, ibm278, ibm280, ibm284, ibm285, ibm290, ibm297, ibm420, ibm424, ibm437, ibm500, ibm775, ibm850, ibm852, ibm855, ibm857, ibm860, ibm861, ibm862, ibm863, ibm864, ibm865, ibm866, ibm868, ibm869, ibm870, ibm871, ibm918, iso-2022-cn, iso-2022-jp, iso-2022-jp-2, iso-2022-kr, iso-8859-1, iso-8859-13, iso-8859-15, iso-8859-2, iso-8859-3, iso-8859-4, iso-8859-5, iso-8859-6, iso-8859-7, iso-8859-8, iso-8859-9, jis_x0201, jis_x0212-1990, koi8-r, koi8-u, shift_jis, tis-620, us-ascii, utf-16, utf-16be, utf-16le, utf-32, utf-32be, utf-32le, utf-8, windows-1250, windows-1251, windows-1252, windows-1253, windows-1254, windows-1255, windows-1256, windows-1257, windows-1258, windows-31j, x-big5-hkscs-2001, x-big5-solaris, x-compound_text, x-euc-jp-linux, x-euc-tw, x-eucjp-open, x-ibm1006, x-ibm1025, x-ibm1046, x-ibm1097, x-ibm1098, x-ibm1112, x-ibm1122, x-ibm1123, x-ibm1124, x-ibm1166, x-ibm1364, x-ibm1381, x-ibm1383, x-ibm300, x-ibm33722, x-ibm737, x-ibm833, x-ibm834, x-ibm856, x-ibm874, x-ibm875, x-ibm921, x-ibm922, x-ibm930, x-ibm933, x-ibm935, x-ibm937, x-ibm939, x-ibm942, x-ibm942c, x-ibm943, x-ibm943c, x-ibm948, x-ibm949, x-ibm949c, x-ibm950, x-ibm964, x-ibm970, x-iscii91, x-iso-2022-cn-cns, x-iso-2022-cn-gb, x-iso-8859-11, x-jis0208, x-jisautodetect, x-johab, x-macarabic, x-maccentraleurope, x-maccroatian, x-maccyrillic, x-macdingbat, x-macgreek, x-machebrew, x-maciceland, x-macroman, x-macromania, x-macsymbol, x-macthai, x-macturkish, x-macukraine, x-ms932_0213, x-ms950-hkscs, x-ms950-hkscs-xp, x-mswin-936, x-pck, x-sjis_0213, x-utf-16le-bom, x-utf-32be-bom, x-utf-32le-bom, x-windows-50220, x-windows-50221, x-windows-874, x-windows-949, x-windows-950, x-windows-iso2022jp 48 | Content-Type: text/html;charset=UTF-8 49 | Content-Length: 50 50 | Date: Thu, 09 Mar 2017 09:22:32 GMT 51 | Connection: close 52 | Server: Tengine/2.1.2 53 | 54 | {"data":[],"hasError":false,"code":200,"msg":"OK"} 55 | ``` 56 | 57 | 抓包(登陆失败) 58 | ``` 59 | 60 | ``` 61 | 62 | ### 查询 63 | |参数名|含义|举个栗子| 64 | |------|----|-----| 65 | |hospitalId|医院ID|270| 66 | |departmentId|科室ID|200003874| 67 | |dutyCode|上午下午|1表示上午,2表示下午| 68 | |dutyDate|挂号日期|2017-03-13| 69 | |isAjax|是个谜|true就好| 70 | ``` 71 | POST /dpt/partduty.htm HTTP/1.1 72 | Host: www.bjguahao.gov.cn 73 | Connection: keep-alive 74 | Content-Length: 80 75 | Accept: application/json, text/javascript, */*; q=0.01 76 | Origin: http://www.bjguahao.gov.cn 77 | X-Requested-With: XMLHttpRequest 78 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36 79 | Content-Type: application/x-www-form-urlencoded; charset=UTF-8 80 | Referer: http://www.bjguahao.gov.cn/dpt/appoint/270-200003874.htm 81 | Accept-Encoding: gzip, deflate 82 | Accept-Language: zh-CN,zh;q=0.8 83 | Cookie: SESSION_COOKIE=3cab1829cea36adbceb47f7e; Hm_lvt_bc7eaca5ef5a22b54dd6ca44a23988fa=1488332034,1488961795,1488964531,1489046102; Hm_lpvt_bc7eaca5ef5a22b54dd6ca44a23988fa=1489051826; JSESSIONID=6DFFC0825127030360E31B2C0E11E031 84 | 85 | hospitalId=270&departmentId=200003874&dutyCode=1&dutyDate=2017-03-13&isAjax=true 86 | ``` 87 | ### 挂号 88 | 89 | ### 关于验证码 90 | 目前支持*ios系统*的手机和*osx*系统的电脑自动填写验证码 91 | ##### 配置自动填写验证码 92 | 1. 在*osx*系统电脑上登录 93 | ![](https://github.com/cuteapi/bjguahao/blob/add_doc/img/step1.jpg) 94 | ![](https://github.com/cuteapi/bjguahao/blob/add_doc/img/step2.jpg) 95 | 96 | 2. 在手机配置信息转发 97 | ![](https://github.com/cuteapi/bjguahao/blob/add_doc/img/step3.png) 98 | ![](https://github.com/cuteapi/bjguahao/blob/add_doc/img/step4.png) 99 | ![](https://github.com/cuteapi/bjguahao/blob/add_doc/img/step5.PNG) 100 | 101 | 3. 修改config.yaml设置useIMessage为true 102 | 103 | 104 | -------------------------------------------------------------------------------- /bjguahao.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 北京市预约挂号统一平台 5 | """ 6 | 7 | import os 8 | import sys 9 | import re 10 | import json 11 | import time 12 | import datetime 13 | import logging 14 | from lib.prettytable import PrettyTable 15 | import base64 16 | from Crypto.Cipher import AES 17 | from tqdm import tqdm, trange 18 | 19 | 20 | if sys.version_info.major != 3: 21 | logging.error("请在python3环境下运行本程序") 22 | sys.exit(-1) 23 | 24 | try: 25 | import requests 26 | except ModuleNotFoundError as e: 27 | logging.error("请安装python3 requests") 28 | sys.exit(-1) 29 | 30 | from browser import Browser 31 | from idcard_information import GetInformation 32 | 33 | try: 34 | import yaml 35 | except ModuleNotFoundError as e: 36 | logging.error("请安装python3 yaml模块") 37 | sys.exit(-1) 38 | 39 | 40 | class Config(object): 41 | 42 | def __init__(self, config_path): 43 | try: 44 | with open(config_path, "r", encoding="utf-8") as yaml_file: 45 | data = yaml.load(yaml_file) 46 | debug_level = data["DebugLevel"] 47 | if debug_level == "debug": 48 | self.debug_level = logging.DEBUG 49 | elif debug_level == "info": 50 | self.debug_level = logging.INFO 51 | elif debug_level == "warning": 52 | self.debug_level = logging.WARNING 53 | elif debug_level == "error": 54 | self.debug_level = logging.ERROR 55 | elif debug_level == "critical": 56 | self.debug_level = logging.CRITICAL 57 | 58 | logging.basicConfig(level=self.debug_level, 59 | format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', 60 | datefmt='%a, %d %b %Y %H:%M:%S') 61 | 62 | self.mobile_no = data["username"] 63 | self.password = data["password"] 64 | self.date = data["date"] 65 | self.hospital_id = data["hospitalId"] 66 | self.department_id = data["departmentId"] 67 | self.duty_code = data["dutyCode"] 68 | self.patient_name = data["patientName"] 69 | self.hospital_card_id = data["hospitalCardId"] 70 | self.medicare_card_id = data["medicareCardId"] 71 | self.reimbursement_type = data["reimbursementType"] 72 | self.doctorName = data["doctorName"] 73 | self.children_name = data["childrenName"] 74 | self.children_idno = data["childrenIdNo"] 75 | self.cid_type = data["cidType"] 76 | self.children = data["children"] 77 | self.assign = data['assign'] 78 | self.remaining = data['remaining'] 79 | self.week = data['week'] 80 | self.chooseBest = {"yes": True, "no": False}[data["chooseBest"]] 81 | self.patient_id = int() 82 | self.web_password = "hyde2019hyde2019" 83 | try: 84 | self.useIMessage = data["useIMessage"] 85 | except KeyError: 86 | self.useIMessage = "false" 87 | try: 88 | self.useQPython3 = data["useQPython3"] 89 | except KeyError: 90 | self.useQPython3 = "false" 91 | try: 92 | self.children = data["children"] 93 | except KeyError: 94 | self.children = "false" 95 | # 96 | logging.info("配置加载完成") 97 | logging.debug("手机号:" + str(self.mobile_no)) 98 | logging.debug("挂号日期:" + str(self.date)) 99 | logging.debug("医院id:" + str(self.hospital_id)) 100 | logging.debug("科室id:" + str(self.department_id)) 101 | logging.debug("上午/下午:" + str(self.duty_code)) 102 | logging.debug("就诊人姓名:" + str(self.patient_name)) 103 | logging.debug("所选医生:" + str(self.doctorName)) 104 | logging.debug("是否挂指定医生:"+str(self.assign)) 105 | logging.debug("检索每天余票:"+str(self.remaining)) 106 | logging.debug("检索周:"+str(self.week)) 107 | logging.debug("是否挂儿童号:" + str(self.children)) 108 | if self.children == "true": 109 | logging.debug("患儿姓名:" + str(self.children_name)) 110 | logging.debug("患儿证件号" + str(self.children_idno)) 111 | logging.debug("患儿证件类型:" + str(self.cid_type)) 112 | logging.debug("患儿性别:" + str(GetInformation(self.children_idno).get_sex())) 113 | logging.debug("患儿生日:" + str(GetInformation(self.children_idno).get_birthday())) 114 | logging.debug("使用mac电脑接收验证码:" + str(self.useIMessage)) 115 | logging.debug("是否使用 QPython3 运行本脚本:" + str(self.useQPython3)) 116 | 117 | if not self.date: 118 | logging.error("请填写挂号时间") 119 | exit(-1) 120 | 121 | except Exception as e: 122 | logging.error(repr(e)) 123 | sys.exit() 124 | 125 | class AES_encrypt(): 126 | 127 | def __init__(self, key, mode='ecb', iv=''): 128 | 129 | self.key = key#.decode("hex") 130 | self.iv = iv 131 | self.cryptor = None 132 | if mode == "ecb": 133 | self.cryptor = AES.new(str.encode(self.key), AES.MODE_ECB) 134 | elif mode == 'cbc': 135 | self.cryptor = AES.new(str.encode(self.key), AES.MODE_CBC, self.iv) 136 | else: 137 | return "Error Mode" 138 | 139 | 140 | 141 | def __pad(self, text): 142 | """填充方式,PKCS7""" 143 | text_length = len(text) 144 | amount_to_pad = AES.block_size - (text_length % AES.block_size) 145 | # if amount_to_pad == 0: 146 | # amount_to_pad = AES.block_size 147 | padd = chr(amount_to_pad) 148 | return text + padd * amount_to_pad 149 | 150 | def __unpad(self, text): 151 | padd = ord(text[-1]) 152 | return text[:-padd] 153 | 154 | def encrypt(self, text): 155 | text = self.__pad(text) 156 | self.ciphertext = self.cryptor.encrypt(str.encode(text)) 157 | word = base64.b64encode(self.ciphertext) 158 | word = str(word,encoding="utf-8").replace('/', '_') 159 | word = word.replace('+', '-') 160 | return word 161 | 162 | def decrypt(self, text): 163 | 164 | plain_text = self.cryptor.decrypt(base64.b64decode(text)) 165 | return self.__unpad(plain_text) 166 | 167 | def get_byte(self,str_in): 168 | str_out = "" 169 | 170 | for i in range(0, len(str_in), 2): 171 | str_out = str_out+"0x%s" % str_in[i:i+2]+"," 172 | str_out = str_out[:-1] 173 | return str_out 174 | 175 | class Guahao(object): 176 | """ 177 | 挂号 178 | """ 179 | 180 | def __init__(self, config_path="config.yaml"): 181 | self.browser = Browser() 182 | self.dutys = "" 183 | self.refresh_time = '' 184 | #self.verify_url = "http://www.114yygh.com/web/verify" # 验证页面 185 | self.verify_url = "http://www.114yygh.com/home/login/loginStep1.html?lti=eyJtb2JpbGVObyI6IjE3NzEwMzM4ODc1IiwibG9naW5TdGF0dXMiOiJTTVNfQ09ERV9MT0dJTiJ9" 186 | #self.login_url = "http://www.114yygh.com/web/login" 187 | self.login_url = "http://www.114yygh.com/home/login/login.html" #登陆页面 188 | self.send_code_url = "http://www.114yygh.com/home/login/loginStep1.html?lti=eyJtb2JpbGVObyI6IjE3NzEwMzM4ODc1IiwibG9naW5TdGF0dXMiOiJTTVNfQ09ERV9MT0dJTiJ9" 189 | self.duty_url = "http://www.114yygh.com/web/product/detail" 190 | self.confirm_url = "http://www.114yygh.com/web/order/saveOrder" 191 | self.patient_id_url = "http://www.114yygh.com/order/confirm/" 192 | self.query_hospital_url = "http://www.114yygh.com/web/queryHospitalById" 193 | self.calendar = "http://www.114yygh.com/web/product/list" 194 | self.order_patient_list = "http://www.114yygh.com/web/patient/orderPatientList" 195 | self.appoint_info_url = "http://www.114yygh.com/web/order/getAppointInfo" 196 | 197 | self.config = Config(config_path) # config对象 198 | if self.config.useIMessage == 'true': 199 | # 按需导入 imessage.py 200 | import imessage 201 | self.imessage = imessage.IMessage() 202 | else: 203 | self.imessage = None 204 | 205 | if self.config.useQPython3 == 'true': 206 | try: # Android QPython3 验证 207 | # 按需导入 qpython3.py 208 | import qpython3 209 | self.qpython3 = qpython3.QPython3() 210 | except ModuleNotFoundError: 211 | self.qpython3 = None 212 | else: 213 | self.qpython3 = None 214 | 215 | def is_login(self): 216 | logging.info("开始检查是否已经登录") 217 | response = self.browser.get("http://www.114yygh.com/web/patient/validPatientList"+"?rd="+str(self.timestamp()), data='') 218 | try: 219 | data = json.loads(response.text) 220 | if data["resCode"] == 0: 221 | logging.debug("response data:" + response.text) 222 | return True 223 | else: 224 | logging.debug("response data: HTML body") 225 | return False 226 | except Exception as e: 227 | logging.error(e) 228 | return False 229 | 230 | def auth_login(self): 231 | """ 232 | 登录 233 | """ 234 | try: 235 | # patch for qpython3 236 | cookies_file = os.path.join(os.path.dirname(sys.argv[0]), "." + self.config.mobile_no + ".cookies") 237 | self.browser.load_cookies(cookies_file) 238 | if self.is_login(): 239 | logging.info("cookies登录成功") 240 | return True 241 | except Exception as e: 242 | pass 243 | 244 | aes = AES_encrypt(self.config.web_password, 'ecb', '') 245 | logging.info("cookies登录失败") 246 | 247 | logging.info("开始使用账号密码登录") 248 | self.browser.get(self.verify_url+"?mobile="+self.config.mobile_no+"&rd="+str(self.timestamp()),"") 249 | sms_code = self.get_sms_verify_code("LOGIN") 250 | time.sleep(1) 251 | 252 | mobile_no = self.config.mobile_no 253 | payload = { 254 | 'loginType': 'SMS_CODE_LOGIN', 255 | 'mobile': aes.encrypt(mobile_no), 256 | 'password': aes.encrypt(sms_code) 257 | } 258 | response = self.browser.post(self.login_url, data=payload) 259 | logging.debug("response data:" + response.text) 260 | try: 261 | data = json.loads(response.text) 262 | if data['resCode'] == 0:#data["msg"] == "OK" and not data["hasError"] and data["code"] == 200: 263 | # patch for qpython3 264 | cookies_file = os.path.join(os.path.dirname(sys.argv[0]), "." + self.config.mobile_no + ".cookies") 265 | self.browser.save_cookies(cookies_file) 266 | logging.info("登录成功") 267 | return True 268 | else: 269 | logging.error(data["msg"]) 270 | raise Exception() 271 | 272 | except Exception as e: 273 | logging.error(e) 274 | logging.error("登录失败") 275 | sys.exit(-1) 276 | def calendar_vec(self): 277 | param = { 278 | "hospitalId": self.config.hospital_id, 279 | "departmentId": self.config.department_id, 280 | "week": "1", 281 | "latitude" : "39.91488908", 282 | "longitude" : "116.40387397" 283 | } 284 | response = self.browser.post(self.calendar,param) 285 | logging.debug("response data:" + response.text) 286 | data = json.loads(response.text) 287 | rt = [] 288 | if data['resCode'] == 0: 289 | for duty in data['data']['calendars']: 290 | if duty['remainAvailableNumberWeb']>=0 and str(duty['dutyWeek']) in self.config.week: 291 | rt.append(duty['dutyDate']) 292 | logging.info('该科室可挂'+','.join(rt)+'号') 293 | self.calendar_vec_param = rt 294 | def select_doctor(self): 295 | if self.config.remaining == "true": 296 | for duty_date in self.calendar_vec_param: 297 | logging.info("查询"+duty_date+"号源") 298 | doctor = self.select_doctor_one_day(duty_date) 299 | if doctor == 'NoDuty' or doctor == 'NotReady' : 300 | time.sleep(3) 301 | continue 302 | else: 303 | return doctor 304 | return 'NoDuty' 305 | else: 306 | doctor = self.select_doctor_one_day(self.config.date) 307 | return doctor 308 | 309 | def select_doctor_one_day(self,duty_date): 310 | """选择合适的大夫""" 311 | hospital_id = self.config.hospital_id 312 | department_id = self.config.department_id 313 | duty_code = self.config.duty_code 314 | # log current date 315 | logging.debug("当前挂号日期: " + self.config.date) 316 | 317 | payload = { 318 | 'hospitalId': hospital_id, 319 | 'departmentId': department_id, 320 | 'dutyDate': duty_date 321 | } 322 | response = self.browser.post(self.duty_url, data=payload) 323 | logging.debug("response data:" + response.text) 324 | try: 325 | data = json.loads(response.text) 326 | if data["resCode"] == 0: 327 | dutys =[] 328 | if self.config.duty_code == '1': #上午 329 | dutys = [1] 330 | elif self.config.duty_code == '2': #下午 331 | dutys = [2] 332 | else: #全天 333 | dutys = [1,2] 334 | for duty in dutys: 335 | for duty_result in data['data']: 336 | if(duty_result['dutyCode'] == duty): 337 | self.dutys = duty_result['detail'] 338 | doctor = self.select_doctor_by_vec() 339 | if doctor == 'NoDuty' or doctor == 'NotReady' : 340 | continue 341 | return doctor 342 | return 'NoDuty' 343 | except Exception as e: 344 | logging.error(repr(e)) 345 | sys.exit() 346 | def select_doctor_by_vec(self): 347 | if len(self.dutys) == 0: 348 | return "NotReady" 349 | self.print_doctor() 350 | doctors = self.dutys 351 | if self.config.assign == 'true': 352 | for doctor_conf in self.config.doctorName: 353 | for doctor in doctors: 354 | if self.get_doctor_name(doctor) == doctor_conf and (doctor['totalCount']%2!=0): 355 | logging.info("选中:" + self.get_doctor_name(doctor)) 356 | return doctor 357 | return "NoDuty" 358 | # 按照配置优先级选择医生 359 | for doctor_conf in self.config.doctorName: 360 | for doctor in doctors: 361 | if self.get_doctor_name(doctor) == doctor_conf and doctor['totalCount']%2!=0: 362 | return doctor 363 | 364 | # 若没有合适的医生,默认返回最好的医生 365 | for doctor in doctors: 366 | if doctor['totalCount']%2 != 0: 367 | logging.info("选中:" + self.get_doctor_name(doctor)) 368 | return doctor 369 | return "NoDuty" 370 | 371 | def get_doctor_name(self,doctor): 372 | if doctor['doctorName'] is not None: 373 | return str(doctor['doctorName']) 374 | else: 375 | return str(doctor['doctorTitleName']) 376 | 377 | def print_doctor(self): 378 | logging.info("当前号余量:") 379 | x = PrettyTable() 380 | x.border = True 381 | x.field_names = ["医生姓名", "擅长", "号余量"] 382 | for doctor in self.dutys: 383 | x.add_row([self.get_doctor_name(doctor), doctor['doctorSkill'], "无" if doctor['totalCount']%2 == 0 else "有"]) 384 | print(x.get_string()) 385 | pass 386 | 387 | def get_fee(self,doctor): 388 | """ 389 | 获取真实挂号费显示数值 390 | """ 391 | hospital_id = self.config.hospital_id 392 | department_id = self.config.department_id 393 | doctor_id = str(doctor['doctorId']) 394 | duty_source_id = str(doctor['dutySourceId']) 395 | 396 | payload = { 397 | 'hospitalId': hospital_id, 398 | 'departmentId': department_id, 399 | 'doctorId': doctor_id, 400 | 'dutySourceId': duty_source_id 401 | } 402 | response = self.browser.post(self.appoint_info_url, data=payload) 403 | logging.debug("response data:" + response.text) 404 | try: 405 | datas = json.loads(response.text) 406 | if datas["resCode"] == 0: 407 | data = datas["data"] 408 | total_fee = str(data['totalFee']) 409 | logging.debug("真实挂号费显示值:" + total_fee) 410 | return total_fee 411 | except Exception as e: 412 | logging.error(e) 413 | sys.exit() 414 | 415 | def get_it(self, doctor, sms_code, total_fee): 416 | """ 417 | 挂号 418 | """ 419 | duty_source_id = str(doctor['dutySourceId']) 420 | hospital_id = self.config.hospital_id 421 | department_id = self.config.department_id 422 | patient_id = str(self.config.patient_id) 423 | hospital_card_id = self.config.hospital_card_id 424 | medicare_card_id = self.config.medicare_card_id 425 | reimbursement_type = self.config.reimbursement_type 426 | doctor_id = str(doctor['doctorId']) 427 | #新版可能不区分儿童与成人,需测试 428 | if self.config.children == 'true': 429 | cid_type = self.config.cid_type 430 | children_name = self.config.children_name 431 | children_idno = self.config.children_idno 432 | children_gender = GetInformation(children_idno).get_sex() 433 | children_birthday = GetInformation(children_idno).get_birthday() 434 | payload = { 435 | 'phone':self.config.mobile_no, 436 | 'dutySourceId': duty_source_id, 437 | 'hospitalId': hospital_id, 438 | 'departmentId': department_id, 439 | 'doctorId': doctor_id, 440 | 'patientId': patient_id, 441 | 'hospitalCardId': hospital_card_id, 442 | 'medicareCardId': medicare_card_id, 443 | "reimbursementType": reimbursement_type, # 报销类型 444 | 'smsVerifyCode': sms_code, # TODO 获取验证码 445 | 'childrenName': children_name, 446 | 'childrenIdNo': children_idno, 447 | 'cidType': cid_type, 448 | 'childrenGender': children_gender, 449 | 'childrenBirthday': children_birthday, 450 | 'isAjax': 'true' 451 | } 452 | else: 453 | payload = { 454 | "hospitalId": hospital_id, 455 | "departmentId": department_id, 456 | "dutySourceId": duty_source_id, 457 | "doctorId": doctor_id, 458 | "patientId": patient_id, 459 | "dutyDate": doctor['dutyDate'], 460 | "dutyCode": doctor['dutyCode'], 461 | "totalFee": total_fee, 462 | "fcode":"", 463 | "period":"", 464 | "mapDepartmentId":"", 465 | "mapDoctorId":"", 466 | "planCode":"", 467 | "medicareCardId": medicare_card_id, 468 | "jytCardId":"", 469 | "hospitalCardId": hospital_card_id, 470 | "mapDutySourceId":"", 471 | "smsCode": sms_code, 472 | "mobileNo": self.config.mobile_no, 473 | "feeColor":"", 474 | "dutyImgType":"" 475 | } 476 | #save order 477 | response = self.browser.post(self.confirm_url, data=payload) 478 | logging.debug("payload:" + json.dumps(payload)) 479 | logging.debug("response data:" + response.text) 480 | 481 | try: 482 | data = json.loads(response.text) 483 | if data["resCode"] == 0: 484 | #20181027,成功result: 485 | #{"msg":"成功","code":1,"orderId":"97465746","isLineUp":false} 486 | logging.info("挂号成功") 487 | return True 488 | if data["resCode"] == 8008: 489 | #重复订单,说明挂号成功 490 | #{"code":8008,"msg":"科室预约规则检查重复订单","data":null} 491 | logging.error(data["msg"]) 492 | return True 493 | else: 494 | logging.error(data["msg"]) 495 | return False 496 | 497 | except Exception as e: 498 | logging.error(repr(e)) 499 | time.sleep(1) 500 | 501 | def get_patient_id(self): 502 | """获取就诊人Id""" 503 | response = self.browser.get(self.order_patient_list+"?rd="+str(self.timestamp()), "") 504 | ret = response.text 505 | data = json.loads(ret) 506 | if data['resCode'] != 0: 507 | sys.exit("获取患者id失败") 508 | else: 509 | #TODO 如果重名,提供输入选择 510 | for patient in data['data']: 511 | if patient['name'][1:] in self.config.patient_name: 512 | self.config.patient_id = patient['id'] 513 | break 514 | logging.info("病人ID:" + str(self.config.patient_id)) 515 | return self.config.patient_id 516 | 517 | def gen_department_url(self): 518 | return self.query_hospital_url + "?hosId="+str(self.config.hospital_id)+"&rd="+str(self.timestamp()) 519 | def timestamp(self): 520 | return int(round(time.time()*1000)) 521 | def get_duty_time(self): 522 | """获取放号时间""" 523 | addr = self.gen_department_url() 524 | response = self.browser.get(addr, "") 525 | ret = response.text 526 | data = json.loads(ret) 527 | if data['resCode'] == 0: 528 | # 放号时间 529 | refresh_time = data['data']['fhTime'] 530 | # 放号日期 531 | appoint_day = data['data']['fhPeriod'] 532 | today = datetime.date.today() 533 | # 优先确认最新可挂号日期 534 | self.stop_date = today + datetime.timedelta(days=int(appoint_day)) 535 | logging.info("今日可挂号到: " + self.stop_date.strftime("%Y-%m-%d")) 536 | # 自动挂最新一天的号 537 | if self.config.date == 'latest': 538 | self.config.date = self.stop_date.strftime("%Y-%m-%d") 539 | logging.info("当前挂号日期变更为: " + self.config.date) 540 | # 生成放号时间和程序开始时间 541 | con_data_str = self.config.date + " " + refresh_time + ":00" 542 | self.start_time = datetime.datetime.strptime(con_data_str, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(days= - int(appoint_day)) 543 | logging.info("放号时间: " + self.start_time.strftime("%Y-%m-%d %H:%M")) 544 | #type:LOGIN 545 | def get_sms_verify_code(self,type): 546 | """获取短信验证码""" 547 | payload = { 548 | "mobile":self.config.mobile_no, 549 | "smsKey":type, 550 | "rd":str(self.timestamp()) 551 | } 552 | response = self.browser.get(self.send_code_url+"?"+'&'.join([ str(key)+'='+str(value) for key,value in payload.items() ]) , "") 553 | data = json.loads(response.text) 554 | logging.debug(response.text) 555 | if data["resCode"] == 0: 556 | logging.info("获取验证码成功") 557 | if self.imessage is not None: # 如果使用 iMessage 558 | code = self.imessage.get_verify_code() 559 | elif self.qpython3 is not None: # 如果使用 QPython3 560 | code = self.qpython3.get_verify_code() 561 | else: 562 | code = input("输入短信验证码: ") 563 | return code 564 | elif data["msg"] == "短信发送太频繁" and data["code"] == 812: 565 | logging.error(data["msg"]) 566 | sys.exit() 567 | elif data["msg"] == "抱歉,短信验证码发送次数已达到今日上限!" and data["code"] == 817: 568 | logging.error(data["msg"]) 569 | sys.exit() 570 | else: 571 | logging.error(data["msg"]) 572 | return None 573 | 574 | def lazy(self): 575 | cur_time = datetime.datetime.now() + datetime.timedelta(seconds=int(time.timezone + 8*60*60)) 576 | if self.start_time > cur_time: 577 | seconds = (self.start_time - cur_time).total_seconds() 578 | logging.info("距离放号时间还有" + str(seconds) + "秒") 579 | hour = seconds // 3600 580 | minute = (seconds % 3600) // 60 581 | second = seconds % 60 582 | logging.info("距离放号时间还有"+str(int(hour))+" h " + str(int(minute))+" m "+ str(int(second))+ " s") 583 | 584 | sleep_time = seconds - 60 585 | if sleep_time > 0: 586 | logging.info("程序休眠" + str(sleep_time) + "秒后开始运行") 587 | # time.sleep(sleep_time) 588 | 589 | if sleep_time > 3600: 590 | sleep_time -= 60 591 | for i in trange(1000): 592 | for j in trange(int(sleep_time/1000), leave=False, unit_scale=True): 593 | time.sleep(1) 594 | else: 595 | for i in tqdm(range(int(sleep_time)-60)): 596 | time.sleep(1) 597 | 598 | # 自动重新登录 599 | self.auth_login() 600 | 601 | def run(self): 602 | """主逻辑""" 603 | self.get_duty_time() 604 | self.auth_login() # 1. 登录 605 | self.lazy() 606 | self.calendar_vec() 607 | self.get_patient_id() # 2. 获取病人id 608 | doctor = "" 609 | while True: 610 | doctor = self.select_doctor() # 3. 选择医生 611 | if doctor == "NoDuty": 612 | # 如果当前时间 > 放号时间 + 30s 613 | if self.start_time + datetime.timedelta(seconds=30) < datetime.datetime.now(): 614 | # 确认无号,终止程序 615 | logging.info("没号了,亲~,休息一下继续刷") 616 | time.sleep(1) 617 | else: 618 | # 未到时间,强制重试 619 | logging.debug("放号时间: " + self.start_time.strftime("%Y-%m-%d %H:%M")) 620 | logging.debug("当前时间: " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M")) 621 | logging.info("没号了,但截止时间未到,重试中") 622 | time.sleep(1) 623 | elif doctor == "NotReady": 624 | logging.info("好像还没放号?重试中") 625 | time.sleep(1) 626 | else: 627 | total_fee = self.get_fee(doctor) # 获取挂号费,需在获取验证码之前 628 | sms_code = self.get_sms_verify_code('ORDER_CODE') # 获取验证码 629 | print('sms_code:',sms_code) 630 | if sms_code is None: 631 | time.sleep(1) 632 | result = self.get_it(doctor, sms_code, total_fee) # 4.挂号 633 | if result: 634 | break # 挂号成功 635 | 636 | 637 | if __name__ == "__main__": 638 | 639 | if (len(sys.argv) == 3) and (sys.argv[1] == '-c') and (isinstance(sys.argv[2], str)): 640 | config_path = sys.argv[2] 641 | guahao = Guahao(config_path) 642 | else: 643 | guahao = Guahao() 644 | guahao.run() 645 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | -------------------------------------------------------------------------------- /lib/prettytable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2009-2014, Luke Maurits 4 | # All rights reserved. 5 | # With contributions from: 6 | # * Chris Clark 7 | # * Klein Stephane 8 | # * John Filleau 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 13 | # * Redistributions of source code must retain the above copyright notice, 14 | # this list of conditions and the following disclaimer. 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # * The name of the author may not be used to endorse or promote products 19 | # derived from this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 25 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | __version__ = "trunk" 34 | 35 | import copy 36 | import csv 37 | import itertools 38 | import math 39 | import random 40 | import re 41 | import sys 42 | import textwrap 43 | import unicodedata 44 | 45 | py3k = sys.version_info[0] >= 3 46 | if py3k: 47 | unicode = str 48 | basestring = str 49 | itermap = map 50 | iterzip = zip 51 | uni_chr = chr 52 | from html.parser import HTMLParser 53 | else: 54 | itermap = itertools.imap 55 | iterzip = itertools.izip 56 | uni_chr = unichr 57 | from HTMLParser import HTMLParser 58 | 59 | if py3k and sys.version_info[1] >= 2: 60 | from html import escape 61 | else: 62 | from cgi import escape 63 | 64 | # hrule styles 65 | FRAME = 0 66 | ALL = 1 67 | NONE = 2 68 | HEADER = 3 69 | 70 | # Table styles 71 | DEFAULT = 10 72 | MSWORD_FRIENDLY = 11 73 | PLAIN_COLUMNS = 12 74 | RANDOM = 20 75 | 76 | _re = re.compile("\033\[[0-9;]*m") 77 | 78 | def _get_size(text): 79 | lines = text.split("\n") 80 | height = len(lines) 81 | width = max([_str_block_width(line) for line in lines]) 82 | return (width, height) 83 | 84 | class PrettyTable(object): 85 | 86 | def __init__(self, field_names=None, **kwargs): 87 | 88 | """Return a new PrettyTable instance 89 | 90 | Arguments: 91 | 92 | encoding - Unicode encoding scheme used to decode any encoded input 93 | title - optional table title 94 | field_names - list or tuple of field names 95 | fields - list or tuple of field names to include in displays 96 | start - index of first data row to include in output 97 | end - index of last data row to include in output PLUS ONE (list slice style) 98 | header - print a header showing field names (True or False) 99 | header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None) 100 | border - print a border around the table (True or False) 101 | hrules - controls printing of horizontal rules after rows. Allowed values: FRAME, HEADER, ALL, NONE 102 | vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE 103 | int_format - controls formatting of integer data 104 | float_format - controls formatting of floating point data 105 | min_table_width - minimum desired table width, in characters 106 | max_table_width - maximum desired table width, in characters 107 | padding_width - number of spaces on either side of column data (only used if left and right paddings are None) 108 | left_padding_width - number of spaces on left hand side of column data 109 | right_padding_width - number of spaces on right hand side of column data 110 | vertical_char - single character string used to draw vertical lines 111 | horizontal_char - single character string used to draw horizontal lines 112 | junction_char - single character string used to draw line junctions 113 | sortby - name of field to sort rows by 114 | sort_key - sorting key function, applied to data points before sorting 115 | valign - default valign for each row (None, "t", "m" or "b") 116 | reversesort - True or False to sort in descending or ascending order 117 | oldsortslice - Slice rows before sorting in the "old style" """ 118 | 119 | self.encoding = kwargs.get("encoding", "UTF-8") 120 | 121 | # Data 122 | self._field_names = [] 123 | self._rows = [] 124 | self.align = {} 125 | self.valign = {} 126 | self.max_width = {} 127 | self.min_width = {} 128 | self.int_format = {} 129 | self.float_format = {} 130 | if field_names: 131 | self.field_names = field_names 132 | else: 133 | self._widths = [] 134 | 135 | # Options 136 | self._options = "title start end fields header border sortby reversesort sort_key attributes format hrules vrules".split() 137 | self._options.extend("int_format float_format min_table_width max_table_width padding_width left_padding_width right_padding_width".split()) 138 | self._options.extend("vertical_char horizontal_char junction_char header_style valign xhtml print_empty oldsortslice".split()) 139 | self._options.extend("align valign max_width min_width".split()) 140 | for option in self._options: 141 | if option in kwargs: 142 | self._validate_option(option, kwargs[option]) 143 | else: 144 | kwargs[option] = None 145 | 146 | self._title = kwargs["title"] or None 147 | self._start = kwargs["start"] or 0 148 | self._end = kwargs["end"] or None 149 | self._fields = kwargs["fields"] or None 150 | 151 | if kwargs["header"] in (True, False): 152 | self._header = kwargs["header"] 153 | else: 154 | self._header = True 155 | self._header_style = kwargs["header_style"] or None 156 | if kwargs["border"] in (True, False): 157 | self._border = kwargs["border"] 158 | else: 159 | self._border = True 160 | self._hrules = kwargs["hrules"] or FRAME 161 | self._vrules = kwargs["vrules"] or ALL 162 | 163 | self._sortby = kwargs["sortby"] or None 164 | if kwargs["reversesort"] in (True, False): 165 | self._reversesort = kwargs["reversesort"] 166 | else: 167 | self._reversesort = False 168 | self._sort_key = kwargs["sort_key"] or (lambda x: x) 169 | 170 | # Column specific arguments, use property.setters 171 | self.align = kwargs["align"] or {} 172 | self.valign = kwargs["valign"] or {} 173 | self.max_width = kwargs["max_width"] or {} 174 | self.min_width = kwargs["min_width"] or {} 175 | self.int_format = kwargs["int_format"] or {} 176 | self.float_format = kwargs["float_format"] or {} 177 | 178 | self._min_table_width = kwargs["min_table_width"] or None 179 | self._max_table_width = kwargs["max_table_width"] or None 180 | self._padding_width = kwargs["padding_width"] or 1 181 | self._left_padding_width = kwargs["left_padding_width"] or None 182 | self._right_padding_width = kwargs["right_padding_width"] or None 183 | 184 | self._vertical_char = kwargs["vertical_char"] or self._unicode("|") 185 | self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-") 186 | self._junction_char = kwargs["junction_char"] or self._unicode("+") 187 | 188 | if kwargs["print_empty"] in (True, False): 189 | self._print_empty = kwargs["print_empty"] 190 | else: 191 | self._print_empty = True 192 | if kwargs["oldsortslice"] in (True, False): 193 | self._oldsortslice = kwargs["oldsortslice"] 194 | else: 195 | self._oldsortslice = False 196 | self._format = kwargs["format"] or False 197 | self._xhtml = kwargs["xhtml"] or False 198 | self._attributes = kwargs["attributes"] or {} 199 | 200 | def _unicode(self, value): 201 | if not isinstance(value, basestring): 202 | value = str(value) 203 | if not isinstance(value, unicode): 204 | value = unicode(value, self.encoding, "strict") 205 | return value 206 | 207 | def _justify(self, text, width, align): 208 | excess = width - _str_block_width(text) 209 | if align == "l": 210 | return text + excess * " " 211 | elif align == "r": 212 | return excess * " " + text 213 | else: 214 | if excess % 2: 215 | # Uneven padding 216 | # Put more space on right if text is of odd length... 217 | if _str_block_width(text) % 2: 218 | return (excess//2)*" " + text + (excess//2 + 1)*" " 219 | # and more space on left if text is of even length 220 | else: 221 | return (excess//2 + 1)*" " + text + (excess//2)*" " 222 | # Why distribute extra space this way? To match the behaviour of 223 | # the inbuilt str.center() method. 224 | else: 225 | # Equal padding on either side 226 | return (excess//2)*" " + text + (excess//2)*" " 227 | 228 | def __getattr__(self, name): 229 | 230 | if name == "rowcount": 231 | return len(self._rows) 232 | elif name == "colcount": 233 | if self._field_names: 234 | return len(self._field_names) 235 | elif self._rows: 236 | return len(self._rows[0]) 237 | else: 238 | return 0 239 | else: 240 | raise AttributeError(name) 241 | 242 | def __getitem__(self, index): 243 | 244 | new = PrettyTable() 245 | new.field_names = self.field_names 246 | for attr in self._options: 247 | setattr(new, "_"+attr, getattr(self, "_"+attr)) 248 | setattr(new, "_align", getattr(self, "_align")) 249 | if isinstance(index, slice): 250 | for row in self._rows[index]: 251 | new.add_row(row) 252 | elif isinstance(index, int): 253 | new.add_row(self._rows[index]) 254 | else: 255 | raise Exception("Index %s is invalid, must be an integer or slice" % str(index)) 256 | return new 257 | 258 | if py3k: 259 | def __str__(self): 260 | return self.__unicode__() 261 | else: 262 | def __str__(self): 263 | return self.__unicode__().encode(self.encoding) 264 | 265 | def __unicode__(self): 266 | return self.get_string() 267 | 268 | ############################## 269 | # ATTRIBUTE VALIDATORS # 270 | ############################## 271 | 272 | # The method _validate_option is all that should be used elsewhere in the code base to validate options. 273 | # It will call the appropriate validation method for that option. The individual validation methods should 274 | # never need to be called directly (although nothing bad will happen if they *are*). 275 | # Validation happens in TWO places. 276 | # Firstly, in the property setters defined in the ATTRIBUTE MANAGMENT section. 277 | # Secondly, in the _get_options method, where keyword arguments are mixed with persistent settings 278 | 279 | def _validate_option(self, option, val): 280 | if option in ("field_names"): 281 | self._validate_field_names(val) 282 | elif option in ("start", "end", "max_width", "min_width", "min_table_width", "max_table_width", "padding_width", "left_padding_width", "right_padding_width", "format"): 283 | self._validate_nonnegative_int(option, val) 284 | elif option in ("sortby"): 285 | self._validate_field_name(option, val) 286 | elif option in ("sort_key"): 287 | self._validate_function(option, val) 288 | elif option in ("hrules"): 289 | self._validate_hrules(option, val) 290 | elif option in ("vrules"): 291 | self._validate_vrules(option, val) 292 | elif option in ("fields"): 293 | self._validate_all_field_names(option, val) 294 | elif option in ("header", "border", "reversesort", "xhtml", "print_empty", "oldsortslice"): 295 | self._validate_true_or_false(option, val) 296 | elif option in ("header_style"): 297 | self._validate_header_style(val) 298 | elif option in ("int_format"): 299 | self._validate_int_format(option, val) 300 | elif option in ("float_format"): 301 | self._validate_float_format(option, val) 302 | elif option in ("vertical_char", "horizontal_char", "junction_char"): 303 | self._validate_single_char(option, val) 304 | elif option in ("attributes"): 305 | self._validate_attributes(option, val) 306 | 307 | def _validate_field_names(self, val): 308 | # Check for appropriate length 309 | if self._field_names: 310 | try: 311 | assert len(val) == len(self._field_names) 312 | except AssertionError: 313 | raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._field_names))) 314 | if self._rows: 315 | try: 316 | assert len(val) == len(self._rows[0]) 317 | except AssertionError: 318 | raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._rows[0]))) 319 | # Check for uniqueness 320 | try: 321 | assert len(val) == len(set(val)) 322 | except AssertionError: 323 | raise Exception("Field names must be unique!") 324 | 325 | def _validate_header_style(self, val): 326 | try: 327 | assert val in ("cap", "title", "upper", "lower", None) 328 | except AssertionError: 329 | raise Exception("Invalid header style, use cap, title, upper, lower or None!") 330 | 331 | def _validate_align(self, val): 332 | try: 333 | assert val in ["l","c","r"] 334 | except AssertionError: 335 | raise Exception("Alignment %s is invalid, use l, c or r!" % val) 336 | 337 | def _validate_valign(self, val): 338 | try: 339 | assert val in ["t","m","b",None] 340 | except AssertionError: 341 | raise Exception("Alignment %s is invalid, use t, m, b or None!" % val) 342 | 343 | def _validate_nonnegative_int(self, name, val): 344 | try: 345 | assert int(val) >= 0 346 | except AssertionError: 347 | raise Exception("Invalid value for %s: %s!" % (name, self._unicode(val))) 348 | 349 | def _validate_true_or_false(self, name, val): 350 | try: 351 | assert val in (True, False) 352 | except AssertionError: 353 | raise Exception("Invalid value for %s! Must be True or False." % name) 354 | 355 | def _validate_int_format(self, name, val): 356 | if val == "": 357 | return 358 | try: 359 | assert type(val) in (str, unicode) 360 | assert val.isdigit() 361 | except AssertionError: 362 | raise Exception("Invalid value for %s! Must be an integer format string." % name) 363 | 364 | def _validate_float_format(self, name, val): 365 | if val == "": 366 | return 367 | try: 368 | assert type(val) in (str, unicode) 369 | assert "." in val 370 | bits = val.split(".") 371 | assert len(bits) <= 2 372 | assert bits[0] == "" or bits[0].isdigit() 373 | assert bits[1] == "" or bits[1].isdigit() 374 | except AssertionError: 375 | raise Exception("Invalid value for %s! Must be a float format string." % name) 376 | 377 | def _validate_function(self, name, val): 378 | try: 379 | assert hasattr(val, "__call__") 380 | except AssertionError: 381 | raise Exception("Invalid value for %s! Must be a function." % name) 382 | 383 | def _validate_hrules(self, name, val): 384 | try: 385 | assert val in (ALL, FRAME, HEADER, NONE) 386 | except AssertionError: 387 | raise Exception("Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name) 388 | 389 | def _validate_vrules(self, name, val): 390 | try: 391 | assert val in (ALL, FRAME, NONE) 392 | except AssertionError: 393 | raise Exception("Invalid value for %s! Must be ALL, FRAME, or NONE." % name) 394 | 395 | def _validate_field_name(self, name, val): 396 | try: 397 | assert (val in self._field_names) or (val is None) 398 | except AssertionError: 399 | raise Exception("Invalid field name: %s!" % val) 400 | 401 | def _validate_all_field_names(self, name, val): 402 | try: 403 | for x in val: 404 | self._validate_field_name(name, x) 405 | except AssertionError: 406 | raise Exception("fields must be a sequence of field names!") 407 | 408 | def _validate_single_char(self, name, val): 409 | try: 410 | assert _str_block_width(val) == 1 411 | except AssertionError: 412 | raise Exception("Invalid value for %s! Must be a string of length 1." % name) 413 | 414 | def _validate_attributes(self, name, val): 415 | try: 416 | assert isinstance(val, dict) 417 | except AssertionError: 418 | raise Exception("attributes must be a dictionary of name/value pairs!") 419 | 420 | ############################## 421 | # ATTRIBUTE MANAGEMENT # 422 | ############################## 423 | 424 | @property 425 | def field_names(self): 426 | """List or tuple of field names""" 427 | return self._field_names 428 | 429 | @field_names.setter 430 | def field_names(self, val): 431 | val = [self._unicode(x) for x in val] 432 | self._validate_option("field_names", val) 433 | if self._field_names: 434 | old_names = self._field_names[:] 435 | self._field_names = val 436 | if self._align and old_names: 437 | for old_name, new_name in zip(old_names, val): 438 | self._align[new_name] = self._align[old_name] 439 | for old_name in old_names: 440 | if old_name not in self._align: 441 | self._align.pop(old_name) 442 | else: 443 | self.align = "c" 444 | if self._valign and old_names: 445 | for old_name, new_name in zip(old_names, val): 446 | self._valign[new_name] = self._valign[old_name] 447 | for old_name in old_names: 448 | if old_name not in self._valign: 449 | self._valign.pop(old_name) 450 | else: 451 | self.valign = "t" 452 | @property 453 | def align(self): 454 | """Controls alignment of fields 455 | Arguments: 456 | 457 | align - alignment, one of "l", "c", or "r" """ 458 | return self._align 459 | 460 | @align.setter 461 | def align(self, val): 462 | if not self._field_names: 463 | self._align = {} 464 | elif val is None or (isinstance(val,dict) and len(val) is 0): 465 | for field in self._field_names: 466 | self._align[field] = "c" 467 | else: 468 | self._validate_align(val) 469 | for field in self._field_names: 470 | self._align[field] = val 471 | 472 | @property 473 | def valign(self): 474 | """Controls vertical alignment of fields 475 | Arguments: 476 | 477 | valign - vertical alignment, one of "t", "m", or "b" """ 478 | return self._valign 479 | @valign.setter 480 | def valign(self, val): 481 | if not self._field_names: 482 | self._valign = {} 483 | elif val is None or (isinstance(val,dict) and len(val) is 0): 484 | for field in self._field_names: 485 | self._valign[field] = "t" 486 | else: 487 | self._validate_valign(val) 488 | for field in self._field_names: 489 | self._valign[field] = val 490 | 491 | @property 492 | def max_width(self): 493 | """Controls maximum width of fields 494 | Arguments: 495 | 496 | max_width - maximum width integer""" 497 | return self._max_width 498 | @max_width.setter 499 | def max_width(self, val): 500 | if val is None or (isinstance(val,dict) and len(val) is 0): 501 | self._max_width = {} 502 | else: 503 | self._validate_option("max_width",val) 504 | for field in self._field_names: 505 | self._max_width[field] = val 506 | 507 | @property 508 | def min_width(self): 509 | """Controls minimum width of fields 510 | Arguments: 511 | 512 | min_width - minimum width integer""" 513 | return self._min_width 514 | @min_width.setter 515 | def min_width(self, val): 516 | if val is None or (isinstance(val,dict) and len(val) is 0): 517 | self._min_width = {} 518 | else: 519 | self._validate_option("min_width",val) 520 | for field in self._field_names: 521 | self._min_width[field] = val 522 | 523 | @property 524 | def min_table_width(self): 525 | return self._min_table_width 526 | 527 | @min_table_width.setter 528 | def min_table_width(self, val): 529 | self._validate_option("min_table_width", val) 530 | self._min_table_width = val 531 | 532 | @property 533 | def max_table_width(self): 534 | return self._max_table_width 535 | 536 | @max_table_width.setter 537 | def max_table_width(self, val): 538 | self._validate_option("max_table_width", val) 539 | self._max_table_width = val 540 | 541 | @property 542 | def fields(self): 543 | """List or tuple of field names to include in displays""" 544 | return self._fields 545 | 546 | @fields.setter 547 | def fields(self, val): 548 | self._validate_option("fields", val) 549 | self._fields = val 550 | 551 | @property 552 | def title(self): 553 | """Optional table title 554 | 555 | Arguments: 556 | 557 | title - table title""" 558 | return self._title 559 | 560 | @title.setter 561 | def title(self, val): 562 | self._title = self._unicode(val) 563 | 564 | @property 565 | def start(self): 566 | """Start index of the range of rows to print 567 | 568 | Arguments: 569 | 570 | start - index of first data row to include in output""" 571 | return self._start 572 | 573 | @start.setter 574 | def start(self, val): 575 | self._validate_option("start", val) 576 | self._start = val 577 | 578 | @property 579 | def end(self): 580 | """End index of the range of rows to print 581 | 582 | Arguments: 583 | 584 | end - index of last data row to include in output PLUS ONE (list slice style)""" 585 | return self._end 586 | @end.setter 587 | def end(self, val): 588 | self._validate_option("end", val) 589 | self._end = val 590 | 591 | @property 592 | def sortby(self): 593 | """Name of field by which to sort rows 594 | 595 | Arguments: 596 | 597 | sortby - field name to sort by""" 598 | return self._sortby 599 | @sortby.setter 600 | def sortby(self, val): 601 | self._validate_option("sortby", val) 602 | self._sortby = val 603 | 604 | @property 605 | def reversesort(self): 606 | """Controls direction of sorting (ascending vs descending) 607 | 608 | Arguments: 609 | 610 | reveresort - set to True to sort by descending order, or False to sort by ascending order""" 611 | return self._reversesort 612 | @reversesort.setter 613 | def reversesort(self, val): 614 | self._validate_option("reversesort", val) 615 | self._reversesort = val 616 | 617 | @property 618 | def sort_key(self): 619 | """Sorting key function, applied to data points before sorting 620 | 621 | Arguments: 622 | 623 | sort_key - a function which takes one argument and returns something to be sorted""" 624 | return self._sort_key 625 | @sort_key.setter 626 | def sort_key(self, val): 627 | self._validate_option("sort_key", val) 628 | self._sort_key = val 629 | 630 | @property 631 | def header(self): 632 | """Controls printing of table header with field names 633 | 634 | Arguments: 635 | 636 | header - print a header showing field names (True or False)""" 637 | return self._header 638 | @header.setter 639 | def header(self, val): 640 | self._validate_option("header", val) 641 | self._header = val 642 | 643 | @property 644 | def header_style(self): 645 | """Controls stylisation applied to field names in header 646 | 647 | Arguments: 648 | 649 | header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None)""" 650 | return self._header_style 651 | @header_style.setter 652 | def header_style(self, val): 653 | self._validate_header_style(val) 654 | self._header_style = val 655 | 656 | @property 657 | def border(self): 658 | """Controls printing of border around table 659 | 660 | Arguments: 661 | 662 | border - print a border around the table (True or False)""" 663 | return self._border 664 | @border.setter 665 | def border(self, val): 666 | self._validate_option("border", val) 667 | self._border = val 668 | 669 | @property 670 | def hrules(self): 671 | """Controls printing of horizontal rules after rows 672 | 673 | Arguments: 674 | 675 | hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE""" 676 | return self._hrules 677 | @hrules.setter 678 | def hrules(self, val): 679 | self._validate_option("hrules", val) 680 | self._hrules = val 681 | 682 | @property 683 | def vrules(self): 684 | """Controls printing of vertical rules between columns 685 | 686 | Arguments: 687 | 688 | vrules - vertical rules style. Allowed values: FRAME, ALL, NONE""" 689 | return self._vrules 690 | @vrules.setter 691 | def vrules(self, val): 692 | self._validate_option("vrules", val) 693 | self._vrules = val 694 | 695 | @property 696 | def int_format(self): 697 | """Controls formatting of integer data 698 | Arguments: 699 | 700 | int_format - integer format string""" 701 | return self._int_format 702 | @int_format.setter 703 | def int_format(self, val): 704 | if val is None or (isinstance(val,dict) and len(val) is 0): 705 | self._int_format = {} 706 | else: 707 | self._validate_option("int_format",val) 708 | for field in self._field_names: 709 | self._int_format[field] = val 710 | 711 | @property 712 | def float_format(self): 713 | """Controls formatting of floating point data 714 | Arguments: 715 | 716 | float_format - floating point format string""" 717 | return self._float_format 718 | @float_format.setter 719 | def float_format(self, val): 720 | if val is None or (isinstance(val,dict) and len(val) is 0): 721 | self._float_format = {} 722 | else: 723 | self._validate_option("float_format",val) 724 | for field in self._field_names: 725 | self._float_format[field] = val 726 | 727 | @property 728 | def padding_width(self): 729 | """The number of empty spaces between a column's edge and its content 730 | 731 | Arguments: 732 | 733 | padding_width - number of spaces, must be a positive integer""" 734 | return self._padding_width 735 | @padding_width.setter 736 | def padding_width(self, val): 737 | self._validate_option("padding_width", val) 738 | self._padding_width = val 739 | 740 | @property 741 | def left_padding_width(self): 742 | """The number of empty spaces between a column's left edge and its content 743 | 744 | Arguments: 745 | 746 | left_padding - number of spaces, must be a positive integer""" 747 | return self._left_padding_width 748 | @left_padding_width.setter 749 | def left_padding_width(self, val): 750 | self._validate_option("left_padding_width", val) 751 | self._left_padding_width = val 752 | 753 | @property 754 | def right_padding_width(self): 755 | """The number of empty spaces between a column's right edge and its content 756 | 757 | Arguments: 758 | 759 | right_padding - number of spaces, must be a positive integer""" 760 | return self._right_padding_width 761 | @right_padding_width.setter 762 | def right_padding_width(self, val): 763 | self._validate_option("right_padding_width", val) 764 | self._right_padding_width = val 765 | 766 | @property 767 | def vertical_char(self): 768 | """The charcter used when printing table borders to draw vertical lines 769 | 770 | Arguments: 771 | 772 | vertical_char - single character string used to draw vertical lines""" 773 | return self._vertical_char 774 | @vertical_char.setter 775 | def vertical_char(self, val): 776 | val = self._unicode(val) 777 | self._validate_option("vertical_char", val) 778 | self._vertical_char = val 779 | 780 | @property 781 | def horizontal_char(self): 782 | """The charcter used when printing table borders to draw horizontal lines 783 | 784 | Arguments: 785 | 786 | horizontal_char - single character string used to draw horizontal lines""" 787 | return self._horizontal_char 788 | @horizontal_char.setter 789 | def horizontal_char(self, val): 790 | val = self._unicode(val) 791 | self._validate_option("horizontal_char", val) 792 | self._horizontal_char = val 793 | 794 | @property 795 | def junction_char(self): 796 | """The charcter used when printing table borders to draw line junctions 797 | 798 | Arguments: 799 | 800 | junction_char - single character string used to draw line junctions""" 801 | return self._junction_char 802 | @junction_char.setter 803 | def junction_char(self, val): 804 | val = self._unicode(val) 805 | self._validate_option("vertical_char", val) 806 | self._junction_char = val 807 | 808 | @property 809 | def format(self): 810 | """Controls whether or not HTML tables are formatted to match styling options 811 | 812 | Arguments: 813 | 814 | format - True or False""" 815 | return self._format 816 | @format.setter 817 | def format(self, val): 818 | self._validate_option("format", val) 819 | self._format = val 820 | 821 | @property 822 | def print_empty(self): 823 | """Controls whether or not empty tables produce a header and frame or just an empty string 824 | 825 | Arguments: 826 | 827 | print_empty - True or False""" 828 | return self._print_empty 829 | @print_empty.setter 830 | def print_empty(self, val): 831 | self._validate_option("print_empty", val) 832 | self._print_empty = val 833 | 834 | @property 835 | def attributes(self): 836 | """A dictionary of HTML attribute name/value pairs to be included in the tag when printing HTML 837 | 838 | Arguments: 839 | 840 | attributes - dictionary of attributes""" 841 | return self._attributes 842 | 843 | @attributes.setter 844 | def attributes(self, val): 845 | self._validate_option("attributes", val) 846 | self._attributes = val 847 | 848 | @property 849 | def oldsortslice(self): 850 | """ oldsortslice - Slice rows before sorting in the "old style" """ 851 | return self._oldsortslice 852 | @oldsortslice.setter 853 | def oldsortslice(self, val): 854 | self._validate_option("oldsortslice", val) 855 | self._oldsortslice = val 856 | 857 | ############################## 858 | # OPTION MIXER # 859 | ############################## 860 | 861 | def _get_options(self, kwargs): 862 | 863 | options = {} 864 | for option in self._options: 865 | if option in kwargs: 866 | self._validate_option(option, kwargs[option]) 867 | options[option] = kwargs[option] 868 | else: 869 | options[option] = getattr(self, "_"+option) 870 | return options 871 | 872 | ############################## 873 | # PRESET STYLE LOGIC # 874 | ############################## 875 | 876 | def set_style(self, style): 877 | 878 | if style == DEFAULT: 879 | self._set_default_style() 880 | elif style == MSWORD_FRIENDLY: 881 | self._set_msword_style() 882 | elif style == PLAIN_COLUMNS: 883 | self._set_columns_style() 884 | elif style == RANDOM: 885 | self._set_random_style() 886 | else: 887 | raise Exception("Invalid pre-set style!") 888 | 889 | def _set_default_style(self): 890 | 891 | self.header = True 892 | self.border = True 893 | self._hrules = FRAME 894 | self._vrules = ALL 895 | self.padding_width = 1 896 | self.left_padding_width = 1 897 | self.right_padding_width = 1 898 | self.vertical_char = "|" 899 | self.horizontal_char = "-" 900 | self.junction_char = "+" 901 | 902 | def _set_msword_style(self): 903 | 904 | self.header = True 905 | self.border = True 906 | self._hrules = NONE 907 | self.padding_width = 1 908 | self.left_padding_width = 1 909 | self.right_padding_width = 1 910 | self.vertical_char = "|" 911 | 912 | def _set_columns_style(self): 913 | 914 | self.header = True 915 | self.border = False 916 | self.padding_width = 1 917 | self.left_padding_width = 0 918 | self.right_padding_width = 8 919 | 920 | def _set_random_style(self): 921 | 922 | # Just for fun! 923 | self.header = random.choice((True, False)) 924 | self.border = random.choice((True, False)) 925 | self._hrules = random.choice((ALL, FRAME, HEADER, NONE)) 926 | self._vrules = random.choice((ALL, FRAME, NONE)) 927 | self.left_padding_width = random.randint(0,5) 928 | self.right_padding_width = random.randint(0,5) 929 | self.vertical_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") 930 | self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") 931 | self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") 932 | 933 | ############################## 934 | # DATA INPUT METHODS # 935 | ############################## 936 | 937 | def add_row(self, row): 938 | 939 | """Add a row to the table 940 | 941 | Arguments: 942 | 943 | row - row of data, should be a list with as many elements as the table 944 | has fields""" 945 | 946 | if self._field_names and len(row) != len(self._field_names): 947 | raise Exception("Row has incorrect number of values, (actual) %d!=%d (expected)" %(len(row),len(self._field_names))) 948 | if not self._field_names: 949 | self.field_names = [("Field %d" % (n+1)) for n in range(0,len(row))] 950 | self._rows.append(list(row)) 951 | 952 | def del_row(self, row_index): 953 | 954 | """Delete a row to the table 955 | 956 | Arguments: 957 | 958 | row_index - The index of the row you want to delete. Indexing starts at 0.""" 959 | 960 | if row_index > len(self._rows)-1: 961 | raise Exception("Cant delete row at index %d, table only has %d rows!" % (row_index, len(self._rows))) 962 | del self._rows[row_index] 963 | 964 | def add_column(self, fieldname, column, align="c", valign="t"): 965 | 966 | """Add a column to the table. 967 | 968 | Arguments: 969 | 970 | fieldname - name of the field to contain the new column of data 971 | column - column of data, should be a list with as many elements as the 972 | table has rows 973 | align - desired alignment for this column - "l" for left, "c" for centre and "r" for right 974 | valign - desired vertical alignment for new columns - "t" for top, "m" for middle and "b" for bottom""" 975 | 976 | if len(self._rows) in (0, len(column)): 977 | self._validate_align(align) 978 | self._validate_valign(valign) 979 | self._field_names.append(fieldname) 980 | self._align[fieldname] = align 981 | self._valign[fieldname] = valign 982 | for i in range(0, len(column)): 983 | if len(self._rows) < i+1: 984 | self._rows.append([]) 985 | self._rows[i].append(column[i]) 986 | else: 987 | raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self._rows))) 988 | 989 | def clear_rows(self): 990 | 991 | """Delete all rows from the table but keep the current field names""" 992 | 993 | self._rows = [] 994 | 995 | def clear(self): 996 | 997 | """Delete all rows and field names from the table, maintaining nothing but styling options""" 998 | 999 | self._rows = [] 1000 | self._field_names = [] 1001 | self._widths = [] 1002 | 1003 | ############################## 1004 | # MISC PUBLIC METHODS # 1005 | ############################## 1006 | 1007 | def copy(self): 1008 | return copy.deepcopy(self) 1009 | 1010 | ############################## 1011 | # MISC PRIVATE METHODS # 1012 | ############################## 1013 | 1014 | def _format_value(self, field, value): 1015 | if isinstance(value, int) and field in self._int_format: 1016 | value = self._unicode(("%%%sd" % self._int_format[field]) % value) 1017 | elif isinstance(value, float) and field in self._float_format: 1018 | value = self._unicode(("%%%sf" % self._float_format[field]) % value) 1019 | return self._unicode(value) 1020 | 1021 | def _compute_table_width(self, options): 1022 | table_width = 2 if options["vrules"] in (FRAME, ALL) else 0 1023 | per_col_padding = sum(self._get_padding_widths(options)) 1024 | for index, fieldname in enumerate(self.field_names): 1025 | if not options["fields"] or (options["fields"] and fieldname in options["fields"]): 1026 | table_width += self._widths[index] + per_col_padding 1027 | return table_width 1028 | 1029 | def _compute_widths(self, rows, options): 1030 | if options["header"]: 1031 | widths = [_get_size(field)[0] for field in self._field_names] 1032 | else: 1033 | widths = len(self.field_names) * [0] 1034 | 1035 | for row in rows: 1036 | for index, value in enumerate(row): 1037 | fieldname = self.field_names[index] 1038 | if fieldname in self.max_width: 1039 | widths[index] = max(widths[index], min(_get_size(value)[0], self.max_width[fieldname])) 1040 | else: 1041 | widths[index] = max(widths[index], _get_size(value)[0]) 1042 | if fieldname in self.min_width: 1043 | widths[index] = max(widths[index], self.min_width[fieldname]) 1044 | self._widths = widths 1045 | 1046 | # Are we exceeding max_table_width? 1047 | if self._max_table_width: 1048 | table_width = self._compute_table_width(options) 1049 | if table_width > self._max_table_width: 1050 | # Shrink widths in proportion 1051 | scale = 1.0*self._max_table_width / table_width 1052 | widths = [int(math.floor(w*scale)) for w in widths] 1053 | self._widths = widths 1054 | 1055 | # Are we under min_table_width or title width? 1056 | if self._min_table_width or options["title"]: 1057 | if options["title"]: 1058 | title_width = len(options["title"])+sum(self._get_padding_widths(options)) 1059 | if options["vrules"] in (FRAME, ALL): 1060 | title_width += 2 1061 | else: 1062 | title_width = 0 1063 | min_table_width = self.min_table_width or 0 1064 | min_width = max(title_width, min_table_width) 1065 | table_width = self._compute_table_width(options) 1066 | if table_width < min_width: 1067 | # Grow widths in proportion 1068 | scale = 1.0*min_width / table_width 1069 | widths = [int(math.ceil(w*scale)) for w in widths] 1070 | self._widths = widths 1071 | 1072 | def _get_padding_widths(self, options): 1073 | 1074 | if options["left_padding_width"] is not None: 1075 | lpad = options["left_padding_width"] 1076 | else: 1077 | lpad = options["padding_width"] 1078 | if options["right_padding_width"] is not None: 1079 | rpad = options["right_padding_width"] 1080 | else: 1081 | rpad = options["padding_width"] 1082 | return lpad, rpad 1083 | 1084 | def _get_rows(self, options): 1085 | """Return only those data rows that should be printed, based on slicing and sorting. 1086 | 1087 | Arguments: 1088 | 1089 | options - dictionary of option settings.""" 1090 | 1091 | if options["oldsortslice"]: 1092 | rows = copy.deepcopy(self._rows[options["start"]:options["end"]]) 1093 | else: 1094 | rows = copy.deepcopy(self._rows) 1095 | 1096 | # Sort 1097 | if options["sortby"]: 1098 | sortindex = self._field_names.index(options["sortby"]) 1099 | # Decorate 1100 | rows = [[row[sortindex]]+row for row in rows] 1101 | # Sort 1102 | rows.sort(reverse=options["reversesort"], key=options["sort_key"]) 1103 | # Undecorate 1104 | rows = [row[1:] for row in rows] 1105 | 1106 | # Slice if necessary 1107 | if not options["oldsortslice"]: 1108 | rows = rows[options["start"]:options["end"]] 1109 | 1110 | return rows 1111 | 1112 | def _format_row(self, row, options): 1113 | return [self._format_value(field, value) for (field, value) in zip(self._field_names, row)] 1114 | 1115 | def _format_rows(self, rows, options): 1116 | return [self._format_row(row, options) for row in rows] 1117 | 1118 | ############################## 1119 | # PLAIN TEXT STRING METHODS # 1120 | ############################## 1121 | 1122 | def get_string(self, **kwargs): 1123 | 1124 | """Return string representation of table in current state. 1125 | 1126 | Arguments: 1127 | 1128 | title - optional table title 1129 | start - index of first data row to include in output 1130 | end - index of last data row to include in output PLUS ONE (list slice style) 1131 | fields - names of fields (columns) to include 1132 | header - print a header showing field names (True or False) 1133 | border - print a border around the table (True or False) 1134 | hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE 1135 | vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE 1136 | int_format - controls formatting of integer data 1137 | float_format - controls formatting of floating point data 1138 | padding_width - number of spaces on either side of column data (only used if left and right paddings are None) 1139 | left_padding_width - number of spaces on left hand side of column data 1140 | right_padding_width - number of spaces on right hand side of column data 1141 | vertical_char - single character string used to draw vertical lines 1142 | horizontal_char - single character string used to draw horizontal lines 1143 | junction_char - single character string used to draw line junctions 1144 | sortby - name of field to sort rows by 1145 | sort_key - sorting key function, applied to data points before sorting 1146 | reversesort - True or False to sort in descending or ascending order 1147 | print empty - if True, stringify just the header for an empty table, if False return an empty string """ 1148 | 1149 | options = self._get_options(kwargs) 1150 | 1151 | lines = [] 1152 | 1153 | # Don't think too hard about an empty table 1154 | # Is this the desired behaviour? Maybe we should still print the header? 1155 | if self.rowcount == 0 and (not options["print_empty"] or not options["border"]): 1156 | return "" 1157 | 1158 | # Get the rows we need to print, taking into account slicing, sorting, etc. 1159 | rows = self._get_rows(options) 1160 | 1161 | # Turn all data in all rows into Unicode, formatted as desired 1162 | formatted_rows = self._format_rows(rows, options) 1163 | 1164 | # Compute column widths 1165 | self._compute_widths(formatted_rows, options) 1166 | self._hrule = self._stringify_hrule(options) 1167 | 1168 | # Add title 1169 | title = options["title"] or self._title 1170 | if title: 1171 | lines.append(self._stringify_title(title, options)) 1172 | 1173 | # Add header or top of border 1174 | if options["header"]: 1175 | lines.append(self._stringify_header(options)) 1176 | elif options["border"] and options["hrules"] in (ALL, FRAME): 1177 | lines.append(self._hrule) 1178 | 1179 | # Add rows 1180 | for row in formatted_rows: 1181 | lines.append(self._stringify_row(row, options)) 1182 | 1183 | # Add bottom of border 1184 | if options["border"] and options["hrules"] == FRAME: 1185 | lines.append(self._hrule) 1186 | 1187 | return self._unicode("\n").join(lines) 1188 | 1189 | def _stringify_hrule(self, options): 1190 | 1191 | if not options["border"]: 1192 | return "" 1193 | lpad, rpad = self._get_padding_widths(options) 1194 | if options['vrules'] in (ALL, FRAME): 1195 | bits = [options["junction_char"]] 1196 | else: 1197 | bits = [options["horizontal_char"]] 1198 | # For tables with no data or fieldnames 1199 | if not self._field_names: 1200 | bits.append(options["junction_char"]) 1201 | return "".join(bits) 1202 | for field, width in zip(self._field_names, self._widths): 1203 | if options["fields"] and field not in options["fields"]: 1204 | continue 1205 | bits.append((width+lpad+rpad)*options["horizontal_char"]) 1206 | if options['vrules'] == ALL: 1207 | bits.append(options["junction_char"]) 1208 | else: 1209 | bits.append(options["horizontal_char"]) 1210 | if options["vrules"] == FRAME: 1211 | bits.pop() 1212 | bits.append(options["junction_char"]) 1213 | return "".join(bits) 1214 | 1215 | def _stringify_title(self, title, options): 1216 | 1217 | lines = [] 1218 | lpad, rpad = self._get_padding_widths(options) 1219 | if options["border"]: 1220 | if options["vrules"] == ALL: 1221 | options["vrules"] = FRAME 1222 | lines.append(self._stringify_hrule(options)) 1223 | options["vrules"] = ALL 1224 | elif options["vrules"] == FRAME: 1225 | lines.append(self._stringify_hrule(options)) 1226 | bits = [] 1227 | endpoint = options["vertical_char"] if options["vrules"] in (ALL, FRAME) else " " 1228 | bits.append(endpoint) 1229 | title = " "*lpad + title + " "*rpad 1230 | bits.append(self._justify(title, len(self._hrule)-2, "c")) 1231 | bits.append(endpoint) 1232 | lines.append("".join(bits)) 1233 | return "\n".join(lines) 1234 | 1235 | def _stringify_header(self, options): 1236 | 1237 | bits = [] 1238 | lpad, rpad = self._get_padding_widths(options) 1239 | if options["border"]: 1240 | if options["hrules"] in (ALL, FRAME): 1241 | bits.append(self._hrule) 1242 | bits.append("\n") 1243 | if options["vrules"] in (ALL, FRAME): 1244 | bits.append(options["vertical_char"]) 1245 | else: 1246 | bits.append(" ") 1247 | # For tables with no data or field names 1248 | if not self._field_names: 1249 | if options["vrules"] in (ALL, FRAME): 1250 | bits.append(options["vertical_char"]) 1251 | else: 1252 | bits.append(" ") 1253 | for field, width, in zip(self._field_names, self._widths): 1254 | if options["fields"] and field not in options["fields"]: 1255 | continue 1256 | if self._header_style == "cap": 1257 | fieldname = field.capitalize() 1258 | elif self._header_style == "title": 1259 | fieldname = field.title() 1260 | elif self._header_style == "upper": 1261 | fieldname = field.upper() 1262 | elif self._header_style == "lower": 1263 | fieldname = field.lower() 1264 | else: 1265 | fieldname = field 1266 | bits.append(" " * lpad + self._justify(fieldname, width, self._align[field]) + " " * rpad) 1267 | if options["border"]: 1268 | if options["vrules"] == ALL: 1269 | bits.append(options["vertical_char"]) 1270 | else: 1271 | bits.append(" ") 1272 | # If vrules is FRAME, then we just appended a space at the end 1273 | # of the last field, when we really want a vertical character 1274 | if options["border"] and options["vrules"] == FRAME: 1275 | bits.pop() 1276 | bits.append(options["vertical_char"]) 1277 | if options["border"] and options["hrules"] != NONE: 1278 | bits.append("\n") 1279 | bits.append(self._hrule) 1280 | return "".join(bits) 1281 | 1282 | def _stringify_row(self, row, options): 1283 | 1284 | for index, field, value, width, in zip(range(0,len(row)), self._field_names, row, self._widths): 1285 | # Enforce max widths 1286 | lines = value.split("\n") 1287 | new_lines = [] 1288 | for line in lines: 1289 | if _str_block_width(line) > width: 1290 | line = textwrap.fill(line, width) 1291 | new_lines.append(line) 1292 | lines = new_lines 1293 | value = "\n".join(lines) 1294 | row[index] = value 1295 | 1296 | row_height = 0 1297 | for c in row: 1298 | h = _get_size(c)[1] 1299 | if h > row_height: 1300 | row_height = h 1301 | 1302 | bits = [] 1303 | lpad, rpad = self._get_padding_widths(options) 1304 | for y in range(0, row_height): 1305 | bits.append([]) 1306 | if options["border"]: 1307 | if options["vrules"] in (ALL, FRAME): 1308 | bits[y].append(self.vertical_char) 1309 | else: 1310 | bits[y].append(" ") 1311 | 1312 | for field, value, width, in zip(self._field_names, row, self._widths): 1313 | 1314 | valign = self._valign[field] 1315 | lines = value.split("\n") 1316 | dHeight = row_height - len(lines) 1317 | if dHeight: 1318 | if valign == "m": 1319 | lines = [""] * int(dHeight / 2) + lines + [""] * (dHeight - int(dHeight / 2)) 1320 | elif valign == "b": 1321 | lines = [""] * dHeight + lines 1322 | else: 1323 | lines = lines + [""] * dHeight 1324 | 1325 | y = 0 1326 | for l in lines: 1327 | if options["fields"] and field not in options["fields"]: 1328 | continue 1329 | 1330 | bits[y].append(" " * lpad + self._justify(l, width, self._align[field]) + " " * rpad) 1331 | if options["border"]: 1332 | if options["vrules"] == ALL: 1333 | bits[y].append(self.vertical_char) 1334 | else: 1335 | bits[y].append(" ") 1336 | y += 1 1337 | 1338 | # If vrules is FRAME, then we just appended a space at the end 1339 | # of the last field, when we really want a vertical character 1340 | for y in range(0, row_height): 1341 | if options["border"] and options["vrules"] == FRAME: 1342 | bits[y].pop() 1343 | bits[y].append(options["vertical_char"]) 1344 | 1345 | if options["border"] and options["hrules"]== ALL: 1346 | bits[row_height-1].append("\n") 1347 | bits[row_height-1].append(self._hrule) 1348 | 1349 | for y in range(0, row_height): 1350 | bits[y] = "".join(bits[y]) 1351 | 1352 | return "\n".join(bits) 1353 | 1354 | def paginate(self, page_length=58, **kwargs): 1355 | 1356 | pages = [] 1357 | kwargs["start"] = kwargs.get("start", 0) 1358 | true_end = kwargs.get("end", self.rowcount) 1359 | while True: 1360 | kwargs["end"] = min(kwargs["start"] + page_length, true_end) 1361 | pages.append(self.get_string(**kwargs)) 1362 | if kwargs["end"] == true_end: 1363 | break 1364 | kwargs["start"] += page_length 1365 | return "\f".join(pages) 1366 | 1367 | ############################## 1368 | # HTML STRING METHODS # 1369 | ############################## 1370 | 1371 | def get_html_string(self, **kwargs): 1372 | 1373 | """Return string representation of HTML formatted version of table in current state. 1374 | 1375 | Arguments: 1376 | 1377 | title - optional table title 1378 | start - index of first data row to include in output 1379 | end - index of last data row to include in output PLUS ONE (list slice style) 1380 | fields - names of fields (columns) to include 1381 | header - print a header showing field names (True or False) 1382 | border - print a border around the table (True or False) 1383 | hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE 1384 | vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE 1385 | int_format - controls formatting of integer data 1386 | float_format - controls formatting of floating point data 1387 | padding_width - number of spaces on either side of column data (only used if left and right paddings are None) 1388 | left_padding_width - number of spaces on left hand side of column data 1389 | right_padding_width - number of spaces on right hand side of column data 1390 | sortby - name of field to sort rows by 1391 | sort_key - sorting key function, applied to data points before sorting 1392 | attributes - dictionary of name/value pairs to include as HTML attributes in the
tag 1393 | xhtml - print
tags if True,
tags if false""" 1394 | 1395 | options = self._get_options(kwargs) 1396 | 1397 | if options["format"]: 1398 | string = self._get_formatted_html_string(options) 1399 | else: 1400 | string = self._get_simple_html_string(options) 1401 | 1402 | return string 1403 | 1404 | def _get_simple_html_string(self, options): 1405 | 1406 | lines = [] 1407 | if options["xhtml"]: 1408 | linebreak = "
" 1409 | else: 1410 | linebreak = "
" 1411 | 1412 | open_tag = [] 1413 | open_tag.append("") 1418 | lines.append("".join(open_tag)) 1419 | 1420 | # Title 1421 | title = options["title"] or self._title 1422 | if title: 1423 | cols = len(options["fields"]) if options["fields"] else len(self.field_names) 1424 | lines.append(" ") 1425 | lines.append(" " % (cols, title)) 1426 | lines.append(" ") 1427 | 1428 | # Headers 1429 | if options["header"]: 1430 | lines.append(" ") 1431 | for field in self._field_names: 1432 | if options["fields"] and field not in options["fields"]: 1433 | continue 1434 | lines.append(" " % escape(field).replace("\n", linebreak)) 1435 | lines.append(" ") 1436 | 1437 | # Data 1438 | rows = self._get_rows(options) 1439 | formatted_rows = self._format_rows(rows, options) 1440 | for row in formatted_rows: 1441 | lines.append(" ") 1442 | for field, datum in zip(self._field_names, row): 1443 | if options["fields"] and field not in options["fields"]: 1444 | continue 1445 | lines.append(" " % escape(datum).replace("\n", linebreak)) 1446 | lines.append(" ") 1447 | 1448 | lines.append("
%s
%s
%s
") 1449 | 1450 | return self._unicode("\n").join(lines) 1451 | 1452 | def _get_formatted_html_string(self, options): 1453 | 1454 | lines = [] 1455 | lpad, rpad = self._get_padding_widths(options) 1456 | if options["xhtml"]: 1457 | linebreak = "
" 1458 | else: 1459 | linebreak = "
" 1460 | 1461 | open_tag = [] 1462 | open_tag.append("") 1482 | lines.append("".join(open_tag)) 1483 | 1484 | # Title 1485 | title = options["title"] or self._title 1486 | if title: 1487 | cols = len(options["fields"]) if options["fields"] else len(self.field_names) 1488 | lines.append(" ") 1489 | lines.append(" %s" % (cols, title)) 1490 | lines.append(" ") 1491 | 1492 | # Headers 1493 | if options["header"]: 1494 | lines.append(" ") 1495 | for field in self._field_names: 1496 | if options["fields"] and field not in options["fields"]: 1497 | continue 1498 | lines.append(" %s" % (lpad, rpad, escape(field).replace("\n", linebreak))) 1499 | lines.append(" ") 1500 | 1501 | # Data 1502 | rows = self._get_rows(options) 1503 | formatted_rows = self._format_rows(rows, options) 1504 | aligns = [] 1505 | valigns = [] 1506 | for field in self._field_names: 1507 | aligns.append({ "l" : "left", "r" : "right", "c" : "center" }[self._align[field]]) 1508 | valigns.append({"t" : "top", "m" : "middle", "b" : "bottom"}[self._valign[field]]) 1509 | for row in formatted_rows: 1510 | lines.append(" ") 1511 | for field, datum, align, valign in zip(self._field_names, row, aligns, valigns): 1512 | if options["fields"] and field not in options["fields"]: 1513 | continue 1514 | lines.append(" %s" % (lpad, rpad, align, valign, escape(datum).replace("\n", linebreak))) 1515 | lines.append(" ") 1516 | lines.append("") 1517 | 1518 | return self._unicode("\n").join(lines) 1519 | 1520 | ############################## 1521 | # UNICODE WIDTH FUNCTIONS # 1522 | ############################## 1523 | 1524 | def _char_block_width(char): 1525 | # Basic Latin, which is probably the most common case 1526 | #if char in xrange(0x0021, 0x007e): 1527 | #if char >= 0x0021 and char <= 0x007e: 1528 | if 0x0021 <= char <= 0x007e: 1529 | return 1 1530 | # Chinese, Japanese, Korean (common) 1531 | if 0x4e00 <= char <= 0x9fff: 1532 | return 2 1533 | # Hangul 1534 | if 0xac00 <= char <= 0xd7af: 1535 | return 2 1536 | # Combining? 1537 | if unicodedata.combining(uni_chr(char)): 1538 | return 0 1539 | # Hiragana and Katakana 1540 | if 0x3040 <= char <= 0x309f or 0x30a0 <= char <= 0x30ff: 1541 | return 2 1542 | # Full-width Latin characters 1543 | if 0xff01 <= char <= 0xff60: 1544 | return 2 1545 | # CJK punctuation 1546 | if 0x3000 <= char <= 0x303e: 1547 | return 2 1548 | # Backspace and delete 1549 | if char in (0x0008, 0x007f): 1550 | return -1 1551 | # Other control characters 1552 | elif char in (0x0000, 0x000f, 0x001f): 1553 | return 0 1554 | # Take a guess 1555 | return 1 1556 | 1557 | def _str_block_width(val): 1558 | 1559 | return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val)))) 1560 | 1561 | ############################## 1562 | # TABLE FACTORIES # 1563 | ############################## 1564 | 1565 | def from_csv(fp, field_names = None, **kwargs): 1566 | 1567 | fmtparams = {} 1568 | for param in ["delimiter", "doublequote", "escapechar", "lineterminator", 1569 | "quotechar", "quoting", "skipinitialspace", "strict"]: 1570 | if param in kwargs: 1571 | fmtparams[param] = kwargs.pop(param) 1572 | if fmtparams: 1573 | reader = csv.reader(fp, **fmtparams) 1574 | else: 1575 | dialect = csv.Sniffer().sniff(fp.read(1024)) 1576 | fp.seek(0) 1577 | reader = csv.reader(fp, dialect) 1578 | 1579 | table = PrettyTable(**kwargs) 1580 | if field_names: 1581 | table.field_names = field_names 1582 | else: 1583 | if py3k: 1584 | table.field_names = [x.strip() for x in next(reader)] 1585 | else: 1586 | table.field_names = [x.strip() for x in reader.next()] 1587 | 1588 | for row in reader: 1589 | table.add_row([x.strip() for x in row]) 1590 | 1591 | return table 1592 | 1593 | def from_db_cursor(cursor, **kwargs): 1594 | 1595 | if cursor.description: 1596 | table = PrettyTable(**kwargs) 1597 | table.field_names = [col[0] for col in cursor.description] 1598 | for row in cursor.fetchall(): 1599 | table.add_row(row) 1600 | return table 1601 | 1602 | class TableHandler(HTMLParser): 1603 | 1604 | def __init__(self, **kwargs): 1605 | HTMLParser.__init__(self) 1606 | self.kwargs = kwargs 1607 | self.tables = [] 1608 | self.last_row = [] 1609 | self.rows = [] 1610 | self.max_row_width = 0 1611 | self.active = None 1612 | self.last_content = "" 1613 | self.is_last_row_header = False 1614 | self.colspan = 0 1615 | 1616 | def handle_starttag(self,tag, attrs): 1617 | self.active = tag 1618 | if tag == "th": 1619 | self.is_last_row_header = True 1620 | for (key, value) in attrs: 1621 | if key == "colspan": 1622 | self.colspan = int(value) 1623 | 1624 | 1625 | def handle_endtag(self,tag): 1626 | if tag in ["th", "td"]: 1627 | stripped_content = self.last_content.strip() 1628 | self.last_row.append(stripped_content) 1629 | if self.colspan: 1630 | for i in range(1, self.colspan): 1631 | self.last_row.append("") 1632 | self.colspan = 0 1633 | 1634 | if tag == "tr": 1635 | self.rows.append( 1636 | (self.last_row, self.is_last_row_header)) 1637 | self.max_row_width = max(self.max_row_width, len(self.last_row)) 1638 | self.last_row = [] 1639 | self.is_last_row_header = False 1640 | if tag == "table": 1641 | table = self.generate_table(self.rows) 1642 | self.tables.append(table) 1643 | self.rows = [] 1644 | self.last_content = " " 1645 | self.active = None 1646 | 1647 | 1648 | def handle_data(self, data): 1649 | self.last_content += data 1650 | 1651 | def generate_table(self, rows): 1652 | """ 1653 | Generates from a list of rows a PrettyTable object. 1654 | """ 1655 | table = PrettyTable(**self.kwargs) 1656 | for row in self.rows: 1657 | if len(row[0]) < self.max_row_width: 1658 | appends = self.max_row_width - len(row[0]) 1659 | for i in range(1,appends): 1660 | row[0].append("-") 1661 | 1662 | if row[1] == True: 1663 | self.make_fields_unique(row[0]) 1664 | table.field_names = row[0] 1665 | else: 1666 | table.add_row(row[0]) 1667 | return table 1668 | 1669 | def make_fields_unique(self, fields): 1670 | """ 1671 | iterates over the row and make each field unique 1672 | """ 1673 | for i in range(0, len(fields)): 1674 | for j in range(i+1, len(fields)): 1675 | if fields[i] == fields[j]: 1676 | fields[j] += "'" 1677 | 1678 | def from_html(html_code, **kwargs): 1679 | """ 1680 | Generates a list of PrettyTables from a string of HTML code. Each in 1681 | the HTML becomes one PrettyTable object. 1682 | """ 1683 | 1684 | parser = TableHandler(**kwargs) 1685 | parser.feed(html_code) 1686 | return parser.tables 1687 | 1688 | def from_html_one(html_code, **kwargs): 1689 | """ 1690 | Generates a PrettyTables from a string of HTML code which contains only a 1691 | single
1692 | """ 1693 | 1694 | tables = from_html(html_code, **kwargs) 1695 | try: 1696 | assert len(tables) == 1 1697 | except AssertionError: 1698 | raise Exception("More than one
in provided HTML code! Use from_html instead.") 1699 | return tables[0] 1700 | 1701 | ############################## 1702 | # MAIN (TEST FUNCTION) # 1703 | ############################## 1704 | 1705 | def main(): 1706 | 1707 | print("Generated using setters:") 1708 | x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"]) 1709 | x.title = "Australian capital cities" 1710 | x.sortby = "Population" 1711 | x.reversesort = True 1712 | x.int_format["Area"] = "04" 1713 | x.float_format = "6.1" 1714 | x.align["City name"] = "l" # Left align city names 1715 | x.add_row(["Adelaide", 1295, 1158259, 600.5]) 1716 | x.add_row(["Brisbane", 5905, 1857594, 1146.4]) 1717 | x.add_row(["Darwin", 112, 120900, 1714.7]) 1718 | x.add_row(["Hobart", 1357, 205556, 619.5]) 1719 | x.add_row(["Sydney", 2058, 4336374, 1214.8]) 1720 | x.add_row(["Melbourne", 1566, 3806092, 646.9]) 1721 | x.add_row(["Perth", 5386, 1554769, 869.4]) 1722 | print(x) 1723 | 1724 | print 1725 | 1726 | print("Generated using constructor arguments:") 1727 | 1728 | y = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"], 1729 | title = "Australian capital cities", 1730 | sortby = "Population", 1731 | reversesort = True, 1732 | int_format = "04", 1733 | float_format = "6.1", 1734 | max_width = 12, 1735 | min_width = 4, 1736 | align = "c", 1737 | valign = "t") 1738 | y.align["City name"] = "l" # Left align city names 1739 | y.add_row(["Adelaide", 1295, 1158259, 600.5]) 1740 | y.add_row(["Brisbane", 5905, 1857594, 1146.4]) 1741 | y.add_row(["Darwin", 112, 120900, 1714.7]) 1742 | y.add_row(["Hobart", 1357, 205556, 619.5]) 1743 | y.add_row(["Sydney", 2058, 4336374, 1214.8]) 1744 | y.add_row(["Melbourne", 1566, 3806092, 646.9]) 1745 | y.add_row(["Perth", 5386, 1554769, 869.4]) 1746 | print(y) 1747 | 1748 | if __name__ == "__main__": 1749 | main() 1750 | --------------------------------------------------------------------------------