├── .gitignore ├── Makefile ├── README.md ├── README_current.json ├── book.json ├── book_current.json ├── node_modules └── src ├── README.md ├── SUMMARY.md ├── appendix ├── README.md └── reference.md ├── assets ├── favicon.ico └── img │ ├── anheijuexing_announcement_popop.jpg │ ├── anheijuexing_first_charge.jpg │ ├── audio_file_mp3_mediainfo.png │ ├── audio_file_mp3_two_channel.png │ ├── enum_comma_str_to_tuple.png │ ├── ffmpeg_extract_audio_segment.png │ ├── mac_python_mpv_play_audio.png │ ├── png_as_wrong_audio.png │ ├── resp_audio_isterminated_4.png │ ├── single_image_add_multiple_rect.png │ └── single_image_add_single_rect.png ├── background └── README.md ├── common_code ├── README.md ├── char_string.md ├── common_logic.md ├── csv_excel.md ├── date_time.md ├── file_system │ ├── README.md │ ├── file.md │ └── folder.md ├── mail.md ├── math.md ├── multimedia.md ├── multimedia │ ├── README.md │ ├── audio.md │ ├── image │ │ ├── README.md │ │ ├── baidu_ocr.md │ │ └── pillow.md │ └── video.md ├── network_related │ ├── README.md │ ├── beautifulSoup.md │ └── requests.md ├── system.md └── variable.md └── common_syntax ├── README.md ├── collections.md ├── dict.md ├── enum.md ├── function_parameter.md ├── list_set.md ├── logging.md └── sort.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | output/ 3 | debug/ 4 | 5 | *.zip 6 | 7 | .DS_Store 8 | 9 | !src/**/output -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include ../../common/gitbook_makefile.mk -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python常用代码段 2 | 3 | * 最新版本:`v1.5` 4 | * 更新时间:`20210817` 5 | 6 | ## 简介 7 | 8 | 整理出crifan总结的Python各个方面常用的代码段,供需要的参考。包括通用逻辑、变量、系统、日期时间、字符和字符串、文件系统,比如文件和文件夹等、以及第三方库,比如BeautifulSoup、以及多媒体音视频类、包括Pillow、以及网络的Requests等,;以及其他常见语法,包括dict字典、list列表、set集合、enum枚举、collections集合等。 9 | 10 | ## 源码+浏览+下载 11 | 12 | 本书的各种源码、在线浏览地址、多种格式文件下载如下: 13 | 14 | ### Gitbook源码 15 | 16 | * [crifan/python_common_code_snippet: Python常用代码段](https://github.com/crifan/python_common_code_snippet) 17 | 18 | #### 如何使用此Gitbook源码去生成发布为电子书 19 | 20 | 详见:[crifan/gitbook_template: demo how to use crifan gitbook template and demo](https://github.com/crifan/gitbook_template) 21 | 22 | ### 在线浏览 23 | 24 | * [Python常用代码段 book.crifan.com](https://book.crifan.com/books/python_common_code_snippet/website) 25 | * [Python常用代码段 crifan.github.io](https://crifan.github.io/python_common_code_snippet/website) 26 | 27 | ### 离线下载阅读 28 | 29 | * [Python常用代码段 PDF](https://book.crifan.com/books/python_common_code_snippet/pdf/python_common_code_snippet.pdf) 30 | * [Python常用代码段 ePub](https://book.crifan.com/books/python_common_code_snippet/epub/python_common_code_snippet.epub) 31 | * [Python常用代码段 Mobi](https://book.crifan.com/books/python_common_code_snippet/mobi/python_common_code_snippet.mobi) 32 | 33 | ## 版权说明 34 | 35 | 此电子书教程的全部内容,如无特别说明,均为本人原创和整理。其中部分内容参考自网络,均已备注了出处。如有发现侵犯您版权,请通过邮箱联系我 `admin 艾特 crifan.com`,我会尽快删除。谢谢合作。 36 | 37 | ## 鸣谢 38 | 39 | 感谢我的老婆**陈雪**的包容理解和悉心照料,才使得我`crifan`有更多精力去专注技术专研和整理归纳出这些电子书和技术教程,特此鸣谢。 40 | 41 | ## 更多其他电子书 42 | 43 | 本人`crifan`还写了其他`100+`本电子书教程,感兴趣可移步至: 44 | 45 | [crifan/crifan_ebook_readme: Crifan的电子书的使用说明](https://github.com/crifan/crifan_ebook_readme) 46 | -------------------------------------------------------------------------------- /README_current.json: -------------------------------------------------------------------------------- 1 | { 2 | "latestVersion": "v1.5", 3 | "lastUpdate": "20210817", 4 | "gitRepoName": "python_common_code_snippet", 5 | "bookName": "Python常用代码段", 6 | "bookDescription": "整理出crifan总结的Python各个方面常用的代码段,供需要的参考。包括通用逻辑、变量、系统、日期时间、字符和字符串、文件系统,比如文件和文件夹等、以及第三方库,比如BeautifulSoup、以及多媒体音视频类、包括Pillow、以及网络的Requests等,;以及其他常见语法,包括dict字典、list列表、set集合、enum枚举、collections集合等。" 7 | } -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Python常用代码段", 3 | "description": "整理出crifan总结的Python各个方面常用的代码段,供需要的参考。包括通用逻辑、变量、系统、日期时间、字符和字符串、文件系统,比如文件和文件夹等、以及第三方库,比如BeautifulSoup、以及多媒体音视频类、包括Pillow、以及网络的Requests等,;以及其他常见语法,包括dict字典、list列表、set集合、enum枚举、collections集合等。", 4 | "pluginsConfig": { 5 | "toolbar-button": { 6 | "url": "https://book.crifan.com/books/python_common_code_snippet/pdf/python_common_code_snippet.pdf", 7 | "icon": "fa-file-pdf-o", 8 | "label": "下载PDF" 9 | }, 10 | "sitemap-general": { 11 | "prefix": "https://book.crifan.com/books/python_common_code_snippet/website/" 12 | }, 13 | "github-buttons": { 14 | "buttons": [ 15 | { 16 | "repo": "python_common_code_snippet", 17 | "user": "crifan", 18 | "type": "star", 19 | "count": true, 20 | "size": "small" 21 | }, 22 | { 23 | "user": "crifan", 24 | "type": "follow", 25 | "width": "120", 26 | "count": false, 27 | "size": "small" 28 | } 29 | ] 30 | }, 31 | "google-adsense": { 32 | "ads": [ 33 | { 34 | "client": "ca-pub-6626240105039250" 35 | } 36 | ] 37 | }, 38 | "callouts": { 39 | "showTypeInHeader": false 40 | }, 41 | "theme-default": { 42 | "showLevel": true 43 | }, 44 | "disqus": { 45 | "shortName": "crifan" 46 | }, 47 | "prism": { 48 | "css": [ 49 | "prism-themes/themes/prism-atom-dark.css" 50 | ] 51 | }, 52 | "sharing": { 53 | "douban": false, 54 | "facebook": true, 55 | "google": false, 56 | "hatenaBookmark": false, 57 | "instapaper": false, 58 | "line": false, 59 | "linkedin": false, 60 | "messenger": false, 61 | "pocket": false, 62 | "qq": true, 63 | "qzone": false, 64 | "stumbleupon": false, 65 | "twitter": true, 66 | "viber": false, 67 | "vk": false, 68 | "weibo": true, 69 | "whatsapp": false, 70 | "all": [ 71 | "douban", 72 | "facebook", 73 | "google", 74 | "instapaper", 75 | "line", 76 | "linkedin", 77 | "messenger", 78 | "pocket", 79 | "qq", 80 | "qzone", 81 | "stumbleupon", 82 | "twitter", 83 | "viber", 84 | "vk", 85 | "weibo", 86 | "whatsapp" 87 | ] 88 | }, 89 | "tbfed-pagefooter": { 90 | "copyright": "crifan.com,使用署名4.0国际(CC BY 4.0)协议发布", 91 | "modify_label": "最后更新:", 92 | "modify_format": "YYYY-MM-DD HH:mm:ss" 93 | }, 94 | "ga": { 95 | "token": "UA-28297199-1" 96 | }, 97 | "donate": { 98 | "wechat": "https://www.crifan.com/files/res/crifan_com/crifan_wechat_pay.jpg", 99 | "alipay": "https://www.crifan.com/files/res/crifan_com/crifan_alipay_pay.jpg", 100 | "title": "", 101 | "button": "打赏", 102 | "alipayText": "支付宝打赏给Crifan", 103 | "wechatText": "微信打赏给Crifan" 104 | } 105 | }, 106 | "author": "Crifan Li ", 107 | "language": "zh-hans", 108 | "gitbook": "3.2.3", 109 | "root": "./src", 110 | "links": { 111 | "sidebar": { 112 | "主页": "http://www.crifan.com" 113 | } 114 | }, 115 | "plugins": [ 116 | "google-adsense", 117 | "theme-comscore", 118 | "anchors", 119 | "-lunr", 120 | "-search", 121 | "search-plus", 122 | "disqus", 123 | "-highlight", 124 | "prism", 125 | "prism-themes", 126 | "github-buttons", 127 | "splitter", 128 | "-sharing", 129 | "sharing-plus", 130 | "tbfed-pagefooter", 131 | "expandable-chapters-small", 132 | "ga", 133 | "donate", 134 | "sitemap-general", 135 | "copy-code-button", 136 | "callouts", 137 | "toolbar-button" 138 | ] 139 | } -------------------------------------------------------------------------------- /book_current.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Python常用代码段", 3 | "description": "整理出crifan总结的Python各个方面常用的代码段,供需要的参考。包括通用逻辑、变量、系统、日期时间、字符和字符串、文件系统,比如文件和文件夹等、以及第三方库,比如BeautifulSoup、以及多媒体音视频类、包括Pillow、以及网络的Requests等,;以及其他常见语法,包括dict字典、list列表、set集合、enum枚举、collections集合等。", 4 | "pluginsConfig": { 5 | "toolbar-button": { 6 | "url": "https://book.crifan.com/books/python_common_code_snippet/pdf/python_common_code_snippet.pdf" 7 | }, 8 | "sitemap-general": { 9 | "prefix": "https://book.crifan.com/books/python_common_code_snippet/website/" 10 | }, 11 | "github-buttons": { 12 | "buttons": [ 13 | { 14 | "repo": "python_common_code_snippet" 15 | } 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /node_modules: -------------------------------------------------------------------------------- 1 | ../../generated/gitbook/node_modules -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Python常用代码段 2 | 3 | * 最新版本:`v1.5` 4 | * 更新时间:`20210817` 5 | 6 | ## 简介 7 | 8 | 整理出crifan总结的Python各个方面常用的代码段,供需要的参考。包括通用逻辑、变量、系统、日期时间、字符和字符串、文件系统,比如文件和文件夹等、以及第三方库,比如BeautifulSoup、以及多媒体音视频类、包括Pillow、以及网络的Requests等,;以及其他常见语法,包括dict字典、list列表、set集合、enum枚举、collections集合等。 9 | 10 | ## 源码+浏览+下载 11 | 12 | 本书的各种源码、在线浏览地址、多种格式文件下载如下: 13 | 14 | ### Gitbook源码 15 | 16 | * [crifan/python_common_code_snippet: Python常用代码段](https://github.com/crifan/python_common_code_snippet) 17 | 18 | #### 如何使用此Gitbook源码去生成发布为电子书 19 | 20 | 详见:[crifan/gitbook_template: demo how to use crifan gitbook template and demo](https://github.com/crifan/gitbook_template) 21 | 22 | ### 在线浏览 23 | 24 | * [Python常用代码段 book.crifan.com](https://book.crifan.com/books/python_common_code_snippet/website) 25 | * [Python常用代码段 crifan.github.io](https://crifan.github.io/python_common_code_snippet/website) 26 | 27 | ### 离线下载阅读 28 | 29 | * [Python常用代码段 PDF](https://book.crifan.com/books/python_common_code_snippet/pdf/python_common_code_snippet.pdf) 30 | * [Python常用代码段 ePub](https://book.crifan.com/books/python_common_code_snippet/epub/python_common_code_snippet.epub) 31 | * [Python常用代码段 Mobi](https://book.crifan.com/books/python_common_code_snippet/mobi/python_common_code_snippet.mobi) 32 | 33 | ## 版权说明 34 | 35 | 此电子书教程的全部内容,如无特别说明,均为本人原创和整理。其中部分内容参考自网络,均已备注了出处。如有发现侵犯您版权,请通过邮箱联系我 `admin 艾特 crifan.com`,我会尽快删除。谢谢合作。 36 | 37 | ## 鸣谢 38 | 39 | 感谢我的老婆**陈雪**的包容理解和悉心照料,才使得我`crifan`有更多精力去专注技术专研和整理归纳出这些电子书和技术教程,特此鸣谢。 40 | 41 | ## 更多其他电子书 42 | 43 | 本人`crifan`还写了其他`100+`本电子书教程,感兴趣可移步至: 44 | 45 | [crifan/crifan_ebook_readme: Crifan的电子书的使用说明](https://github.com/crifan/crifan_ebook_readme) 46 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Python常用代码段 2 | 3 | * [前言](README.md) 4 | * [背景](background/README.md) 5 | * [常用代码段](common_code/README.md) 6 | * [通用逻辑](common_code/common_logic.md) 7 | * [变量](common_code/variable.md) 8 | * [系统](common_code/system.md) 9 | * [日期时间](common_code/date_time.md) 10 | * [字符和字符串](common_code/char_string.md) 11 | * [文件系统](common_code/file_system/README.md) 12 | * [文件](common_code/file_system/file.md) 13 | * [文件夹](common_code/file_system/folder.md) 14 | * [多媒体](common_code/multimedia/README.md) 15 | * [图像](common_code/multimedia/image/README.md) 16 | * [Pillow](common_code/multimedia/image/pillow.md) 17 | * [百度OCR](common_code/multimedia/image/baidu_ocr.md) 18 | * [音频](common_code/multimedia/audio.md) 19 | * [视频](common_code/multimedia/video.md) 20 | * [网络相关](common_code/network_related/README.md) 21 | * [BeautifulSoup](common_code/network_related/beautifulSoup.md) 22 | * [requests](common_code/network_related/requests.md) 23 | * [数学](common_code/math.md) 24 | * [邮件](common_code/mail.md) 25 | * [csv和Excel](common_code/csv_excel.md) 26 | * [常见语法](common_syntax/README.md) 27 | * [函数参数](common_syntax/function_parameter.md) 28 | * [dict字典](common_syntax/dict.md) 29 | * [list列表和set集合](common_syntax/list_set.md) 30 | * [sort排序](common_syntax/sort.md) 31 | * [enum枚举](common_syntax/enum.md) 32 | * [collections集合](common_syntax/collections.md) 33 | * [logging日志](common_syntax/logging.md) 34 | * [附录](appendix/README.md) 35 | * [参考资料](appendix/reference.md) 36 | -------------------------------------------------------------------------------- /src/appendix/README.md: -------------------------------------------------------------------------------- 1 | # 附录 2 | 3 | 下面列出相关参考资料。 4 | -------------------------------------------------------------------------------- /src/appendix/reference.md: -------------------------------------------------------------------------------- 1 | # 参考资料 2 | 3 | * 【已解决】AppStore自动安装iOS的app:逻辑优化加等待和多试几次 4 | * 【已解决】Python中如何实现函数调用时多个可变数量的参数传递 5 | * 【已解决】Python中如何合并2个dict字典变量的值 6 | * 【已解决】Python中给Mac中文件加上可执行权限 7 | * 【已解决】Python如何从二进制数据中生成Pillow的Image 8 | * 【已解决】Python的Pillow如何从二进制数据中读取图像数据 9 | * 【已解决】Python的requests中如何下载二进制数据保存为图片文件 10 | * 【已解决】Python中用Pillow去缩小分辨率以及保持画质同时最大程度压缩图片 11 | * 【已解决】Python中实现二进制数据的图片的压缩 12 | * 【已解决】Python中如何解析mp3等音频文件得到时长信息 13 | * 【已解决】用Python代码从视频中提取出音频mp3文件 14 | * 【已解决】Python 3中通过二进制生成文件类型对象 15 | * 【已解决】用ffmpeg从mp4视频中提取出整个mp3以及根据时间段去分割mp3 16 | * 【已解决】python中从文件名后缀推断出MIME类型 17 | * 【已解决】Python中从int值生成Enum枚举和获取枚举值的字符串或名字 18 | * 【未解决】python的wda中调整appium的settings参数实现功能优化 19 | * 【已解决】Python中实现类似touch创建一个空文件 20 | * 【已解决】Python中根据key去对字典排序 21 | * 【已解决】Python 3中如何把字符串str转换成字节码bytes 22 | * 【已解决】Python的md5运行出错:发生异常AttributeError builtin_function_or_method object has no attribute new 23 | * 【已解决】Python 3中md5报错:Unicode-objects must be encoded before hashing 24 | * 【已解决】Python 3中判断变量类型 25 | * 【已解决】Python中删除字典dict中的键值 26 | * 【已解决】Python中获取文件最后更新时间 27 | * 【已解决】Python中获取OrderedDict中最后一个元素 28 | * [【记录】python中smtp发送gmail邮箱](http://www.crifan.com/python_smtp_send_gmail_email) 29 | * [【已解决】Python中smtp如何发送多个收件人地址且带名字的且可以被格式化](https://www.crifan.com/python_use_smtp_how_send_multiple_receiver_address_with_name_and_can_be_format/) 30 | * [【已解决】python中判断单个或多个单词是否是全部小写或首字母小写](http://www.crifan.com/python_check_single_or_multiple_word_all_low_case_or_first_low_case) 31 | * [【已解决】Python中如何让Enum的字符串输出字段的值而不带类型的前缀 – 在路上](https://www.crifan.com/python_output_enum_value_without_type_prefix/) 32 | * [【已解决】Python中给枚举添加内置函数或属性](http://www.crifan.com/python_add_buildin_function_or_property_for_enum) 33 | * [【基本解决】Python中把wma、wav等格式音频转换为mp3 – 在路上](https://www.crifan.com/python_convert_wma_wav_audio_to_mp3_format/) 34 | * [【已解决】Python中如何格式化大小为人类易读的效果](https://www.crifan.com/python_format_file_size_to_human_readable_effect/) 35 | * [【已解决】Python中获取带毫秒的时间戳](http://www.crifan.com/python_get_timestamp_with_milliseconds) 36 | * [【已解决】Python中实现dict的递归的合并更新](http://www.crifan.com/python_dict_merge_recursively) 37 | * [【已解决】Python中把list换成set](http://www.crifan.com/python_change_list_to_set) 38 | * [【整理】python中一次性创建多级文件夹,判断一个文件夹是否已经存在 – 在路上](https://www.crifan.com/python_check_folder_exist_create_multiple_level_folder_once/) 39 | * [【已解决】Python中如何递归的删除整个非空文件夹 – 在路上](https://www.crifan.com/python_recursively_delete_non_empty_folder/) 40 | * [Python表格处理:CSV和Excel](http://book.crifan.com/books/python_process_csv_excel/website) 41 | * 42 | * [python - Correct way to write line to file? - Stack Overflow](https://stackoverflow.com/questions/6159900/correct-way-to-write-line-to-file) 43 | * [How do you do a simple "chmod +x" from within python? - Stack Overflow](https://stackoverflow.com/questions/12791997/how-do-you-do-a-simple-chmod-x-from-within-python) 44 | * [io.BytesIO.getvalue](https://docs.python.org/3/library/io.html#io.BytesIO.getvalue) 45 | * [排序指南 — Python 3.8.2 文档](https://docs.python.org/zh-cn/3/howto/sorting.html) 46 | * [Python 常用指引 — Python 3.8.2 文档](https://docs.python.org/zh-cn/3/howto/index.html) 47 | * [Built-in Functions — Python 3.8.2 documentation](https://docs.python.org/3/library/functions.html#sorted) 48 | * [operator — Standard operators as functions — Python 3.8.2 documentation](https://docs.python.org/3/library/operator.html) 49 | * [Python 常用指引 — Python 3.8.2 文档](https://docs.python.org/zh-cn/3/howto/index.html) 50 | * [3.8.2 Documentation](https://docs.python.org/zh-cn/3/index.html) 51 | * [编程常见问题 — Python 3.8.2 文档](https://docs.python.org/zh-cn/3/faq/programming.html#what-is-a-class) 52 | * [术语对照表 — Python 3.8.2 文档](https://docs.python.org/zh-cn/3/glossary.html) 53 | * [enum — Support for enumerations — Python 3.8.2 documentation](https://docs.python.org/3/library/enum.html) 54 | * [enum --- 对枚举的支持 — Python 3.9.0a4 文档](https://docs.python.org/zh-cn/3.9/library/enum.html) 55 | * [collections --- 容器数据类型 — Python 3.8.1 文档](https://docs.python.org/zh-cn/3/library/collections.html#collections.UserDict) 56 | * [md5 not support in python 3.6 and django 1.10 - Stack Overflow](https://stackoverflow.com/questions/42829945/md5-not-support-in-python-3-6-and-django-1-10/57984443#57984443) 57 | * [list - Python: how to join entries in a set into one string? - Stack Overflow](https://stackoverflow.com/questions/7323782/python-how-to-join-entries-in-a-set-into-one-string) 58 | * -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/anheijuexing_announcement_popop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/anheijuexing_announcement_popop.jpg -------------------------------------------------------------------------------- /src/assets/img/anheijuexing_first_charge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/anheijuexing_first_charge.jpg -------------------------------------------------------------------------------- /src/assets/img/audio_file_mp3_mediainfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/audio_file_mp3_mediainfo.png -------------------------------------------------------------------------------- /src/assets/img/audio_file_mp3_two_channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/audio_file_mp3_two_channel.png -------------------------------------------------------------------------------- /src/assets/img/enum_comma_str_to_tuple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/enum_comma_str_to_tuple.png -------------------------------------------------------------------------------- /src/assets/img/ffmpeg_extract_audio_segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/ffmpeg_extract_audio_segment.png -------------------------------------------------------------------------------- /src/assets/img/mac_python_mpv_play_audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/mac_python_mpv_play_audio.png -------------------------------------------------------------------------------- /src/assets/img/png_as_wrong_audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/png_as_wrong_audio.png -------------------------------------------------------------------------------- /src/assets/img/resp_audio_isterminated_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/resp_audio_isterminated_4.png -------------------------------------------------------------------------------- /src/assets/img/single_image_add_multiple_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/single_image_add_multiple_rect.png -------------------------------------------------------------------------------- /src/assets/img/single_image_add_single_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crifan/python_common_code_snippet/7cf7f3e99e98453b48929fc501469942222627e7/src/assets/img/single_image_add_single_rect.png -------------------------------------------------------------------------------- /src/background/README.md: -------------------------------------------------------------------------------- 1 | # 背景 2 | 3 | 多年的技术开发,积累了很多关于 [Python](https://book.crifan.com/books/make_life_better_python/website/) 的常用的一些函数和功能,都已整理到对应的函数库中: 4 | 5 | https://github.com/crifan/crifanLibPython 6 | 7 | 且也给出了很多函数的demo演示如何使用: 8 | 9 | https://github.com/crifan/crifanLibPython/tree/master/crifanLib/demo 10 | 11 | 但还是解释的不够清楚和全面。 12 | 13 | 故此处专门把Python的常用的代码段和调用举例,都整理至此,详细解释。 14 | 15 | 目的: 16 | 17 | 方便需要时能**快速查阅**:直接看代码,一看就懂,无需额外解释。 18 | -------------------------------------------------------------------------------- /src/common_code/README.md: -------------------------------------------------------------------------------- 1 | # 常用代码段 2 | 3 | 下面总结各个方面的Python常用代码段。 -------------------------------------------------------------------------------- /src/common_code/char_string.md: -------------------------------------------------------------------------------- 1 | # 字符和字符串 2 | 3 | 详见: 4 | 5 | https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanFile.py 6 | 7 | --- 8 | 9 | ## Python3 str转bytes 10 | 11 | Python 3中,把字符串转换成字节码,可以有两种写法: 12 | 13 | * 方法1:用bytes去转换 14 | ```python 15 | convertedBytes = bytes(originStr) 16 | ``` 17 | * 方法2:用字符串str的编码encode 18 | ```python 19 | encodedBytes = originStr.encode() 20 | ``` 21 | 22 | 其中: 23 | 24 | 都有额外的encoding参数: 25 | 26 | * 由于默认都是UTF-8 27 | * 所以可加可不加 28 | * 也可以根据需要,去加其他你要的编码 29 | * 如果要加,就是: 30 | ```python 31 | convertedBytes = bytes(originStr, "UTF-8") 32 | encodedBytes = originStr.encode("UTF-8") 33 | ``` 34 | 35 | ## 字符串格式化为人类易读格式 36 | 37 | ```python 38 | 39 | def formatSize(sizeInBytes, decimalNum=1, isUnitWithI=False, sizeUnitSeperator=""): 40 | """ 41 | format size to human readable string 42 | 43 | example: 44 | 3746 -> 3.7KB 45 | 87533 -> 85.5KiB 46 | 98654 -> 96.3 KB 47 | 352 -> 352.0B 48 | 76383285 -> 72.84MB 49 | 763832854988542 -> 694.70TB 50 | 763832854988542665 -> 678.4199PB 51 | 52 | refer: 53 | https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size 54 | """ 55 | # https://en.wikipedia.org/wiki/Binary_prefix#Specific_units_of_IEC_60027-2_A.2_and_ISO.2FIEC_80000 56 | # K=kilo, M=mega, G=giga, T=tera, P=peta, E=exa, Z=zetta, Y=yotta 57 | sizeUnitList = ['','K','M','G','T','P','E','Z'] 58 | largestUnit = 'Y' 59 | 60 | if isUnitWithI: 61 | sizeUnitListWithI = [] 62 | for curIdx, eachUnit in enumerate(sizeUnitList): 63 | unitWithI = eachUnit 64 | if curIdx >= 1: 65 | unitWithI += 'i' 66 | sizeUnitListWithI.append(unitWithI) 67 | 68 | # sizeUnitListWithI = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi'] 69 | sizeUnitList = sizeUnitListWithI 70 | 71 | largestUnit += 'i' 72 | 73 | suffix = "B" 74 | decimalFormat = "." + str(decimalNum) + "f" # ".1f" 75 | finalFormat = "%" + decimalFormat + sizeUnitSeperator + "%s%s" # "%.1f%s%s" 76 | sizeNum = sizeInBytes 77 | for sizeUnit in sizeUnitList: 78 | if abs(sizeNum) < 1024.0: 79 | return finalFormat % (sizeNum, sizeUnit, suffix) 80 | sizeNum /= 1024.0 81 | return finalFormat % (sizeNum, largestUnit, suffix) 82 | ``` 83 | 84 | 调用: 85 | 86 | ```python 87 | 88 | def testKb(): 89 | kbSize = 3746 90 | kbStr = formatSize(kbSize) 91 | print("%s -> %s" % (kbSize, kbStr)) 92 | 93 | def testI(): 94 | iSize = 87533 95 | iStr = formatSize(iSize, isUnitWithI=True) 96 | print("%s -> %s" % (iSize, iStr)) 97 | 98 | def testSeparator(): 99 | seperatorSize = 98654 100 | seperatorStr = formatSize(seperatorSize, sizeUnitSeperator=" ") 101 | print("%s -> %s" % (seperatorSize, seperatorStr)) 102 | 103 | def testBytes(): 104 | bytesSize = 352 105 | bytesStr = formatSize(bytesSize) 106 | print("%s -> %s" % (bytesSize, bytesStr)) 107 | 108 | def testMb(): 109 | mbSize = 76383285 110 | mbStr = formatSize(mbSize, decimalNum=2) 111 | print("%s -> %s" % (mbSize, mbStr)) 112 | 113 | def testTb(): 114 | tbSize = 763832854988542 115 | tbStr = formatSize(tbSize, decimalNum=2) 116 | print("%s -> %s" % (tbSize, tbStr)) 117 | 118 | def testPb(): 119 | pbSize = 763832854988542665 120 | pbStr = formatSize(pbSize, decimalNum=4) 121 | print("%s -> %s" % (pbSize, pbStr)) 122 | 123 | def demoFormatSize(): 124 | testKb() 125 | testI() 126 | testSeparator() 127 | testBytes() 128 | testMb() 129 | testTb() 130 | testPb() 131 | ``` 132 | 133 | -------------------------------------------------------------------------------- /src/common_code/common_logic.md: -------------------------------------------------------------------------------- 1 | # 通用逻辑 2 | 3 | ## 多次运行一个函数,直到成功运行 4 | 5 | 执行一个函数(可能有多个可变数量的参数),且尝试多次,直到成功或超出最大此时,最终实现是: 6 | 7 | ```python 8 | def multipleRetry(functionInfoDict, maxRetryNum=5, sleepInterval=0.1, isShowErrWhenFail=True): 9 | """ 10 | do something, retry mutiple time if fail 11 | 12 | Args: 13 | functionInfoDict (dict): function info dict contain functionCallback and [optional] functionParaDict 14 | maxRetryNum (int): max retry number 15 | sleepInterval (float): sleep time of each interval when fail 16 | isShowErrWhenFail (bool): show error when fail if true 17 | Returns: 18 | bool 19 | Raises: 20 | """ 21 | doSuccess = False 22 | functionCallback = functionInfoDict["functionCallback"] 23 | functionParaDict = functionInfoDict.get("functionParaDict", None) 24 | 25 | curRetryNum = maxRetryNum 26 | while curRetryNum > 0: 27 | if functionParaDict: 28 | doSuccess = functionCallback(**functionParaDict) 29 | else: 30 | doSuccess = functionCallback() 31 | 32 | if doSuccess: 33 | break 34 | 35 | time.sleep(sleepInterval) 36 | curRetryNum -= 1 37 | 38 | if not doSuccess: 39 | if isShowErrWhenFail: 40 | functionName = str(functionCallback) 41 | # '>' 42 | logging.error("Still fail after %d retry for %s", maxRetryNum, functionName) 43 | return doSuccess 44 | ``` 45 | 46 | 说明: 47 | 48 | `functionCallback`函数类型都要符合:返回值是bool类型才可以 49 | 50 | 调用举例: 51 | 52 | (1)没有额外参数 53 | 54 | ```python 55 | foundAndClickedWifi = CommonUtils.multipleRetry({"functionCallback": self.iOSFromSettingsIntoWifiList}) 56 | ``` 57 | 58 | 其中: 59 | 60 | ```python 61 | def iOSFromSettingsIntoWifiList(self): 62 | 。。。 63 | foundAndClickedWifi = self.findAndClickElement(query=wifiTextQuery, timeout=0.1) 64 | return foundAndClickedWifi 65 | ``` 66 | 67 | 类似例子: 68 | 69 | ```python 70 | isSwitchOk = self.multipleRetry({"functionCallback": self.switchToAppStoreSearchTab}) 71 | ``` 72 | 73 | 对比之前原始写法: 74 | 75 | ```python 76 | isSwitchOk = self.switchToAppStoreSearchTab() 77 | ``` 78 | 79 | 其他类似例子: 80 | 81 | ```python 82 | foundAndClickedDownload = self.multipleRetry({"functionCallback": self.appStoreStartDownload}) 83 | ``` 84 | 85 | 详见: 86 | 87 | 【已解决】AppStore自动安装iOS的app:逻辑优化加等待和多试几次 88 | 89 | (2)有额外参数,参数个数:2个 90 | 91 | ```python 92 | searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"} 93 | isInputOk = CommonUtils.multipleRetry( 94 | { 95 | "functionCallback": self.wait_element_setText, 96 | "functionParaDict": { 97 | "locator": searchInputQuery, 98 | "text": appName, 99 | } 100 | } 101 | ) 102 | ``` 103 | 104 | 对比之前原始写法: 105 | 106 | ```python 107 | searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"} 108 | isInputOk = self.wait_element_setText(searchInputQuery, appName) 109 | ``` 110 | 111 | 其中wait_element_setText的定义是: 112 | 113 | ```python 114 | def wait_element_setText(self, locator, text): 115 | ``` 116 | 117 | 对应着之前传入时的: 118 | 119 | ```python 120 | "functionParaDict": { 121 | "locator": searchInputQuery, 122 | "text": appName, 123 | } 124 | ``` 125 | 126 | (3)有额外参数,且加上multipleRetry的额外参数 127 | 128 | ```python 129 | isSwitchOk = CommonUtils.multipleRetry( 130 | {"functionCallback": self.switchToAppStoreSearchTab}, 131 | maxRetryNum = 10, 132 | sleepInterval = 0.5, 133 | ) 134 | ``` 135 | 136 | 以及类似的: 137 | 138 | ```python 139 | isIntoDetailOk = self.multipleRetry( 140 | { 141 | "functionCallback": self.appStoreSearchResultIntoDetail, 142 | "functionParaDict": { 143 | "appName": appName, 144 | } 145 | }, 146 | sleepInterval=0.5 147 | ) 148 | ``` 149 | 150 | 之前原始写法: 151 | 152 | ```python 153 | isIntoDetailOk = self.appStoreSearchResultIntoDetail(appName) 154 | ``` 155 | 156 | 注: 157 | 158 | 此处是后来加上的 159 | 160 | `sleepInterval=0.5` 161 | 162 | 是因为后来遇到了,即使尝试了5次,依旧没找到,所以增加了没找到的延迟等待时间。 163 | 164 | 详见: 165 | 166 | 【已解决】AppStore自动安装iOS的app:逻辑优化加等待和多试几次 167 | 168 | (4) 169 | 170 | ```python 171 | isIntoDetailOk = CommonUtils.multipleRetry( 172 | { 173 | "functionCallback": self.appStoreSearchResultIntoDetail, 174 | "functionParaDict": { 175 | "appName": appName, 176 | } 177 | }, 178 | maxRetryNum = 10, 179 | sleepInterval = 0.5, 180 | ) 181 | ``` 182 | 183 | ### 新版:新增参数isRespFullRetValue 184 | 185 | 此处最后更新:`20200925` 186 | 187 | 后续多次优化新增参数:是否返回完整信息 188 | 189 | 代码: 190 | 191 | ```python 192 | def multipleRetry(functionInfoDict, maxRetryNum=5, sleepInterval=0.1, isShowErrWhenFail=True, isRespFullRetValue=False): 193 | """do something, retry if single call failed, retry mutiple time until max retry number 194 | 195 | Args: 196 | functionInfoDict (dict): function info dict contain functionCallback and [optional] functionParaDict 197 | maxRetryNum (int): max retry number 198 | sleepInterval (float): sleep time (seconds) of each interval when fail 199 | isShowErrWhenFail (bool): show error when fail if true 200 | isRespFullRetValue (bool): whether return full return value of function call 201 | Returns: 202 | isRespFullRetValue=False: bool 203 | isRespFullRetValue=True: bool / tuple/list/... 204 | Raises: 205 | """ 206 | finalReturnValue = None 207 | doSuccess = False 208 | functionCallback = functionInfoDict["functionCallback"] 209 | functionParaDict = functionInfoDict.get("functionParaDict", None) 210 | 211 | curRetryNum = maxRetryNum 212 | while curRetryNum > 0: 213 | if functionParaDict: 214 | # doSuccess = functionCallback(**functionParaDict) 215 | respValue = functionCallback(**functionParaDict) 216 | else: 217 | # doSuccess = functionCallback() 218 | respValue = functionCallback() 219 | 220 | doSuccess = False 221 | if isinstance(respValue, bool): 222 | doSuccess = bool(respValue) 223 | elif isinstance(respValue, tuple): 224 | doSuccess = bool(respValue[0]) 225 | elif isinstance(respValue, list): 226 | doSuccess = bool(respValue[0]) 227 | else: 228 | Exception("multipleRetry: Not support type of return value: %s" % respValue) 229 | 230 | if isRespFullRetValue: 231 | finalReturnValue = respValue 232 | else: 233 | finalReturnValue = doSuccess 234 | 235 | if doSuccess: 236 | break 237 | 238 | time.sleep(sleepInterval) 239 | curRetryNum -= 1 240 | 241 | if not doSuccess: 242 | if isShowErrWhenFail: 243 | functionName = str(functionCallback) 244 | # '>' 245 | logging.error("Still fail after %d retry for %s", maxRetryNum, functionName) 246 | # return doSuccess 247 | return finalReturnValue 248 | ``` 249 | 250 | 调用举例: 251 | 252 | (1)默认不返回完整信息,只返回bool值 253 | 254 | ```python 255 | respBoolOrTuple = CommonUtils.multipleRetry( 256 | functionInfoDict = { 257 | "functionCallback": self.isGotoPayPopupPage, 258 | "functionParaDict": { 259 | "isRespLocation": False, 260 | }, 261 | }, 262 | ) 263 | ``` 264 | 265 | (2)返回完整信息 266 | 267 | ```python 268 | respBoolOrTuple = CommonUtils.multipleRetry( 269 | functionInfoDict = { 270 | "functionCallback": self.isGotoPayPopupPage, 271 | "functionParaDict": { 272 | "isRespLocation": True, 273 | }, 274 | }, 275 | isRespFullRetValue = True, 276 | ) 277 | ``` 278 | -------------------------------------------------------------------------------- /src/common_code/csv_excel.md: -------------------------------------------------------------------------------- 1 | # csv和Excel 2 | 3 | 用Python操作`csv`和`excel`,详见独立教程: 4 | 5 | [Python表格处理:CSV和Excel](http://book.crifan.com/books/python_process_csv_excel/website) 6 | -------------------------------------------------------------------------------- /src/common_code/date_time.md: -------------------------------------------------------------------------------- 1 | # 日期时间 2 | 3 | 详见: 4 | 5 | https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanDatetime.py 6 | 7 | --- 8 | 9 | ## getCurDatetimeStr 生成当前日期时间字符串 10 | 11 | ```python 12 | def getCurDatetimeStr(outputFormat="%Y%m%d_%H%M%S"): 13 | """ 14 | get current datetime then format to string 15 | 16 | eg: 17 | 20171111_220722 18 | 19 | :param outputFormat: datetime output format 20 | :return: current datetime formatted string 21 | """ 22 | curDatetime = datetime.now() # 2017-11-11 22:07:22.705101 23 | curDatetimeStr = curDatetime.strftime(format=outputFormat) #'20171111_220722' 24 | return curDatetimeStr 25 | ``` 26 | 27 | 调用举例: 28 | 29 | ```python 30 | curDatetimeStr = getCurDatetimeStr() # '20191219_143400' 31 | ``` 32 | 33 | ## datetime转时间戳 34 | 35 | ```python 36 | import time 37 | 38 | def datetimeToTimestamp(self, datetimeVal, withMilliseconds=False) : 39 | """ 40 | convert datetime value to timestamp 41 | eg: 42 | "2006-06-01 00:00:00.123" -> 1149091200 43 | if with milliseconds -> 1149091200123 44 | :param datetimeVal: 45 | :return: 46 | """ 47 | timetupleValue = datetimeVal.timetuple() 48 | timestampFloat = time.mktime(timetupleValue) # 1531468736.0 -> 10 digits 49 | timestamp10DigitInt = int(timestampFloat) # 1531468736 50 | timestampInt = timestamp10DigitInt 51 | 52 | if withMilliseconds: 53 | microsecondInt = datetimeVal.microsecond # 817762 54 | microsecondFloat = float(microsecondInt)/float(1000000) # 0.817762 55 | timestampFloat = timestampFloat + microsecondFloat # 1531468736.817762 56 | timestampFloat = timestampFloat * 1000 # 1531468736817.7621 -> 13 digits 57 | timestamp13DigitInt = int(timestampFloat) # 1531468736817 58 | timestampInt = timestamp13DigitInt 59 | 60 | return timestampInt 61 | ``` 62 | 63 | ## 获取当前时间戳 64 | 65 | ```python 66 | from datetime import datetime 67 | 68 | def getCurTimestamp(withMilliseconds=False): 69 | """ 70 | get current time's timestamp 71 | (default)not milliseconds -> 10 digits: 1351670162 72 | with milliseconds -> 13 digits: 1531464292921 73 | """ 74 | curDatetime = datetime.now() 75 | return datetimeToTimestamp(curDatetime, withMilliseconds) 76 | ``` 77 | 78 | ## 时间戳精确到毫秒 79 | 80 | ```python 81 | from datetime import datetime,timedelta 82 | timestampStr = datetime.now().strftime("%Y%m%d_%H%M%S_%f") 83 | # 20180712_154134_660436 84 | ``` 85 | -------------------------------------------------------------------------------- /src/common_code/file_system/README.md: -------------------------------------------------------------------------------- 1 | # 文件系统 2 | 3 | 最新代码详见: 4 | 5 | * https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanFile.py 6 | * https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/demo/crifanFileDemo.py 7 | -------------------------------------------------------------------------------- /src/common_code/file_system/file.md: -------------------------------------------------------------------------------- 1 | # 文件 2 | 3 | ## 获取文件最后更新时间 4 | 5 | ```python 6 | import os 7 | 8 | def getUpdateTime(curFileOrPath): 9 | """get file/folder latest update time = modify time 10 | 11 | Args: 12 | curFileOrPath (str): some file or folder path 13 | Returns: 14 | int: time stamp int of 13 digit, with milliseconds 15 | Raises: 16 | """ 17 | updateTime = None 18 | try: 19 | modifyTime = os.path.getmtime(curFileOrPath) # 1593748641.3270357 20 | updateTime = int(modifyTime * 1000) # 1593748641327 21 | except OSError as err: 22 | errMsg = str(err) 23 | # print("errMsg=%s" % errMsg) 24 | pass 25 | 26 | return updateTime 27 | ``` 28 | 29 | 详见: 30 | 31 | 【已解决】Python中获取文件最后更新时间 32 | 33 | ## 提取文件名后缀 34 | 35 | ```python 36 | def extractSuffix(fileNameOrUrl): 37 | """ 38 | extract file suffix from name or url 39 | eg: 40 | https://cdn2.qupeiyin.cn/2018-09-10/15365514898246.mp4 -> mp4 41 | 15365514894833.srt -> srt 42 | """ 43 | return fileNameOrUrl.split('.')[-1] 44 | ``` 45 | 46 | ## 创建空文件 47 | 48 | ```python 49 | import os 50 | 51 | def createEmptyFile(fullFilename): 52 | """Create a empty file like touch""" 53 | filePath = os.path.dirname(fullFilename) 54 | # create folder if not exist 55 | if not os.path.exists(filePath): 56 | os.makedirs(filePath) 57 | 58 | with open(fullFilename, 'a'): 59 | # Note: not use 'w' for maybe conflict for others constantly writing to it 60 | os.utime(fullFilename, None) 61 | ``` 62 | 63 | ## 读取文件二进制数据 64 | 65 | ```python 66 | def readBinDataFromFile(fileToRead): 67 | """Read binary data from file""" 68 | binaryData = None 69 | try: 70 | readFp = open(fileToRead, "rb") 71 | binaryData = readFp.read() 72 | readFp.close() 73 | except: 74 | binaryData = None 75 | 76 | 77 | return binaryData 78 | ``` 79 | 80 | 调用: 81 | 82 | ```python 83 | imgBinData = readBinDataFromFile(imageFullPath) 84 | ``` 85 | 86 | ## 保存二进制数据到文件 87 | 88 | ```python 89 | def saveDataToFile(fullFilename, binaryData): 90 | """save binary data info file""" 91 | with open(fullFilename, 'wb') as fp: 92 | fp.write(binaryData) 93 | fp.close() 94 | # print("Complete save file %s" % fullFilename) 95 | ``` 96 | 97 | ## 保存json到文件 98 | 99 | ```python 100 | import json 101 | import codecs 102 | 103 | def saveJsonToFile(fullFilename, jsonValue): 104 | """save json dict into file""" 105 | with codecs.open(fullFilename, 'w', encoding="utf-8") as jsonFp: 106 | json.dump(jsonValue, jsonFp, indent=2, ensure_ascii=False) 107 | # print("Complete save json %s" % fullFilename) 108 | ``` 109 | 110 | ## 从文件中加载出json 111 | 112 | ```python 113 | import json 114 | import codecs 115 | 116 | def loadJsonFromFile(fullFilename): 117 | """load and parse json dict from file""" 118 | with codecs.open(fullFilename, 'r', encoding="utf-8") as jsonFp: 119 | jsonDict = json.load(jsonFp) 120 | # print("Complete load json from %s" % fullFilename) 121 | return jsonDict 122 | ``` 123 | 124 | ## 通过二进制生成文件类型对象 125 | 126 | (1)Python 3 127 | 128 | ```python 129 | import io 130 | 131 | audioBinaryData = audioObj.read() 132 | audioFileLikeObj = io.BytesIO(audioBinaryData) 133 | ``` 134 | 135 | 得到对应的文件类型的对象的:`<_io.BytesIO object at 0x115964468>`,即可去像操作文件一样去操作这个io。 136 | 137 | (2)Python 2 138 | 139 | ```python 140 | import StringIO 141 | 142 | audioFileLikeObj = StringIO.StringIO() 143 | audioFileLikeObj.write(audioBinaryData) 144 | ``` 145 | 146 | ## 一行代码把字符串写入保存到文件 147 | 148 | ```python 149 | open(fullFilePath, "w").write(fileContentStr).close() 150 | ``` 151 | 152 | 举例: 153 | 154 | ```python 155 | open("0408_1600.xml", "w").write(page).close() 156 | ``` 157 | 158 | ## 给文件增加可执行权限:实现`chmod +x`的效果 159 | 160 | 代码: 161 | 162 | ```python 163 | import os 164 | import stat 165 | 166 | curState = os.stat(someFile) 167 | newState = curState.st_mode | stat.S_IEXEC 168 | os.chmod(someFile, newState) 169 | ``` 170 | 171 | 再去优化为函数: 172 | 173 | ```python 174 | import os 175 | import stat 176 | 177 | def chmodAddX(someFile): 178 | """add file executable mode, like chmod +x 179 | 180 | Args: 181 | someFile (str): file full path 182 | Returns: 183 | soup 184 | Raises: 185 | """ 186 | if os.path.exists(someFile): 187 | if os.path.isfile(someFile): 188 | # add executable 189 | curState = os.stat(someFile) 190 | newState = curState.st_mode | stat.S_IEXEC 191 | os.chmod(someFile, newState) 192 | ``` 193 | 194 | 调用: 195 | 196 | ```python 197 | chmodAddX(shellFullPath) 198 | ``` 199 | 200 | 继续优化: 201 | 202 | 参考 [How do you do a simple "chmod +x" from within python? - Stack Overflow](https://stackoverflow.com/questions/12791997/how-do-you-do-a-simple-chmod-x-from-within-python) 203 | 204 | 如果想要加上,给任何人都有可执行权限,则可以用: 205 | 206 | ```python 207 | def chmodAddX(someFile): 208 | """add file executable mode, like chmod +x 209 | 210 | Args: 211 | someFile (str): file full path 212 | Returns: 213 | soup 214 | Raises: 215 | """ 216 | if os.path.exists(someFile): 217 | if os.path.isfile(someFile): 218 | # add executable 219 | curState = os.stat(someFile) 220 | # STAT_OWNER_EXECUTABLE = stat.S_IEXEC 221 | # executableMode = STAT_OWNER_EXECUTABLE 222 | STAT_EVERYONE_EXECUTABLE = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH 223 | executableMode = STAT_EVERYONE_EXECUTABLE 224 | newState = curState.st_mode | executableMode 225 | os.chmod(someFile, newState) 226 | ``` 227 | 228 | 效果: 229 | 230 | * 之前:`-rw-r--r--` 231 | * 之后:`-rwxr-xr-x` 232 | * 给 user group other 都加上`x`的**可执行权限** 233 | 234 | 详见: 235 | 236 | 【已解决】Python中给Mac中文件加上可执行权限 237 | 238 | ## 判断是否是文件对象 239 | 240 | ```python 241 | import sys 242 | 243 | def isFileObject(fileObj): 244 | """"check is file like object or not""" 245 | if sys.version_info[0] == 2: 246 | return isinstance(fileObj, file) 247 | else: 248 | # for python 3: 249 | # has read() method for: 250 | # io.IOBase 251 | # io.BytesIO 252 | # io.StringIO 253 | # io.RawIOBase 254 | return hasattr(fileObj, 'read') 255 | ``` 256 | 257 | ## 计算当前文件名,如果重名,则位数加1 258 | 259 | ```python 260 | import os 261 | import re 262 | 263 | def findNextNumberFilename(curFilename): 264 | """Find the next available filename from current name 265 | 266 | Args: 267 | curFilename (str): current filename 268 | Returns: 269 | next available (not existed) filename 270 | Raises: 271 | Examples: 272 | (1) 'crifanLib/demo/input/image/20201219_172616_drawRect_40x40.jpg' 273 | not exist -> 'crifanLib/demo/input/image/20201219_172616_drawRect_40x40.jpg' 274 | (2) 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40.jpg' 275 | exsit -> next until not exist 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_3.jpg' 276 | """ 277 | newFilename = curFilename 278 | 279 | newPathRootPart, pointSuffix = os.path.splitext(newFilename) 280 | # 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_1' 281 | filenamePrefix = newPathRootPart 282 | while os.path.exists(newFilename): 283 | newTailNumberInt = 1 284 | foundTailNumber = re.search("^(?P.+)_(?P\d+)$", newPathRootPart) 285 | if foundTailNumber: 286 | tailNumberStr = foundTailNumber.group("tailNumber") # '1' 287 | tailNumberInt = int(tailNumberStr) 288 | newTailNumberInt = tailNumberInt + 1 # 2 289 | filenamePrefix = foundTailNumber.group("filenamePrefix") # 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40' 290 | # existed previously saved, change to new name 291 | newPathRootPart = "%s_%s" % (filenamePrefix, newTailNumberInt) 292 | # 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_2' 293 | newFilename = newPathRootPart + pointSuffix 294 | # 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_2.jpg' 295 | 296 | return newFilename 297 | ``` 298 | 299 | 调用: 300 | 301 | ```python 302 | notExistFile = "crifanLib/demo/input/image/some_not_exist_filename.jpg" 303 | nextFilename = findNextNumberFilename(notExistFile) 304 | print("notExistFile=%s -> nextFilename=%s" % (notExistFile, nextFilename)) 305 | # notExistFile=crifanLib/demo/input/image/some_not_exist_filename.jpg -> nextFilename=crifanLib/demo/input/image/some_not_exist_filename.jpg 306 | 307 | realExistFile = "crifanLib/demo/input/image/20191219_172616_drawRect_40x40.jpg" 308 | nextUntilNotExistFilename = findNextNumberFilename(realExistFile) 309 | print("realExistFile=%s -> nextUntilNotExistFilename=%s" % (realExistFile, nextUntilNotExistFilename)) 310 | # realExistFile=crifanLib/demo/input/image/20191219_172616_drawRect_40x40.jpg -> nextUntilNotExistFilename=crifanLib/demo/input/image/20191219_172616_drawRect_40x40_2.jpg 311 | ``` 312 | 313 | ## 从文件名后缀推断出MIME类型 314 | 315 | 用库: 316 | * mime 317 | * GitHub 318 | * https://github.com/liluo/mime 319 | 320 | 安装mime: 321 | 322 | ```bash 323 | pip install mime 324 | ``` 325 | 326 | 代码: 327 | 328 | ```python 329 | import mime 330 | 331 | fileMimeType = mime.Types.of(curAudioFullFilename)[0].content_type 332 | ``` 333 | 334 | * 输入文件:`'Lots of Hearts.mp3'` 335 | * 输出信息:`'audio/mpeg'` 336 | -------------------------------------------------------------------------------- /src/common_code/file_system/folder.md: -------------------------------------------------------------------------------- 1 | # 文件夹=文件路径 2 | 3 | ## 新建文件夹 4 | 5 | 对于:`python 3.2+` 6 | 7 | ```python 8 | import os 9 | 10 | def createFolder(folderFullPath): 11 | """ 12 | create folder, even if already existed 13 | Note: for Python 3.2+ 14 | """ 15 | os.makedirs(folderFullPath, exist_ok=True) 16 | # print("Created folder: %s" % folderFullPath) 17 | ``` 18 | 19 | 或: 20 | 21 | 对于:`python 3.5+` 22 | 23 | ```python 24 | import pathlib 25 | pathlib.Path('/my/directory').mkdir(parents=True, exist_ok=True) 26 | ``` 27 | 28 | ## 批量删除非空文件夹 29 | 30 | ```python 31 | import shutil 32 | 33 | if os.path.exists(folderToDelete): 34 | shutil.rmtree(folderToDelete) 35 | ``` 36 | 37 | 注意: 38 | 39 | * 删除之前要先用`os.path.exists`判断是非存在该目录 40 | * 如果不存在就去删除,则会报错:`OSError: [Errno 2] No such file or directory` 41 | 42 | ## os.path 路径处理 43 | 44 | ```python 45 | #!/usr/bin/python 46 | # -*- coding: utf-8 -*- 47 | # Author: Crifan Li 48 | # Update: 20191219 49 | # Function: Demo os.path common used functions 50 | 51 | import os 52 | 53 | def osPathDemo(): 54 | currentSystemInfo = os.uname() 55 | print("currentSystemInfo=%s" % (currentSystemInfo, )) 56 | # currentSystemInfo=posix.uname_result(sysname='Darwin', nodename='xxx', release='18.7.0', version='Darwin Kernel Version 18.7.0: Sat Oct 12 00:02:19 PDT 2019; root:xnu-4903.278.12~1/RELEASE_X86_64', machine='x86_64') 57 | 58 | pathSeparatorInCurrentOS = os.path.sep 59 | print("pathSeparatorInCurrentOS=%s" % pathSeparatorInCurrentOS) 60 | # pathSeparatorInCurrentOS=/ 61 | 62 | fullFilePath = "/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png" 63 | print("fullFilePath=%s" % fullFilePath) 64 | # fullFilePath=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png 65 | 66 | dirname = os.path.dirname(fullFilePath) 67 | print("dirname=%s" % dirname) 68 | # dirname=/Users/limao/dev/crifan/python/notEnoughUnpack 69 | basename = os.path.basename(fullFilePath) 70 | print("basename=%s" % basename) 71 | # basename=Snip20191212_113.png 72 | joinedFullPath = os.path.join(dirname, basename) 73 | print("joinedFullPath=%s" % joinedFullPath) 74 | # joinedFullPath=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png 75 | isSame = (fullFilePath == joinedFullPath) 76 | print("isSame=%s" % isSame) 77 | # isSame=True 78 | 79 | root, pointSuffix = os.path.splitext(fullFilePath) 80 | print("root=%s, pointSuffix=%s" % (root, pointSuffix)) 81 | # root=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113, pointSuffix=.png 82 | head, tail = os.path.split(fullFilePath) 83 | print("head=%s, tail=%s" % (head, tail)) 84 | # head=/Users/limao/dev/crifan/python/notEnoughUnpack, tail=Snip20191212_113.png 85 | drive, tail = os.path.splitdrive(fullFilePath) 86 | print("drive=%s, tail=%s" % (drive, tail)) 87 | # drive=, tail=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png 88 | 89 | curPath = os.getcwd() 90 | print("curPath=%s" % curPath) 91 | # curPath=/Users/limao/dev/crifan/python 92 | relativePath = os.path.relpath(fullFilePath) 93 | print("relativePath=%s" % relativePath) 94 | # relativePath=notEnoughUnpack/Snip20191212_113.png 95 | 96 | isFile = os.path.isfile(fullFilePath) 97 | print("isFile=%s" % isFile) 98 | # isFile=True 99 | isDirectory = os.path.isdir(fullFilePath) 100 | print("isDirectory=%s" % isDirectory) 101 | # isDirectory=False 102 | 103 | fileSize = os.path.getsize(fullFilePath) 104 | print("fileSize=%s" % fileSize) 105 | # fileSize=368810 106 | 107 | isFileOrFolderRealExist = os.path.exists(fullFilePath) 108 | print("isFileOrFolderRealExist=%s" % isFileOrFolderRealExist) 109 | # isFileOrFolderRealExist=True 110 | 111 | if __name__ == "__main__": 112 | osPathDemo() 113 | ``` 114 | 115 | ## 列出目录中的文件(和文件夹,且支持递归) 116 | 117 | ```python 118 | def listSubfolderFiles(subfolder, isIncludeFolder=True, isRecursive=False): 119 | """os.listdir recursively 120 | 121 | Args: 122 | subfolder (str): sub folder path 123 | isIncludeFolder (bool): whether is include folder. Default is True. If True, result contain folder 124 | isRecursive (bool): whether is recursive, means contain sub folder. Default is False 125 | Returns: 126 | list of str 127 | Raises: 128 | """ 129 | allSubItemList = [] 130 | curSubItemList = os.listdir(path=subfolder) 131 | for curSubItem in curSubItemList: 132 | curSubItemFullPath = os.path.join(subfolder, curSubItem) 133 | if os.path.isfile(curSubItemFullPath): 134 | allSubItemList.append(curSubItemFullPath) 135 | else: 136 | if isIncludeFolder: 137 | if os.path.isdir(curSubItemFullPath): 138 | subSubItemList = listSubfolderFiles(curSubItemFullPath, isIncludeFolder, isRecursive) 139 | allSubItemList.extend(subSubItemList) 140 | 141 | if isIncludeFolder: 142 | allSubItemList.append(subfolder) 143 | 144 | return allSubItemList 145 | ``` 146 | -------------------------------------------------------------------------------- /src/common_code/mail.md: -------------------------------------------------------------------------------- 1 | # 邮件 2 | 3 | ## 发送邮件 4 | 5 | 函数: 6 | 7 | ```python 8 | 9 | def sendEmail( sender, senderPassword, receiverList, 10 | senderName="", receiverNameList= "", 11 | smtpServer = "", smtpPort = None, useSSL=False, 12 | type = "plain", title = "", body = ""): 13 | """ 14 | send email 15 | :param sender: 16 | :param senderPassword: 17 | :param receiverList: 18 | :param senderName: 19 | :param receiverNameList: 20 | :param smtpServer: 21 | :param smtpPort: 22 | :param type: html/plain 23 | :param title: 24 | :param body: 25 | :return: 26 | """ 27 | logging.debug("sender=%s, senderName=%s, smtpServer=%s, smtpPort=%s, useSSL=%s, type=%s, title=%s, body=%s", 28 | sender, senderName, smtpServer, smtpPort, useSSL, type, title, body) 29 | logging.debug("receiverList=%s, receiverNameList=%s", receiverList, receiverNameList) 30 | 31 | defaultPort = None 32 | SMTP_PORT_NO_SSL = 25 33 | SMTP_PORT_SSL = 465 34 | if useSSL: 35 | defaultPort = SMTP_PORT_SSL 36 | else: 37 | defaultPort = SMTP_PORT_NO_SSL 38 | 39 | if not smtpPort: 40 | smtpPort = defaultPort 41 | 42 | # init smtp server if necessary 43 | if not smtpServer: 44 | # extract domain from sender email 45 | # crifan2003@163.com -> 163.com 46 | atIdx = sender.index('@') 47 | afterAtIdx = atIdx + 1 48 | lastDomain = sender[afterAtIdx:] 49 | smtpServer = 'smtp.' + lastDomain 50 | # smtpServer = "smtp.163.com" 51 | # smtpPort = 25 52 | 53 | # RECEIVER_SEPERATOR = '; ' 54 | RECEIVER_SEPERATOR = ', ' 55 | 56 | senderNameAddr = "%s <%s>" % (senderName, sender) 57 | receiversAddr = RECEIVER_SEPERATOR.join(receiverList) 58 | receiverNameAddrList = [] 59 | formatedReceiverNameAddrList = [] 60 | for curIdx, eachReceiver in enumerate(receiverList): 61 | eachReceiverName = receiverNameList[curIdx] 62 | eachNameAddr = "%s <%s>" % (eachReceiverName, eachReceiver) 63 | eachFormatedNameAddr = formatEmailNameAddrHeader(eachNameAddr) 64 | receiverNameAddrList.append(eachNameAddr) 65 | formatedReceiverNameAddrList.append(eachFormatedNameAddr) 66 | 67 | formatedReceiversNameAddr = RECEIVER_SEPERATOR.join(formatedReceiverNameAddrList) # '=?utf-8?b?57u/6Imy5Z6D5Zy+?= , =?utf-8?b?5YWL55Ge6Iqs?= ' 68 | mergedReceiversNameAddr = RECEIVER_SEPERATOR.join(receiverNameAddrList) # u'Crifan2003 , 克瑞芬 ' 69 | # formatedReceiversNameAddr = formatEmailHeader(mergedReceiversNameAddr) #=?utf-8?b?Q3JpZmFuMjAwMyA8Y3JpZmFuMjAwM0AxNjMuY29tPiwg5YWL55Ge6IqsIDxh?= 70 | # =?utf-8?q?dmin=40crifan=2Ecom=3E?= 71 | 72 | msg = MIMEText(body, _subtype=type, _charset="utf-8") 73 | # msg["From"] = _format_addr(senderNameAddr) 74 | # msg["To"] = _format_addr(receiversNameAddr) 75 | msg["From"] = formatEmailHeader(senderNameAddr) 76 | # msg["From"] = senderNameAddr 77 | # msg["To"] = formatEmailHeader(formatedReceiversNameAddr) 78 | # msg["To"] = formatedReceiversNameAddr 79 | # msg["To"] = mergedReceiversNameAddr 80 | # msg["To"] = formatEmailHeader(receiversAddr) 81 | msg["To"] = formatEmailHeader(mergedReceiversNameAddr) 82 | # titleHeader = Header(title, "utf-8") 83 | # encodedTitleHeader = titleHeader.encode() 84 | # msg['Subject'] = encodedTitleHeader 85 | msg['Subject'] = formatEmailHeader(title) 86 | # msg['Subject'] = title 87 | msgStr = msg.as_string() 88 | 89 | # try: 90 | # smtpObj = smtplib.SMTP('localhost') 91 | smtpObj = None 92 | if useSSL: 93 | smtpObj = smtplib.SMTP_SSL(smtpServer, smtpPort) 94 | else: 95 | smtpObj = smtplib.SMTP(smtpServer, smtpPort) 96 | # start TLS for security 97 | # smtpObj.starttls() 98 | # smtpObj.set_debuglevel(1) 99 | smtpObj.login(sender, senderPassword) 100 | # smtpObj.sendmail(sender, receiversAddr, msgStr) 101 | smtpObj.sendmail(sender, receiverList, msgStr) 102 | logging.info("Successfully sent email: message=%s", msgStr) 103 | # except smtplib.SMTPException: 104 | # logging.error("Fail to sent email: message=%s", message) 105 | 106 | return 107 | ``` 108 | 109 | 调用: 110 | 111 | ```python 112 | productName = "First 163 then crifan. Dell XPS 13 XPS9360-5797SLV-PUS Laptop" 113 | productUrl = "https://www.microsoft.com/en-us/store/d/dell-xps-13-xps-9360-laptop-pc/8q17384grz37/GV5D?activetab=pivot%253aoverviewtab" 114 | notifType = "HighPrice" 115 | title = "[%s] %s" % (notifType, productName) 116 | notifContent = """ 117 | 118 | 119 |

