'
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 | * 
90 |
91 | 播放的效果:
92 |
93 | mac系统中播放音频
94 |
95 | PyCharm的console中输出当前播放的信息 -》 如果是mac的terminal中,则是覆盖式的,不会这么多行
96 | 同时弹框GUI窗口
97 |
98 | 
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 | * 
193 | * 
194 | * 异常mp3:
195 | * 故意把png图片改成mp3
196 | * 
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 | 
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 | 
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 | 
330 |
331 | (2)多次循环后,给同一张图中多个元素加上边框后
332 |
333 | 
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 | 
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 html: %s", htmlStr)
149 | # 'Illegal access address!\n'
150 | #
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 |
294 | in :
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
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 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
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 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
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 |
458 | 。。。
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
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 ",
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,使用署名4.0国际(CC \"BY 4.0)协议发布",
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)=
130 | ```
131 |
132 | 
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 | # '>'
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\d+)_(?P%s)_(?P[a-zA-Z\d]+)_(?P%s)(_(?P%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 |
--------------------------------------------------------------------------------