%s

120 |

Not buy %s for current price $699.00 > expected price $599.00

121 |

So save for later process

122 | 123 | 124 | """ % (title, productUrl, productName) 125 | receiversDictList = gCfg["notification"]["receivers"] 126 | receiverList = [] 127 | receiverNameList = [] 128 | for eachReceiverDict in receiversDictList: 129 | receiverList.append(eachReceiverDict["email"]) 130 | receiverNameList.append(eachReceiverDict["username"]) 131 | 132 | sendEmail( 133 | sender = gCfg["notification"]["sender"]["email"], 134 | senderPassword = gCfg["notification"]["sender"]["password"], 135 | receiverList = receiverList, 136 | senderName = gCfg["notification"]["sender"]["username"], 137 | receiverNameList= receiverNameList, 138 | type = "html", 139 | title = title, 140 | body = notifContent 141 | ) 142 | ``` 143 | 144 | 附录: 145 | 146 | * 最新代码详见: 147 | * https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanEmail.py 148 | 149 | -------------------------------------------------------------------------------- /src/common_code/math.md: -------------------------------------------------------------------------------- 1 | # 数学 2 | 3 | 详见: 4 | 5 | https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanMath.py 6 | 7 | --- 8 | 9 | ## md5 10 | 11 | ### md5计算 12 | 13 | * `md5` 14 | * `Python 3`中已改名`hashlib` 15 | * 且update参数只允许`bytes` 16 | * 不允许`str` 17 | 18 | 的md5代码: 19 | 20 | ```python 21 | from hashlib import md5 # only for python 3.x 22 | 23 | def generateMd5(strToMd5) : 24 | """ 25 | generate md5 string from input string 26 | eg: 27 | xxxxxxxx -> af0230c7fcc75b34cbb268b9bf64da79 28 | :param strToMd5: input string 29 | :return: md5 string of 32 chars 30 | """ 31 | encrptedMd5 = "" 32 | md5Instance = md5() 33 | # print("type(md5Instance)=%s" % type(md5Instance)) # type(md5Instance)= 34 | # print("type(strToMd5)=%s" % type(strToMd5)) # type(strToMd5)= 35 | bytesToMd5 = bytes(strToMd5, "UTF-8") 36 | # print("type(bytesToMd5)=%s" % type(bytesToMd5)) # type(bytesToMd5)= 37 | md5Instance.update(bytesToMd5) 38 | encrptedMd5 = md5Instance.hexdigest() 39 | # print("type(encrptedMd5)=%s" % type(encrptedMd5)) # type(encrptedMd5)= 40 | # print("encrptedMd5=%s" % encrptedMd5) # encrptedMd5=3a821616bec2e86e3e232d0c7f392cf5 41 | return encrptedMd5 42 | ``` 43 | 44 | 45 | 之前旧版本的`Python 2`(`<= 2.7`)版本: 46 | 47 | * md5还是个独立模块 48 | * 还没有并入`hashlib` 49 | * 注: 50 | * 好像`python 2.7`中已将md5并入`hashlib` 51 | * 但是`update`参数还允许`str`(而不是`bytes`) 52 | * update参数允许str 53 | 54 | 的md5代码: 55 | 56 | ```python 57 | try: 58 | import md5 59 | except ImportError: 60 | from hashlib import md5 61 | 62 | def generateMd5(strToMd5) : 63 | encrptedMd5 = "" 64 | md5Instance = md5.new() 65 | #md5Instance= 66 | md5Instance.update(strToMd5) 67 | encrptedMd5 = md5Instance.hexdigest() 68 | #encrptedMd5=af0230c7fcc75b34cbb268b9bf64da79 69 | return encrptedMd5 70 | ``` 71 | -------------------------------------------------------------------------------- /src/common_code/multimedia.md: -------------------------------------------------------------------------------- 1 | # 多媒体 2 | 3 | -------------------------------------------------------------------------------- /src/common_code/multimedia/README.md: -------------------------------------------------------------------------------- 1 | # 多媒体 2 | 3 | 详见: 4 | 5 | * https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanMultimedia.py 6 | * https://github.com/crifan/crifanLibPython/blob/master/crifanLib/demo/crifanMultimediaDemo.py 7 | 8 | --- 9 | 10 | 此处整理多媒体相关的常用Python代码段,主要包含如下内容: 11 | 12 | * 图片=图像 13 | * 音频 14 | * 视频 15 | -------------------------------------------------------------------------------- /src/common_code/multimedia/audio.md: -------------------------------------------------------------------------------- 1 | # 音频 2 | 3 | ## 播放音频 4 | 5 | ### 树莓派中用python播放音频 6 | 7 | 前提: 8 | 9 | 树莓派中,先去安装vlc: 10 | 11 | ```bash 12 | sudo apt-get install vlc 13 | ``` 14 | 15 | 代码: 16 | 17 | ```python 18 | import vlc 19 | instance = vlc.Instance('--aout=alsa') 20 | p = instance.media_player_new() 21 | m = instance.media_new('/home/pi/Music/lizhongsheng_massif_live.mp3') 22 | p.set_media(m) 23 | p.play() 24 | ``` 25 | 26 | 即可播放音频。 27 | 28 | 实现设置音量,暂停,继续播放等操作的代码是: 29 | 30 | ```python 31 | p.pause() 32 | vlc.libvlc_audio_set_volume(p, 40) 33 | p.play() 34 | vlc.libvlc_audio_set_volume(p, 90) 35 | ``` 36 | 37 | ### Mac中调用mpv播放音频 38 | 39 | 播放音频: 40 | 41 | ```python 42 | cmdPlayer = "mpv" 43 | cmdParaFilePath = tmpAudioFileFullPath 44 | cmdArgList = [cmdPlayer, cmdParaFilePath] 45 | 46 | if gCurSubProcess: 47 | gCurSubProcess.terminate() 48 | 49 | gCurSubProcess = subprocess.Popen(cmdArgList) 50 | log.debug("gCurSubProcess=%s", gCurSubProcess) 51 | ``` 52 | 53 | 停止播放: 54 | 55 | ```python 56 | if audioControl == "stop": 57 | if gCurSubProcess: 58 | gCurSubProcess.terminate() 59 | 60 | respData = { 61 | audioControl: "ok" 62 | } 63 | 64 | else: 65 | respData = { 66 | audioControl: "Unsupport command" 67 | } 68 | ``` 69 | 70 | 获取播放效果: 71 | 72 | ```python 73 | if gCurSubProcess: 74 | isTerminated = gCurSubProcess.poll() # None 75 | # stdout_data, stderr_data = gCurSubProcess.communicate() 76 | # stdoutStr = str(stdout_data) 77 | # stderrStr = str(stderr_data) 78 | respData = { 79 | "isTerminated": isTerminated, 80 | # "stdout_data": stdoutStr, 81 | # "stderr_data": stderrStr, 82 | } 83 | ``` 84 | 85 | 返回结果: 86 | 87 | * 正在播放:返回isTerminated为`null` 88 | * 被终止后,返回isTerminated为`4` 89 | * ![resp_audio_isterminated_4](../../assets/img/resp_audio_isterminated_4.png) 90 | 91 | 播放的效果: 92 | 93 | mac系统中播放音频 94 | 95 | PyCharm的console中输出当前播放的信息 -》 如果是mac的terminal中,则是覆盖式的,不会这么多行 96 | 同时弹框GUI窗口 97 | 98 | ![mac_python_mpv_play_audio](../../assets/img/mac_python_mpv_play_audio.png) 99 | 100 | ## mp3 101 | 102 | ### 解析mp3等音频文件得到时长信息 103 | 104 | 用库: 105 | 106 | * audioread 107 | * GitHub 108 | * [beetbox/audioread: cross-library (GStreamer + Core Audio + MAD + FFmpeg) audio decoding for Python](https://github.com/beetbox/audioread) 109 | 110 | ```python 111 | import audioread 112 | 113 | try: 114 | audioFullFilePath = "/your/input/audio/file.mp3" 115 | 116 | with audioread.audio_open(audioFullFilePath) as audioFp: 117 | audioInfo["duration"] = audioFp.duration 118 | audioInfo["channels"] = audioFp.channels 119 | audioInfo["sampleRate"] = audioFp.samplerate 120 | 121 | except OSError as osErr: 122 | logging.error("OSError when open %s error %s", audioFullFilePath, osErr) 123 | except EOFError as eofErr: 124 | logging.error("EOFError when open %s error %s", audioFullFilePath, eofErr) 125 | except audioread.DecodeError as decodeErr: 126 | logging.error("Decode audio %s error %s", audioFullFilePath, decodeErr) 127 | ``` 128 | 129 | 后经整理成函数: 130 | 131 | ```python 132 | import audioread 133 | 134 | def detectAudioMetaInfo(audioFullPath): 135 | """ 136 | detect audio meta info: duration, channels, sampleRate 137 | """ 138 | isOk = False 139 | errMsg = "" 140 | audioMetaInfo = { 141 | "duration": 0, 142 | "channels": 0, 143 | "sampleRate": 0, 144 | } 145 | 146 | try: 147 | with audioread.audio_open(audioFullPath) as audioFp: 148 | audioMetaInfo["duration"] = audioFp.duration 149 | audioMetaInfo["channels"] = audioFp.channels 150 | audioMetaInfo["sampleRate"] = audioFp.samplerate 151 | 152 | isOk = True 153 | except OSError as osErr: 154 | errMsg = "detect audio info error: %s" % str(osErr) 155 | except EOFError as eofErr: 156 | errMsg = "detect audio info error: %s" % str(eofErr) 157 | except audioread.DecodeError as decodeErr: 158 | errMsg = "detect audio info error: %s" % str(decodeErr) 159 | 160 | if isOk: 161 | return isOk, audioMetaInfo 162 | else: 163 | return isOk, errMsg 164 | ``` 165 | 166 | 调用: 167 | 168 | ```python 169 | def demoDetectAudioMeta(): 170 | curPath = os.path.dirname(__file__) 171 | inputAudioList = [ 172 | "input/audio/actual_aac_but_suffix_mp3.mp3", 173 | "input/audio/real_mp3_format.mp3", 174 | "not_exist_audio.wav", 175 | "input/audio/fake_audio_actual_image.wav", 176 | ] 177 | 178 | 179 | for eachAudioPath in inputAudioList: 180 | eachAudioFullPath = os.path.join(curPath, eachAudioPath) 181 | isOk, errOrInfo = detectAudioMetaInfo(eachAudioFullPath) 182 | print("isOk=%s, errOrInfo=%s" % (isOk, errOrInfo)) 183 | 184 | 185 | if __name__ == "__main__": 186 | demoDetectAudioMeta() 187 | ``` 188 | 189 | 对应的音频文件,用MediaInfo检测出的信息: 190 | 191 | * 正常mp3 192 | * ![audio_file_mp3_mediainfo](../../assets/img/audio_file_mp3_mediainfo.png) 193 | * ![audio_file_mp3_two_channel](../../assets/img/audio_file_mp3_two_channel.png) 194 | * 异常mp3: 195 | * 故意把png图片改成mp3 196 | * ![png_as_wrong_audio](../../assets/img/png_as_wrong_audio.png) 197 | 198 | 输出: 199 | 200 | ```bash 201 | # isOk=True, errOrInfo={'duration': 637.8, 'channels': 2, 'sampleRate': 44100} 202 | # isOk=True, errOrInfo={'duration': 2.3510204081632655, 'channels': 2, 'sampleRate': 44100} 203 | # isOk=False, errOrInfo=detect audio info error: [Errno 2] No such file or directory: '/Users/crifan/dev/dev_root/crifan/crifanLibPython/crifanLib/demo/not_exist_audio.wav' 204 | # isOk=False, errOrInfo=detect audio info error: 205 | ``` -------------------------------------------------------------------------------- /src/common_code/multimedia/image/README.md: -------------------------------------------------------------------------------- 1 | # 图像 2 | 3 | Python中图像处理用的最多是:`Pillow` 4 | 5 | 下面图像处理处理的代码,基本上都是用`Pillow`实现的。 6 | -------------------------------------------------------------------------------- /src/common_code/multimedia/image/baidu_ocr.md: -------------------------------------------------------------------------------- 1 | # 百度OCR 2 | 3 | 详见: 4 | 5 | https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanBaiduOcr.py 6 | 7 | --- 8 | 9 | 10 | 在做安卓和iOS的移动端自动化测试期间,会涉及到**从图像中提取文字**,用的是百度OCR。 11 | 12 | 其中有些通用的功能,整理出函数,贴出供参考。 13 | 14 | ## 百度OCR初始化 15 | 16 | ```python 17 | import os 18 | import re 19 | import base64 20 | import requests 21 | import time 22 | import logging 23 | from collections import OrderedDict 24 | from PIL import Image, ImageDraw 25 | 26 | class BaiduOCR(): 27 | # OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic" # 通用文字识别 28 | # OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/general" # 通用文字识别(含位置信息版) 29 | # OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic" # 通用文字识别(高精度版) 30 | OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate" # 通用文字识别(高精度含位置版) 31 | 32 | 33 | TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token' 34 | 35 | 36 | RESP_ERR_CODE_QPS_LIMIT_REACHED = 18 37 | RESP_ERR_TEXT_QPS_LIMIT_REACHED = "Open api qps request limit reached" 38 | 39 | 40 | RESP_ERR_CODE_DAILY_LIMIT_REACHED = 17 41 | RESP_ERR_TEXT_DAILY_LIMIT_REACHED = "Open api daily request limit reached" 42 | 43 | 44 | API_KEY = 'SOxxxxxxxxxxnu' 45 | SECRET_KEY = 'wlxxxxxxxxxxxxxxxxxxxpL' 46 | 47 | 48 | def initOcr(self): 49 | self.curToken = self.baiduFetchToken() 50 | 51 | 52 | def baiduFetchToken(self): 53 | """Fetch Baidu token for OCR""" 54 | params = { 55 | 'grant_type': 'client_credentials', 56 | 'client_id': self.API_KEY, 57 | 'client_secret': self.SECRET_KEY 58 | } 59 | 60 | 61 | resp = requests.get(self.TOKEN_URL, params=params) 62 | respJson = resp.json() 63 | 64 | 65 | respToken = "" 66 | 67 | 68 | if ('access_token' in respJson.keys() and 'scope' in respJson.keys()): 69 | if not 'brain_all_scope' in respJson['scope'].split(' '): 70 | logging.error('please ensure has check the ability') 71 | else: 72 | respToken = respJson['access_token'] 73 | else: 74 | logging.error('please overwrite the correct API_KEY and SECRET_KEY') 75 | 76 | 77 | # '24.8691f3c6dedd0d0d0b30a9dfec604d52.2592000.1578465979.282335-17921535' 78 | return respToken 79 | ``` 80 | 81 | ## 百度OCR图片转文字 82 | 83 | ```python 84 | def baiduImageToWords(self, imageFullPath): 85 | """Detect text from image using Baidu OCR api""" 86 | 87 | # # Note: if using un-paid = free baidu api, need following wait sometime to reduce: qps request limit 88 | # time.sleep(0.15) 89 | 90 | respWordsResutJson = "" 91 | 92 | # 读取图片二进制数据 93 | imgBinData = readBinDataFromFile(imageFullPath) 94 | encodedImgData = base64.b64encode(imgBinData) 95 | 96 | paramDict = { 97 | "access_token": self.curToken 98 | } 99 | 100 | headerDict = { 101 | "Content-Type": "application/x-www-form-urlencoded" 102 | } 103 | 104 | # 参数含义:http://ai.baidu.com/ai-doc/OCR/vk3h7y58v 105 | dataDict = { 106 | "image": encodedImgData, 107 | "recognize_granularity": "small", 108 | # "vertexes_location": "true", 109 | } 110 | resp = requests.post(self.OCR_URL, params=paramDict, headers=headerDict, data=dataDict) 111 | respJson = resp.json() 112 | 113 | logging.debug("baidu OCR: imgage=%s -> respJson=%s", imageFullPath, respJson) 114 | 115 | if "error_code" in respJson: 116 | logging.warning("respJson=%s" % respJson) 117 | errorCode = respJson["error_code"] 118 | # {'error_code': 17, 'error_msg': 'Open api daily request limit reached'} 119 | # {'error_code': 18, 'error_msg': 'Open api qps request limit reached'} 120 | # the limit count can found from 121 | # 文字识别 - 免费额度 | 百度AI开放平台 122 | # https://ai.baidu.com/ai-doc/OCR/fk3h7xu7h 123 | # for "通用文字识别(高精度含位置版)" is "50次/天" 124 | if errorCode == self.RESP_ERR_CODE_QPS_LIMIT_REACHED: 125 | # wait sometime and try again 126 | time.sleep(1.0) 127 | resp = requests.post(self.OCR_URL, params=paramDict, headers=headerDict, data=dataDict) 128 | respJson = resp.json() 129 | logging.debug("baidu OCR: for errorCode=%s, do again, imgage=%s -> respJson=%s", errorCode, imageFullPath, respJson) 130 | elif errorCode == self.RESP_ERR_CODE_DAILY_LIMIT_REACHED: 131 | logging.error("Fail to continue using baidu OCR api today !!!") 132 | respJson = None 133 | 134 | """ 135 | { 136 | "log_id": 6937531796498618000, 137 | "words_result_num": 32, 138 | "words_result": [ 139 | { 140 | "chars": [ 141 | ... 142 | """ 143 | if "words_result" in respJson: 144 | respWordsResutJson = respJson 145 | 146 | return respWordsResutJson 147 | ``` 148 | 149 | 调用: 150 | 151 | ```python 152 | wordsResultJson = self.baiduImageToWords(imgPath) 153 | 154 | respJson = self.baiduImageToWords(screenImgPath) 155 | ``` 156 | 157 | ### 返回结果举例 158 | 159 | #### 安卓游戏 暗黑觉醒 首充豪礼 160 | 161 | 图片: 162 | 163 | ![anheijuexing_first_charge](../../../assets/img/anheijuexing_first_charge.jpg) 164 | 165 | 返回解析后出`json`格式的文字信息: 166 | 167 | ```json 168 | { 169 | "log_id": 9009770747370640007, 170 | "words_result_num": 12, 171 | "words_result": [ 172 | { 173 | "chars": [ 174 | { 175 | "char": "首", 176 | "location": { "width": 94, "top": 105, "left": 989, "height": 158 } 177 | }, 178 | { 179 | "char": "充", 180 | "location": { "width": 94, "top": 105, "left": 1086, "height": 158 } 181 | }, 182 | { 183 | "char": "豪", 184 | "location": { "width": 95, "top": 105, "left": 1183, "height": 158 } 185 | }, 186 | { 187 | "char": "礼", 188 | "location": { "width": 77, "top": 105, "left": 1281, "height": 158 } 189 | } 190 | ], 191 | "location": { "width": 370, "top": 105, "left": 989, "height": 158 }, 192 | "words": "首充豪礼" 193 | }, 194 | { 195 | "chars": [ 196 | { 197 | "char": "×", 198 | "location": { "width": 30, "top": 161, "left": 1887, "height": 61 } 199 | } 200 | ], 201 | "location": { "width": 60, "top": 161, "left": 1887, "height": 61 }, 202 | "words": "×" 203 | }, 204 | { 205 | "chars": [ 206 | { 207 | "char": "充", 208 | "location": { "width": 43, "top": 273, "left": 758, "height": 73 } 209 | }, 210 | { 211 | "char": "值", 212 | "location": { "width": 43, "top": 273, "left": 803, "height": 73 } 213 | }, 214 | { 215 | "char": "元", 216 | "location": { "width": 67, "top": 273, "left": 912, "height": 73 } 217 | }, 218 | { 219 | "char": "可", 220 | "location": { "width": 44, "top": 273, "left": 979, "height": 73 } 221 | }, 222 | { 223 | "char": "领", 224 | "location": { "width": 43, "top": 273, "left": 1023, "height": 73 } 225 | }, 226 | { 227 | "char": "总", 228 | "location": { "width": 44, "top": 273, "left": 1067, "height": 73 } 229 | }, 230 | { 231 | "char": "价", 232 | "location": { "width": 23, "top": 273, "left": 1111, "height": 73 } 233 | }, 234 | { 235 | "char": "值", 236 | "location": { "width": 89, "top": 273, "left": 1134, "height": 73 } 237 | }, 238 | { 239 | "char": "8", 240 | "location": { "width": 36, "top": 273, "left": 1259, "height": 73 } 241 | }, 242 | { 243 | "char": "8", 244 | "location": { "width": 36, "top": 273, "left": 1326, "height": 73 } 245 | }, 246 | { 247 | "char": "8", 248 | "location": { "width": 35, "top": 273, "left": 1371, "height": 73 } 249 | }, 250 | { 251 | "char": "钻", 252 | "location": { "width": 43, "top": 273, "left": 1444, "height": 73 } 253 | }, 254 | { 255 | "char": "豪", 256 | "location": { "width": 44, "top": 273, "left": 1510, "height": 73 } 257 | }, 258 | { 259 | "char": "华", 260 | "location": { "width": 43, "top": 273, "left": 1555, "height": 73 } 261 | }, 262 | { 263 | "char": "大", 264 | "location": { "width": 43, "top": 273, "left": 1599, "height": 73 } 265 | }, 266 | { 267 | "char": "礼", 268 | "location": { "width": 27, "top": 273, "left": 1643, "height": 73 } 269 | } 270 | ], 271 | "location": { "width": 911, "top": 273, "left": 758, "height": 73 }, 272 | "words": "充值元可领总价值888钻豪华大礼" 273 | }, 274 | { 275 | "chars": [ 276 | { 277 | "char": "送", 278 | "location": { "width": 65, "top": 369, "left": 832, "height": 107 } 279 | } 280 | ], 281 | "location": { "width": 107, "top": 369, "left": 832, "height": 107 }, 282 | "words": "送" 283 | }, 284 | { 285 | "chars": [ 286 | { 287 | "char": "绝", 288 | "location": { "width": 38, "top": 390, "left": 974, "height": 65 } 289 | }, 290 | { 291 | "char": "版", 292 | "location": { "width": 38, "top": 390, "left": 1032, "height": 65 } 293 | }, 294 | { 295 | "char": "萌", 296 | "location": { "width": 38, "top": 390, "left": 1092, "height": 65 } 297 | }, 298 | { 299 | "char": "宠", 300 | "location": { "width": 38, "top": 390, "left": 1150, "height": 65 } 301 | }, 302 | { 303 | "char": "、", 304 | "location": { "width": 31, "top": 390, "left": 1184, "height": 65 } 305 | }, 306 | { 307 | "char": "专", 308 | "location": { "width": 39, "top": 390, "left": 1230, "height": 65 } 309 | }, 310 | { 311 | "char": "属", 312 | "location": { "width": 38, "top": 390, "left": 1289, "height": 65 } 313 | }, 314 | { 315 | "char": "神", 316 | "location": { "width": 38, "top": 390, "left": 1368, "height": 65 } 317 | }, 318 | { 319 | "char": "兵", 320 | "location": { "width": 39, "top": 390, "left": 1408, "height": 65 } 321 | } 322 | ], 323 | "location": { "width": 524, "top": 390, "left": 934, "height": 65 }, 324 | "words": "绝版萌宠、专属神兵" 325 | }, 326 | { 327 | "chars": [ 328 | { 329 | "char": "绝", 330 | "location": { "width": 20, "top": 515, "left": 378, "height": 33 } 331 | } 332 | ], 333 | "location": { "width": 33, "top": 515, "left": 378, "height": 33 }, 334 | "words": "绝" 335 | }, 336 | { 337 | "chars": [ 338 | { 339 | "char": "珍", 340 | "location": { "width": 33, "top": 516, "left": 1992, "height": 42 } 341 | } 342 | ], 343 | "location": { "width": 33, "top": 516, "left": 1992, "height": 42 }, 344 | "words": "珍" 345 | }, 346 | { 347 | "chars": [ 348 | { 349 | "char": "版", 350 | "location": { "width": 20, "top": 545, "left": 379, "height": 34 } 351 | } 352 | ], 353 | "location": { "width": 31, "top": 545, "left": 379, "height": 34 }, 354 | "words": "版" 355 | }, 356 | { 357 | "chars": [ 358 | { 359 | "char": "额", 360 | "location": { "width": 26, "top": 776, "left": 1225, "height": 44 } 361 | }, 362 | { 363 | "char": "外", 364 | "location": { "width": 26, "top": 776, "left": 1264, "height": 44 } 365 | }, 366 | { 367 | "char": "礼", 368 | "location": { "width": 27, "top": 776, "left": 1291, "height": 44 } 369 | }, 370 | { 371 | "char": "包", 372 | "location": { "width": 27, "top": 776, "left": 1317, "height": 44 } 373 | } 374 | ], 375 | "location": { "width": 125, "top": 776, "left": 1225, "height": 44 }, 376 | "words": "额外礼包" 377 | }, 378 | { 379 | "chars": [ 380 | { 381 | "char": "首", 382 | "location": { "width": 38, "top": 830, "left": 935, "height": 64 } 383 | }, 384 | { 385 | "char": "充", 386 | "location": { "width": 38, "top": 830, "left": 994, "height": 64 } 387 | }, 388 | { 389 | "char": "元", 390 | "location": { "width": 38, "top": 830, "left": 1092, "height": 64 } 391 | }, 392 | { 393 | "char": "充", 394 | "location": { "width": 38, "top": 830, "left": 1286, "height": 64 } 395 | }, 396 | { 397 | "char": "9", 398 | "location": { "width": 31, "top": 830, "left": 1339, "height": 64 } 399 | }, 400 | { 401 | "char": "8", 402 | "location": { "width": 31, "top": 830, "left": 1377, "height": 64 } 403 | }, 404 | { 405 | "char": "元", 406 | "location": { "width": 38, "top": 830, "left": 1444, "height": 64 } 407 | } 408 | ], 409 | "location": { "width": 549, "top": 830, "left": 935, "height": 64 }, 410 | "words": "首充元充98元" 411 | }, 412 | { 413 | "chars": [ 414 | { 415 | "char": "战", 416 | "location": { "width": 42, "top": 970, "left": 373, "height": 69 } 417 | }, 418 | { 419 | "char": "斗", 420 | "location": { "width": 42, "top": 970, "left": 437, "height": 69 } 421 | }, 422 | { 423 | "char": "1", 424 | "location": { "width": 35, "top": 970, "left": 515, "height": 69 } 425 | }, 426 | { 427 | "char": "5", 428 | "location": { "width": 35, "top": 970, "left": 537, "height": 69 } 429 | }, 430 | { 431 | "char": "0", 432 | "location": { "width": 34, "top": 970, "left": 580, "height": 69 } 433 | }, 434 | { 435 | "char": "0", 436 | "location": { "width": 35, "top": 970, "left": 622, "height": 69 } 437 | }, 438 | { 439 | "char": "0", 440 | "location": { "width": 34, "top": 970, "left": 666, "height": 69 } 441 | } 442 | ], 443 | "location": { "width": 327, "top": 970, "left": 373, "height": 69 }, 444 | "words": "战斗15000" 445 | }, 446 | { 447 | "chars": [ 448 | { 449 | "char": "战", 450 | "location": { "width": 43, "top": 969, "left": 1648, "height": 73 } 451 | }, 452 | { 453 | "char": "斗", 454 | "location": { "width": 43, "top": 969, "left": 1713, "height": 73 } 455 | }, 456 | { 457 | "char": "1", 458 | "location": { "width": 36, "top": 969, "left": 1793, "height": 73 } 459 | }, 460 | { 461 | "char": "6", 462 | "location": { "width": 36, "top": 969, "left": 1816, "height": 73 } 463 | }, 464 | { 465 | "char": "0", 466 | "location": { "width": 35, "top": 969, "left": 1861, "height": 73 } 467 | }, 468 | { 469 | "char": "0", 470 | "location": { "width": 36, "top": 969, "left": 1904, "height": 73 } 471 | }, 472 | { 473 | "char": "0", 474 | "location": { "width": 29, "top": 969, "left": 1949, "height": 73 } 475 | } 476 | ], 477 | "location": { "width": 330, "top": 969, "left": 1648, "height": 73 }, 478 | "words": "战斗16000" 479 | } 480 | ] 481 | } 482 | ``` 483 | 484 | 其中: 485 | 486 | * 首充豪礼 487 | * 都能完整检测出来:已经是效果很不错了 488 | * 当然偶尔也会有失误,比如 偶尔 489 | * 只解析出部分内容:首充豪 490 | * 或个别字错了:首充豪机 491 | * 本身图片上 礼 也的确很像 机 492 | * 作为OCR犯此错误,完全可以理解 493 | 494 | #### 安卓游戏 暗黑觉醒 公告弹框 495 | 496 | 图片: 497 | 498 | ![anheijuexing_announcement_popop](../../../assets/img/anheijuexing_announcement_popop.jpg) 499 | 500 | 返回结果json: 501 | 502 | ```json 503 | { 504 | "log_id": 2793391773289550472, 505 | "words_result_num": 23, 506 | "words_result": [ 507 | { 508 | "chars": [ 509 | { 510 | "char": "公", 511 | "location": { "width": 28, "top": 125, "left": 634, "height": 48 } 512 | }, 513 | { 514 | "char": "告", 515 | "location": { "width": 29, "top": 125, "left": 691, "height": 48 } 516 | } 517 | ], 518 | "location": { "width": 92, "top": 125, "left": 634, "height": 48 }, 519 | "words": "公告" 520 | }, 521 | { 522 | "chars": [ 523 | { 524 | "char": "最", 525 | "location": { "width": 21, "top": 240, "left": 535, "height": 36 } 526 | } 527 | ], 528 | "location": { "width": 33, "top": 240, "left": 535, "height": 36 }, 529 | "words": "最" 530 | }, 531 | { 532 | "chars": [ 533 | { 534 | "char": "亲", 535 | "location": { "width": 26, "top": 233, "left": 922, "height": 42 } 536 | }, 537 | { 538 | "char": "爱", 539 | "location": { "width": 26, "top": 233, "left": 959, "height": 42 } 540 | }, 541 | { 542 | "char": "的", 543 | "location": { "width": 26, "top": 233, "left": 986, "height": 42 } 544 | }, 545 | { 546 | "char": "觉", 547 | "location": { "width": 26, "top": 233, "left": 1024, "height": 42 } 548 | }, 549 | { 550 | "char": "醒", 551 | "location": { "width": 26, "top": 233, "left": 1063, "height": 42 } 552 | }, 553 | { 554 | "char": "勇", 555 | "location": { "width": 26, "top": 233, "left": 1088, "height": 42 } 556 | }, 557 | { 558 | "char": "士", 559 | "location": { "width": 25, "top": 233, "left": 1127, "height": 42 } 560 | }, 561 | { 562 | "char": ":", 563 | "location": { "width": 21, "top": 233, "left": 1148, "height": 42 } 564 | } 565 | ], 566 | "location": { "width": 253, "top": 233, "left": 922, "height": 42 }, 567 | "words": "亲爱的觉醒勇士:" 568 | }, 569 | { 570 | "chars": [ 571 | { 572 | "char": "新", 573 | "location": { "width": 26, "top": 266, "left": 535, "height": 44 } 574 | }, 575 | { 576 | "char": "新", 577 | "location": { "width": 27, "top": 266, "left": 588, "height": 44 } 578 | }, 579 | { 580 | "char": "服", 581 | "location": { "width": 26, "top": 266, "left": 628, "height": 44 } 582 | }, 583 | { 584 | "char": "公", 585 | "location": { "width": 27, "top": 266, "left": 668, "height": 44 } 586 | }, 587 | { 588 | "char": "告", 589 | "location": { "width": 26, "top": 266, "left": 709, "height": 44 } 590 | } 591 | ], 592 | "location": { "width": 201, "top": 266, "left": 535, "height": 44 }, 593 | "words": "新新服公告" 594 | }, 595 | { 596 | "chars": [ 597 | { 598 | "char": "承", 599 | "location": { "width": 26, "top": 282, "left": 984, "height": 43 } 600 | }, 601 | { 602 | "char": "载", 603 | "location": { "width": 26, "top": 282, "left": 1023, "height": 43 } 604 | }, 605 | { 606 | "char": "六", 607 | "location": { "width": 26, "top": 282, "left": 1063, "height": 43 } 608 | }, 609 | { 610 | "char": "大", 611 | "location": { "width": 26, "top": 282, "left": 1090, "height": 43 } 612 | }, 613 | { 614 | "char": "种", 615 | "location": { "width": 26, "top": 282, "left": 1116, "height": 43 } 616 | }, 617 | { 618 | "char": "族", 619 | "location": { "width": 26, "top": 282, "left": 1155, "height": 43 } 620 | }, 621 | { 622 | "char": "的", 623 | "location": { "width": 26, "top": 282, "left": 1181, "height": 43 } 624 | }, 625 | { 626 | "char": "重", 627 | "location": { "width": 26, "top": 282, "left": 1220, "height": 43 } 628 | }, 629 | { 630 | "char": "生", 631 | "location": { "width": 26, "top": 282, "left": 1260, "height": 43 } 632 | }, 633 | { 634 | "char": "之", 635 | "location": { "width": 26, "top": 282, "left": 1286, "height": 43 } 636 | }, 637 | { 638 | "char": "使", 639 | "location": { "width": 27, "top": 282, "left": 1325, "height": 43 } 640 | }, 641 | { 642 | "char": "命", 643 | "location": { "width": 26, "top": 282, "left": 1351, "height": 43 } 644 | }, 645 | { 646 | "char": ",", 647 | "location": { "width": 21, "top": 282, "left": 1385, "height": 43 } 648 | }, 649 | { 650 | "char": "超", 651 | "location": { "width": 26, "top": 282, "left": 1416, "height": 43 } 652 | }, 653 | { 654 | "char": "现", 655 | "location": { "width": 26, "top": 282, "left": 1456, "height": 43 } 656 | }, 657 | { 658 | "char": "实", 659 | "location": { "width": 26, "top": 282, "left": 1483, "height": 43 } 660 | }, 661 | { 662 | "char": "3", 663 | "location": { "width": 22, "top": 282, "left": 1517, "height": 43 } 664 | }, 665 | { 666 | "char": "D", 667 | "location": { "width": 21, "top": 282, "left": 1531, "height": 43 } 668 | }, 669 | { 670 | "char": "魔", 671 | "location": { "width": 26, "top": 282, "left": 1560, "height": 43 } 672 | }, 673 | { 674 | "char": "M", 675 | "location": { "width": 61, "top": 284, "left": 1583, "height": 40 } 676 | }, 677 | { 678 | "char": "M", 679 | "location": { "width": 35, "top": 284, "left": 1639, "height": 40 } 680 | }, 681 | { 682 | "char": "O", 683 | "location": { "width": 31, "top": 284, "left": 1669, "height": 40 } 684 | }, 685 | { 686 | "char": "A", 687 | "location": { "width": 25, "top": 284, "left": 1696, "height": 40 } 688 | }, 689 | { 690 | "char": "R", 691 | "location": { "width": 25, "top": 284, "left": 1716, "height": 40 } 692 | }, 693 | { 694 | "char": "P", 695 | "location": { "width": 25, "top": 284, "left": 1735, "height": 40 } 696 | }, 697 | { 698 | "char": "G", 699 | "location": { "width": 25, "top": 284, "left": 1755, "height": 40 } 700 | }, 701 | { 702 | "char": "幻", 703 | "location": { "width": 26, "top": 282, "left": 1588, "height": 43 } 704 | }, 705 | { 706 | "char": "手", 707 | "location": { "width": 26, "top": 282, "left": 1784, "height": 43 } 708 | }, 709 | { 710 | "char": "游", 711 | "location": { "width": 26, "top": 282, "left": 1823, "height": 43 } 712 | } 713 | ], 714 | "location": { "width": 867, "top": 282, "left": 984, "height": 43 }, 715 | "words": "承载六大种族的重生之使命,超现实3D魔 MMOARPG幻手游" 716 | }, 717 | { 718 | "chars": [ 719 | { 720 | "char": "不", 721 | "location": { "width": 37, "top": 337, "left": 923, "height": 40 } 722 | }, 723 | { 724 | "char": "负", 725 | "location": { "width": 23, "top": 337, "left": 959, "height": 40 } 726 | }, 727 | { 728 | "char": "觉", 729 | "location": { "width": 25, "top": 337, "left": 996, "height": 40 } 730 | }, 731 | { 732 | "char": "醒", 733 | "location": { "width": 23, "top": 337, "left": 1021, "height": 40 } 734 | }, 735 | { 736 | "char": "勇", 737 | "location": { "width": 23, "top": 337, "left": 1058, "height": 40 } 738 | }, 739 | { 740 | "char": "士", 741 | "location": { "width": 25, "top": 337, "left": 1094, "height": 40 } 742 | }, 743 | { 744 | "char": "们", 745 | "location": { "width": 25, "top": 337, "left": 1119, "height": 40 } 746 | }, 747 | { 748 | "char": "的", 749 | "location": { "width": 25, "top": 337, "left": 1156, "height": 40 } 750 | }, 751 | { 752 | "char": "使", 753 | "location": { "width": 25, "top": 337, "left": 1192, "height": 40 } 754 | }, 755 | { 756 | "char": "命", 757 | "location": { "width": 25, "top": 337, "left": 1230, "height": 40 } 758 | }, 759 | { 760 | "char": "之", 761 | "location": { "width": 25, "top": 337, "left": 1254, "height": 40 } 762 | }, 763 | { 764 | "char": "约", 765 | "location": { "width": 25, "top": 337, "left": 1292, "height": 40 } 766 | }, 767 | { 768 | "char": ",", 769 | "location": { "width": 20, "top": 337, "left": 1324, "height": 40 } 770 | }, 771 | { 772 | "char": "震", 773 | "location": { "width": 25, "top": 337, "left": 1353, "height": 40 } 774 | }, 775 | { 776 | "char": "撼", 777 | "location": { "width": 25, "top": 337, "left": 1390, "height": 40 } 778 | }, 779 | { 780 | "char": "来", 781 | "location": { "width": 23, "top": 337, "left": 1428, "height": 40 } 782 | }, 783 | { 784 | "char": "袭", 785 | "location": { "width": 25, "top": 337, "left": 1452, "height": 40 } 786 | }, 787 | { 788 | "char": "。", 789 | "location": { "width": 18, "top": 337, "left": 1485, "height": 40 } 790 | } 791 | ], 792 | "location": { "width": 580, "top": 337, "left": 923, "height": 40 }, 793 | "words": "不负觉醒勇士们的使命之约,震撼来袭。" 794 | }, 795 | { 796 | "chars": [ 797 | { 798 | "char": "最", 799 | "location": { "width": 20, "top": 364, "left": 535, "height": 33 } 800 | } 801 | ], 802 | "location": { "width": 33, "top": 364, "left": 535, "height": 33 }, 803 | "words": "最" 804 | }, 805 | { 806 | "chars": [ 807 | { 808 | "char": "新", 809 | "location": { "width": 26, "top": 389, "left": 535, "height": 43 } 810 | }, 811 | { 812 | "char": "违", 813 | "location": { "width": 26, "top": 389, "left": 588, "height": 43 } 814 | }, 815 | { 816 | "char": "规", 817 | "location": { "width": 25, "top": 389, "left": 627, "height": 43 } 818 | }, 819 | { 820 | "char": "发", 821 | "location": { "width": 26, "top": 389, "left": 666, "height": 43 } 822 | }, 823 | { 824 | "char": "言", 825 | "location": { "width": 26, "top": 389, "left": 704, "height": 43 } 826 | }, 827 | { 828 | "char": "处", 829 | "location": { "width": 26, "top": 389, "left": 743, "height": 43 } 830 | }, 831 | { 832 | "char": "理", 833 | "location": { "width": 26, "top": 389, "left": 770, "height": 43 } 834 | }, 835 | { 836 | "char": "机", 837 | "location": { "width": 26, "top": 389, "left": 808, "height": 43 } 838 | }, 839 | { 840 | "char": "制", 841 | "location": { "width": 26, "top": 389, "left": 848, "height": 43 } 842 | } 843 | ], 844 | "location": { "width": 343, "top": 389, "left": 535, "height": 43 }, 845 | "words": "新违规发言处理机制" 846 | }, 847 | { 848 | "chars": [ 849 | { 850 | "char": "【", 851 | "location": { "width": 21, "top": 387, "left": 997, "height": 43 } 852 | }, 853 | { 854 | "char": "普", 855 | "location": { "width": 25, "top": 387, "left": 1023, "height": 43 } 856 | }, 857 | { 858 | "char": "纳", 859 | "location": { "width": 26, "top": 387, "left": 1061, "height": 43 } 860 | }, 861 | { 862 | "char": "山", 863 | "location": { "width": 25, "top": 387, "left": 1087, "height": 43 } 864 | }, 865 | { 866 | "char": "谷", 867 | "location": { "width": 26, "top": 387, "left": 1126, "height": 43 } 868 | }, 869 | { 870 | "char": "1", 871 | "location": { "width": 21, "top": 387, "left": 1148, "height": 43 } 872 | }, 873 | { 874 | "char": "2", 875 | "location": { "width": 21, "top": 387, "left": 1160, "height": 43 } 876 | }, 877 | { 878 | "char": "0", 879 | "location": { "width": 21, "top": 387, "left": 1187, "height": 43 } 880 | }, 881 | { 882 | "char": "服", 883 | "location": { "width": 26, "top": 387, "left": 1204, "height": 43 } 884 | }, 885 | { 886 | "char": "】", 887 | "location": { "width": 21, "top": 387, "left": 1238, "height": 43 } 888 | }, 889 | { 890 | "char": "将", 891 | "location": { "width": 25, "top": 387, "left": 1269, "height": 43 } 892 | }, 893 | { 894 | "char": "于", 895 | "location": { "width": 26, "top": 387, "left": 1308, "height": 43 } 896 | }, 897 | { 898 | "char": "0", 899 | "location": { "width": 20, "top": 387, "left": 1329, "height": 43 } 900 | }, 901 | { 902 | "char": "7", 903 | "location": { "width": 21, "top": 387, "left": 1355, "height": 43 } 904 | }, 905 | { 906 | "char": "月", 907 | "location": { "width": 25, "top": 387, "left": 1385, "height": 43 } 908 | }, 909 | { 910 | "char": "0", 911 | "location": { "width": 21, "top": 387, "left": 1406, "height": 43 } 912 | }, 913 | { 914 | "char": "8", 915 | "location": { "width": 21, "top": 387, "left": 1420, "height": 43 } 916 | }, 917 | { 918 | "char": "日", 919 | "location": { "width": 25, "top": 387, "left": 1451, "height": 43 } 920 | }, 921 | { 922 | "char": "0", 923 | "location": { "width": 21, "top": 387, "left": 1471, "height": 43 } 924 | }, 925 | { 926 | "char": "0", 927 | "location": { "width": 21, "top": 387, "left": 1497, "height": 43 } 928 | }, 929 | { 930 | "char": ":", 931 | "location": { "width": 21, "top": 387, "left": 1510, "height": 43 } 932 | }, 933 | { 934 | "char": "1", 935 | "location": { "width": 21, "top": 387, "left": 1523, "height": 43 } 936 | }, 937 | { 938 | "char": "5", 939 | "location": { "width": 21, "top": 387, "left": 1535, "height": 43 } 940 | }, 941 | { 942 | "char": "震", 943 | "location": { "width": 25, "top": 387, "left": 1567, "height": 43 } 944 | }, 945 | { 946 | "char": "撼", 947 | "location": { "width": 26, "top": 387, "left": 1592, "height": 43 } 948 | }, 949 | { 950 | "char": "开", 951 | "location": { "width": 25, "top": 387, "left": 1631, "height": 43 } 952 | }, 953 | { 954 | "char": "启", 955 | "location": { "width": 26, "top": 387, "left": 1656, "height": 43 } 956 | }, 957 | { 958 | "char": "。", 959 | "location": { "width": 21, "top": 387, "left": 1678, "height": 43 } 960 | } 961 | ], 962 | "location": { "width": 711, "top": 387, "left": 997, "height": 43 }, 963 | "words": "【普纳山谷120服】将于07月08日00:15震撼开启。" 964 | }, 965 | { 966 | "chars": [ 967 | { 968 | "char": "各", 969 | "location": { "width": 23, "top": 438, "left": 987, "height": 40 } 970 | }, 971 | { 972 | "char": "位", 973 | "location": { "width": 25, "top": 438, "left": 1022, "height": 40 } 974 | }, 975 | { 976 | "char": "勇", 977 | "location": { "width": 25, "top": 438, "left": 1059, "height": 40 } 978 | }, 979 | { 980 | "char": "士", 981 | "location": { "width": 25, "top": 438, "left": 1095, "height": 40 } 982 | }, 983 | { 984 | "char": "请", 985 | "location": { "width": 25, "top": 438, "left": 1120, "height": 40 } 986 | }, 987 | { 988 | "char": "拿", 989 | "location": { "width": 23, "top": 438, "left": 1157, "height": 40 } 990 | }, 991 | { 992 | "char": "起", 993 | "location": { "width": 25, "top": 438, "left": 1181, "height": 40 } 994 | }, 995 | { 996 | "char": "手", 997 | "location": { "width": 23, "top": 438, "left": 1230, "height": 40 } 998 | }, 999 | { 1000 | "char": "中", 1001 | "location": { "width": 23, "top": 438, "left": 1255, "height": 40 } 1002 | }, 1003 | { 1004 | "char": "武", 1005 | "location": { "width": 25, "top": 438, "left": 1291, "height": 40 } 1006 | }, 1007 | { 1008 | "char": "器", 1009 | "location": { "width": 23, "top": 438, "left": 1316, "height": 40 } 1010 | }, 1011 | { 1012 | "char": ",", 1013 | "location": { "width": 20, "top": 438, "left": 1348, "height": 40 } 1014 | }, 1015 | { 1016 | "char": "与", 1017 | "location": { "width": 25, "top": 438, "left": 1389, "height": 40 } 1018 | }, 1019 | { 1020 | "char": "我", 1021 | "location": { "width": 23, "top": 438, "left": 1414, "height": 40 } 1022 | }, 1023 | { 1024 | "char": "们", 1025 | "location": { "width": 25, "top": 438, "left": 1449, "height": 40 } 1026 | }, 1027 | { 1028 | "char": "一", 1029 | "location": { "width": 23, "top": 438, "left": 1487, "height": 40 } 1030 | }, 1031 | { 1032 | "char": "同", 1033 | "location": { "width": 23, "top": 438, "left": 1524, "height": 40 } 1034 | }, 1035 | { 1036 | "char": "踏", 1037 | "location": { "width": 25, "top": 438, "left": 1548, "height": 40 } 1038 | }, 1039 | { 1040 | "char": "上", 1041 | "location": { "width": 23, "top": 438, "left": 1584, "height": 40 } 1042 | }, 1043 | { 1044 | "char": "王", 1045 | "location": { "width": 25, "top": 438, "left": 1621, "height": 40 } 1046 | }, 1047 | { 1048 | "char": "者", 1049 | "location": { "width": 25, "top": 438, "left": 1645, "height": 40 } 1050 | }, 1051 | { 1052 | "char": "觉", 1053 | "location": { "width": 23, "top": 438, "left": 1683, "height": 40 } 1054 | }, 1055 | { 1056 | "char": "醒", 1057 | "location": { "width": 25, "top": 438, "left": 1718, "height": 40 } 1058 | }, 1059 | { 1060 | "char": "之", 1061 | "location": { "width": 23, "top": 438, "left": 1756, "height": 40 } 1062 | }, 1063 | { 1064 | "char": "路", 1065 | "location": { "width": 25, "top": 438, "left": 1780, "height": 40 } 1066 | }, 1067 | { 1068 | "char": "!", 1069 | "location": { "width": 20, "top": 438, "left": 1813, "height": 40 } 1070 | } 1071 | ], 1072 | "location": { "width": 853, "top": 438, "left": 987, "height": 40 }, 1073 | "words": "各位勇士请拿起手中武器,与我们一同踏上王者觉醒之路!" 1074 | }, 1075 | { 1076 | "chars": [ 1077 | { 1078 | "char": "限", 1079 | "location": { "width": 21, "top": 486, "left": 535, "height": 35 } 1080 | } 1081 | ], 1082 | "location": { "width": 35, "top": 486, "left": 535, "height": 35 }, 1083 | "words": "限" 1084 | }, 1085 | { 1086 | "chars": [ 1087 | { 1088 | "char": "时", 1089 | "location": { "width": 26, "top": 512, "left": 534, "height": 42 } 1090 | }, 1091 | { 1092 | "char": "新", 1093 | "location": { "width": 26, "top": 512, "left": 597, "height": 42 } 1094 | }, 1095 | { 1096 | "char": "服", 1097 | "location": { "width": 26, "top": 512, "left": 622, "height": 42 } 1098 | }, 1099 | { 1100 | "char": "活", 1101 | "location": { "width": 25, "top": 512, "left": 661, "height": 42 } 1102 | }, 1103 | { 1104 | "char": "动", 1105 | "location": { "width": 26, "top": 512, "left": 699, "height": 42 } 1106 | } 1107 | ], 1108 | "location": { "width": 202, "top": 512, "left": 534, "height": 42 }, 1109 | "words": "时新服活动" 1110 | }, 1111 | { 1112 | "chars": [ 1113 | { 1114 | "char": "【", 1115 | "location": { "width": 22, "top": 538, "left": 927, "height": 43 } 1116 | }, 1117 | { 1118 | "char": "游", 1119 | "location": { "width": 26, "top": 538, "left": 955, "height": 43 } 1120 | }, 1121 | { 1122 | "char": "戏", 1123 | "location": { "width": 26, "top": 538, "left": 994, "height": 43 } 1124 | }, 1125 | { 1126 | "char": "特", 1127 | "location": { "width": 26, "top": 538, "left": 1020, "height": 43 } 1128 | }, 1129 | { 1130 | "char": "色", 1131 | "location": { "width": 27, "top": 538, "left": 1060, "height": 43 } 1132 | }, 1133 | { 1134 | "char": "】", 1135 | "location": { "width": 19, "top": 538, "left": 1095, "height": 43 } 1136 | } 1137 | ], 1138 | "location": { "width": 187, "top": 538, "left": 927, "height": 43 }, 1139 | "words": "【游戏特色】" 1140 | }, 1141 | { 1142 | "chars": [ 1143 | { 1144 | "char": "火", 1145 | "location": { "width": 20, "top": 612, "left": 537, "height": 33 } 1146 | } 1147 | ], 1148 | "location": { "width": 33, "top": 612, "left": 537, "height": 33 }, 1149 | "words": "火" 1150 | }, 1151 | { 1152 | "chars": [ 1153 | { 1154 | "char": "1", 1155 | "location": { "width": 21, "top": 588, "left": 992, "height": 43 } 1156 | }, 1157 | { 1158 | "char": ".", 1159 | "location": { "width": 21, "top": 588, "left": 1005, "height": 43 } 1160 | }, 1161 | { 1162 | "char": "超", 1163 | "location": { "width": 26, "top": 588, "left": 1036, "height": 43 } 1164 | }, 1165 | { 1166 | "char": "宏", 1167 | "location": { "width": 26, "top": 588, "left": 1076, "height": 43 } 1168 | }, 1169 | { 1170 | "char": "伟", 1171 | "location": { "width": 26, "top": 588, "left": 1101, "height": 43 } 1172 | }, 1173 | { 1174 | "char": "世", 1175 | "location": { "width": 26, "top": 588, "left": 1141, "height": 43 } 1176 | }, 1177 | { 1178 | "char": "界", 1179 | "location": { "width": 26, "top": 588, "left": 1167, "height": 43 } 1180 | }, 1181 | { 1182 | "char": "观", 1183 | "location": { "width": 26, "top": 588, "left": 1206, "height": 43 } 1184 | }, 1185 | { 1186 | "char": "、", 1187 | "location": { "width": 21, "top": 588, "left": 1228, "height": 43 } 1188 | }, 1189 | { 1190 | "char": "六", 1191 | "location": { "width": 26, "top": 588, "left": 1271, "height": 43 } 1192 | }, 1193 | { 1194 | "char": "大", 1195 | "location": { "width": 26, "top": 588, "left": 1297, "height": 43 } 1196 | }, 1197 | { 1198 | "char": "种", 1199 | "location": { "width": 26, "top": 588, "left": 1337, "height": 43 } 1200 | }, 1201 | { 1202 | "char": "族", 1203 | "location": { "width": 26, "top": 588, "left": 1363, "height": 43 } 1204 | }, 1205 | { 1206 | "char": "秘", 1207 | "location": { "width": 26, "top": 588, "left": 1403, "height": 43 } 1208 | }, 1209 | { 1210 | "char": "密", 1211 | "location": { "width": 26, "top": 588, "left": 1429, "height": 43 } 1212 | }, 1213 | { 1214 | "char": "等", 1215 | "location": { "width": 26, "top": 588, "left": 1468, "height": 43 } 1216 | }, 1217 | { 1218 | "char": "你", 1219 | "location": { "width": 26, "top": 588, "left": 1493, "height": 43 } 1220 | }, 1221 | { 1222 | "char": "探", 1223 | "location": { "width": 26, "top": 588, "left": 1533, "height": 43 } 1224 | }, 1225 | { 1226 | "char": "索", 1227 | "location": { "width": 26, "top": 588, "left": 1559, "height": 43 } 1228 | }, 1229 | { 1230 | "char": ";", 1231 | "location": { "width": 18, "top": 588, "left": 1595, "height": 43 } 1232 | } 1233 | ], 1234 | "location": { "width": 642, "top": 588, "left": 971, "height": 43 }, 1235 | "words": "1.超宏伟世界观、六大种族秘密等你探索;" 1236 | }, 1237 | { 1238 | "chars": [ 1239 | { 1240 | "char": "爆", 1241 | "location": { "width": 27, "top": 635, "left": 534, "height": 44 } 1242 | }, 1243 | { 1244 | "char": "严", 1245 | "location": { "width": 27, "top": 635, "left": 588, "height": 44 } 1246 | }, 1247 | { 1248 | "char": "禁", 1249 | "location": { "width": 26, "top": 635, "left": 628, "height": 44 } 1250 | }, 1251 | { 1252 | "char": "代", 1253 | "location": { "width": 27, "top": 635, "left": 668, "height": 44 } 1254 | }, 1255 | { 1256 | "char": "充", 1257 | "location": { "width": 27, "top": 635, "left": 708, "height": 44 } 1258 | }, 1259 | { 1260 | "char": "公", 1261 | "location": { "width": 26, "top": 635, "left": 735, "height": 44 } 1262 | }, 1263 | { 1264 | "char": "告", 1265 | "location": { "width": 27, "top": 635, "left": 774, "height": 44 } 1266 | } 1267 | ], 1268 | "location": { "width": 273, "top": 635, "left": 534, "height": 44 }, 1269 | "words": "爆严禁代充公告" 1270 | }, 1271 | { 1272 | "chars": [ 1273 | { 1274 | "char": "2", 1275 | "location": { "width": 22, "top": 642, "left": 992, "height": 43 } 1276 | }, 1277 | { 1278 | "char": ".", 1279 | "location": { "width": 21, "top": 642, "left": 1015, "height": 43 } 1280 | }, 1281 | { 1282 | "char": "全", 1283 | "location": { "width": 26, "top": 642, "left": 1032, "height": 43 } 1284 | }, 1285 | { 1286 | "char": "民", 1287 | "location": { "width": 26, "top": 642, "left": 1072, "height": 43 } 1288 | }, 1289 | { 1290 | "char": "打", 1291 | "location": { "width": 26, "top": 642, "left": 1099, "height": 43 } 1292 | }, 1293 | { 1294 | "char": "宝", 1295 | "location": { "width": 27, "top": 642, "left": 1138, "height": 43 } 1296 | }, 1297 | { 1298 | "char": "得", 1299 | "location": { "width": 26, "top": 642, "left": 1165, "height": 43 } 1300 | }, 1301 | { 1302 | "char": "神", 1303 | "location": { "width": 27, "top": 642, "left": 1204, "height": 43 } 1304 | }, 1305 | { 1306 | "char": "装", 1307 | "location": { "width": 26, "top": 642, "left": 1231, "height": 43 } 1308 | }, 1309 | { 1310 | "char": ",", 1311 | "location": { "width": 21, "top": 642, "left": 1267, "height": 43 } 1312 | }, 1313 | { 1314 | "char": "自", 1315 | "location": { "width": 26, "top": 642, "left": 1297, "height": 43 } 1316 | }, 1317 | { 1318 | "char": "由", 1319 | "location": { "width": 26, "top": 642, "left": 1337, "height": 43 } 1320 | }, 1321 | { 1322 | "char": "交", 1323 | "location": { "width": 26, "top": 642, "left": 1364, "height": 43 } 1324 | }, 1325 | { 1326 | "char": "易", 1327 | "location": { "width": 27, "top": 642, "left": 1403, "height": 43 } 1328 | }, 1329 | { 1330 | "char": "换", 1331 | "location": { "width": 27, "top": 642, "left": 1429, "height": 43 } 1332 | }, 1333 | { 1334 | "char": "货", 1335 | "location": { "width": 27, "top": 642, "left": 1469, "height": 43 } 1336 | }, 1337 | { 1338 | "char": "币", 1339 | "location": { "width": 26, "top": 642, "left": 1496, "height": 43 } 1340 | }, 1341 | { 1342 | "char": ",", 1343 | "location": { "width": 21, "top": 642, "left": 1532, "height": 43 } 1344 | }, 1345 | { 1346 | "char": "极", 1347 | "location": { "width": 26, "top": 642, "left": 1563, "height": 43 } 1348 | }, 1349 | { 1350 | "char": "速", 1351 | "location": { "width": 26, "top": 642, "left": 1589, "height": 43 } 1352 | }, 1353 | { 1354 | "char": "致", 1355 | "location": { "width": 26, "top": 642, "left": 1629, "height": 43 } 1356 | }, 1357 | { 1358 | "char": "富", 1359 | "location": { "width": 27, "top": 642, "left": 1668, "height": 43 } 1360 | }, 1361 | { 1362 | "char": ";", 1363 | "location": { "width": 22, "top": 642, "left": 1689, "height": 43 } 1364 | } 1365 | ], 1366 | "location": { "width": 719, "top": 642, "left": 992, "height": 43 }, 1367 | "words": "2.全民打宝得神装,自由交易换货币,极速致富;" 1368 | }, 1369 | { 1370 | "chars": [ 1371 | { 1372 | "char": "3", 1373 | "location": { "width": 21, "top": 693, "left": 994, "height": 42 } 1374 | }, 1375 | { 1376 | "char": ".", 1377 | "location": { "width": 21, "top": 693, "left": 1014, "height": 42 } 1378 | }, 1379 | { 1380 | "char": "神", 1381 | "location": { "width": 25, "top": 693, "left": 1031, "height": 42 } 1382 | }, 1383 | { 1384 | "char": "装", 1385 | "location": { "width": 25, "top": 693, "left": 1069, "height": 42 } 1386 | }, 1387 | { 1388 | "char": "极", 1389 | "location": { "width": 26, "top": 693, "left": 1094, "height": 42 } 1390 | }, 1391 | { 1392 | "char": "速", 1393 | "location": { "width": 25, "top": 693, "left": 1133, "height": 42 } 1394 | }, 1395 | { 1396 | "char": "进", 1397 | "location": { "width": 26, "top": 693, "left": 1170, "height": 42 } 1398 | }, 1399 | { 1400 | "char": "阶", 1401 | "location": { "width": 26, "top": 693, "left": 1195, "height": 42 } 1402 | }, 1403 | { 1404 | "char": ",", 1405 | "location": { "width": 20, "top": 693, "left": 1229, "height": 42 } 1406 | }, 1407 | { 1408 | "char": "炼", 1409 | "location": { "width": 25, "top": 693, "left": 1259, "height": 42 } 1410 | }, 1411 | { 1412 | "char": "化", 1413 | "location": { "width": 26, "top": 693, "left": 1296, "height": 42 } 1414 | }, 1415 | { 1416 | "char": "玩", 1417 | "location": { "width": 25, "top": 693, "left": 1334, "height": 42 } 1418 | }, 1419 | { 1420 | "char": "法", 1421 | "location": { "width": 25, "top": 693, "left": 1359, "height": 42 } 1422 | }, 1423 | { 1424 | "char": "打", 1425 | "location": { "width": 26, "top": 693, "left": 1397, "height": 42 } 1426 | }, 1427 | { 1428 | "char": "造", 1429 | "location": { "width": 25, "top": 693, "left": 1436, "height": 42 } 1430 | }, 1431 | { 1432 | "char": "独", 1433 | "location": { "width": 25, "top": 693, "left": 1461, "height": 42 } 1434 | }, 1435 | { 1436 | "char": "特", 1437 | "location": { "width": 26, "top": 693, "left": 1497, "height": 42 } 1438 | }, 1439 | { 1440 | "char": "个", 1441 | "location": { "width": 25, "top": 693, "left": 1536, "height": 42 } 1442 | }, 1443 | { 1444 | "char": "性", 1445 | "location": { "width": 25, "top": 693, "left": 1561, "height": 42 } 1446 | }, 1447 | { 1448 | "char": "神", 1449 | "location": { "width": 26, "top": 693, "left": 1599, "height": 42 } 1450 | }, 1451 | { 1452 | "char": "装", 1453 | "location": { "width": 26, "top": 693, "left": 1624, "height": 42 } 1454 | }, 1455 | { 1456 | "char": ";", 1457 | "location": { "width": 21, "top": 693, "left": 1657, "height": 42 } 1458 | } 1459 | ], 1460 | "location": { "width": 686, "top": 693, "left": 994, "height": 42 }, 1461 | "words": "3.神装极速进阶,炼化玩法打造独特个性神装;" 1462 | }, 1463 | { 1464 | "chars": [ 1465 | { 1466 | "char": "4", 1467 | "location": { "width": 19, "top": 746, "left": 996, "height": 38 } 1468 | }, 1469 | { 1470 | "char": ".", 1471 | "location": { "width": 19, "top": 746, "left": 1008, "height": 38 } 1472 | }, 1473 | { 1474 | "char": "独", 1475 | "location": { "width": 22, "top": 746, "left": 1036, "height": 38 } 1476 | }, 1477 | { 1478 | "char": "创", 1479 | "location": { "width": 23, "top": 746, "left": 1070, "height": 38 } 1480 | }, 1481 | { 1482 | "char": "十", 1483 | "location": { "width": 23, "top": 746, "left": 1106, "height": 38 } 1484 | }, 1485 | { 1486 | "char": "二", 1487 | "location": { "width": 23, "top": 746, "left": 1141, "height": 38 } 1488 | }, 1489 | { 1490 | "char": "星", 1491 | "location": { "width": 23, "top": 746, "left": 1165, "height": 38 } 1492 | }, 1493 | { 1494 | "char": "使", 1495 | "location": { "width": 23, "top": 746, "left": 1199, "height": 38 } 1496 | }, 1497 | { 1498 | "char": "助", 1499 | "location": { "width": 23, "top": 746, "left": 1235, "height": 38 } 1500 | }, 1501 | { 1502 | "char": "战", 1503 | "location": { "width": 23, "top": 746, "left": 1270, "height": 38 } 1504 | }, 1505 | { 1506 | "char": "系", 1507 | "location": { "width": 23, "top": 746, "left": 1305, "height": 38 } 1508 | }, 1509 | { 1510 | "char": "统", 1511 | "location": { "width": 23, "top": 746, "left": 1328, "height": 38 } 1512 | }, 1513 | { 1514 | "char": ",", 1515 | "location": { "width": 19, "top": 746, "left": 1360, "height": 38 } 1516 | }, 1517 | { 1518 | "char": "五", 1519 | "location": { "width": 22, "top": 746, "left": 1400, "height": 38 } 1520 | }, 1521 | { 1522 | "char": "大", 1523 | "location": { "width": 23, "top": 746, "left": 1435, "height": 38 } 1524 | }, 1525 | { 1526 | "char": "魔", 1527 | "location": { "width": 23, "top": 746, "left": 1457, "height": 38 } 1528 | }, 1529 | { 1530 | "char": "幻", 1531 | "location": { "width": 22, "top": 746, "left": 1494, "height": 38 } 1532 | }, 1533 | { 1534 | "char": "星", 1535 | "location": { "width": 23, "top": 746, "left": 1529, "height": 38 } 1536 | }, 1537 | { 1538 | "char": "使", 1539 | "location": { "width": 23, "top": 746, "left": 1564, "height": 38 } 1540 | }, 1541 | { 1542 | "char": "技", 1543 | "location": { "width": 23, "top": 746, "left": 1599, "height": 38 } 1544 | }, 1545 | { 1546 | "char": "能", 1547 | "location": { "width": 23, "top": 746, "left": 1623, "height": 38 } 1548 | }, 1549 | { 1550 | "char": "逆", 1551 | "location": { "width": 23, "top": 746, "left": 1659, "height": 38 } 1552 | }, 1553 | { 1554 | "char": "转", 1555 | "location": { "width": 23, "top": 746, "left": 1693, "height": 38 } 1556 | }, 1557 | { 1558 | "char": "战", 1559 | "location": { "width": 23, "top": 746, "left": 1728, "height": 38 } 1560 | }, 1561 | { 1562 | "char": "局", 1563 | "location": { "width": 22, "top": 746, "left": 1765, "height": 38 } 1564 | }, 1565 | { 1566 | "char": ";", 1567 | "location": { "width": 15, "top": 746, "left": 1794, "height": 38 } 1568 | } 1569 | ], 1570 | "location": { "width": 821, "top": 746, "left": 989, "height": 38 }, 1571 | "words": "4.独创十二星使助战系统,五大魔幻星使技能逆转战局;" 1572 | }, 1573 | { 1574 | "chars": [ 1575 | { 1576 | "char": "5", 1577 | "location": { "width": 21, "top": 795, "left": 992, "height": 41 } 1578 | }, 1579 | { 1580 | "char": ".", 1581 | "location": { "width": 21, "top": 795, "left": 1013, "height": 41 } 1582 | }, 1583 | { 1584 | "char": "六", 1585 | "location": { "width": 25, "top": 795, "left": 1043, "height": 41 } 1586 | }, 1587 | { 1588 | "char": "大", 1589 | "location": { "width": 25, "top": 795, "left": 1067, "height": 41 } 1590 | }, 1591 | { 1592 | "char": "化", 1593 | "location": { "width": 25, "top": 795, "left": 1103, "height": 41 } 1594 | }, 1595 | { 1596 | "char": "神", 1597 | "location": { "width": 25, "top": 795, "left": 1128, "height": 41 } 1598 | }, 1599 | { 1600 | "char": "任", 1601 | "location": { "width": 25, "top": 795, "left": 1166, "height": 41 } 1602 | }, 1603 | { 1604 | "char": "意", 1605 | "location": { "width": 23, "top": 795, "left": 1204, "height": 41 } 1606 | }, 1607 | { 1608 | "char": "搭", 1609 | "location": { "width": 25, "top": 795, "left": 1228, "height": 41 } 1610 | }, 1611 | { 1612 | "char": "配", 1613 | "location": { "width": 25, "top": 795, "left": 1264, "height": 41 } 1614 | }, 1615 | { 1616 | "char": ",", 1617 | "location": { "width": 20, "top": 795, "left": 1297, "height": 41 } 1618 | }, 1619 | { 1620 | "char": "助", 1621 | "location": { "width": 23, "top": 795, "left": 1327, "height": 41 } 1622 | }, 1623 | { 1624 | "char": "力", 1625 | "location": { "width": 25, "top": 795, "left": 1364, "height": 41 } 1626 | }, 1627 | { 1628 | "char": "培", 1629 | "location": { "width": 25, "top": 795, "left": 1400, "height": 41 } 1630 | }, 1631 | { 1632 | "char": "养", 1633 | "location": { "width": 25, "top": 795, "left": 1438, "height": 41 } 1634 | }, 1635 | { 1636 | "char": "个", 1637 | "location": { "width": 25, "top": 795, "left": 1463, "height": 41 } 1638 | }, 1639 | { 1640 | "char": "性", 1641 | "location": { "width": 23, "top": 795, "left": 1501, "height": 41 } 1642 | }, 1643 | { 1644 | "char": "化", 1645 | "location": { "width": 25, "top": 795, "left": 1525, "height": 41 } 1646 | }, 1647 | { 1648 | "char": "职", 1649 | "location": { "width": 25, "top": 795, "left": 1563, "height": 41 } 1650 | }, 1651 | { 1652 | "char": "业", 1653 | "location": { "width": 25, "top": 795, "left": 1599, "height": 41 } 1654 | }, 1655 | { 1656 | "char": "属", 1657 | "location": { "width": 23, "top": 795, "left": 1624, "height": 41 } 1658 | }, 1659 | { 1660 | "char": "性", 1661 | "location": { "width": 25, "top": 795, "left": 1661, "height": 41 } 1662 | }, 1663 | { 1664 | "char": ";", 1665 | "location": { "width": 18, "top": 795, "left": 1694, "height": 41 } 1666 | } 1667 | ], 1668 | "location": { "width": 719, "top": 795, "left": 992, "height": 41 }, 1669 | "words": "5.六大化神任意搭配,助力培养个性化职业属性;" 1670 | }, 1671 | { 1672 | "chars": [ 1673 | { 1674 | "char": "6", 1675 | "location": { "width": 20, "top": 844, "left": 994, "height": 42 } 1676 | }, 1677 | { 1678 | "char": ".", 1679 | "location": { "width": 20, "top": 844, "left": 1014, "height": 42 } 1680 | }, 1681 | { 1682 | "char": "勇", 1683 | "location": { "width": 25, "top": 844, "left": 1030, "height": 42 } 1684 | }, 1685 | { 1686 | "char": "者", 1687 | "location": { "width": 26, "top": 844, "left": 1068, "height": 42 } 1688 | }, 1689 | { 1690 | "char": "转", 1691 | "location": { "width": 25, "top": 844, "left": 1106, "height": 42 } 1692 | }, 1693 | { 1694 | "char": "职", 1695 | "location": { "width": 25, "top": 844, "left": 1131, "height": 42 } 1696 | }, 1697 | { 1698 | "char": "突", 1699 | "location": { "width": 26, "top": 844, "left": 1168, "height": 42 } 1700 | }, 1701 | { 1702 | "char": "破", 1703 | "location": { "width": 26, "top": 844, "left": 1193, "height": 42 } 1704 | }, 1705 | { 1706 | "char": "属", 1707 | "location": { "width": 25, "top": 844, "left": 1232, "height": 42 } 1708 | }, 1709 | { 1710 | "char": "性", 1711 | "location": { "width": 25, "top": 844, "left": 1269, "height": 42 } 1712 | }, 1713 | { 1714 | "char": ",", 1715 | "location": { "width": 21, "top": 844, "left": 1291, "height": 42 } 1716 | }, 1717 | { 1718 | "char": "职", 1719 | "location": { "width": 25, "top": 844, "left": 1333, "height": 42 } 1720 | }, 1721 | { 1722 | "char": "业", 1723 | "location": { "width": 25, "top": 844, "left": 1371, "height": 42 } 1724 | }, 1725 | { 1726 | "char": "时", 1727 | "location": { "width": 25, "top": 844, "left": 1396, "height": 42 } 1728 | }, 1729 | { 1730 | "char": "装", 1731 | "location": { "width": 25, "top": 844, "left": 1433, "height": 42 } 1732 | }, 1733 | { 1734 | "char": "免", 1735 | "location": { "width": 38, "top": 844, "left": 1459, "height": 42 } 1736 | }, 1737 | { 1738 | "char": "费", 1739 | "location": { "width": 25, "top": 844, "left": 1496, "height": 42 } 1740 | }, 1741 | { 1742 | "char": "领", 1743 | "location": { "width": 25, "top": 844, "left": 1534, "height": 42 } 1744 | }, 1745 | { 1746 | "char": ";", 1747 | "location": { "width": 21, "top": 844, "left": 1555, "height": 42 } 1748 | } 1749 | ], 1750 | "location": { "width": 586, "top": 844, "left": 994, "height": 42 }, 1751 | "words": "6.勇者转职突破属性,职业时装免费领;" 1752 | }, 1753 | { 1754 | "chars": [ 1755 | { 1756 | "char": "7", 1757 | "location": { "width": 23, "top": 893, "left": 992, "height": 46 } 1758 | }, 1759 | { 1760 | "char": ".", 1761 | "location": { "width": 22, "top": 893, "left": 1002, "height": 46 } 1762 | }, 1763 | { 1764 | "char": "B", 1765 | "location": { "width": 23, "top": 893, "left": 1029, "height": 46 } 1766 | }, 1767 | { 1768 | "char": "O", 1769 | "location": { "width": 22, "top": 893, "left": 1044, "height": 46 } 1770 | }, 1771 | { 1772 | "char": "S", 1773 | "location": { "width": 23, "top": 893, "left": 1071, "height": 46 } 1774 | }, 1775 | { 1776 | "char": "S", 1777 | "location": { "width": 23, "top": 893, "left": 1100, "height": 46 } 1778 | }, 1779 | { 1780 | "char": "爆", 1781 | "location": { "width": 28, "top": 893, "left": 1118, "height": 46 } 1782 | }, 1783 | { 1784 | "char": "率", 1785 | "location": { "width": 27, "top": 893, "left": 1147, "height": 46 } 1786 | }, 1787 | { 1788 | "char": "1", 1789 | "location": { "width": 22, "top": 893, "left": 1171, "height": 46 } 1790 | }, 1791 | { 1792 | "char": "0", 1793 | "location": { "width": 22, "top": 893, "left": 1184, "height": 46 } 1794 | }, 1795 | { 1796 | "char": "0", 1797 | "location": { "width": 23, "top": 893, "left": 1212, "height": 46 } 1798 | }, 1799 | { 1800 | "char": "%", 1801 | "location": { "width": 28, "top": 893, "left": 1231, "height": 46 } 1802 | }, 1803 | { 1804 | "char": ",", 1805 | "location": { "width": 22, "top": 893, "left": 1255, "height": 46 } 1806 | }, 1807 | { 1808 | "char": "狂", 1809 | "location": { "width": 28, "top": 893, "left": 1287, "height": 46 } 1810 | }, 1811 | { 1812 | "char": "爆", 1813 | "location": { "width": 27, "top": 893, "left": 1331, "height": 46 } 1814 | }, 1815 | { 1816 | "char": "极", 1817 | "location": { "width": 28, "top": 893, "left": 1358, "height": 46 } 1818 | }, 1819 | { 1820 | "char": "品", 1821 | "location": { "width": 27, "top": 893, "left": 1387, "height": 46 } 1822 | }, 1823 | { 1824 | "char": "装", 1825 | "location": { "width": 28, "top": 893, "left": 1428, "height": 46 } 1826 | }, 1827 | { 1828 | "char": "备", 1829 | "location": { "width": 27, "top": 893, "left": 1456, "height": 46 } 1830 | }, 1831 | { 1832 | "char": ",", 1833 | "location": { "width": 22, "top": 893, "left": 1480, "height": 46 } 1834 | }, 1835 | { 1836 | "char": "轻", 1837 | "location": { "width": 28, "top": 893, "left": 1526, "height": 46 } 1838 | }, 1839 | { 1840 | "char": "松", 1841 | "location": { "width": 28, "top": 893, "left": 1555, "height": 46 } 1842 | }, 1843 | { 1844 | "char": "集", 1845 | "location": { "width": 28, "top": 893, "left": 1597, "height": 46 } 1846 | }, 1847 | { 1848 | "char": "齐", 1849 | "location": { "width": 28, "top": 893, "left": 1625, "height": 46 } 1850 | }, 1851 | { 1852 | "char": "全", 1853 | "location": { "width": 28, "top": 893, "left": 1653, "height": 46 } 1854 | }, 1855 | { 1856 | "char": "套", 1857 | "location": { "width": 27, "top": 893, "left": 1696, "height": 46 } 1858 | }, 1859 | { 1860 | "char": "神", 1861 | "location": { "width": 28, "top": 893, "left": 1724, "height": 46 } 1862 | }, 1863 | { 1864 | "char": "装", 1865 | "location": { "width": 28, "top": 893, "left": 1752, "height": 46 } 1866 | }, 1867 | { 1868 | "char": ";", 1869 | "location": { "width": 23, "top": 893, "left": 1775, "height": 46 } 1870 | } 1871 | ], 1872 | "location": { "width": 813, "top": 893, "left": 992, "height": 46 }, 1873 | "words": "7.BOSS爆率100%,狂爆极品装备,轻松集齐全套神装;" 1874 | }, 1875 | { 1876 | "chars": [ 1877 | { 1878 | "char": "8", 1879 | "location": { "width": 25, "top": 945, "left": 994, "height": 49 } 1880 | }, 1881 | { 1882 | "char": ".", 1883 | "location": { "width": 23, "top": 945, "left": 1008, "height": 49 } 1884 | }, 1885 | { 1886 | "char": "首", 1887 | "location": { "width": 44, "top": 945, "left": 1029, "height": 49 } 1888 | }, 1889 | { 1890 | "char": "日", 1891 | "location": { "width": 29, "top": 945, "left": 1072, "height": 49 } 1892 | }, 1893 | { 1894 | "char": "直", 1895 | "location": { "width": 29, "top": 945, "left": 1102, "height": 49 } 1896 | }, 1897 | { 1898 | "char": "升", 1899 | "location": { "width": 29, "top": 945, "left": 1132, "height": 49 } 1900 | }, 1901 | { 1902 | "char": "1", 1903 | "location": { "width": 23, "top": 945, "left": 1158, "height": 49 } 1904 | }, 1905 | { 1906 | "char": "5", 1907 | "location": { "width": 25, "top": 945, "left": 1172, "height": 49 } 1908 | }, 1909 | { 1910 | "char": "0", 1911 | "location": { "width": 25, "top": 945, "left": 1201, "height": 49 } 1912 | }, 1913 | { 1914 | "char": "级", 1915 | "location": { "width": 29, "top": 945, "left": 1222, "height": 49 } 1916 | }, 1917 | { 1918 | "char": ",", 1919 | "location": { "width": 25, "top": 945, "left": 1247, "height": 49 } 1920 | }, 1921 | { 1922 | "char": "登", 1923 | "location": { "width": 30, "top": 945, "left": 1281, "height": 49 } 1924 | }, 1925 | { 1926 | "char": "录", 1927 | "location": { "width": 29, "top": 945, "left": 1327, "height": 49 } 1928 | }, 1929 | { 1930 | "char": "即", 1931 | "location": { "width": 29, "top": 945, "left": 1357, "height": 49 } 1932 | }, 1933 | { 1934 | "char": "送", 1935 | "location": { "width": 29, "top": 945, "left": 1387, "height": 49 } 1936 | }, 1937 | { 1938 | "char": ":", 1939 | "location": { "width": 23, "top": 945, "left": 1412, "height": 49 } 1940 | }, 1941 | { 1942 | "char": "兽", 1943 | "location": { "width": 29, "top": 945, "left": 1446, "height": 49 } 1944 | }, 1945 | { 1946 | "char": "王", 1947 | "location": { "width": 29, "top": 945, "left": 1492, "height": 49 } 1948 | }, 1949 | { 1950 | "char": "天", 1951 | "location": { "width": 29, "top": 945, "left": 1521, "height": 49 } 1952 | }, 1953 | { 1954 | "char": "神", 1955 | "location": { "width": 29, "top": 945, "left": 1551, "height": 49 } 1956 | }, 1957 | { 1958 | "char": "、", 1959 | "location": { "width": 25, "top": 945, "left": 1575, "height": 49 } 1960 | }, 1961 | { 1962 | "char": "春", 1963 | "location": { "width": 29, "top": 945, "left": 1611, "height": 49 } 1964 | }, 1965 | { 1966 | "char": "日", 1967 | "location": { "width": 30, "top": 945, "left": 1655, "height": 49 } 1968 | }, 1969 | { 1970 | "char": "时", 1971 | "location": { "width": 29, "top": 945, "left": 1686, "height": 49 } 1972 | }, 1973 | { 1974 | "char": "装", 1975 | "location": { "width": 29, "top": 945, "left": 1716, "height": 49 } 1976 | }, 1977 | { 1978 | "char": "、", 1979 | "location": { "width": 25, "top": 945, "left": 1740, "height": 49 } 1980 | }, 1981 | { 1982 | "char": "雪", 1983 | "location": { "width": 29, "top": 945, "left": 1789, "height": 49 } 1984 | }, 1985 | { 1986 | "char": "豹", 1987 | "location": { "width": 28, "top": 945, "left": 1820, "height": 49 } 1988 | } 1989 | ], 1990 | "location": { "width": 879, "top": 945, "left": 968, "height": 49 }, 1991 | "words": "8.首日直升150级,登录即送:兽王天神、春日时装、雪豹" 1992 | } 1993 | ] 1994 | } 1995 | ``` 1996 | 1997 | ## 计算文字的位置 1998 | 1999 | 背景:百度OCR返回的文字都是json字典,希望能从对应匹配到的单词,找到对应的位置坐标信息 2000 | 2001 | 代码: 2002 | 2003 | ```python 2004 | def calcWordsLocation(self, wordStr, curWordsResult): 2005 | """Calculate words location from result 2006 | 2007 | 2008 | Args: 2009 | wordStr (str): the words to check 2010 | curWordsResult (dict): the baidu OCR result of current words 2011 | Returns: 2012 | location, a tuple (x, y, width, height) 2013 | Raises: 2014 | Examples 2015 | wordStr="首充" 2016 | curWordsResult= { 2017 | "chars": [ 2018 | { 2019 | "char": "寻", 2020 | "location": { 2021 | "width": 15, 2022 | "top": 51, 2023 | "left": 725, 2024 | "height": 24 2025 | } 2026 | }, 2027 | ... 2028 | { 2029 | "char": "首", 2030 | "location": { 2031 | "width": 15, 2032 | "top": 51, 2033 | "left": 971, 2034 | "height": 24 2035 | } 2036 | }, 2037 | { 2038 | "char": "充", 2039 | "location": { 2040 | "width": 15, 2041 | "top": 51, 2042 | "left": 986, 2043 | "height": 24 2044 | } 2045 | } 2046 | ], 2047 | "location": { 2048 | "width": 280, 2049 | "top": 51, 2050 | "left": 725, 2051 | "height": 24 2052 | }, 2053 | "words": "寻宝福利大厅商城首充" 2054 | } 2055 | -> (971, 51, 30, 24) 2056 | """ 2057 | (x, y, width, height) = (0, 0, 0, 0) 2058 | matchedStr = curWordsResult["words"] 2059 | # Note: for special, contain multilple words, here only process firt words 2060 | foundWords = re.search(wordStr, matchedStr) 2061 | if foundWords: 2062 | logging.debug("foundWords=%s" % foundWords) 2063 | 2064 | 2065 | firstMatchedPos = foundWords.start() 2066 | lastMatchedPos = foundWords.end() - 1 2067 | 2068 | 2069 | matchedStrLen = len(matchedStr) 2070 | charResultList = curWordsResult["chars"] 2071 | charResultListLen = len(charResultList) 2072 | 2073 | 2074 | firstCharResult = None 2075 | lastCharResult = None 2076 | if matchedStrLen == charResultListLen: 2077 | firstCharResult = charResultList[firstMatchedPos] 2078 | lastCharResult = charResultList[lastMatchedPos] 2079 | else: 2080 | # Special: for 'Loading' matched ' Loading', but charResultList not include first space ' ', but from fisrt='L' to end='g' 2081 | # so using find the corresponding char, then got its location 2082 | # Note: following method not work for regex str, like '^游戏公告$' 2083 | 2084 | 2085 | firtToMatchChar = wordStr[0] 2086 | lastToMatchChar = wordStr[-1] 2087 | 2088 | 2089 | for eachCharResult in charResultList: 2090 | if firstCharResult and lastCharResult: 2091 | break 2092 | 2093 | 2094 | eachChar = eachCharResult["char"] 2095 | if firtToMatchChar == eachChar: 2096 | firstCharResult = eachCharResult 2097 | elif lastToMatchChar == eachChar: 2098 | lastCharResult = eachCharResult 2099 | 2100 | 2101 | # Note: follow no need check words, to support input ^游戏公告$ to match "游戏公告" 2102 | # firstLocation = None 2103 | # lastLocation = None 2104 | # if firstCharResult["char"] == firtToMatchChar: 2105 | # firstLocation = firstCharResult["location"] 2106 | # if lastCharResult["char"] == lastToMatchChar: 2107 | # lastLocation = lastCharResult["location"] 2108 | firstLocation = firstCharResult["location"] 2109 | lastLocation = lastCharResult["location"] 2110 | 2111 | 2112 | # if firstLocation and lastLocation: 2113 | 2114 | 2115 | # support both horizontal and vertical words 2116 | firstLeft = firstLocation["left"] 2117 | lastLeft = lastLocation["left"] 2118 | minLeft = min(firstLeft, lastLeft) 2119 | x = minLeft 2120 | 2121 | 2122 | firstHorizontalEnd = firstLeft + firstLocation["width"] 2123 | lastHorizontalEnd = lastLeft + lastLocation["width"] 2124 | maxHorizontalEnd = max(firstHorizontalEnd, lastHorizontalEnd) 2125 | width = maxHorizontalEnd - x 2126 | 2127 | 2128 | lastTop = lastLocation["top"] 2129 | minTop = min(firstLocation["top"], lastTop) 2130 | y = minTop 2131 | 2132 | 2133 | lastVerticalEnd = lastTop + lastLocation["height"] 2134 | height = lastVerticalEnd - y 2135 | 2136 | 2137 | return x, y, width, height 2138 | ``` 2139 | 2140 | 调用: 2141 | 2142 | ```python 2143 | calculatedLocation = self.calcWordsLocation(eachInputWords, eachWordsMatchedResult) 2144 | ``` 2145 | 2146 | ## 把坐标位置转成中间坐标值 2147 | 2148 | 作用:用于后续点击按钮中间坐标值 2149 | 2150 | 代码: 2151 | 2152 | ```python 2153 | def locationToCenterPos(self, wordslocation): 2154 | """Convert location of normal button to center position 2155 | 2156 | 2157 | Args: 2158 | wordslocation (tuple): words location, (x, y, width, height) 2159 | Example: (267, 567, 140, 39) 2160 | Returns: 2161 | tuple, (x, y), the location's center position, normal used later to click it 2162 | Example: (337.0, 586.5) 2163 | Raises: 2164 | """ 2165 | x, y, width, height = wordslocation 2166 | centerX = x + width/2 2167 | centerY = y + height/2 2168 | centerPosition = (centerX, centerY) 2169 | return centerPosition 2170 | ``` 2171 | 2172 | 调用: 2173 | 2174 | ```python 2175 | curCenterX, curCenterY = self.locationToCenterPos(eachLocation) 2176 | ``` 2177 | 2178 | ## 检测文字是否在结果中 2179 | 2180 | 代码: 2181 | 2182 | ```python 2183 | def isWordsInResult(self, respJson, wordsOrWordsList, isMatchMultiple=False): 2184 | """Check words is in result or not 2185 | 2186 | 2187 | Args: 2188 | respJson (dict): Baidu OCR responsed json 2189 | wordsOrWordsList (str/list): single input str or str list 2190 | isMatchMultiple (bool): for each single str, to match multiple output or only match one output 2191 | Returns: 2192 | dict, matched result 2193 | Raises: 2194 | """ 2195 | # Note: use OrderedDict instead dict to keep order, for later get first match result to process 2196 | orderedMatchedResultDict = OrderedDict() 2197 | 2198 | 2199 | inputWordsList = wordsOrWordsList 2200 | if isinstance(wordsOrWordsList, str): 2201 | inputWords = str(wordsOrWordsList) 2202 | inputWordsList = [inputWords] 2203 | 2204 | 2205 | wordsResultList = respJson["words_result"] 2206 | for curInputWords in inputWordsList: 2207 | curMatchedResultList = [] 2208 | for eachWordsResult in wordsResultList: 2209 | eachWords = eachWordsResult["words"] 2210 | foundCurWords = re.search(curInputWords, eachWords) 2211 | if foundCurWords: 2212 | curMatchedResultList.append(eachWordsResult) 2213 | if not isMatchMultiple: 2214 | break 2215 | 2216 | 2217 | orderedMatchedResultDict[curInputWords] = curMatchedResultList 2218 | return orderedMatchedResultDict 2219 | ``` 2220 | 2221 | 调用: 2222 | 2223 | ```python 2224 | matchedResultDict = self.isWordsInResult(wordsResultJson, wordsOrWordsList, isMatchMultiple) 2225 | ``` 2226 | 2227 | ## 检测当前屏幕中是否包含对应文字 2228 | 2229 | ```python 2230 | def isWordsInCurScreen(self, wordsOrWordsList, imgPath=None, isMatchMultiple=False, isRespShortInfo=False): 2231 | """Found words in current screen 2232 | 2233 | 2234 | Args: 2235 | wordsOrWordsList (str/list): single input str or str list 2236 | imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image 2237 | isMatchMultiple (bool): for each single str, to match multiple output or only match one output; default=False 2238 | isRespShortInfo (bool): return simple=short=nomarlly bool or list[bool] info or return full info which contain imgPath and full matched result. 2239 | Returns: 2240 | matched result, type=bool/list[bool]/dict/tuple, depends on diffrent condition 2241 | Raises: 2242 | """ 2243 | retValue = None 2244 | 2245 | 2246 | if not imgPath: 2247 | # do screenshot 2248 | imgPath = self.getCurScreenshot() 2249 | 2250 | 2251 | wordsResultJson = self.baiduImageToWords(imgPath) 2252 | 2253 | 2254 | isMultipleInput = False 2255 | inputWords = None 2256 | inputWordsList = [] 2257 | 2258 | 2259 | if isinstance(wordsOrWordsList, list): 2260 | isMultipleInput = True 2261 | inputWordsList = list(wordsOrWordsList) 2262 | elif isinstance(wordsOrWordsList, str): 2263 | isMultipleInput = False 2264 | inputWords = str(wordsOrWordsList) 2265 | inputWordsList = [inputWords] 2266 | 2267 | 2268 | matchedResultDict = self.isWordsInResult(wordsResultJson, wordsOrWordsList, isMatchMultiple) 2269 | 2270 | 2271 | # add caclulated location and words 2272 | # Note: use OrderedDict instead dict to keep order, for later get first match result to process 2273 | processedResultDict = OrderedDict() 2274 | for eachInputWords in inputWordsList: 2275 | isCurFound = False 2276 | # curLocatoinList = [] 2277 | # curWordsList = [] 2278 | curResultList = [] 2279 | 2280 | 2281 | curWordsMatchedResultList = matchedResultDict[eachInputWords] 2282 | if curWordsMatchedResultList: 2283 | isCurFound = True 2284 | for curIdx, eachWordsMatchedResult in enumerate(curWordsMatchedResultList): 2285 | curMatchedWords = eachWordsMatchedResult["words"] 2286 | calculatedLocation = self.calcWordsLocation(eachInputWords, eachWordsMatchedResult) 2287 | # curLocatoinList.append(calculatedLocation) 2288 | # curWordsList.append(curMatchedWords) 2289 | curResult = (curMatchedWords, calculatedLocation) 2290 | curResultList.append(curResult) 2291 | 2292 | 2293 | # processedResultDict[eachInputWords] = (isCurFound, curLocatoinList, curWordsList) 2294 | processedResultDict[eachInputWords] = (isCurFound, curResultList) 2295 | logging.debug("For %s, matchedResult=%s from imgPath=%s", wordsOrWordsList, processedResultDict, imgPath) 2296 | 2297 | 2298 | if isMultipleInput: 2299 | if isRespShortInfo: 2300 | isFoundList = [] 2301 | for eachInputWords in processedResultDict.keys(): 2302 | isCurFound, noUse = processedResultDict[eachInputWords] 2303 | isFoundList.append(isCurFound) 2304 | # Note: no mattter isMatchMultiple, both only return single boolean for each input words 2305 | retBoolList = isFoundList 2306 | retValue = retBoolList 2307 | else: 2308 | if isMatchMultiple: 2309 | retTuple = processedResultDict, imgPath 2310 | retValue = retTuple 2311 | else: 2312 | # Note: use OrderedDict instead dict to keep order, for later get first match result to process 2313 | respResultDict = OrderedDict() 2314 | for eachInputWords in processedResultDict.keys(): 2315 | # isCurFound, curLocatoinList, curWordsList = processedResultDict[eachInputWords] 2316 | isCurFound, curResultList = processedResultDict[eachInputWords] 2317 | # singleLocation = None 2318 | # singleWords = None 2319 | singleResult = (None, None) 2320 | if isCurFound: 2321 | # singleLocation = curLocatoinList[0] 2322 | # singleWords = curWordsList[0] 2323 | singleResult = curResultList[0] 2324 | # respResultDict[eachInputWords] = (isCurFound, singleLocation, singleWords) 2325 | respResultDict[eachInputWords] = (isCurFound, singleResult) 2326 | retTuple = respResultDict, imgPath 2327 | retValue = retTuple 2328 | else: 2329 | singleInputResult = processedResultDict[inputWords] 2330 | # isCurFound, curLocatoinList, curWordsList = singleInputResult 2331 | isCurFound, curResultList = singleInputResult 2332 | if isRespShortInfo: 2333 | # Note: no mattter isMatchMultiple, both only return single boolean for each input words 2334 | retBool = isCurFound 2335 | retValue = retBool 2336 | else: 2337 | if isMatchMultiple: 2338 | # retTuple = isCurFound, curLocatoinList, curWordsList, imgPath 2339 | retTuple = isCurFound, curResultList, imgPath 2340 | retValue = retTuple 2341 | else: 2342 | singleResult = (None, None) 2343 | # singleLocation = None 2344 | # singleWords = None 2345 | if isCurFound: 2346 | # singleLocation = curLocatoinList[0] 2347 | # singleWords = curWordsList[0] 2348 | singleResult = curResultList[0] 2349 | # retTuple = isCurFound, singleLocation, singleWords, imgPath 2350 | retTuple = isCurFound, singleResult, imgPath 2351 | retValue = retTuple 2352 | 2353 | 2354 | logging.debug("Input: %s, output=%s", wordsOrWordsList, retValue) 2355 | return retValue 2356 | ``` 2357 | 2358 | 调用: 2359 | 2360 | ```python 2361 | allResultDict, _ = self.isWordsInCurScreen(allStrList, imgPath, isMatchMultiple=True) 2362 | ``` 2363 | 2364 | ## 获取当前屏幕中的文字 2365 | 2366 | ```python 2367 | def getWordsInCurScreen(self): 2368 | """get words in current screenshot""" 2369 | screenImgPath = self.getCurScreenshot() 2370 | wordsResultJson = self.baiduImageToWords(screenImgPath) 2371 | return wordsResultJson 2372 | ``` 2373 | 2374 | 调用: 2375 | 2376 | ```python 2377 | curScreenWords = self.getWordsInCurScreen() 2378 | ``` 2379 | 2380 | ## 检测当前屏幕中是否存在某些信息 2381 | 2382 | ```python 2383 | def checkExistInScreen(self, 2384 | imgPath=None, 2385 | mandatoryStrList=[], 2386 | mandatoryMinMatchCount=0, 2387 | optionalStrList=[], 2388 | # optionalMinMatchCount=2, 2389 | optionalMinMatchCount=1, 2390 | isRespFullInfo=False 2391 | ): 2392 | """Check whether mandatory and optional str list in current screen or not 2393 | 2394 | 2395 | Args: 2396 | imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image 2397 | mandatoryStrList (list): mandatory str, at least match `mandatoryMinMatchCount`, or all must match if `mandatoryMinMatchCount`=0 2398 | mandatoryMinMatchCount (int): minimal match count for mandatory list 2399 | optionalStrList (list): optional str, some may match 2400 | optionalMinMatchCount (int): for `optionalStrList`, the minimal match count, consider to match or not 2401 | isRespFullInfo (bool): return full info or not, full info means match location result and imgPath 2402 | Returns: 2403 | matched result, type=bool/tuple, depends on `isRespFullInfo` 2404 | Raises: 2405 | """ 2406 | if not imgPath: 2407 | imgPath = self.getCurScreenshot() 2408 | logging.debug("imgPath=%s", imgPath) 2409 | 2410 | 2411 | isExist = False 2412 | # Note: use OrderedDict instead dict to keep order, for later get first match result to process 2413 | respMatchLocation = OrderedDict() 2414 | 2415 | 2416 | isMandatoryMatch = True 2417 | isMandatoryShouldMatchAll = (mandatoryMinMatchCount <= 0) 2418 | isOptionalMatch = True 2419 | 2420 | 2421 | allStrList = [] 2422 | allStrList.extend(mandatoryStrList) 2423 | allStrList.extend(optionalStrList) 2424 | 2425 | 2426 | optionalMatchCount = 0 2427 | mandatoryMatchCount = 0 2428 | allResultDict, _ = self.isWordsInCurScreen(allStrList, imgPath, isMatchMultiple=True) 2429 | for eachStr, (isFoundCur, curResultList) in allResultDict.items(): 2430 | if eachStr in mandatoryStrList: 2431 | if isFoundCur: 2432 | mandatoryMatchCount += 1 2433 | respMatchLocation[eachStr] = curResultList 2434 | else: 2435 | if isMandatoryShouldMatchAll: 2436 | isMandatoryMatch = False 2437 | break 2438 | elif eachStr in optionalStrList: 2439 | if isFoundCur: 2440 | optionalMatchCount += 1 2441 | respMatchLocation[eachStr] = curResultList 2442 | 2443 | 2444 | if mandatoryStrList: 2445 | if not isMandatoryShouldMatchAll: 2446 | if mandatoryMatchCount >= mandatoryMinMatchCount: 2447 | isMandatoryMatch = True 2448 | else: 2449 | isMandatoryMatch = False 2450 | 2451 | 2452 | if optionalStrList: 2453 | if optionalMatchCount >= optionalMinMatchCount: 2454 | isOptionalMatch = True 2455 | else: 2456 | isOptionalMatch = False 2457 | 2458 | 2459 | isExist = isMandatoryMatch and isOptionalMatch 2460 | logging.debug("isMandatoryMatch=%s, isOptionalMatch=%s -> isExist=%s", isMandatoryMatch, isOptionalMatch, isExist) 2461 | 2462 | 2463 | if isRespFullInfo: 2464 | logging.debug("mandatoryStrList=%s, optionalStrList=%s -> isExist=%s, respMatchLocation=%s, imgPath=%s", 2465 | mandatoryStrList, optionalStrList, isExist, respMatchLocation, imgPath) 2466 | return (isExist, respMatchLocation, imgPath) 2467 | else: 2468 | logging.debug("mandatoryStrList=%s, optionalStrList=%s -> isExist=%s", 2469 | mandatoryStrList, optionalStrList, isExist) 2470 | return isExist 2471 | ``` 2472 | 2473 | 调用: 2474 | 2475 | ```python 2476 | checkResult = self.checkExistInScreen( 2477 | imgPath=imgPath, 2478 | optionalStrList=strList, 2479 | optionalMinMatchCount=1, 2480 | isRespFullInfo=isRespFullInfo, 2481 | ) 2482 | ``` 2483 | 2484 | 和: 2485 | 2486 | ```python 2487 | minOptionalMatchCount = 2 2488 | 2489 | mandatoryList = [ 2490 | # "公告", 2491 | "^公告$", 2492 | '^强力推荐$', # 王城英雄, 不规则弹框 2493 | 。。。 2494 | ] 2495 | 2496 | possibleTitleList = [ 2497 | "^游戏公告$", 2498 | ] 2499 | optionalList = [] 2500 | otherOptionalList = [ 2501 | "新增(内容)?", 2502 | "游戏特色", 2503 | "(主要)?更新(内容)?", 2504 | 。。。 2505 | "登录即送", 2506 | ] 2507 | optionalList.extend(possibleTitleList) 2508 | optionalList.extend(otherOptionalList) 2509 | 2510 | 2511 | checkResult = self.checkExistInScreen( 2512 | imgPath=imgPath, 2513 | mandatoryStrList=mandatoryList, 2514 | mandatoryMinMatchCount=1, 2515 | optionalStrList=optionalList, 2516 | optionalMinMatchCount=minOptionalMatchCount, 2517 | isRespFullInfo=isRespFullInfo, 2518 | ) 2519 | ``` 2520 | 2521 | 和: 2522 | 2523 | ```python 2524 | mandatoryList = [ 2525 | "^购买$", # 造梦西游:'购买' 2526 | "^¥\d+元?", # 剑玲珑, 至尊屠龙 2527 | "^\d+元", # 造梦西游:'6元券3元券3’ 2528 | "^充值$", # 剑玲珑, 至尊屠龙 2529 | ] 2530 | optionalList = [ 2531 | # common 2532 | "元宝", 2533 | "月卡", 2534 | 。。。 2535 | # 剑玲珑 2536 | "赠", 2537 | ] 2538 | 2539 | isRealPay, matchResult, imgPath = self.checkExistInScreen( 2540 | mandatoryStrList=mandatoryList, 2541 | mandatoryMinMatchCount=2, 2542 | optionalStrList=optionalList, 2543 | optionalMinMatchCount=2, 2544 | isRespFullInfo=True, 2545 | ) 2546 | ``` 2547 | 2548 | 和: 2549 | 2550 | ```python 2551 | mandatoryList = [ 2552 | "^((继续)|(结束))$", # 暗黑觉醒:'继续' or '结束' 2553 | "^\d秒$", # 暗黑觉醒:'5秒', '1秒' 2554 | ] 2555 | 2556 | isAutoConverstion, matchResult, imgPath = self.checkExistInScreen( 2557 | imgPath=imgPath, 2558 | mandatoryStrList=mandatoryList, 2559 | mandatoryMinMatchCount=2, 2560 | isRespFullInfo=True, 2561 | ) 2562 | ``` 2563 | 2564 | 和: 2565 | 2566 | ```python 2567 | mandatoryList = [ 2568 | "^充值", # 造梦西游: "充值战神榜邮件各但,挑战竞技好友" 2569 | "首充$", # 剑玲珑,至尊屠龙 2570 | 。。。 2571 | ] 2572 | optionalList = [ 2573 | # common 2574 | "组队", # 至尊屠龙, 剑玲珑 2575 | "任务", # 至尊屠龙, 剑玲珑 2576 | 2577 | # "商城", # 剑玲珑 2578 | "寻宝", # 剑玲珑 2579 | "福利大厅", # 剑玲珑 2580 | 。。。 2581 | "背包", 2582 | ] 2583 | 2584 | isHome, matchResult, imgPath = self.checkExistInScreen( 2585 | mandatoryStrList=mandatoryList, 2586 | mandatoryMinMatchCount=1, 2587 | optionalStrList=optionalList, 2588 | isRespFullInfo=True, 2589 | ) 2590 | ``` 2591 | 2592 | 和: 2593 | 2594 | ```python 2595 | mandatoryList = [ 2596 | "立即登录", # 剑玲珑, 至尊屠龙 2597 | 。。。 2598 | "^登录$", # 暗黑觉醒 2599 | ] 2600 | optionalList = [ 2601 | # common 2602 | "一键((注册)|(试玩))", 2603 | "忘记密码", # 至尊屠龙, 青云诀 2604 | "((用户)|(账号)|(手机))登录", # 剑玲珑, 至尊屠龙, 2605 | "点击选服", # 青云诀,暗黑觉醒 2606 | 。。。 2607 | # 造梦西游 2608 | "游客登录", #更新:不能用游客登录,否则后续无法弹出支付页面 2609 | ] 2610 | 2611 | respUserLogin = self.checkExistInScreen( 2612 | mandatoryStrList=mandatoryList, 2613 | mandatoryMinMatchCount=1, 2614 | optionalStrList=optionalList, 2615 | isRespFullInfo=isRespFullInfo 2616 | ) 2617 | ``` 2618 | 2619 | ## 是否存在任意一个词组 2620 | 2621 | ```python 2622 | def isExistAnyStr(self, strList, imgPath=None, isRespFullInfo=False): 2623 | """Is any str exist or not 2624 | 2625 | 2626 | Args: 2627 | strList (list): str list to check exist or not 2628 | imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image 2629 | isRespFullInfo (bool): return full info or not, full info means match location result and imgPath 2630 | Returns: 2631 | matched result, type=bool/tuple, depends on `isRespFullInfo` 2632 | Raises: 2633 | """ 2634 | if not imgPath: 2635 | imgPath = self.getCurScreenshot() 2636 | 2637 | 2638 | checkResult = self.checkExistInScreen( 2639 | imgPath=imgPath, 2640 | optionalStrList=strList, 2641 | optionalMinMatchCount=1, 2642 | isRespFullInfo=isRespFullInfo, 2643 | ) 2644 | if isRespFullInfo: 2645 | isExistAny, matchResult, imgPath = checkResult 2646 | logging.debug("isExistAny=%s, matchResult=%s, imgPath=%s for %s", isExistAny, matchResult, imgPath, strList) 2647 | return (isExistAny, matchResult, imgPath) 2648 | else: 2649 | isExistAny = checkResult 2650 | logging.debug("isExistAny=%s, for %s", isExistAny, strList) 2651 | return isExistAny 2652 | ``` 2653 | 2654 | 调用: 2655 | 2656 | ```python 2657 | isExist, matchResult, imgPath = self.isExistAnyStr(buttonStrList, imgPath=imgPath, isRespFullInfo=True) 2658 | ``` 2659 | 2660 | 和: 2661 | 2662 | ```python 2663 | mandatoryList = [ 2664 | # 御剑仙缘 2665 | """^(请)?点击\s?[“”"'][^“”"',]{1,6}[“”"']?""", # '请点击“战骑','请点击“魂兽”','请点击“人物”','请点击“领取”','请点击“仙盟”','点击“+”,放入吞噬','点击“进阶”,提升战',请点击“每日必做”按 2666 | ] 2667 | 2668 | respResult = self.isExistAnyStr(mandatoryList, imgPath=imgPath, isRespFullInfo=isRespFullInfo) 2669 | ``` 2670 | 2671 | 和: 2672 | 2673 | ```python 2674 | requireManualOperationList = [ 2675 | "完成指定操作", # speical:造梦西游 的 '(请完成指定操作)梦口' 的顶部弹框 2676 | ] 2677 | isRequireManual, _, imgPath = self.isExistAnyStr(requireManualOperationList, isRespFullInfo=True) 2678 | ``` 2679 | 2680 | 和: 2681 | 2682 | ```python 2683 | lanuchStrList = [ 2684 | "^4399手机游戏$", # 剑玲珑 2685 | "^西瓜游戏$", # 青云诀 2686 | ] 2687 | isLaunch, _, imgPath = self.isExistAnyStr(lanuchStrList, isRespFullInfo=True) 2688 | ``` 2689 | 2690 | 和: 2691 | 2692 | ```python 2693 | loadingStrList = [ 2694 | # 登录类 2695 | "正在登录", # 正在登录 2696 | "logging", 2697 | 。。。 2698 | "游戏资源", # 青云诀:本地游戏资源已是最新 2699 | 。。。 2700 | ] 2701 | 2702 | isLoadingSth, _, imgPath = self.isExistAnyStr(loadingStrList, isRespFullInfo=True) 2703 | ``` 2704 | 2705 | 和: 2706 | 2707 | ```python 2708 | gotoPayStrList = [ 2709 | "^前往充值$", # 剑玲珑 2710 | "^立即充值$", # 至尊屠龙 2711 | 。。。 2712 | ] 2713 | respBoolOrTuple = self.isExistAnyStr(gotoPayStrList, isRespFullInfo=isRespLocation) 2714 | ``` 2715 | 2716 | ## 判断是否所有的字符都存在 2717 | 2718 | ```python 2719 | def isExistAllStr(self, strList, imgPath=None, isRespFullInfo=False): 2720 | """Is all str exist or not 2721 | 2722 | 2723 | Args: 2724 | strList (list): str list to check exist or not 2725 | imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image 2726 | isRespFullInfo (bool): return full info or not, full info means match location result and imgPath 2727 | Returns: 2728 | matched result, type=bool/tuple, depends on `isRespFullInfo` 2729 | Raises: 2730 | """ 2731 | if not imgPath: 2732 | imgPath = self.getCurScreenshot() 2733 | checkResult = self.checkExistInScreen(imgPath=imgPath, mandatoryStrList=strList, isRespFullInfo=isRespFullInfo) 2734 | if isRespFullInfo: 2735 | isExistAll, matchResult, imgPath = checkResult 2736 | logging.debug("isExistAll=%s, matchResult=%s, imgPath=%s for %s", isExistAll, matchResult, imgPath, strList) 2737 | return (isExistAll, matchResult, imgPath) 2738 | else: 2739 | isExistAll = checkResult 2740 | logging.debug("isExistAll=%s, for %s", isExistAll, strList) 2741 | return isExistAll 2742 | ``` 2743 | 2744 | 调用: 2745 | 2746 | ```python 2747 | realPayStrList = [ 2748 | "^¥\d+元?", # 剑玲珑, 至尊屠龙 2749 | "^充值$", # 剑玲珑, 至尊屠龙 2750 | ] 2751 | return self.isExistAllStr(realPayStrList, isRespFullInfo=isRespLocation) 2752 | ``` 2753 | -------------------------------------------------------------------------------- /src/common_code/multimedia/image/pillow.md: -------------------------------------------------------------------------------- 1 | # Pillow 2 | 3 | * Pillow 4 | * 继承自:`PIL` 5 | * `PIL` = `Python Imaging Library` 6 | * 官网资料: 7 | * [Image Module — Pillow (PIL Fork) 7.0.0 documentation](https://pillow.readthedocs.io/en/stable/reference/Image.html) 8 | * [Image Module — Pillow (PIL Fork) 3.1.2 documentation](https://pillow.readthedocs.io/en/3.1.x/reference/Image.html) 9 | 10 | ## 从二进制生成Image 11 | 12 | ```python 13 | if isinstance(inputImage, bytes): 14 | openableImage = io.BytesIO(inputImage) 15 | curPillowImage = Image.open(openableImage) 16 | ``` 17 | 18 | pillow变量是: 19 | 20 | ```bash 21 | # 22 | # 23 | ``` 24 | 25 | 详见: 26 | 27 | * 【已解决】Python如何从二进制数据中生成Pillow的Image 28 | * 【已解决】Python的Pillow如何从二进制数据中读取图像数据 29 | 30 | ## 从Pillow的Image获取二进制数据 31 | 32 | ```python 33 | import io 34 | 35 | imageIO = io.BytesIO() 36 | curImg.save(imageIO, curImg.format) 37 | imgBytes = imageIO.getvalue() 38 | ``` 39 | 40 | 详见: 41 | 42 | 【已解决】Python的Pillow如何返回图像的二进制数据 43 | 44 | ## 缩放图片 45 | 46 | ```python 47 | import io 48 | from PIL import Image, ImageDraw 49 | 50 | def resizeImage(inputImage, 51 | newSize, 52 | resample=Image.BICUBIC, # Image.LANCZOS, 53 | outputFormat=None, 54 | outputImageFile=None 55 | ): 56 | """ 57 | resize input image 58 | resize normally means become smaller, reduce size 59 | :param inputImage: image file object(fp) / filename / binary bytes 60 | :param newSize: (width, height) 61 | :param resample: PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC, or PIL.Image.LANCZOS 62 | https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.thumbnail 63 | :param outputFormat: PNG/JPEG/BMP/GIF/TIFF/WebP/..., more refer: 64 | https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html 65 | if input image is filename with suffix, can omit this -> will infer from filename suffix 66 | :param outputImageFile: output image file filename 67 | :return: 68 | input image file filename: output resized image to outputImageFile 69 | input image binary bytes: resized image binary bytes 70 | """ 71 | openableImage = None 72 | if isinstance(inputImage, str): 73 | openableImage = inputImage 74 | elif isFileObject(inputImage): 75 | openableImage = inputImage 76 | elif isinstance(inputImage, bytes): 77 | inputImageLen = len(inputImage) 78 | openableImage = io.BytesIO(inputImage) 79 | 80 | imageFile = Image.open(openableImage) # 81 | imageFile.thumbnail(newSize, resample) 82 | if outputImageFile: 83 | # save to file 84 | imageFile.save(outputImageFile) 85 | imageFile.close() 86 | else: 87 | # save and return binary byte 88 | imageOutput = io.BytesIO() 89 | # imageFile.save(imageOutput) 90 | outputImageFormat = None 91 | if outputFormat: 92 | outputImageFormat = outputFormat 93 | elif imageFile.format: 94 | outputImageFormat = imageFile.format 95 | imageFile.save(imageOutput, outputImageFormat) 96 | imageFile.close() 97 | compressedImageBytes = imageOutput.getvalue() 98 | compressedImageLen = len(compressedImageBytes) 99 | compressRatio = float(compressedImageLen)/float(inputImageLen) 100 | print("%s -> %s, resize ratio: %d%%" % (inputImageLen, compressedImageLen, int(compressRatio * 100))) 101 | return compressedImageBytes 102 | ``` 103 | 104 | 调用: 105 | 106 | ```python 107 | import sys 108 | import os 109 | curFolder = os.path.abspath(__file__) 110 | parentFolder = os.path.dirname(curFolder) 111 | parentParentFolder = os.path.dirname(parentFolder) 112 | parentParentParentFolder = os.path.dirname(parentParentFolder) 113 | sys.path.append(curFolder) 114 | sys.path.append(parentFolder) 115 | sys.path.append(parentParentFolder) 116 | sys.path.append(parentParentParentFolder) 117 | 118 | import datetime 119 | from crifanMultimedia import resizeImage 120 | 121 | def testFilename(): 122 | imageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day.png" 123 | outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_300x300.png" 124 | print("imageFilename=%s" % imageFilename) 125 | beforeTime = datetime.datetime.now() 126 | resizeImage(imageFilename, (300, 300), outputImageFile=outputImageFilename) 127 | afterTime = datetime.datetime.now() 128 | print("procesTime: %s" % (afterTime - beforeTime)) 129 | 130 | outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_800x800.png" 131 | beforeTime = datetime.datetime.now() 132 | resizeImage(imageFilename, (800, 800), outputImageFile=outputImageFilename) 133 | afterTime = datetime.datetime.now() 134 | print("procesTime: %s" % (afterTime - beforeTime)) 135 | 136 | def testFileObject(): 137 | imageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day.png" 138 | imageFileObj = open(imageFilename, "rb") 139 | outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_600x600.png" 140 | beforeTime = datetime.datetime.now() 141 | resizeImage(imageFileObj, (600, 600), outputImageFile=outputImageFilename) 142 | afterTime = datetime.datetime.now() 143 | print("procesTime: %s" % (afterTime - beforeTime)) 144 | 145 | def testBinaryBytes(): 146 | imageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/take tomato.png" 147 | imageFileObj = open(imageFilename, "rb") 148 | imageBytes = imageFileObj.read() 149 | # return binary bytes 150 | beforeTime = datetime.datetime.now() 151 | resizedImageBytes = resizeImage(imageBytes, (800, 800)) 152 | afterTime = datetime.datetime.now() 153 | print("procesTime: %s" % (afterTime - beforeTime)) 154 | print("len(resizedImageBytes)=%s" % len(resizedImageBytes)) 155 | 156 | # save to file 157 | outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_750x750.png" 158 | beforeTime = datetime.datetime.now() 159 | resizeImage(imageBytes, (750, 750), outputImageFile=outputImageFilename) 160 | afterTime = datetime.datetime.now() 161 | print("procesTime: %s" % (afterTime - beforeTime)) 162 | 163 | imageFileObj.close() 164 | 165 | def demoResizeImage(): 166 | testFilename() 167 | testFileObject() 168 | testBinaryBytes() 169 | 170 | if __name__ == "__main__": 171 | demoResizeImage() 172 | 173 | # imageFilename=/Users/crifan/dev/tmp/python/resize_image_demo/hot day.png 174 | # procesTime: 0:00:00.619377 175 | # procesTime: 0:00:00.745228 176 | # procesTime: 0:00:00.606060 177 | # 1146667 -> 753258, resize ratio: 65% 178 | # procesTime: 0:00:00.773289 179 | # len(resizedImageBytes)=753258 180 | # procesTime: 0:00:00.738237 181 | ``` 182 | 183 | ## 给图片画元素所属区域的边框,且带自动保存加了框后的图片 184 | 185 | ```python 186 | from PIL import Image 187 | from PIL import ImageDraw 188 | 189 | def imageDrawRectangle(inputImgOrImgPath, 190 | rectLocation, 191 | outlineColor="green", 192 | outlineWidth=0, 193 | isShow=False, 194 | isAutoSave=True, 195 | saveTail="_drawRect_%wx%h", 196 | isDrawClickedPosCircle=True, 197 | clickedPos=None, 198 | ): 199 | """Draw a rectangle for image (and a small circle), and show it, 200 | 201 | Args: 202 | inputImgOrImgPath (Image/str): a pillow(PIL) Image instance or image file path 203 | rectLocation (tuple/list/Rect): the rectangle location, (x, y, width, height) 204 | outlineColor (str): Color name 205 | outlineWidth (int): rectangle outline width 206 | isShow (bool): True to call image.show() for debug 207 | isAutoSave (bool): True to auto save the image file with drawed rectangle 208 | saveTail(str): save filename tail part. support format %x/%y/%w/%h use only when isAutoSave=True 209 | clickedPos (tuple): x,y of clicked postion; default None; if None, use the center point 210 | isDrawClickedPosCircle (bool): draw small circle in clicked point 211 | Returns: 212 | modified image 213 | Raises: 214 | """ 215 | inputImg = inputImgOrImgPath 216 | if isinstance(inputImgOrImgPath, str): 217 | inputImg = Image.open(inputImgOrImgPath) 218 | draw = ImageDraw.Draw(inputImg) 219 | 220 | isRectObj = False 221 | hasX = hasattr(rectLocation, "x") 222 | hasY = hasattr(rectLocation, "y") 223 | hasWidth = hasattr(rectLocation, "width") 224 | hasHeight = hasattr(rectLocation, "height") 225 | isRectObj = hasX and hasY and hasWidth and hasHeight 226 | if isinstance(rectLocation, tuple): 227 | x, y, w, h = rectLocation 228 | if isinstance(rectLocation, list): 229 | x = rectLocation[0] 230 | y = rectLocation[1] 231 | w = rectLocation[2] 232 | h = rectLocation[3] 233 | elif isRectObj: 234 | x = rectLocation.x 235 | y = rectLocation.y 236 | w = rectLocation.width 237 | h = rectLocation.height 238 | 239 | w = int(w) 240 | h = int(h) 241 | x0 = x 242 | y0 = y 243 | x1 = x0 + w 244 | y1 = y0 + h 245 | draw.rectangle( 246 | [x0, y0, x1, y1], 247 | # fill="yellow", 248 | # outline="yellow", 249 | outline=outlineColor, 250 | width=outlineWidth, 251 | ) 252 | 253 | if isDrawClickedPosCircle: 254 | # radius = 3 255 | # radius = 2 256 | radius = 4 257 | # circleOutline = "yellow" 258 | circleOutline = "red" 259 | circleLineWidthInt = 1 260 | # circleLineWidthInt = 3 261 | 262 | if clickedPos: 263 | clickedX, clickedY = clickedPos 264 | else: 265 | clickedX = x + w/2 266 | clickedY = y + h/2 267 | startPointInt = (int(clickedX - radius), int(clickedY - radius)) 268 | endPointInt = (int(clickedX + radius), int(clickedY + radius)) 269 | draw.ellipse([startPointInt, endPointInt], outline=circleOutline, width=circleLineWidthInt) 270 | 271 | if isShow: 272 | inputImg.show() 273 | 274 | if isAutoSave: 275 | saveTail = saveTail.replace("%x", str(x)) 276 | saveTail = saveTail.replace("%y", str(y)) 277 | saveTail = saveTail.replace("%w", str(w)) 278 | saveTail = saveTail.replace("%h", str(h)) 279 | 280 | inputImgPath = None 281 | if isinstance(inputImgOrImgPath, str): 282 | inputImgPath = str(inputImgOrImgPath) 283 | elif inputImg.filename: 284 | inputImgPath = str(inputImg.filename) 285 | 286 | if inputImgPath: 287 | imgFolderAndName, pointSuffix = os.path.splitext(inputImgPath) 288 | imgFolderAndName = imgFolderAndName + saveTail 289 | newImgPath = imgFolderAndName + pointSuffix 290 | newImgPath = findNextNumberFilename(newImgPath) 291 | else: 292 | curDatetimeStr = getCurDatetimeStr() # '20191219_143400' 293 | suffix = str(inputImg.format).lower() # 'jpeg' 294 | newImgFilename = "%s%s.%s" % (curDatetimeStr, saveTail, suffix) 295 | imgPathRoot = os.getcwd() 296 | newImgPath = os.path.join(imgPathRoot, newImgFilename) 297 | 298 | inputImg.save(newImgPath) 299 | 300 | return inputImg 301 | ``` 302 | 303 | 说明: 304 | 305 | 相关函数,详见:[findNextNumberFilename](),或者干脆去掉这个逻辑即可。 306 | 307 | 调用: 308 | 309 | ```python 310 | curBoundList = self.get_ElementBounds(eachElement) 311 | curWidth = curBoundList[2] - curBoundList[0] 312 | curHeight = curBoundList[3] - curBoundList[1] 313 | curRect = [curBoundList[0], curBoundList[1], curWidth, curHeight] 314 | curImg = CommonUtils.imageDrawRectangle(curImg, curRect, isShow=True, saveTail="_rect_%x|%y|%w|%h", isDrawClickedPosCircle=False) 315 | ``` 316 | 317 | 或: 318 | 319 | ```python 320 | curTimeStr = CommonUtils.getCurDatetimeStr("%H%M%S") 321 | curSaveTal = "_rect_{}_%x|%y|%w|%h".format(curTimeStr) 322 | curImg = CommonUtils.imageDrawRectangle(imgPath, curRect, isShow=True, saveTail=curSaveTal, isDrawClickedPosCircle=False) 323 | ``` 324 | 325 | 效果: 326 | 327 | (1)给原图加上单个元素所属边框 328 | 329 | ![single_image_add_single_rect](../../../assets/img/single_image_add_single_rect.png) 330 | 331 | (2)多次循环后,给同一张图中多个元素加上边框后 332 | 333 | ![single_image_add_multiple_rect](../../../assets/img/single_image_add_multiple_rect.png) 334 | 335 | 其他调用: 336 | 337 | ```python 338 | imageDrawRectangle(curPillowImg, curLocation) 339 | 340 | imageDrawRectangle(curPillowImg, calculatedLocation) 341 | 342 | curImg = imageDrawRectangle(imgPath, firstMatchLocation, clickedPos=clickedPos) 343 | 344 | curImg = imageDrawRectangle(imgPath, firstMatchLocation) 345 | ``` 346 | 347 | 348 | -------------------------------------------------------------------------------- /src/common_code/multimedia/video.md: -------------------------------------------------------------------------------- 1 | # 视频 2 | 3 | ## 从视频中提取出音频mp3文件 4 | 5 | ```python 6 | import os 7 | import logging 8 | import subprocess 9 | 10 | videoFullpath = "show_157712932_video.mp4" 11 | startTimeStr = "00:00:11.270" 12 | # startTimeStr = "%02d:%02d:%02d.%03d" % (startTime.hours, startTime.minutes, startTime.seconds, startTime.milliseconds) 13 | endTimeStr = "00:00:14.550" 14 | # endTimeStr = "%02d:%02d:%02d.%03d" % (endTime.hours, endTime.minutes, endTime.seconds, endTime.milliseconds) 15 | outputAudioFullpath = "show_157712932_audio_000011270_000014550.mp3" 16 | 17 | # extract audio segment from video 18 | # ffmpeg -i show_157712932_video.mp4 -ss 00:00:11.270 -to 00:00:14.550 -b:a 128k show_157712932_audio_000011270_000014550.mp3 19 | if not os.path.exists(outputAudioFullpath): 20 | ffmpegCmd = "ffmpeg -i %s -ss %s -to %s -b:a 128k %s" % (videoFullpath, startTimeStr, endTimeStr, outputAudioFullpath) 21 | subprocess.call(ffmpegCmd, shell=True) 22 | logging.info("Complete use ffmpeg extract audio: %s", ffmpegCmd) 23 | ``` 24 | 25 | 可以从mp4中提取出mp3音频: 26 | 27 | ![ffmpeg_extract_audio_segment](../../assets/img/ffmpeg_extract_audio_segment.png) 28 | -------------------------------------------------------------------------------- /src/common_code/network_related/README.md: -------------------------------------------------------------------------------- 1 | # 网络相关 2 | 3 | 此处整理和网络相关的一些常用代码段。 4 | -------------------------------------------------------------------------------- /src/common_code/network_related/beautifulSoup.md: -------------------------------------------------------------------------------- 1 | # BeautifulSoup 2 | 3 | 用网络库下载到页面源码后,就是去解析(HTML等)内容了。 4 | 5 | Python中最常用的HTML(和XML)解析库之一就是:`BeautifulSoup` 6 | 7 | * 最新代码详见:https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/thirdParty/crifanBeautifulsoup.py 8 | 9 | ## 从html转soup 10 | 11 | ```python 12 | from bs4 import BeautifulSoup 13 | 14 | def htmlToSoup(curHtml): 15 | """convert html to soup 16 | 17 | Args: 18 | curHtml (str): html str 19 | Returns: 20 | soup 21 | Raises: 22 | """ 23 | soup = BeautifulSoup(curHtml, 'html.parser') 24 | return soup 25 | ``` 26 | 27 | ## 从xml转换出soup 28 | 29 | 背景: 30 | 31 | iOS自动化期间,常会涉及到,获取到当前页面源码,是xml字符串,需要转换为soup,才能后续操作 32 | 33 | 所以整理出通用转换逻辑 34 | 35 | ```python 36 | def xmlToSoup(xmlStr): 37 | """convert to xml string to soup 38 | Note: xml is tag case sensitive -> retain tag upper case -> NOT convert tag to lowercase 39 | 40 | 41 | Args: 42 | xmlStr (str): xml str, normally page source 43 | Returns: 44 | soup 45 | Raises: 46 | """ 47 | # HtmlParser = 'html.parser' 48 | # XmlParser = 'xml' 49 | XmlParser = 'lxml-xml' 50 | curParser = XmlParser 51 | soup = BeautifulSoup(xmlStr, curParser) 52 | return soup 53 | ``` 54 | 55 | 举例: 56 | 57 | (1) 58 | 59 | ```python 60 | curPageXml = self.get_page_source() 61 | soup = CommonUtils.xmlToSoup(curPageXml) 62 | ``` 63 | 64 | 获取到xml字符串后,去转换为soup 65 | 66 | ## soup转html 67 | 68 | ```python 69 | 70 | def soupToHtml(soup, isFormat=True): 71 | """Convert soup to html string 72 | 73 | Args: 74 | soup (Soup): BeautifulSoup soup 75 | isFormat (bool): use prettify to format html 76 | Returns: 77 | html (str) 78 | Raises: 79 | """ 80 | if isFormat: 81 | curHtml = soup.prettify() # formatted html 82 | else: 83 | curHtml = str(soup) # not formatted html 84 | return curHtml 85 | ``` 86 | 87 | ## 获取soup节点所有的文字内容 88 | 89 | ```python 90 | 91 | def getAllContents(curNode, isStripped=False): 92 | """Get all contents of current and children nodes 93 | 94 | Args: 95 | curNode (soup node): current Beautifulsoup node 96 | isStripped (bool): return stripped string or not 97 | Returns: 98 | str 99 | Raises: 100 | """ 101 | # codeSnippetStr = curNode.prettify() 102 | # codeSnippetStr = curNode.string 103 | # codeSnippetStr = curNode.contents 104 | codeSnippetStr = "" 105 | stringList = [] 106 | if isStripped: 107 | stringGenerator = curNode.stripped_strings 108 | else: 109 | stringGenerator = curNode.strings 110 | 111 | # stringGenerator = curNode.strings 112 | for eachStr in stringGenerator: 113 | # logging.debug("eachStr=%s", eachStr) 114 | stringList.append(eachStr) 115 | codeSnippetStr = "\n".join(stringList) 116 | logging.debug("codeSnippetStr=%s", codeSnippetStr) 117 | return codeSnippetStr 118 | ``` 119 | 120 | ## 从html中提取title值 121 | 122 | ```python 123 | def extractHtmlTitle_BeautifulSoup(htmlStr): 124 | """ 125 | Extract title from html, use BeautifulSoup 126 | 127 | Args: 128 | htmlStr (str): html string 129 | Returns: 130 | str 131 | Raises: 132 | Examples: 133 | """ 134 | curTitle = "" 135 | 136 | soup = BeautifulSoup(htmlStr, "html.parser") 137 | if soup: 138 | if soup.title and soup.title.string: 139 | curTitle = soup.title.string 140 | curTitle = curTitle.strip() 141 | else: 142 | # logging.warning("Empty title for html: %s", htmlStr) 143 | logging.debug("Empty title for html: %s", htmlStr) 144 | # Empty title for html: 145 | 146 | # for debug 147 | if "" not in htmlStr: 148 | logging.warning("Special not incldue <title> html: %s", htmlStr) 149 | # 'Illegal access address!\n' 150 | # <script type="text/javascript">top.location.href='https://login.zhongan.com/passport/login.htm?sourceApp=1&target=http://www.zhongan.com/open/member/loginJump?logincallback=%2Fahita';</script> 151 | # 152 | else: 153 | logging.error("Failed to convert to soup for html: %s", htmlStr) 154 | # 155 | 156 | return curTitle 157 | ``` 158 | 159 | ## 是否包含符合特定条件的soup节点 160 | 161 | ```python 162 | def isContainSpecificSoup(soupList, elementName, isSizeValidCallback, matchNum=1): 163 | """ 164 | 判断BeautifulSoup的soup的list中,是否包含符合条件的特定的元素: 165 | 只匹配指定个数的元素才视为找到了 166 | 元素名相同 167 | 面积大小是否符合条件 168 | Args: 169 | elementName (str): element name 170 | isSizeValidCallback (function): callback function to check whether element size(width * height) is valid or not 171 | matchNum (int): sould only matched specific number consider as valid 172 | Returns: 173 | bool 174 | Raises: 175 | """ 176 | isFound = False 177 | 178 | matchedSoupList = [] 179 | 180 | for eachSoup in soupList: 181 | # if hasattr(eachSoup, "tag"): 182 | if hasattr(eachSoup, "name"): 183 | # curSoupTag = eachSoup.tag 184 | curSoupTag = eachSoup.name 185 | if curSoupTag == elementName: 186 | if hasattr(eachSoup, "attrs"): 187 | soupAttr = eachSoup.attrs 188 | soupWidth = int(soupAttr["width"]) 189 | soupHeight = int(soupAttr["height"]) 190 | curSoupSize = soupWidth * soupHeight # 326 * 270 191 | isSizeValid = isSizeValidCallback(curSoupSize) 192 | if isSizeValid: 193 | matchedSoupList.append(eachSoup) 194 | 195 | matchedSoupNum = len(matchedSoupList) 196 | if matchNum == 0: 197 | isFound = True 198 | else: 199 | if matchedSoupNum == matchNum: 200 | isFound = True 201 | 202 | return isFound 203 | ``` 204 | 205 | 说明: 206 | 207 | 判断soup内,是否有符合特定条件的soup 208 | 209 | 举例: 210 | 211 | (1)iOS的弹框,有上角带关闭按钮时,去判断一个弹框,是否符合对应条件,以便于判断是否可能是弹框 212 | 213 | ```python 214 | nextSiblingeSoupGenerator = possibleCloseSoup.next_siblings 215 | nextSiblingeSoupList = list(nextSiblingeSoupGenerator) 216 | 217 | hasLargeImage = CommonUtils.isContainSpecificSoup(nextSiblingeSoupList, "XCUIElementTypeImage", self.isPopupWindowSize) 218 | isPossibleClose = hasLargeImage 219 | ``` 220 | 221 | 相关函数 222 | 223 | ```python 224 | def isPopupWindowSize(self, curSize): 225 | """判断一个soup的宽高大小是否是弹框类窗口(Image,Other等)的大小""" 226 | # global FullScreenSize 227 | FullScreenSize = self.X * self.totalY 228 | curSizeRatio = curSize / FullScreenSize # 0.289 229 | PopupWindowSizeMinRatio = 0.25 230 | # PopupWindowSizeMaxRatio = 0.9 231 | PopupWindowSizeMaxRatio = 0.8 232 | # isSizeValid = curSizeRatio >= MinPopupWindowSizeRatio 233 | # is popup like window, size should large enough, but should not full screen 234 | isSizeValid = PopupWindowSizeMinRatio <= curSizeRatio <= PopupWindowSizeMaxRatio 235 | return isSizeValid 236 | ``` 237 | 238 | (2) 239 | 240 | ```python 241 | hasNormalButton = CommonUtils.isContainSpecificSoup(nextSiblingeSoupList, "XCUIElementTypeButton", self.isNormalButtonSize) 242 | ``` 243 | 244 | 相关函数: 245 | 246 | ```python 247 | def isNormalButtonSize(self, curSize): 248 | """判断一个soup的宽高大小是否是普通的按钮大小""" 249 | NormalButtonSizeMin = 30*30 250 | NormalButtonSizeMax = 100*100 251 | isNormalSize = NormalButtonSizeMin <= curSize <= NormalButtonSizeMax 252 | return isNormalSize 253 | ``` 254 | 255 | ## 查找元素,限定条件是符合对应的几级的父元素的条件 256 | 257 | 背景: 258 | 259 | 很多时候,需要对于iOS的app的页面的源码,即xml中,查找符合特定情况的的元素 260 | 261 | 这些特定情况,往往是parent或者前几层级的parent中,元素符合一定条件,往往是type,以及宽度是屏幕宽度,高度是屏幕高度等等 262 | 263 | 所以提取出公共函数,用于bs的find查找元素 264 | 265 | ```python 266 | def bsChainFind(curLevelSoup, queryChainList): 267 | """BeautifulSoup find with query chain 268 | 269 | Args: 270 | curLevelSoup (soup): BeautifulSoup 271 | queryChainList (list): str list of all level query dict 272 | Returns: 273 | soup 274 | Raises: 275 | Examples: 276 | input: 277 | [ 278 | { 279 | "tag": "XCUIElementTypeWindow", 280 | "attrs": {"visible":"true", "enabled":"true", "width": "%s" % ScreenX, "height": "%s" % ScreenY} 281 | }, 282 | { 283 | "tag": "XCUIElementTypeButton", 284 | "attrs": {"visible":"true", "enabled":"true", "width": "%s" % ScreenX, "height": "%s" % ScreenY} 285 | }, 286 | { 287 | "tag": "XCUIElementTypeStaticText", 288 | "attrs": {"visible":"true", "enabled":"true", "value":"可能离开微信,打开第三方应用"} 289 | }, 290 | ] 291 | output: 292 | soup node of 293 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="可能离开微信,打开第三方应用" name="可能离开微信,打开第三方应用" label="可能离开微信,打开第三方应用" enabled="true" visible="true" x="71" y="331" width="272" height="18"/> 294 | in : 295 | <XCUIElementTypeWindow type="XCUIElementTypeWindow" enabled="true" visible="true" x="0" y="0" width="414" height="736"> 296 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="414" height="736"> 297 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="414" height="736"> 298 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="414" height="736"> 299 | <XCUIElementTypeButton type="XCUIElementTypeButton" enabled="true" visible="true" x="0" y="0" width="414" height="736"> 300 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" enabled="true" visible="false" x="47" y="288" width="0" height="0"/> 301 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="可能离开微信,打开第三方应用" name="可能离开微信,打开第三方应用" label="可能离开微信,打开第三方应用" enabled="true" visible="true" x="71" y="331" width="272" height="18"/> 302 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="取消" name="取消" label="取消" enabled="true" visible="true" x="109" y="409" width="36" height="22"/> 303 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="继续" name="继续" label="继续" enabled="true" visible="true" x="269" y="409" width="36" height="22"/> 304 | </XCUIElementTypeButton> 305 | </XCUIElementTypeOther> 306 | </XCUIElementTypeOther> 307 | </XCUIElementTypeOther> 308 | </XCUIElementTypeWindow> 309 | """ 310 | foundSoup = None 311 | if queryChainList: 312 | chainListLen = len(queryChainList) 313 | 314 | if chainListLen == 1: 315 | # last one 316 | curLevelFindDict = queryChainList[0] 317 | curTag = curLevelFindDict["tag"] 318 | curAttrs = curLevelFindDict["attrs"] 319 | foundSoup = curLevelSoup.find(curTag, attrs=curAttrs) 320 | else: 321 | highestLevelFindDict = queryChainList[0] 322 | curTag = highestLevelFindDict["tag"] 323 | curAttrs = highestLevelFindDict["attrs"] 324 | foundSoupList = curLevelSoup.find_all(curTag, attrs=curAttrs) 325 | if foundSoupList: 326 | childrenChainList = queryChainList[1:] 327 | for eachSoup in foundSoupList: 328 | eachSoupResult = CommonUtils.bsChainFind(eachSoup, childrenChainList) 329 | if eachSoupResult: 330 | foundSoup = eachSoupResult 331 | break 332 | 333 | return foundSoup 334 | ``` 335 | 336 | 举例: 337 | 338 | (1) 339 | 340 | ```python 341 | """ 342 | 微信-小程序 弹框 警告 尚未进行授权: 343 | <XCUIElementTypeButton type="XCUIElementTypeButton" enabled="true" visible="true" x="0" y="0" width="375" height="667"> 344 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="32" y="240" width="311" height="187"> 345 | <XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="32" y="240" width="311" height="187"/> 346 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="警告" name="警告" label="警告" enabled="true" visible="true" x="52" y="265" width="271" height="21"/> 347 | <XCUIElementTypeTextView type="XCUIElementTypeTextView" value="尚未进行授权,请点击确定跳转到授权页面进行授权。" enabled="true" visible="true" x="60" y="300" width="255" height="57"/> 348 | <XCUIElementTypeButton type="XCUIElementTypeButton" name="取消" label="取消" enabled="true" visible="true" x="32" y="376" width="156" height="51"/> 349 | <XCUIElementTypeButton type="XCUIElementTypeButton" name="确定" label="确定" enabled="true" visible="true" x="187" y="376" width="156" height="51"/> 350 | </XCUIElementTypeOther> 351 | </XCUIElementTypeButton> 352 | """ 353 | warningChainList = [ 354 | { 355 | "tag": "XCUIElementTypeButton", 356 | "attrs": {"visible":"true", "enabled":"true", "width": "%s" % self.X, "height": "%s" % self.totalY} 357 | }, 358 | { 359 | "tag": "XCUIElementTypeOther", 360 | "attrs": {"visible":"true", "enabled":"true"} 361 | }, 362 | { 363 | "tag": "XCUIElementTypeStaticText", 364 | "attrs": {"visible":"true", "enabled":"true", "value":"警告"} 365 | }, 366 | ] 367 | warningSoup = CommonUtils.bsChainFind(soup, warningChainList) 368 | ``` 369 | 370 | 相关: 371 | 372 | 找到元素后,再去点击: 373 | 374 | ```python 375 | if warningSoup: 376 | parentOtherSoup = warningSoup.parent 377 | confirmSoup = parentOtherSoup.find( 378 | "XCUIElementTypeButton", 379 | attrs={"visible":"true", "enabled":"true", "name": "确定"} 380 | ) 381 | if confirmSoup: 382 | self.clickElementCenterPosition(confirmSoup) 383 | foundAndProcessedPopup = True 384 | ``` 385 | 386 | (2) 387 | 388 | ```python 389 | """ 390 | 系统弹框 拍照或录像: 391 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="530" width="398" height="133"> 392 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="530" width="398" height="133"> 393 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="530" width="398" height="133"> 394 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="0" y="0" width="398" height="133"> 395 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133"> 396 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133"> 397 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133"> 398 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133"> 399 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133"> 400 | <XCUIElementTypeTable type="XCUIElementTypeTable" enabled="true" visible="true" x="0" y="0" width="398" height="133"> 401 | <XCUIElementTypeCell type="XCUIElementTypeCell" enabled="true" visible="true" x="0" y="0" width="398" height="45"> 402 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="43" width="398" height="2"/> 403 | <XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="13" y="6" width="1" height="32"/> 404 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="拍照或录像" name="拍照或录像" label="拍照或录像" enabled="true" visible="true" x="15" y="11" width="87" height="22"/> 405 | <XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="351" y="6" width="32" height="32"/> 406 | </XCUIElementTypeCell> 407 | <XCUIElementTypeCell type="XCUIElementTypeCell" enabled="true" visible="true" x="0" y="44" width="398" height="45"> 408 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="88" width="398" height="1"/> 409 | <XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="13" y="50" width="1" height="33"/> 410 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="照片图库" name="照片图库" label="照片图库" enabled="true" visible="true" x="15" y="56" width="70" height="21"/> 411 | <XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="351" y="50" width="32" height="33"/> 412 | </XCUIElementTypeCell> 413 | <XCUIElementTypeCell type="XCUIElementTypeCell" enabled="true" visible="true" x="0" y="88" width="398" height="45"> 414 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="0" y="132" width="398" height="1"/> 415 | <XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="13" y="94" width="1" height="33"/> 416 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="浏览" name="浏览" label="浏览" enabled="true" visible="true" x="14" y="100" width="36" height="21"/> 417 | <XCUIElementTypeImage type="XCUIElementTypeImage" name="UIDocumentPicker-more" enabled="true" visible="false" x="351" y="94" width="32" height="33"/> 418 | </XCUIElementTypeCell> 419 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="132" width="398" height="1"/> 420 | </XCUIElementTypeTable> 421 | </XCUIElementTypeOther> 422 | </XCUIElementTypeOther> 423 | </XCUIElementTypeOther> 424 | </XCUIElementTypeOther> 425 | </XCUIElementTypeOther> 426 | </XCUIElementTypeOther> 427 | </XCUIElementTypeOther> 428 | </XCUIElementTypeOther> 429 | </XCUIElementTypeOther> 430 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="671" width="398" height="57"> 431 | <XCUIElementTypeButton type="XCUIElementTypeButton" name="取消" label="取消" enabled="true" visible="true" x="8" y="671" width="398" height="57"/> 432 | </XCUIElementTypeOther> 433 | """ 434 | photoCameraChainList = [ 435 | { 436 | "tag": "XCUIElementTypeOther", 437 | "attrs": {"enabled":"true", "visible":"true"} 438 | }, 439 | { 440 | "tag": "XCUIElementTypeTable", 441 | "attrs": {"enabled":"true", "visible":"true", "x":"0", "y":"0"} 442 | }, 443 | { 444 | "tag": "XCUIElementTypeStaticText", 445 | "attrs": {"enabled":"true", "visible":"true", "value":"拍照或录像"} 446 | }, 447 | ] 448 | photoCameraSoup = CommonUtils.bsChainFind(soup, photoCameraChainList) 449 | ``` 450 | 451 | 452 | (3)iOS 设置 无线局域网 列表页 找 当前已连接的WiFi,特征是带蓝色✅的: 453 | 454 | ```python 455 | """ 456 | 设置 无线局域网 列表页: 457 | <XCUIElementTypeTable type="XCUIElementTypeTable" enabled="true" visible="true" x="0" y="0" width="414" height="736"> 458 | 。。。 459 | <XCUIElementTypeCell type="XCUIElementTypeCell" name=“xxx_guest_5G, 安全网络, 信号强度 3 格,共 3 格" label=“xxx_guest_5G, 安全网络, 信号强度 3 格,共 3 格" enabled="true" visible="true" x="0" y="144" width="414" height="43"> 460 | <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value=“xxx_guest_5G" name=“xxx_guest_5G" label=“xxx_guest_5G" enabled="true" visible="true" x="40" y="155" width="278" height="21"/> 461 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="186" width="414" height="1"/> 462 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="151" width="28" height="29"> 463 | <XCUIElementTypeImage type="XCUIElementTypeImage" name="UIPreferencesBlueCheck" enabled="true" visible="false" x="8" y="151" width="28" height="29"/> 464 | </XCUIElementTypeOther> 465 | <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="15" y="187" width="245" height="1"/> 466 | <XCUIElementTypeImage type="XCUIElementTypeImage" name="Lock" enabled="true" visible="false" x="326" y="159" width="8" height="12"/> 467 | <XCUIElementTypeImage type="XCUIElementTypeImage" name="WifiBars3" enabled="true" visible="false" x="346" y="153" width="16" height="25"/> 468 | <XCUIElementTypeButton type="XCUIElementTypeButton" name="更多信息" label="更多信息" enabled="true" visible="true" x="372" y="154" width="22" height="22"/> 469 | </XCUIElementTypeCell> 470 | """ 471 | curPageXml = self.get_page_source() 472 | soup = CommonUtils.xmlToSoup(curPageXml) 473 | blueCheckChainList = [ 474 | { 475 | "tag": "XCUIElementTypeCell", 476 | "attrs": {"enabled":"true", "visible":"true", "x":"0", "width":"%s" % self.X} 477 | }, 478 | { 479 | "tag": "XCUIElementTypeOther", 480 | "attrs": {"enabled":"true", "visible":"true"} 481 | }, 482 | { 483 | "tag": "XCUIElementTypeImage", 484 | # "attrs": {"enabled":"true", "visible":"true", "name": "UIPreferencesBlueCheck"} 485 | "attrs": {"enabled":"true", "name": "UIPreferencesBlueCheck"} 486 | }, 487 | ] 488 | blueCheckSoup = CommonUtils.bsChainFind(soup, blueCheckChainList) 489 | if blueCheckSoup: 490 | ``` 491 | -------------------------------------------------------------------------------- /src/common_code/network_related/requests.md: -------------------------------------------------------------------------------- 1 | # requests 2 | 3 | ## 下载图片,保存为二进制文件 4 | 5 | ```python 6 | import requests 7 | resp = requests.get(pictureUrl) 8 | with open(saveFullPath, 'wb') as saveFp: 9 | saveFp.write(resp.content) 10 | ``` 11 | 12 | 详见: 13 | 14 | 【已解决】Python的requests中如何下载二进制数据保存为图片文件 15 | -------------------------------------------------------------------------------- /src/common_code/system.md: -------------------------------------------------------------------------------- 1 | # 系统 2 | 3 | 此处整理用Python处理系统相关的通用的代码。 4 | 5 | ## 系统类型 6 | 7 | ```python 8 | import sys 9 | 10 | def osIsWinows(): 11 | return sys.platform == "win32" 12 | 13 | def osIsCygwin(): 14 | return sys.platform == "cygwin" 15 | 16 | def osIsMacOS(): 17 | return sys.platform == "darwin" 18 | 19 | def osIsLinux(): 20 | return sys.platform == "linux" 21 | 22 | def osIsAix(): 23 | return sys.platform == "aix" 24 | ``` 25 | 26 | ## 命令行 27 | 28 | ### 获取命令行执行命令返回结果 29 | 30 | 代码: 31 | 32 | ```python 33 | def get_cmd_lines(cmd, text=False): 34 | # 执行cmd命令,将结果保存为列表 35 | resultStr = "" 36 | resultStrList = [] 37 | try: 38 | consoleOutputByte = subprocess.check_output(cmd, shell=True) # b'C02Y3N10JHC8\n' 39 | try: 40 | resultStr = consoleOutputByte.decode("utf-8") 41 | except UnicodeDecodeError: 42 | # TODO: use chardet auto detect encoding 43 | # consoleOutputStr = consoleOutputByte.decode("gbk") 44 | resultStr = consoleOutputByte.decode("gb18030") 45 | 46 | if not text: 47 | resultStrList = resultStr.splitlines() 48 | except Exception as err: 49 | print("err=%s when run cmd=%s" % (err, cmd)) 50 | 51 | if text: 52 | return resultStr 53 | else: 54 | return resultStrList 55 | ``` 56 | 57 | ## 硬件信息 58 | 59 | ## 获取当前电脑(Win或Mac)的序列号 60 | 61 | 代码: 62 | 63 | ```python 64 | def getSerialNumber(self): 65 | """get current computer serial number""" 66 | # cmd = "wmic bios get serialnumber" 67 | cmd = "" 68 | if CommonUtils.osIsWinows(): 69 | # Windows 70 | cmd = "wmic bios get serialnumber" 71 | elif CommonUtils.osIsMacOS(): 72 | # macOS 73 | cmd = "system_profiler SPHardwareDataType | awk '/Serial/ {print $4}'" 74 | # TODO: add support other OS 75 | # AIX: aix 76 | # Linux: linux 77 | # Windows/Cygwin: cygwin 78 | 79 | serialNumber = "" 80 | lines = CommonUtils.get_cmd_lines(cmd) 81 | if CommonUtils.osIsWinows(): 82 | # Windows 83 | serialNumber = lines[1] 84 | elif CommonUtils.osIsMacOS(): 85 | # macOS 86 | serialNumber = lines[0] # C02Y3N10JHC8 87 | 88 | return serialNumber 89 | ``` 90 | -------------------------------------------------------------------------------- /src/common_code/variable.md: -------------------------------------------------------------------------------- 1 | # 变量 2 | 3 | ## 判断变量类型 4 | 5 | 优先用`isinstance`,而不是`type` 6 | 7 | ```python 8 | >>> isinstance(2, float) 9 | False 10 | >>> isinstance('a', (str, unicode)) 11 | True 12 | >>> isinstance((2, 3), (str, list, tuple)) 13 | True 14 | ``` 15 | -------------------------------------------------------------------------------- /src/common_syntax/README.md: -------------------------------------------------------------------------------- 1 | # 常见语法 2 | -------------------------------------------------------------------------------- /src/common_syntax/collections.md: -------------------------------------------------------------------------------- 1 | # collections集合 2 | 3 | 根据官网: 4 | 5 | [collections --- 容器数据类型 — Python 3.8.1 文档](https://docs.python.org/zh-cn/3/library/collections.html#collections.UserDict) 6 | 7 | 介绍,集合有很多种,列出供了解: 8 | 9 | * `namedtuple()`:创建命名元组子类的工厂函数 10 | * `deque`:类似列表(list)的容器,实现了在两端快速添加(append)和弹出(pop) 11 | * `ChainMap`:类似字典(dict)的容器类,将多个映射集合到一个视图里面 12 | * `Counter`:字典的子类,提供了可哈希对象的计数功能 13 | * `OrderedDict`:字典的子类,保存了他们被添加的顺序 14 | * `defaultdict`:字典的子类,提供了一个工厂函数,为字典查询提供一个默认值 15 | * `UserDict`:封装了字典对象,简化了字典子类化 16 | * `UserList`:封装了列表对象,简化了列表子类化 17 | * `UserString`:封装了列表对象,简化了字符串子类化 18 | 19 | 待以后用到了,再详细总结。 20 | -------------------------------------------------------------------------------- /src/common_syntax/dict.md: -------------------------------------------------------------------------------- 1 | # dict字典 2 | 3 | ## 删除dict中某个键(和值) 4 | 5 | * 常见写法: 6 | ```python 7 | del yourDict["keyToDelete"] 8 | ``` 9 | * 更加Pythonic的写法: 10 | ```python 11 | yourDict.pop("keyToDelete") 12 | ``` 13 | 14 | 注意: 15 | 16 | 为了防止出现`KeyError`,注意确保要删除的key都是存在的,否则就要先判断存在,再去删除。 17 | 18 | ## OrderedDict 19 | 20 | ### 想要获取OrderedDict的最后一个item(的key和value) 21 | 22 | ```python 23 | next(reversed(someOrderedDict.items())) 24 | ``` 25 | 26 | 另外,只需要获取最后一个元素的key,则可以: 27 | 28 | ```python 29 | next(reversed(someOrderedDict.keys())) 30 | ``` 31 | 32 | 或: 33 | 34 | ```python 35 | next(reversed(someOrderedDict)) 36 | ``` 37 | 38 | 详见: 39 | 40 | 【已解决】Python中获取OrderedDict中最后一个元素 41 | 42 | ## 合并2个dict的值 43 | 44 | (1)如果无需保留原有(第一个dict)的值,则用update即可: 45 | 46 | ```python 47 | firstDict.update(secondDict) 48 | ``` 49 | 50 | 支持:`Python >=3.5` 51 | 52 | (2)如果要保留之前的dict的值,则用**展开 53 | 54 | ```python 55 | thirdDict = (**firstDict, **secondDict) 56 | ``` 57 | 58 | 支持:`Python 2`和 `Python <=3.4` 59 | 60 | 详见: 61 | 62 | 【已解决】Python中如何合并2个dict字典变量的值 63 | 64 | ## 有序字典OrderedDict的初始化 65 | 66 | ```python 67 | from collections import OrderedDict 68 | 69 | orderedDict = OrderedDict() 70 | ``` 71 | 72 | 后续正常作为普通dict使用 73 | 74 | ```python 75 | >>> from collections import OrderedDict 76 | >>> orderedDict = OrderedDict() 77 | >>> orderedDict["key2"] = "value2" 78 | >>> orderedDict["key1"] = "value1" 79 | >>> orderedDict["key3"] = "value3" 80 | >>> orderedDict 81 | OrderedDict([('key2', 'value2'), ('key1', 'value1'), ('key3', 'value3')]) 82 | ``` 83 | 84 | ## dict的递归的合并更新 85 | 86 | ```python 87 | 88 | def recursiveMergeDict(aDict, bDict): 89 | """ 90 | Recursively merge dict a to b, return merged dict b 91 | Note: Sub dict and sub list's won't be overwritten but also updated/merged 92 | 93 | example: 94 | (1) input and output example: 95 | input: 96 | { 97 | "keyStr": "strValueA", 98 | "keyInt": 1, 99 | "keyBool": true, 100 | "keyList": [ 101 | { 102 | "index0Item1": "index0Item1", 103 | "index0Item2": "index0Item2" 104 | }, 105 | { 106 | "index1Item1": "index1Item1" 107 | }, 108 | { 109 | "index2Item1": "index2Item1" 110 | } 111 | ] 112 | } 113 | 114 | and 115 | 116 | { 117 | "keyStr": "strValueB", 118 | "keyInt": 2, 119 | "keyList": [ 120 | { 121 | "index0Item1": "index0Item1_b" 122 | }, 123 | { 124 | "index1Item1": "index1Item1_b" 125 | } 126 | ] 127 | } 128 | 129 | output: 130 | 131 | { 132 | "keyStr": "strValueB", 133 | "keyBool": true, 134 | "keyInt": 2, 135 | "keyList": [ 136 | { 137 | "index0Item1": "index0Item1_b", 138 | "index0Item2": "index0Item2" 139 | }, 140 | { 141 | "index1Item1": "index1Item1_b" 142 | }, 143 | { 144 | "index2Item1": "index2Item1" 145 | } 146 | ] 147 | } 148 | 149 | (2) code usage example: 150 | import copy 151 | cDict = recursiveMergeDict(aDict, copy.deepcopy(bDict)) 152 | 153 | Note: 154 | bDict should use deepcopy, otherwise will be altered after call this function !!! 155 | 156 | """ 157 | aDictItems = None 158 | if (sys.version_info[0] == 2): # is python 2 159 | aDictItems = aDict.iteritems() 160 | else: # is python 3 161 | aDictItems = aDict.items() 162 | 163 | for aKey, aValue in aDictItems: 164 | # print("------ [%s]=%s" % (aKey, aValue)) 165 | if aKey not in bDict: 166 | bDict[aKey] = aValue 167 | else: 168 | bValue = bDict[aKey] 169 | # print("aValue=%s" % aValue) 170 | # print("bValue=%s" % bValue) 171 | if isinstance(aValue, dict): 172 | recursiveMergeDict(aValue, bValue) 173 | elif isinstance(aValue, list): 174 | aValueListLen = len(aValue) 175 | bValueListLen = len(bValue) 176 | bValueListMaxIdx = bValueListLen - 1 177 | for aListIdx in range(aValueListLen): 178 | # print("---[%d]" % aListIdx) 179 | aListItem = aValue[aListIdx] 180 | # print("aListItem=%s" % aListItem) 181 | if aListIdx <= bValueListMaxIdx: 182 | bListItem = bValue[aListIdx] 183 | # print("bListItem=%s" % bListItem) 184 | recursiveMergeDict(aListItem, bListItem) 185 | else: 186 | # print("bDict=%s" % bDict) 187 | # print("aKey=%s" % aKey) 188 | # print("aListItem=%s" % aListItem) 189 | bDict[aKey].append(aListItem) 190 | 191 | return bDict 192 | ``` 193 | 194 | 调用举例: 195 | 196 | ```python 197 | 198 | templateJson = { 199 | "author": "Crifan Li <admin@crifan.com>", 200 | "description": "gitbook书的描述", 201 | "gitbook": "3.2.3", 202 | "language": "zh-hans", 203 | "links": { "sidebar": { "主页": "http://www.crifan.com" } }, 204 | "plugins": [ 205 | "theme-comscore", 206 | "anchors", 207 | "-lunr", 208 | "-search", 209 | "search-plus", 210 | "disqus", 211 | "-highlight", 212 | "prism", 213 | "prism-themes", 214 | "github-buttons", 215 | "splitter", 216 | "-sharing", 217 | "sharing-plus", 218 | "tbfed-pagefooter", 219 | "expandable-chapters-small", 220 | "ga", 221 | "donate", 222 | "sitemap-general", 223 | "copy-code-button", 224 | "callouts", 225 | "toolbar-button" 226 | ], 227 | "pluginsConfig": { 228 | "callouts": { "showTypeInHeader": false }, 229 | "disqus": { "shortName": "crifan" }, 230 | "donate": { 231 | "alipay": "https://www.crifan.com/files/res/crifan_com/crifan_alipay_pay.jpg", 232 | "alipayText": "支付宝打赏给Crifan", 233 | "button": "打赏", 234 | "title": "", 235 | "wechat": "https://www.crifan.com/files/res/crifan_com/crifan_wechat_pay.jpg", 236 | "wechatText": "微信打赏给Crifan" 237 | }, 238 | "ga": { "token": "UA-28297199-1" }, 239 | "github-buttons": { 240 | "buttons": [ 241 | { 242 | "count": true, 243 | "repo": "gitbook_name", 244 | "size": "small", 245 | "type": "star", 246 | "user": "crifan" 247 | }, 248 | { 249 | "count": false, 250 | "size": "small", 251 | "type": "follow", 252 | "user": "crifan", 253 | "width": "120" 254 | } 255 | ] 256 | }, 257 | "prism": { "css": ["prism-themes/themes/prism-atom-dark.css"] }, 258 | "sharing": { 259 | "all": [ 260 | "douban", 261 | "facebook", 262 | "google", 263 | "instapaper", 264 | "line", 265 | "linkedin", 266 | "messenger", 267 | "pocket", 268 | "qq", 269 | "qzone", 270 | "stumbleupon", 271 | "twitter", 272 | "viber", 273 | "vk", 274 | "weibo", 275 | "whatsapp" 276 | ], 277 | "douban": false, 278 | "facebook": true, 279 | "google": false, 280 | "hatenaBookmark": false, 281 | "instapaper": false, 282 | "line": false, 283 | "linkedin": false, 284 | "messenger": false, 285 | "pocket": false, 286 | "qq": true, 287 | "qzone": false, 288 | "stumbleupon": false, 289 | "twitter": true, 290 | "viber": false, 291 | "vk": false, 292 | "weibo": true, 293 | "whatsapp": false 294 | }, 295 | "sitemap-general": { 296 | "prefix": "https://book.crifan.com/gitbook/gitbook_name/website/" 297 | }, 298 | "tbfed-pagefooter": { 299 | "copyright": "crifan.com,使用<a \"href=\"https://creativecommons.org/licenses/by/4.0/deed.zh\">署名4.0国际(CC \"BY 4.0)协议</a>发布", 300 | "modify_format": "YYYY-MM-DD HH:mm:ss", 301 | "modify_label": "最后更新:" 302 | }, 303 | "theme-default": { "showLevel": true }, 304 | "toolbar-button": { 305 | "icon": "fa-file-pdf-o", 306 | "label": "下载PDF", 307 | "url": "http://book.crifan.com/books/gitbook_name/pdf/gitbook_name.pdf" 308 | } 309 | }, 310 | "root": "./src", 311 | "title": "Gitbook的书名" 312 | } 313 | 314 | currentJson = { 315 | "description": "crifan整理的Python各个方面常用的代码段,供需要的参考。", 316 | "pluginsConfig": { 317 | "github-buttons": { "buttons": [{ "repo": "python_common_code_snippet" }] }, 318 | "sitemap-general": { 319 | "prefix": "https://book.crifan.com/gitbook/python_common_code_snippet/website/" 320 | }, 321 | "toolbar-button": { 322 | "url": "http://book.crifan.com/books/python_common_code_snippet/pdf/python_common_code_snippet.pdf" 323 | } 324 | }, 325 | "title": "Python常用代码段" 326 | } 327 | 328 | 329 | bookJson = recursiveMergeDict(templateJson, copy.deepcopy(currentJson)) 330 | 331 | ``` 332 | -------------------------------------------------------------------------------- /src/common_syntax/enum.md: -------------------------------------------------------------------------------- 1 | # enum枚举 2 | 3 | ## 枚举基本用法 4 | 5 | ### 枚举定义 6 | 7 | 举例1: 8 | 9 | ```python 10 | from enum import Enum 11 | 12 | class BatteryState(Enum): 13 | Unknown = 0 14 | Unplugged = 1 15 | Charging = 2 16 | Full = 3 17 | ``` 18 | 19 | 举例2: 20 | 21 | ```python 22 | import enum 23 | 24 | class ScreenshotQuality(enum.Enum): 25 | Original = 0 26 | Medium = 1 27 | Low = 2 28 | ``` 29 | 30 | 举例3: 31 | 32 | ```python 33 | class SentenceInvalidReason(Enum): 34 | NONE = "none" 35 | UNKNOWN = "unknown" 36 | EMPTY = "empty 37 | TOO_SHORT = "too short" 38 | TOO_LONG = "too long" 39 | TOO_MANY_INVALID_WORD = "contain too many invalid words" 40 | ``` 41 | 42 | ### 初始化创建枚举值 43 | 44 | 直接传入对应的(此处是int)值即可: 45 | 46 | ```python 47 | batteryStateInt = 2 48 | curBattryStateEnum = BatteryState(batteryStateInt) 49 | ``` 50 | 51 | log输出是: 52 | 53 | ```bash 54 | curBattryStateEnum=BatteryState.Charging 55 | ``` 56 | 57 | ### 获取枚举的名称 58 | 59 | ```python 60 | curBattryStateName = curBattryStateEnum.name 61 | ``` 62 | 63 | 输出:`'Charging'` 64 | 65 | ### 获取枚举的值 66 | 67 | ```python 68 | curBattryStateValue = curBattryStateEnum.value 69 | ``` 70 | 输出:`2` 71 | 72 | 类似,直接从定义中获取值: 73 | 74 | ```python 75 | gScreenQuality = ScreenshotQuality.Low.value # 2 76 | ``` 77 | 78 | ## 枚举高级用法 79 | 80 | ### 给枚举中添加函数 81 | 82 | ```python 83 | class TipType(enum.Enum): 84 | NoTip = "NoTip" 85 | TenPercent = "TenPercent" 86 | FifthPercent = "FifthPercent" 87 | TwentyPercent = "TwentyPercent" 88 | 89 | # @property 90 | def getTipPercent(self): 91 | tipPercent = 0.0 92 | if self == TipType.NoTip: 93 | tipPercent = 0.0 94 | elif self == TipType.TenPercent: 95 | tipPercent = 0.10 96 | elif self == TipType.FifthPercent: 97 | tipPercent = 0.15 98 | elif self == TipType.TwentyPercent: 99 | tipPercent = 0.20 100 | gLog.debug("self=%s -> tipPercent=%s", self, tipPercent) 101 | return tipPercent 102 | ``` 103 | 104 | 调用: 105 | 106 | ```python 107 | tipPercent = initiatorTipType.getTipPercent() 108 | # tipPercent=0.1 109 | ``` 110 | 111 | ## 注意事项 112 | 113 | ### 字符串枚举定义最后不要加逗号 114 | 115 | enum定义期间不要加(多余的)逗号: 116 | 117 | ```python 118 | class ScreenshotQuality(enum.Enum): 119 | Original = 0, 120 | Medium = 1, 121 | Low = 2, 122 | ``` 123 | 124 | 否则`value`就是`tuple`**元祖**了: 125 | 126 | ```python 127 | gScreenQuality = ScreenshotQuality.Low.value # 实际上是 (2,) 128 | print("gScreenQuality=%s" % gScreenQuality) # gScreenQuality=2 129 | print("type(gScreenQuality)=%s" % type(gScreenQuality)) # type(gScreenQuality)=<class 'tuple'> 130 | ``` 131 | 132 | ![enum_comma_str_to_tuple](../assets/img/enum_comma_str_to_tuple.png) 133 | -------------------------------------------------------------------------------- /src/common_syntax/function_parameter.md: -------------------------------------------------------------------------------- 1 | # 函数参数 2 | 3 | ## 可变参数 4 | 5 | 之前一个用到了可变参数的函数是: 6 | 7 | ```python 8 | def multipleRetry(self, functionInfoDict, maxRetryNum=5, sleepInterval=0.1): 9 | """ 10 | do something, retry mutiple time if fail 11 | 12 | Args: 13 | functionInfoDict (dict): function info dict contain functionCallback and [optional] functionParaDict 14 | maxRetryNum (int): max retry number 15 | sleepInterval (float): sleep time of each interval when fail 16 | Returns: 17 | bool 18 | Raises: 19 | """ 20 | doSuccess = False 21 | functionCallback = functionInfoDict["functionCallback"] 22 | functionParaDict = functionInfoDict.get("functionParaDict", None) 23 | 24 | curRetryNum = maxRetryNum 25 | while curRetryNum > 0: 26 | if functionParaDict: 27 | doSuccess = functionCallback(**functionParaDict) 28 | else: 29 | doSuccess = functionCallback() 30 | 31 | if doSuccess: 32 | break 33 | 34 | time.sleep(sleepInterval) 35 | curRetryNum -= 1 36 | 37 | if not doSuccess: 38 | functionName = str(functionCallback) 39 | # '<bound method DevicesMethods.switchToAppStoreSearchTab of <src.AppCrawler.AppCrawler object at 0x1053abe80>>' 40 | logging.error("Still fail after %d retry for %s", functionName) 41 | return doSuccess 42 | ``` 43 | 44 | 其中的: 45 | 46 | `functionCallback(**functionParaDict)` 47 | 48 | 中的: 49 | 50 | `**functionParaDict` 51 | 52 | 表示,dict类型的参数,内部包含多个key和value,用**去展开后,传入真正要执行的函数 53 | 54 | 几种调用中带参数的例子是: 55 | 56 | ```python 57 | searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"} 58 | isInputOk = self.multipleRetry( 59 | { 60 | "functionCallback": self.wait_element_setText, 61 | "functionParaDict": { 62 | "locator": searchInputQuery, 63 | "text": appName, 64 | } 65 | } 66 | ) 67 | ``` 68 | 69 | 之前原始写法: 70 | 71 | ```python 72 | searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"} 73 | isInputOk = self.wait_element_setText(searchInputQuery, appName) 74 | ``` 75 | 76 | 其中wait_element_setText的定义是: 77 | 78 | ```python 79 | def wait_element_setText(self, locator, text): 80 | ``` 81 | 82 | 对应着之前传入时的: 83 | 84 | ```python 85 | "functionParaDict": { 86 | "locator": searchInputQuery, 87 | "text": appName, 88 | } 89 | ``` 90 | 91 | 即可,给出上述细节,便于理解,传入的参数是如何用`**`展开的。 92 | 93 | 详见: 94 | 95 | 【已解决】Python中如何实现函数调用时多个可变数量的参数传递 96 | -------------------------------------------------------------------------------- /src/common_syntax/list_set.md: -------------------------------------------------------------------------------- 1 | # list列表和set集合 2 | 3 | ## list vs set 4 | 5 | * set 6 | * 适用于检测某元素是否在集合内、对集合进行一定的数学操作 7 | * 不支持indexing,slicing 8 | * list 9 | * 普通的数组 10 | * 支持indexing,slicing 11 | 12 | ## 把list换成set 13 | 14 | ```python 15 | someSet = set([]) 16 | for eachItem in someList: 17 | someSet.add(eachItem) 18 | ``` 19 | 20 | ## set集合转换为字符串 21 | 22 | ```python 23 | someSetStr = ", ".join(someSet) 24 | ``` 25 | 26 | ## 把列表转为python正则中的group中可能出现的选项 27 | 28 | ```python 29 | def listToPatternGroup(curList): 30 | """Convert list to pattern group""" 31 | patternGroupList = list(map(lambda curType: "(%s)" % curType, curList)) # ['(aaa)', '(bbb)', '(ccc)', '(zzz)', '(eee)', '(yyy)', '(ddd)', '(xxx)'] 32 | groupP = "|".join(patternGroupList) # '(aaa)|(bbb)|(ccc)|(zzz)|(eee)|(yyy)|(ddd)|(xxx)' 33 | return groupP 34 | ``` 35 | 36 | 调用: 37 | 38 | ```python 39 | ValidPlatformTypeList = ["iOS", "Android"] 40 | ValidPlatformRule = listToPatternGroup(ValidPlatformTypeList) # '(iOS)|(Android)' 41 | ``` 42 | 43 | 目的是用于后续的正则判断 44 | 45 | ```python 46 | TaskFilenamePattern = "(?P<taskDate>\d+)_(?P<businessType>%s)_(?P<taskName>[a-zA-Z\d]+)_(?P<crawlerType>%s)(_(?P<platform>%s))?" % (ValidBusinessTypeRule, ValidCrawlerTypeRule, ValidPlatformRule) 47 | ``` 48 | -------------------------------------------------------------------------------- /src/common_syntax/logging.md: -------------------------------------------------------------------------------- 1 | # logging日志 2 | 3 | ## 彩色日志+日志初始化 4 | 5 | 自己的库:[crifanLogging.py](https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanLogging.py) 6 | 7 | 已实现常用的功能,包括: 8 | 9 | * 彩色日志 10 | * 初始化 11 | 12 | 使用方式 = 典型调用代码: 13 | 14 | 先下载我的库: 15 | 16 | * [crifanLogging.py](https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanLogging.py) 17 | * https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanLogging.py 18 | 19 | 对于文件:`somePythonFile.py` 20 | 21 | 调用和初始化代码: 22 | 23 | ```python 24 | import crifanLogging 25 | 26 | CurFilePath = os.path.abspath(__file__) 27 | # print("CurFilePath=%s" % CurFilePath) 28 | CurFilename = os.path.basename(CurFilePath) 29 | # 'autoSearchGame_YingYongBao.py' 30 | CurFileNoSuffix, pointSuffix = os.path.splitext(CurFilename) 31 | 32 | CurFolder = os.path.dirname(CurFilePath) 33 | # print("CurFolder=%s" % CurFolder) 34 | 35 | LogFolder = os.path.join(CurFolder, "logs") 36 | 37 | def initLog(): 38 | curDatetimeStr = utils.getCurDatetimeStr() # '20200316_155954' 39 | utils.createFolder(LogFolder) 40 | curLogFile = "%s_%s.log" % (CurFileNoSuffix, curDatetimeStr) 41 | logFullPath = os.path.join(LogFolder, curLogFile) 42 | crifanLogging.loggingInit(logFullPath) 43 | 44 | def main(): 45 | initLog() 46 | ``` 47 | 48 | 即可生成log文件:`logs/somePythonFile.log` 49 | 50 | 注:相关函数: 51 | 52 | * `createFolder` 53 | * [新建文件夹](https://book.crifan.com/books/python_common_code_snippet/website/common_code/file_system/folder.html#%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9) 54 | * `getCurDatetimeStr` 55 | * [getCurDatetimeStr 生成当前日期时间字符串](https://book.crifan.com/books/python_common_code_snippet/website/common_code/date_time.html#getcurdatetimestr-%E7%94%9F%E6%88%90%E5%BD%93%E5%89%8D%E6%97%A5%E6%9C%9F%E6%97%B6%E9%97%B4%E5%AD%97%E7%AC%A6%E4%B8%B2) 56 | -------------------------------------------------------------------------------- /src/common_syntax/sort.md: -------------------------------------------------------------------------------- 1 | # sort排序 2 | 3 | 详见: 4 | 5 | * https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanDict.py 6 | * https://github.com/crifan/crifanLibPython/blob/master/crifanLib/demo/crifanDictDemo.py 7 | 8 | ## 对字典根据key去排序 9 | 10 | ```python 11 | from collections import OrderedDict 12 | 13 | def sortDictByKey(originDict): 14 | """ 15 | Sort dict by key 16 | """ 17 | originItems = originDict.items() 18 | sortedOriginItems = sorted(originItems) 19 | sortedOrderedDict = OrderedDict(sortedOriginItems) 20 | return sortedOrderedDict 21 | ``` 22 | 23 | 调用: 24 | 25 | ```python 26 | def demoSortDictByKey(): 27 | originDict = { 28 | "c": "abc", 29 | "a": 1, 30 | "b": 22 31 | } 32 | print("originDict=%s" % originDict) 33 | # originDict={'c': 'abc', 'a': 1, 'b': 22} 34 | sortedOrderedDict = sortDictByKey(originDict) 35 | print("sortedOrderedDict=%s" % sortedOrderedDict) 36 | # sortedOrderedDict=OrderedDict([('a', 1), ('b', 22), ('c', 'abc')]) 37 | ``` 38 | 39 | ## sort和sorted 40 | 41 | ```python 42 | # Function: Demo sorted 43 | # mainly refer official doc: 44 | # 排序指南 — Python 3.8.2 文档 45 | # https://docs.python.org/zh-cn/3/howto/sorting.html 46 | # Author: Crifan Li 47 | # Update: 20200304 48 | 49 | 50 | from operator import itemgetter, attrgetter 51 | 52 | 53 | print("%s %s %s" % ('='*40, "sort", '='*40)) 54 | 55 | 56 | originIntList = [5, 2, 3, 1, 4] 57 | originIntList.sort() 58 | sortedSelfIntList = originIntList 59 | print("sortedSelfIntList=%s" % sortedSelfIntList) 60 | # sortedSelfIntList=[1, 2, 3, 4, 5] 61 | 62 | 63 | print("%s %s %s" % ('='*40, "sorted", '='*40)) 64 | 65 | 66 | intList = [5, 2, 3, 1, 4] 67 | sortedIntList = sorted(intList) 68 | print("sortedIntList=%s" % sortedIntList) 69 | # sortedIntList=[1, 2, 3, 4, 5] 70 | 71 | 72 | reversedSortIntList = sorted(intList, reverse=True) 73 | print("reversedSortIntList=%s" % reversedSortIntList) 74 | # reversedSortIntList=[5, 4, 3, 2, 1] 75 | 76 | 77 | intStrDict = {5: 'A', 1: 'D', 2: 'B', 4: 'E', 3: 'B'} 78 | dictSortedIntList = sorted(intStrDict) 79 | print("dictSortedIntList=%s" % dictSortedIntList) 80 | # dictSortedIntList=[1, 2, 3, 4, 5] 81 | 82 | 83 | normalStr = "Crifan Li best love language is Python" 84 | strList = normalStr.split() 85 | print("strList=%s" % strList) 86 | sortedStrList = sorted(strList, key=str.lower) 87 | print("sortedStrList=%s" % sortedStrList) 88 | # strList=['Crifan', 'Li', 'best', 'love', 'language', 'is', 'Python'] 89 | # sortedStrList=['best', 'Crifan', 'is', 'language', 'Li', 'love', 'Python'] 90 | 91 | 92 | studentTupleList = [ 93 | # name, grade, age 94 | ('Cindy', 'A', 15), 95 | ('Crifan', 'B', 12), 96 | ('Tony', 'B', 10), 97 | ] 98 | sortedTupleList_lambda = sorted(studentTupleList, key=lambda student: student[2]) # [2] is age 99 | print("sortedTupleList_lambda=%s" % sortedTupleList_lambda) 100 | # sortedTupleList_lambda=[('Tony', 'B', 10), ('Crifan', 'B', 12), ('Cindy', 'A', 15)] 101 | 102 | 103 | # same as single function: 104 | def getStudentAge(curStudentTuple): 105 | return curStudentTuple[2] # [2] is age 106 | sortedTupleList_singleFunction = sorted(studentTupleList, key=getStudentAge) 107 | print("sortedTupleList_singleFunction=%s" % sortedTupleList_singleFunction) 108 | # sortedTupleList_singleFunction=[('Tony', 'B', 10), ('Crifan', 'B', 12), ('Cindy', 'A', 15)] 109 | 110 | 111 | # same as operator itemgetter: 112 | sortedTupleList_operator = sorted(studentTupleList, key=itemgetter(2)) 113 | print("sortedTupleList_operator=%s" % sortedTupleList_operator) 114 | # sortedTupleList_operator=[('Tony', 'B', 10), ('Crifan', 'B', 12), ('Cindy', 'A', 15)] 115 | 116 | 117 | class Student: 118 | def __init__(self, name, grade, age): 119 | self.name = name 120 | self.grade = grade 121 | self.age = age 122 | def __repr__(self): 123 | return repr((self.name, self.grade, self.age)) 124 | 125 | 126 | studentObjectList = [ 127 | Student('john', 'A', 15), 128 | Student('jane', 'A', 15), 129 | Student('dave', 'A', 15), 130 | ] 131 | sortedObjectList = sorted(studentObjectList, key=lambda student: student.age) 132 | print("sortedObjectList=%s" % sortedObjectList) 133 | # sortedObjectList=[('john', 'A', 15), ('jane', 'A', 15), ('dave', 'A', 15)] 134 | 135 | 136 | # same as operator attrgetter: 137 | sortedObjectList_operator = sorted(sortedObjectList, key=attrgetter('age')) 138 | print("sortedObjectList_operator=%s" % sortedObjectList_operator) 139 | # sortedObjectList_operator=[('john', 'A', 15), ('jane', 'A', 15), ('dave', 'A', 15)] 140 | ``` 141 | 142 | 143 | --------------------------------------------------------------------------------