目录

1. 前言   1.1

2. 背景   1.2

3. 常用代码段   1.3

1. 通用逻辑   1.3.1

2. 变量   1.3.2

3. 系统   1.3.3

4. 日期时间   1.3.4

5. 字符和字符串   1.3.5

6. 文件系统   1.3.6

1. 文件   1.3.6.1

2. 文件夹   1.3.6.2

7. 多媒体   1.3.7

1. 图像   1.3.7.1

1. Pillow   1.3.7.1.1

2. 百度OCR   1.3.7.1.2

2. 音频   1.3.7.2

3. 视频   1.3.7.3

8. 网络相关   1.3.8

1. BeautifulSoup   1.3.8.1

2. requests   1.3.8.2

9. 数学   1.3.9

10. 邮件   1.3.10

11. csv和Excel   1.3.11

4. 常见语法   1.4

1. 函数参数   1.4.1

2. dict字典   1.4.2

3. list列表和set集合   1.4.3

4. sort排序   1.4.4

5. enum枚举   1.4.5

6. collections集合   1.4.6

7. logging日志   1.4.7

5. 附录   1.5

1. 参考资料   1.5.1

Python常用代码段

- 最新版本： v1.5

- 更新时间： 20210817

简介

整理出crifan总结的Python各个方面常用的代码段，供需要的参考。包括通用逻辑、变量、系统、日期时间、字符和字符串、文件系统，比如文件和文件夹等、以及第三方库，比如BeautifulSoup、以及多媒体音视频类、包括Pillow、以及网络的Requests等，；以及其他常见语法，包括dict字典、list列表、set集合、enum枚举、collections集合等。

源码+浏览+下载

本书的各种源码、在线浏览地址、多种格式文件下载如下：

Gitbook源码

- crifan/python_common_code_snippet: Python常用代码段

如何使用此Gitbook源码去生成发布为电子书

详见：crifan/gitbook_template: demo how to use crifan gitbook template and demo

在线浏览

- Python常用代码段 book.crifan.com

- Python常用代码段 crifan.github.io

离线下载阅读

- Python常用代码段 PDF

- Python常用代码段 ePub

- Python常用代码段 Mobi

版权说明

此电子书教程的全部内容，如无特别说明，均为本人原创和整理。其中部分内容参考自网络，均已备注了出处。如有发现侵犯您版权，请通过邮箱联系我  admin 艾特 crifan.com ，我会尽快删除。谢谢合作。

鸣谢

感谢我的老婆陈雪的包容理解和悉心照料，才使得我 crifan 有更多精力去专注技术专研和整理归纳出这些电子书和技术教程，特此鸣谢。

更多其他电子书

本人 crifan 还写了其他 100+ 本电子书教程，感兴趣可移步至：

crifan/crifan_ebook_readme: Crifan的电子书的使用说明

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-09-08 10:54:43

背景

多年的技术开发，积累了很多关于 Python 的常用的一些函数和功能，都已整理到对应的函数库中：

https://github.com/crifan/crifanLibPython

且也给出了很多函数的demo演示如何使用：

https://github.com/crifan/crifanLibPython/tree/master/crifanLib/demo

但还是解释的不够清楚和全面。

故此处专门把Python的常用的代码段和调用举例，都整理至此，详细解释。

目的：

方便需要时能快速查阅：直接看代码，一看就懂，无需额外解释。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-27 20:21:05

常用代码段

下面总结各个方面的Python常用代码段。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:46:30

通用逻辑

多次运行一个函数，直到成功运行

执行一个函数（可能有多个可变数量的参数），且尝试多次，直到成功或超出最大此时，最终实现是：

def multipleRetry(functionInfoDict, maxRetryNum=5, sleepInterval=0.1, isShowErrWhenFail=True):

"""

do something, retry mutiple time if fail

Args:

functionInfoDict (dict): function info dict contain functionCallback and [optional] functionParaDict

maxRetryNum (int): max retry number

sleepInterval (float): sleep time of each interval when fail

isShowErrWhenFail (bool): show error when fail if true

Returns:

bool

Raises:

"""

doSuccess = False

functionCallback = functionInfoDict["functionCallback"]

functionParaDict = functionInfoDict.get("functionParaDict", None)

curRetryNum = maxRetryNum

while curRetryNum > 0:

if functionParaDict:

doSuccess = functionCallback(**functionParaDict)

else:

doSuccess = functionCallback()

if doSuccess:

break

time.sleep(sleepInterval)

curRetryNum -= 1

if not doSuccess:

if isShowErrWhenFail:

functionName = str(functionCallback)

# '<bound method DevicesMethods.switchToAppStoreSearchTab of <src.AppCrawler.AppCrawler object at 0x1053abe80>>'

logging.error("Still fail after %d retry for %s", maxRetryNum, functionName)

return doSuccess

说明：

functionCallback 函数类型都要符合：返回值是bool类型才可以

调用举例：

（1）没有额外参数

foundAndClickedWifi = CommonUtils.multipleRetry({"functionCallback": self.iOSFromSettingsIntoWifiList})

其中：

def iOSFromSettingsIntoWifiList(self):

。。。

foundAndClickedWifi = self.findAndClickElement(query=wifiTextQuery, timeout=0.1)

return foundAndClickedWifi

类似例子：

isSwitchOk = self.multipleRetry({"functionCallback": self.switchToAppStoreSearchTab})

对比之前原始写法：

isSwitchOk = self.switchToAppStoreSearchTab()

其他类似例子：

foundAndClickedDownload = self.multipleRetry({"functionCallback": self.appStoreStartDownload})

详见：

【已解决】AppStore自动安装iOS的app：逻辑优化加等待和多试几次

（2）有额外参数，参数个数：2个

searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"}

isInputOk = CommonUtils.multipleRetry(

{

"functionCallback": self.wait_element_setText,

"functionParaDict": {

"locator": searchInputQuery,

"text": appName,

}

}

)

对比之前原始写法：

searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"}

isInputOk = self.wait_element_setText(searchInputQuery, appName)

其中wait_element_setText的定义是：

def wait_element_setText(self, locator, text):

对应着之前传入时的：

"functionParaDict": {

"locator": searchInputQuery,

"text": appName,

}

（3）有额外参数，且加上multipleRetry的额外参数

isSwitchOk = CommonUtils.multipleRetry(

{"functionCallback": self.switchToAppStoreSearchTab},

maxRetryNum = 10,

sleepInterval = 0.5,

)

以及类似的：

isIntoDetailOk = self.multipleRetry(

{

"functionCallback": self.appStoreSearchResultIntoDetail,

"functionParaDict": {

"appName": appName,

}

},

sleepInterval=0.5

)

之前原始写法：

isIntoDetailOk = self.appStoreSearchResultIntoDetail(appName)

注：

此处是后来加上的

sleepInterval=0.5

是因为后来遇到了，即使尝试了5次，依旧没找到，所以增加了没找到的延迟等待时间。

详见：

【已解决】AppStore自动安装iOS的app：逻辑优化加等待和多试几次

（4）

isIntoDetailOk = CommonUtils.multipleRetry(

{

"functionCallback": self.appStoreSearchResultIntoDetail,

"functionParaDict": {

"appName": appName,

}

},

maxRetryNum = 10,

sleepInterval = 0.5,

)

新版：新增参数isRespFullRetValue

此处最后更新： 20200925

后续多次优化新增参数：是否返回完整信息

代码：

def multipleRetry(functionInfoDict, maxRetryNum=5, sleepInterval=0.1, isShowErrWhenFail=True, isRespFullRetValue=False):

"""do something, retry if single call failed, retry mutiple time until max retry number

Args:

functionInfoDict (dict): function info dict contain functionCallback and [optional] functionParaDict

maxRetryNum (int): max retry number

sleepInterval (float): sleep time (seconds) of each interval when fail

isShowErrWhenFail (bool): show error when fail if true

isRespFullRetValue (bool): whether return full return value of function call

Returns:

isRespFullRetValue=False: bool

isRespFullRetValue=True: bool / tuple/list/...

Raises:

"""

finalReturnValue = None

doSuccess = False

functionCallback = functionInfoDict["functionCallback"]

functionParaDict = functionInfoDict.get("functionParaDict", None)

curRetryNum = maxRetryNum

while curRetryNum > 0:

if functionParaDict:

# doSuccess = functionCallback(**functionParaDict)

respValue = functionCallback(**functionParaDict)

else:

# doSuccess = functionCallback()

respValue = functionCallback()

doSuccess = False

if isinstance(respValue, bool):

doSuccess = bool(respValue)

elif isinstance(respValue, tuple):

doSuccess = bool(respValue[0])

elif isinstance(respValue, list):

doSuccess = bool(respValue[0])

else:

Exception("multipleRetry: Not support type of return value: %s" % respValue)

if isRespFullRetValue:

finalReturnValue = respValue

else:

finalReturnValue = doSuccess

if doSuccess:

break

time.sleep(sleepInterval)

curRetryNum -= 1

if not doSuccess:

if isShowErrWhenFail:

functionName = str(functionCallback)

# '<bound method DevicesMethods.switchToAppStoreSearchTab of <src.AppCrawler.AppCrawler object at 0x1053abe80>>'

logging.error("Still fail after %d retry for %s", maxRetryNum, functionName)

# return doSuccess

return finalReturnValue

调用举例：

(1)默认不返回完整信息，只返回bool值

respBoolOrTuple = CommonUtils.multipleRetry(

functionInfoDict = {

"functionCallback": self.isGotoPayPopupPage,

"functionParaDict": {

"isRespLocation": False,

},

},

)

(2)返回完整信息

respBoolOrTuple = CommonUtils.multipleRetry(

functionInfoDict = {

"functionCallback": self.isGotoPayPopupPage,

"functionParaDict": {

"isRespLocation": True,

},

},

isRespFullRetValue = True,

)

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-09-25 15:55:19

变量

判断变量类型

优先用 isinstance ，而不是 type

>>> isinstance(2, float)

False

>>> isinstance('a', (str, unicode))

True

>>> isinstance((2, 3), (str, list, tuple))

True

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-27 18:24:15

系统

此处整理用Python处理系统相关的通用的代码。

系统类型

import sys

def osIsWinows():

return sys.platform == "win32"

def osIsCygwin():

return sys.platform == "cygwin"

def osIsMacOS():

return sys.platform == "darwin"

def osIsLinux():

return sys.platform == "linux"

def osIsAix():

return sys.platform == "aix"

命令行

获取命令行执行命令返回结果

代码：

def get_cmd_lines(cmd, text=False):

# 执行cmd命令，将结果保存为列表

resultStr = ""

resultStrList = []

try:

consoleOutputByte = subprocess.check_output(cmd, shell=True) # b'C02Y3N10JHC8\n'

try:

resultStr = consoleOutputByte.decode("utf-8")

except UnicodeDecodeError:

# TODO: use chardet auto detect encoding

# consoleOutputStr = consoleOutputByte.decode("gbk")

resultStr = consoleOutputByte.decode("gb18030")

if not text:

resultStrList = resultStr.splitlines()

except Exception as err:

print("err=%s when run cmd=%s" % (err, cmd))

if text:

return resultStr

else:

return resultStrList

硬件信息

获取当前电脑（Win或Mac）的序列号

代码：

def getSerialNumber(self):

"""get current computer serial number"""

# cmd = "wmic bios get serialnumber"

cmd = ""

if CommonUtils.osIsWinows():

# Windows

cmd = "wmic bios get serialnumber"

elif CommonUtils.osIsMacOS():

# macOS

cmd = "system_profiler SPHardwareDataType | awk '/Serial/ {print $4}'"

# TODO: add support other OS

# AIX: aix

# Linux: linux

# Windows/Cygwin: cygwin

serialNumber = ""

lines = CommonUtils.get_cmd_lines(cmd)

if CommonUtils.osIsWinows():

# Windows

serialNumber = lines[1]

elif CommonUtils.osIsMacOS():

# macOS

serialNumber = lines[0] # C02Y3N10JHC8

return serialNumber

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-26 10:24:06

日期时间

详见：

https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanDatetime.py

getCurDatetimeStr 生成当前日期时间字符串

def getCurDatetimeStr(outputFormat="%Y%m%d_%H%M%S"):

"""

get current datetime then format to string

eg:

20171111_220722

:param outputFormat: datetime output format

:return: current datetime formatted string

"""

curDatetime = datetime.now() # 2017-11-11 22:07:22.705101

curDatetimeStr = curDatetime.strftime(format=outputFormat) #'20171111_220722'

return curDatetimeStr

调用举例：

curDatetimeStr = getCurDatetimeStr() # '20191219_143400'

datetime转时间戳

import time

def datetimeToTimestamp(self, datetimeVal, withMilliseconds=False) :

"""

convert datetime value to timestamp

eg:

"2006-06-01 00:00:00.123" -> 1149091200

if with milliseconds -> 1149091200123

:param datetimeVal:

:return:

"""

timetupleValue = datetimeVal.timetuple()

timestampFloat = time.mktime(timetupleValue) # 1531468736.0 -> 10 digits

timestamp10DigitInt = int(timestampFloat) # 1531468736

timestampInt = timestamp10DigitInt

if withMilliseconds:

microsecondInt = datetimeVal.microsecond # 817762

microsecondFloat = float(microsecondInt)/float(1000000) # 0.817762

timestampFloat = timestampFloat + microsecondFloat # 1531468736.817762

timestampFloat = timestampFloat * 1000 # 1531468736817.7621 -> 13 digits

timestamp13DigitInt = int(timestampFloat) # 1531468736817

timestampInt = timestamp13DigitInt

return timestampInt

获取当前时间戳

from datetime import datetime

def getCurTimestamp(withMilliseconds=False):

"""

get current time's timestamp

(default)not milliseconds -> 10 digits: 1351670162

with milliseconds -> 13 digits: 1531464292921

"""

curDatetime = datetime.now()

return datetimeToTimestamp(curDatetime, withMilliseconds)

时间戳精确到毫秒

from datetime import datetime,timedelta

timestampStr = datetime.now().strftime("%Y%m%d_%H%M%S_%f")

# 20180712_154134_660436

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:46:56

字符和字符串

详见：

https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanFile.py

Python3 str转bytes

Python 3中，把字符串转换成字节码，可以有两种写法：

- 方法1：用bytes去转换

convertedBytes = bytes(originStr)

- 方法2：用字符串str的编码encode

encodedBytes = originStr.encode()

其中：

都有额外的encoding参数：

- 由于默认都是UTF-8

- 所以可加可不加

- 也可以根据需要，去加其他你要的编码

- 如果要加，就是：

convertedBytes = bytes(originStr, "UTF-8")

encodedBytes = originStr.encode("UTF-8")

字符串格式化为人类易读格式

def formatSize(sizeInBytes, decimalNum=1, isUnitWithI=False, sizeUnitSeperator=""):

"""

format size to human readable string

example:

3746 -> 3.7KB

87533 -> 85.5KiB

98654 -> 96.3 KB

352 -> 352.0B

76383285 -> 72.84MB

763832854988542 -> 694.70TB

763832854988542665 -> 678.4199PB

refer:

https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size

"""

# https://en.wikipedia.org/wiki/Binary_prefix#Specific_units_of_IEC_60027-2_A.2_and_ISO.2FIEC_80000

# K=kilo, M=mega, G=giga, T=tera, P=peta, E=exa, Z=zetta, Y=yotta

sizeUnitList = ['','K','M','G','T','P','E','Z']

largestUnit = 'Y'

if isUnitWithI:

sizeUnitListWithI = []

for curIdx, eachUnit in enumerate(sizeUnitList):

unitWithI = eachUnit

if curIdx >= 1:

unitWithI += 'i'

sizeUnitListWithI.append(unitWithI)

# sizeUnitListWithI = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']

sizeUnitList = sizeUnitListWithI

largestUnit += 'i'

suffix = "B"

decimalFormat = "." + str(decimalNum) + "f" # ".1f"

finalFormat = "%" + decimalFormat + sizeUnitSeperator + "%s%s" # "%.1f%s%s"

sizeNum = sizeInBytes

for sizeUnit in sizeUnitList:

if abs(sizeNum) < 1024.0:

return finalFormat % (sizeNum, sizeUnit, suffix)

sizeNum /= 1024.0

return finalFormat % (sizeNum, largestUnit, suffix)

调用：

def testKb():

kbSize = 3746

kbStr = formatSize(kbSize)

print("%s -&gt; %s" % (kbSize, kbStr))

def testI():

iSize = 87533

iStr = formatSize(iSize, isUnitWithI=True)

print("%s -&gt; %s" % (iSize, iStr))

def testSeparator():

seperatorSize = 98654

seperatorStr = formatSize(seperatorSize, sizeUnitSeperator=" ")

print("%s -&gt; %s" % (seperatorSize, seperatorStr))

def testBytes():

bytesSize = 352

bytesStr = formatSize(bytesSize)

print("%s -&gt; %s" % (bytesSize, bytesStr))

def testMb():

mbSize = 76383285

mbStr = formatSize(mbSize, decimalNum=2)

print("%s -&gt; %s" % (mbSize, mbStr))

def testTb():

tbSize = 763832854988542

tbStr = formatSize(tbSize, decimalNum=2)

print("%s -&gt; %s" % (tbSize, tbStr))

def testPb():

pbSize = 763832854988542665

pbStr = formatSize(pbSize, decimalNum=4)

print("%s -&gt; %s" % (pbSize, pbStr))

def demoFormatSize():

testKb()

testI()

testSeparator()

testBytes()

testMb()

testTb()

testPb()

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:47:01

文件系统

最新代码详见：

- https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanFile.py

- https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/demo/crifanFileDemo.py

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-08-12 15:49:05

文件

获取文件最后更新时间

import os

def getUpdateTime(curFileOrPath):

"""get file/folder latest update time = modify time

Args:

curFileOrPath (str): some file or folder path

Returns:

int: time stamp int of 13 digit, with milliseconds

Raises:

"""

updateTime = None

try:

modifyTime = os.path.getmtime(curFileOrPath) # 1593748641.3270357

updateTime = int(modifyTime * 1000) # 1593748641327

except OSError as err:

errMsg = str(err)

# print("errMsg=%s" % errMsg)

pass

return updateTime

详见：

【已解决】Python中获取文件最后更新时间

提取文件名后缀

def extractSuffix(fileNameOrUrl):

"""

extract file suffix from name or url

eg:

https://cdn2.qupeiyin.cn/2018-09-10/15365514898246.mp4 -> mp4

15365514894833.srt -> srt

"""

return fileNameOrUrl.split('.')[-1]

创建空文件

import os

def createEmptyFile(fullFilename):

"""Create a empty file like touch"""

filePath = os.path.dirname(fullFilename)

# create folder if not exist

if not os.path.exists(filePath):

os.makedirs(filePath)

with open(fullFilename, 'a'):

# Note: not use 'w' for maybe conflict for others constantly writing to it

os.utime(fullFilename, None)

读取文件二进制数据

def readBinDataFromFile(fileToRead):

"""Read binary data from file"""

binaryData = None

try:

readFp = open(fileToRead, "rb")

binaryData = readFp.read()

readFp.close()

except:

binaryData = None

return binaryData

调用：

imgBinData = readBinDataFromFile(imageFullPath)

保存二进制数据到文件

def saveDataToFile(fullFilename, binaryData):

"""save binary data info file"""

with open(fullFilename, 'wb') as fp:

fp.write(binaryData)

fp.close()

# print("Complete save file %s" % fullFilename)

保存json到文件

import json

import codecs

def saveJsonToFile(fullFilename, jsonValue):

"""save json dict into file"""

with codecs.open(fullFilename, 'w', encoding="utf-8") as jsonFp:

json.dump(jsonValue, jsonFp, indent=2, ensure_ascii=False)

# print("Complete save json %s" % fullFilename)

从文件中加载出json

import json

import codecs

def loadJsonFromFile(fullFilename):

"""load and parse json dict from file"""

with codecs.open(fullFilename, 'r', encoding="utf-8") as jsonFp:

jsonDict = json.load(jsonFp)

# print("Complete load json from %s" % fullFilename)

return jsonDict

通过二进制生成文件类型对象

（1）Python 3

import io

audioBinaryData = audioObj.read()

audioFileLikeObj = io.BytesIO(audioBinaryData)

得到对应的文件类型的对象的：  <_io.BytesIO object at 0x115964468>，即可去像操作文件一样去操作这个io。

（2）Python 2

import StringIO

audioFileLikeObj = StringIO.StringIO()

audioFileLikeObj.write(audioBinaryData)

一行代码把字符串写入保存到文件

open(fullFilePath, "w").write(fileContentStr).close()

举例：

open("0408_1600.xml", "w").write(page).close()

给文件增加可执行权限：实现 chmod +x 的效果

代码：

import os

import stat

curState = os.stat(someFile)

newState = curState.st_mode | stat.S_IEXEC

os.chmod(someFile, newState)

再去优化为函数：

import os

import stat

def chmodAddX(someFile):

"""add file executable mode, like chmod +x

Args:

someFile (str): file full path

Returns:

soup

Raises:

"""

if os.path.exists(someFile):

if os.path.isfile(someFile):

# add executable

curState = os.stat(someFile)

newState = curState.st_mode | stat.S_IEXEC

os.chmod(someFile, newState)

调用：

chmodAddX(shellFullPath)

继续优化：

参考 How do you do a simple "chmod +x" from within python? - Stack Overflow

如果想要加上，给任何人都有可执行权限，则可以用：

def chmodAddX(someFile):

"""add file executable mode, like chmod +x

Args:

someFile (str): file full path

Returns:

soup

Raises:

"""

if os.path.exists(someFile):

if os.path.isfile(someFile):

# add executable

curState = os.stat(someFile)

# STAT_OWNER_EXECUTABLE = stat.S_IEXEC

# executableMode = STAT_OWNER_EXECUTABLE

STAT_EVERYONE_EXECUTABLE = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH

executableMode = STAT_EVERYONE_EXECUTABLE

newState = curState.st_mode | executableMode

os.chmod(someFile, newState)

效果：

- 之前： -rw-r--r--

- 之后： -rwxr-xr-x

- 给 user group other 都加上 x 的可执行权限

详见：

【已解决】Python中给Mac中文件加上可执行权限

判断是否是文件对象

import sys

def isFileObject(fileObj):

""""check is file like object or not"""

if sys.version_info[0] == 2:

return isinstance(fileObj, file)

else:

# for python 3:

# has read() method for:

# io.IOBase

# io.BytesIO

# io.StringIO

# io.RawIOBase

return hasattr(fileObj, 'read')

计算当前文件名，如果重名，则位数加1

import os

import re

def findNextNumberFilename(curFilename):

"""Find the next available filename from current name

Args:

curFilename (str): current filename

Returns:

next available (not existed) filename

Raises:

Examples:

(1) 'crifanLib/demo/input/image/20201219_172616_drawRect_40x40.jpg'

not exist -> 'crifanLib/demo/input/image/20201219_172616_drawRect_40x40.jpg'

(2) 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40.jpg'

exsit -> next until not exist 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_3.jpg'

"""

newFilename = curFilename

newPathRootPart, pointSuffix = os.path.splitext(newFilename)

# 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_1'

filenamePrefix = newPathRootPart

while os.path.exists(newFilename):

newTailNumberInt = 1

foundTailNumber = re.search("^(?P<filenamePrefix>.+)_(?P<tailNumber>\d+)$", newPathRootPart)

if foundTailNumber:

tailNumberStr = foundTailNumber.group("tailNumber") # '1'

tailNumberInt = int(tailNumberStr)

newTailNumberInt = tailNumberInt + 1 # 2

filenamePrefix = foundTailNumber.group("filenamePrefix") # 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40'

# existed previously saved, change to new name

newPathRootPart = "%s_%s" % (filenamePrefix, newTailNumberInt)

# 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_2'

newFilename = newPathRootPart + pointSuffix

# 'crifanLib/demo/input/image/20191219_172616_drawRect_40x40_2.jpg'

return newFilename

调用：

notExistFile = "crifanLib/demo/input/image/some_not_exist_filename.jpg"

nextFilename = findNextNumberFilename(notExistFile)

print("notExistFile=%s -> nextFilename=%s" % (notExistFile, nextFilename))

# notExistFile=crifanLib/demo/input/image/some_not_exist_filename.jpg -> nextFilename=crifanLib/demo/input/image/some_not_exist_filename.jpg

realExistFile = "crifanLib/demo/input/image/20191219_172616_drawRect_40x40.jpg"

nextUntilNotExistFilename = findNextNumberFilename(realExistFile)

print("realExistFile=%s -> nextUntilNotExistFilename=%s" % (realExistFile, nextUntilNotExistFilename))

# realExistFile=crifanLib/demo/input/image/20191219_172616_drawRect_40x40.jpg -> nextUntilNotExistFilename=crifanLib/demo/input/image/20191219_172616_drawRect_40x40_2.jpg

从文件名后缀推断出MIME类型

用库：

- mime

- GitHub

- https://github.com/liluo/mime

安装mime：

pip install mime

代码：

import mime

fileMimeType = mime.Types.of(curAudioFullFilename)[0].content_type

- 输入文件： 'Lots of Hearts.mp3'

- 输出信息： 'audio/mpeg'

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 10:18:00

文件夹=文件路径

新建文件夹

对于： python 3.2+

import os

def createFolder(folderFullPath):

"""

create folder, even if already existed

Note: for Python 3.2+

"""

os.makedirs(folderFullPath, exist_ok=True)

# print("Created folder: %s" % folderFullPath)

或：

对于： python 3.5+

import pathlib

pathlib.Path('/my/directory').mkdir(parents=True, exist_ok=True)

批量删除非空文件夹

import shutil

if os.path.exists(folderToDelete):

shutil.rmtree(folderToDelete)

注意：

- 删除之前要先用 os.path.exists 判断是非存在该目录

- 如果不存在就去删除，则会报错： OSError: [Errno 2] No such file or directory

os.path 路径处理

#!/usr/bin/python

# -*- coding: utf-8 -*-

# Author: Crifan Li

# Update: 20191219

# Function: Demo os.path common used functions

import os

def osPathDemo():

currentSystemInfo = os.uname()

print("currentSystemInfo=%s" % (currentSystemInfo, ))

# currentSystemInfo=posix.uname_result(sysname='Darwin', nodename='xxx', release='18.7.0', version='Darwin Kernel Version 18.7.0: Sat Oct 12 00:02:19 PDT 2019; root:xnu-4903.278.12~1/RELEASE_X86_64', machine='x86_64')

pathSeparatorInCurrentOS = os.path.sep

print("pathSeparatorInCurrentOS=%s" % pathSeparatorInCurrentOS)

# pathSeparatorInCurrentOS=/

fullFilePath = "/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png"

print("fullFilePath=%s" % fullFilePath)

# fullFilePath=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png

dirname = os.path.dirname(fullFilePath)

print("dirname=%s" % dirname)

# dirname=/Users/limao/dev/crifan/python/notEnoughUnpack

basename = os.path.basename(fullFilePath)

print("basename=%s" % basename)

# basename=Snip20191212_113.png

joinedFullPath = os.path.join(dirname, basename)

print("joinedFullPath=%s" % joinedFullPath)

# joinedFullPath=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png

isSame = (fullFilePath == joinedFullPath)

print("isSame=%s" % isSame)

# isSame=True

root, pointSuffix = os.path.splitext(fullFilePath)

print("root=%s, pointSuffix=%s" % (root, pointSuffix))

# root=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113, pointSuffix=.png

head, tail = os.path.split(fullFilePath)

print("head=%s, tail=%s" % (head, tail))

# head=/Users/limao/dev/crifan/python/notEnoughUnpack, tail=Snip20191212_113.png

drive, tail = os.path.splitdrive(fullFilePath)

print("drive=%s, tail=%s" % (drive, tail))

# drive=, tail=/Users/limao/dev/crifan/python/notEnoughUnpack/Snip20191212_113.png

curPath = os.getcwd()

print("curPath=%s" % curPath)

# curPath=/Users/limao/dev/crifan/python

relativePath = os.path.relpath(fullFilePath)

print("relativePath=%s" % relativePath)

# relativePath=notEnoughUnpack/Snip20191212_113.png

isFile = os.path.isfile(fullFilePath)

print("isFile=%s" % isFile)

# isFile=True

isDirectory = os.path.isdir(fullFilePath)

print("isDirectory=%s" % isDirectory)

# isDirectory=False

fileSize = os.path.getsize(fullFilePath)

print("fileSize=%s" % fileSize)

# fileSize=368810

isFileOrFolderRealExist = os.path.exists(fullFilePath)

print("isFileOrFolderRealExist=%s" % isFileOrFolderRealExist)

# isFileOrFolderRealExist=True

if __name__ == "__main__":

osPathDemo()

列出目录中的文件（和文件夹，且支持递归）

def listSubfolderFiles(subfolder, isIncludeFolder=True, isRecursive=False):

"""os.listdir recursively

Args:

subfolder (str): sub folder path

isIncludeFolder (bool): whether is include folder. Default is True. If True, result contain folder

isRecursive (bool): whether is recursive, means contain sub folder. Default is False

Returns:

list of str

Raises:

"""

allSubItemList = []

curSubItemList = os.listdir(path=subfolder)

for curSubItem in curSubItemList:

curSubItemFullPath = os.path.join(subfolder, curSubItem)

if os.path.isfile(curSubItemFullPath):

allSubItemList.append(curSubItemFullPath)

else:

if isIncludeFolder:

if os.path.isdir(curSubItemFullPath):

subSubItemList = listSubfolderFiles(curSubItemFullPath, isIncludeFolder, isRecursive)

allSubItemList.extend(subSubItemList)

if isIncludeFolder:

allSubItemList.append(subfolder)

return allSubItemList

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-08-12 15:48:31

多媒体

详见：

- https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanMultimedia.py

- https://github.com/crifan/crifanLibPython/blob/master/crifanLib/demo/crifanMultimediaDemo.py

此处整理多媒体相关的常用Python代码段，主要包含如下内容：

- 图片=图像

- 音频

- 视频

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:24:39

图像

Python中图像处理用的最多是： Pillow

下面图像处理处理的代码，基本上都是用 Pillow 实现的。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:23:36

Pillow

- Pillow

- 继承自： PIL

- PIL  =  Python Imaging Library

- 官网资料：

- Image Module — Pillow (PIL Fork) 7.0.0 documentation

- Image Module — Pillow (PIL Fork) 3.1.2 documentation

从二进制生成Image

if isinstance(inputImage, bytes):

openableImage = io.BytesIO(inputImage)

curPillowImage = Image.open(openableImage)

pillow变量是：

# <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=3543x3543 at 0x1065F7A20>

# <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1080x1920 at 0x1026D7278>

详见：

- 【已解决】Python如何从二进制数据中生成Pillow的Image

- 【已解决】Python的Pillow如何从二进制数据中读取图像数据

从Pillow的Image获取二进制数据

import io

imageIO = io.BytesIO()

curImg.save(imageIO, curImg.format)

imgBytes = imageIO.getvalue()

详见：

【已解决】Python的Pillow如何返回图像的二进制数据

缩放图片

import io

from PIL import Image, ImageDraw

def resizeImage(inputImage,

newSize,

resample=Image.BICUBIC, # Image.LANCZOS,

outputFormat=None,

outputImageFile=None

):

"""

resize input image

resize normally means become smaller, reduce size

:param inputImage: image file object(fp) / filename / binary bytes

:param newSize: (width, height)

:param resample: PIL.Image.NEAREST, PIL.Image.BILINEAR, PIL.Image.BICUBIC, or PIL.Image.LANCZOS

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.thumbnail

:param outputFormat: PNG/JPEG/BMP/GIF/TIFF/WebP/..., more refer:

https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html

if input image is filename with suffix, can omit this -> will infer from filename suffix

:param outputImageFile: output image file filename

:return:

input image file filename: output resized image to outputImageFile

input image binary bytes: resized image binary bytes

"""

openableImage = None

if isinstance(inputImage, str):

openableImage = inputImage

elif isFileObject(inputImage):

openableImage = inputImage

elif isinstance(inputImage, bytes):

inputImageLen = len(inputImage)

openableImage = io.BytesIO(inputImage)

imageFile = Image.open(openableImage) # <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=3543x3543 at 0x1065F7A20>

imageFile.thumbnail(newSize, resample)

if outputImageFile:

# save to file

imageFile.save(outputImageFile)

imageFile.close()

else:

# save and return binary byte

imageOutput = io.BytesIO()

# imageFile.save(imageOutput)

outputImageFormat = None

if outputFormat:

outputImageFormat = outputFormat

elif imageFile.format:

outputImageFormat = imageFile.format

imageFile.save(imageOutput, outputImageFormat)

imageFile.close()

compressedImageBytes = imageOutput.getvalue()

compressedImageLen = len(compressedImageBytes)

compressRatio = float(compressedImageLen)/float(inputImageLen)

print("%s -> %s, resize ratio: %d%%" % (inputImageLen, compressedImageLen, int(compressRatio * 100)))

return compressedImageBytes

调用：

import sys

import os

curFolder = os.path.abspath(__file__)

parentFolder = os.path.dirname(curFolder)

parentParentFolder = os.path.dirname(parentFolder)

parentParentParentFolder = os.path.dirname(parentParentFolder)

sys.path.append(curFolder)

sys.path.append(parentFolder)

sys.path.append(parentParentFolder)

sys.path.append(parentParentParentFolder)

import datetime

from crifanMultimedia import resizeImage

def testFilename():

imageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day.png"

outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_300x300.png"

print("imageFilename=%s" % imageFilename)

beforeTime = datetime.datetime.now()

resizeImage(imageFilename, (300, 300), outputImageFile=outputImageFilename)

afterTime = datetime.datetime.now()

print("procesTime: %s" % (afterTime - beforeTime))

outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_800x800.png"

beforeTime = datetime.datetime.now()

resizeImage(imageFilename, (800, 800), outputImageFile=outputImageFilename)

afterTime = datetime.datetime.now()

print("procesTime: %s" % (afterTime - beforeTime))

def testFileObject():

imageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day.png"

imageFileObj = open(imageFilename, "rb")

outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_600x600.png"

beforeTime = datetime.datetime.now()

resizeImage(imageFileObj, (600, 600), outputImageFile=outputImageFilename)

afterTime = datetime.datetime.now()

print("procesTime: %s" % (afterTime - beforeTime))

def testBinaryBytes():

imageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/take tomato.png"

imageFileObj = open(imageFilename, "rb")

imageBytes = imageFileObj.read()

# return binary bytes

beforeTime = datetime.datetime.now()

resizedImageBytes = resizeImage(imageBytes, (800, 800))

afterTime = datetime.datetime.now()

print("procesTime: %s" % (afterTime - beforeTime))

print("len(resizedImageBytes)=%s" % len(resizedImageBytes))

# save to file

outputImageFilename = "/Users/crifan/dev/tmp/python/resize_image_demo/hot day_750x750.png"

beforeTime = datetime.datetime.now()

resizeImage(imageBytes, (750, 750), outputImageFile=outputImageFilename)

afterTime = datetime.datetime.now()

print("procesTime: %s" % (afterTime - beforeTime))

imageFileObj.close()

def demoResizeImage():

testFilename()

testFileObject()

testBinaryBytes()

if __name__ == "__main__":

demoResizeImage()

# imageFilename=/Users/crifan/dev/tmp/python/resize_image_demo/hot day.png

# procesTime: 0:00:00.619377

# procesTime: 0:00:00.745228

# procesTime: 0:00:00.606060

# 1146667 -> 753258, resize ratio: 65%

# procesTime: 0:00:00.773289

# len(resizedImageBytes)=753258

# procesTime: 0:00:00.738237

给图片画元素所属区域的边框，且带自动保存加了框后的图片

from PIL import Image

from PIL import ImageDraw

def imageDrawRectangle(inputImgOrImgPath,

rectLocation,

outlineColor="green",

outlineWidth=0,

isShow=False,

isAutoSave=True,

saveTail="_drawRect_%wx%h",

isDrawClickedPosCircle=True,

clickedPos=None,

):

"""Draw a rectangle for image (and a small circle), and show it,

Args:

inputImgOrImgPath (Image/str): a pillow(PIL) Image instance or image file path

rectLocation (tuple/list/Rect): the rectangle location, (x, y, width, height)

outlineColor (str): Color name

outlineWidth (int): rectangle outline width

isShow (bool): True to call image.show() for debug

isAutoSave (bool): True to auto save the image file with drawed rectangle

saveTail(str): save filename tail part. support format %x/%y/%w/%h use only when isAutoSave=True

clickedPos (tuple): x,y of clicked postion; default None; if None, use the center point

isDrawClickedPosCircle (bool): draw small circle in clicked point

Returns:

modified image

Raises:

"""

inputImg = inputImgOrImgPath

if isinstance(inputImgOrImgPath, str):

inputImg = Image.open(inputImgOrImgPath)

draw = ImageDraw.Draw(inputImg)

isRectObj = False

hasX = hasattr(rectLocation, "x")

hasY = hasattr(rectLocation, "y")

hasWidth = hasattr(rectLocation, "width")

hasHeight = hasattr(rectLocation, "height")

isRectObj = hasX and hasY and hasWidth and hasHeight

if isinstance(rectLocation, tuple):

x, y, w, h = rectLocation

if isinstance(rectLocation, list):

x = rectLocation[0]

y = rectLocation[1]

w = rectLocation[2]

h = rectLocation[3]

elif isRectObj:

x = rectLocation.x

y = rectLocation.y

w = rectLocation.width

h = rectLocation.height

w = int(w)

h = int(h)

x0 = x

y0 = y

x1 = x0 + w

y1 = y0 + h

draw.rectangle(

[x0, y0, x1, y1],

# fill="yellow",

# outline="yellow",

outline=outlineColor,

width=outlineWidth,

)

if isDrawClickedPosCircle:

# radius = 3

# radius = 2

radius = 4

# circleOutline = "yellow"

circleOutline = "red"

circleLineWidthInt = 1

# circleLineWidthInt = 3

if clickedPos:

clickedX, clickedY = clickedPos

else:

clickedX = x + w/2

clickedY = y + h/2

startPointInt = (int(clickedX - radius), int(clickedY - radius))

endPointInt = (int(clickedX + radius), int(clickedY + radius))

draw.ellipse([startPointInt, endPointInt], outline=circleOutline, width=circleLineWidthInt)

if isShow:

inputImg.show()

if isAutoSave:

saveTail = saveTail.replace("%x", str(x))

saveTail = saveTail.replace("%y", str(y))

saveTail = saveTail.replace("%w", str(w))

saveTail = saveTail.replace("%h", str(h))

inputImgPath = None

if isinstance(inputImgOrImgPath, str):

inputImgPath = str(inputImgOrImgPath)

elif inputImg.filename:

inputImgPath = str(inputImg.filename)

if inputImgPath:

imgFolderAndName, pointSuffix = os.path.splitext(inputImgPath)

imgFolderAndName = imgFolderAndName + saveTail

newImgPath = imgFolderAndName + pointSuffix

newImgPath = findNextNumberFilename(newImgPath)

else:

curDatetimeStr = getCurDatetimeStr() # '20191219_143400'

suffix = str(inputImg.format).lower() # 'jpeg'

newImgFilename = "%s%s.%s" % (curDatetimeStr, saveTail, suffix)

imgPathRoot = os.getcwd()

newImgPath = os.path.join(imgPathRoot, newImgFilename)

inputImg.save(newImgPath)

return inputImg

说明：

相关函数，详见：findNextNumberFilename，或者干脆去掉这个逻辑即可。

调用：

curBoundList = self.get_ElementBounds(eachElement)

curWidth = curBoundList[2] - curBoundList[0]

curHeight = curBoundList[3] - curBoundList[1]

curRect = [curBoundList[0], curBoundList[1], curWidth, curHeight]

curImg = CommonUtils.imageDrawRectangle(curImg, curRect, isShow=True, saveTail="_rect_%x|%y|%w|%h", isDrawClickedPosCircle=False)

或：

curTimeStr = CommonUtils.getCurDatetimeStr("%H%M%S")

curSaveTal = "_rect_{}_%x|%y|%w|%h".format(curTimeStr)

curImg = CommonUtils.imageDrawRectangle(imgPath, curRect, isShow=True, saveTail=curSaveTal, isDrawClickedPosCircle=False)

效果：

（1）给原图加上单个元素所属边框

（2）多次循环后，给同一张图中多个元素加上边框后

其他调用：

imageDrawRectangle(curPillowImg, curLocation)

imageDrawRectangle(curPillowImg, calculatedLocation)

curImg = imageDrawRectangle(imgPath, firstMatchLocation, clickedPos=clickedPos)

curImg = imageDrawRectangle(imgPath, firstMatchLocation)

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:25:40

百度OCR

详见：

https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanBaiduOcr.py

在做安卓和iOS的移动端自动化测试期间，会涉及到从图像中提取文字，用的是百度OCR。

其中有些通用的功能，整理出函数，贴出供参考。

百度OCR初始化

import os

import re

import base64

import requests

import time

import logging

from collections import OrderedDict

from PIL import Image, ImageDraw

class BaiduOCR():

# OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic" # 通用文字识别

# OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/general"        # 通用文字识别（含位置信息版）

# OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic" # 通用文字识别（高精度版）

OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate"       # 通用文字识别（高精度含位置版）

TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token'

RESP_ERR_CODE_QPS_LIMIT_REACHED = 18

RESP_ERR_TEXT_QPS_LIMIT_REACHED = "Open api qps request limit reached"

RESP_ERR_CODE_DAILY_LIMIT_REACHED = 17

RESP_ERR_TEXT_DAILY_LIMIT_REACHED = "Open api daily request limit reached"

API_KEY = 'SOxxxxxxxxxxnu'

SECRET_KEY = 'wlxxxxxxxxxxxxxxxxxxxpL'

def initOcr(self):

self.curToken = self.baiduFetchToken()

def baiduFetchToken(self):

"""Fetch Baidu token for OCR"""

params = {

'grant_type': 'client_credentials',

'client_id': self.API_KEY,

'client_secret': self.SECRET_KEY

}

resp = requests.get(self.TOKEN_URL, params=params)

respJson = resp.json()

respToken = ""

if ('access_token' in respJson.keys() and 'scope' in respJson.keys()):

if not 'brain_all_scope' in respJson['scope'].split(' '):

logging.error('please ensure has check the  ability')

else:

respToken = respJson['access_token']

else:

logging.error('please overwrite the correct API_KEY and SECRET_KEY')

# '24.8691f3c6dedd0d0d0b30a9dfec604d52.2592000.1578465979.282335-17921535'

return respToken

百度OCR图片转文字

def baiduImageToWords(self, imageFullPath):

"""Detect text from image using Baidu OCR api"""

# # Note: if using un-paid = free baidu api, need following wait sometime to reduce: qps request limit

# time.sleep(0.15)

respWordsResutJson = ""

# 读取图片二进制数据

imgBinData = readBinDataFromFile(imageFullPath)

encodedImgData = base64.b64encode(imgBinData)

paramDict = {

"access_token": self.curToken

}

headerDict = {

"Content-Type": "application/x-www-form-urlencoded"

}

# 参数含义：http://ai.baidu.com/ai-doc/OCR/vk3h7y58v

dataDict = {

"image": encodedImgData,

"recognize_granularity": "small",

# "vertexes_location": "true",

}

resp = requests.post(self.OCR_URL, params=paramDict, headers=headerDict, data=dataDict)

respJson = resp.json()

logging.debug("baidu OCR: imgage=%s -> respJson=%s", imageFullPath, respJson)

if "error_code" in respJson:

logging.warning("respJson=%s" % respJson)

errorCode = respJson["error_code"]

# {'error_code': 17, 'error_msg': 'Open api daily request limit reached'}

# {'error_code': 18, 'error_msg': 'Open api qps request limit reached'}

# the limit count can found from

# 文字识别 - 免费额度 | 百度AI开放平台

# https://ai.baidu.com/ai-doc/OCR/fk3h7xu7h

# for "通用文字识别（高精度含位置版）" is "50次/天"

if errorCode == self.RESP_ERR_CODE_QPS_LIMIT_REACHED:

# wait sometime and try again

time.sleep(1.0)

resp = requests.post(self.OCR_URL, params=paramDict, headers=headerDict, data=dataDict)

respJson = resp.json()

logging.debug("baidu OCR: for errorCode=%s, do again, imgage=%s -> respJson=%s", errorCode, imageFullPath, respJson)

elif errorCode == self.RESP_ERR_CODE_DAILY_LIMIT_REACHED:

logging.error("Fail to continue using baidu OCR api today !!!")

respJson = None

"""

{

"log_id": 6937531796498618000,

"words_result_num": 32,

"words_result": [

{

"chars": [

...

"""

if "words_result" in respJson:

respWordsResutJson = respJson

return respWordsResutJson

调用：

wordsResultJson = self.baiduImageToWords(imgPath)

respJson = self.baiduImageToWords(screenImgPath)

返回结果举例

安卓游戏 暗黑觉醒 首充豪礼

图片：

返回解析后出 json 格式的文字信息：

{

"log_id": 9009770747370640007,

"words_result_num": 12,

"words_result": [

{

"chars": [

{

"char": "首",

"location": { "width": 94, "top": 105, "left": 989, "height": 158 }

},

{

"char": "充",

"location": { "width": 94, "top": 105, "left": 1086, "height": 158 }

},

{

"char": "豪",

"location": { "width": 95, "top": 105, "left": 1183, "height": 158 }

},

{

"char": "礼",

"location": { "width": 77, "top": 105, "left": 1281, "height": 158 }

}

],

"location": { "width": 370, "top": 105, "left": 989, "height": 158 },

"words": "首充豪礼"

},

{

"chars": [

{

"char": "×",

"location": { "width": 30, "top": 161, "left": 1887, "height": 61 }

}

],

"location": { "width": 60, "top": 161, "left": 1887, "height": 61 },

"words": "×"

},

{

"chars": [

{

"char": "充",

"location": { "width": 43, "top": 273, "left": 758, "height": 73 }

},

{

"char": "值",

"location": { "width": 43, "top": 273, "left": 803, "height": 73 }

},

{

"char": "元",

"location": { "width": 67, "top": 273, "left": 912, "height": 73 }

},

{

"char": "可",

"location": { "width": 44, "top": 273, "left": 979, "height": 73 }

},

{

"char": "领",

"location": { "width": 43, "top": 273, "left": 1023, "height": 73 }

},

{

"char": "总",

"location": { "width": 44, "top": 273, "left": 1067, "height": 73 }

},

{

"char": "价",

"location": { "width": 23, "top": 273, "left": 1111, "height": 73 }

},

{

"char": "值",

"location": { "width": 89, "top": 273, "left": 1134, "height": 73 }

},

{

"char": "8",

"location": { "width": 36, "top": 273, "left": 1259, "height": 73 }

},

{

"char": "8",

"location": { "width": 36, "top": 273, "left": 1326, "height": 73 }

},

{

"char": "8",

"location": { "width": 35, "top": 273, "left": 1371, "height": 73 }

},

{

"char": "钻",

"location": { "width": 43, "top": 273, "left": 1444, "height": 73 }

},

{

"char": "豪",

"location": { "width": 44, "top": 273, "left": 1510, "height": 73 }

},

{

"char": "华",

"location": { "width": 43, "top": 273, "left": 1555, "height": 73 }

},

{

"char": "大",

"location": { "width": 43, "top": 273, "left": 1599, "height": 73 }

},

{

"char": "礼",

"location": { "width": 27, "top": 273, "left": 1643, "height": 73 }

}

],

"location": { "width": 911, "top": 273, "left": 758, "height": 73 },

"words": "充值元可领总价值888钻豪华大礼"

},

{

"chars": [

{

"char": "送",

"location": { "width": 65, "top": 369, "left": 832, "height": 107 }

}

],

"location": { "width": 107, "top": 369, "left": 832, "height": 107 },

"words": "送"

},

{

"chars": [

{

"char": "绝",

"location": { "width": 38, "top": 390, "left": 974, "height": 65 }

},

{

"char": "版",

"location": { "width": 38, "top": 390, "left": 1032, "height": 65 }

},

{

"char": "萌",

"location": { "width": 38, "top": 390, "left": 1092, "height": 65 }

},

{

"char": "宠",

"location": { "width": 38, "top": 390, "left": 1150, "height": 65 }

},

{

"char": "、",

"location": { "width": 31, "top": 390, "left": 1184, "height": 65 }

},

{

"char": "专",

"location": { "width": 39, "top": 390, "left": 1230, "height": 65 }

},

{

"char": "属",

"location": { "width": 38, "top": 390, "left": 1289, "height": 65 }

},

{

"char": "神",

"location": { "width": 38, "top": 390, "left": 1368, "height": 65 }

},

{

"char": "兵",

"location": { "width": 39, "top": 390, "left": 1408, "height": 65 }

}

],

"location": { "width": 524, "top": 390, "left": 934, "height": 65 },

"words": "绝版萌宠、专属神兵"

},

{

"chars": [

{

"char": "绝",

"location": { "width": 20, "top": 515, "left": 378, "height": 33 }

}

],

"location": { "width": 33, "top": 515, "left": 378, "height": 33 },

"words": "绝"

},

{

"chars": [

{

"char": "珍",

"location": { "width": 33, "top": 516, "left": 1992, "height": 42 }

}

],

"location": { "width": 33, "top": 516, "left": 1992, "height": 42 },

"words": "珍"

},

{

"chars": [

{

"char": "版",

"location": { "width": 20, "top": 545, "left": 379, "height": 34 }

}

],

"location": { "width": 31, "top": 545, "left": 379, "height": 34 },

"words": "版"

},

{

"chars": [

{

"char": "额",

"location": { "width": 26, "top": 776, "left": 1225, "height": 44 }

},

{

"char": "外",

"location": { "width": 26, "top": 776, "left": 1264, "height": 44 }

},

{

"char": "礼",

"location": { "width": 27, "top": 776, "left": 1291, "height": 44 }

},

{

"char": "包",

"location": { "width": 27, "top": 776, "left": 1317, "height": 44 }

}

],

"location": { "width": 125, "top": 776, "left": 1225, "height": 44 },

"words": "额外礼包"

},

{

"chars": [

{

"char": "首",

"location": { "width": 38, "top": 830, "left": 935, "height": 64 }

},

{

"char": "充",

"location": { "width": 38, "top": 830, "left": 994, "height": 64 }

},

{

"char": "元",

"location": { "width": 38, "top": 830, "left": 1092, "height": 64 }

},

{

"char": "充",

"location": { "width": 38, "top": 830, "left": 1286, "height": 64 }

},

{

"char": "9",

"location": { "width": 31, "top": 830, "left": 1339, "height": 64 }

},

{

"char": "8",

"location": { "width": 31, "top": 830, "left": 1377, "height": 64 }

},

{

"char": "元",

"location": { "width": 38, "top": 830, "left": 1444, "height": 64 }

}

],

"location": { "width": 549, "top": 830, "left": 935, "height": 64 },

"words": "首充元充98元"

},

{

"chars": [

{

"char": "战",

"location": { "width": 42, "top": 970, "left": 373, "height": 69 }

},

{

"char": "斗",

"location": { "width": 42, "top": 970, "left": 437, "height": 69 }

},

{

"char": "1",

"location": { "width": 35, "top": 970, "left": 515, "height": 69 }

},

{

"char": "5",

"location": { "width": 35, "top": 970, "left": 537, "height": 69 }

},

{

"char": "0",

"location": { "width": 34, "top": 970, "left": 580, "height": 69 }

},

{

"char": "0",

"location": { "width": 35, "top": 970, "left": 622, "height": 69 }

},

{

"char": "0",

"location": { "width": 34, "top": 970, "left": 666, "height": 69 }

}

],

"location": { "width": 327, "top": 970, "left": 373, "height": 69 },

"words": "战斗15000"

},

{

"chars": [

{

"char": "战",

"location": { "width": 43, "top": 969, "left": 1648, "height": 73 }

},

{

"char": "斗",

"location": { "width": 43, "top": 969, "left": 1713, "height": 73 }

},

{

"char": "1",

"location": { "width": 36, "top": 969, "left": 1793, "height": 73 }

},

{

"char": "6",

"location": { "width": 36, "top": 969, "left": 1816, "height": 73 }

},

{

"char": "0",

"location": { "width": 35, "top": 969, "left": 1861, "height": 73 }

},

{

"char": "0",

"location": { "width": 36, "top": 969, "left": 1904, "height": 73 }

},

{

"char": "0",

"location": { "width": 29, "top": 969, "left": 1949, "height": 73 }

}

],

"location": { "width": 330, "top": 969, "left": 1648, "height": 73 },

"words": "战斗16000"

}

]

}

其中：

- 首充豪礼

- 都能完整检测出来：已经是效果很不错了

- 当然偶尔也会有失误，比如 偶尔

- 只解析出部分内容：首充豪

- 或个别字错了：首充豪机

- 本身图片上 礼 也的确很像 机

- 作为OCR犯此错误，完全可以理解

安卓游戏 暗黑觉醒 公告弹框

图片：

返回结果json：

{

"log_id": 2793391773289550472,

"words_result_num": 23,

"words_result": [

{

"chars": [

{

"char": "公",

"location": { "width": 28, "top": 125, "left": 634, "height": 48 }

},

{

"char": "告",

"location": { "width": 29, "top": 125, "left": 691, "height": 48 }

}

],

"location": { "width": 92, "top": 125, "left": 634, "height": 48 },

"words": "公告"

},

{

"chars": [

{

"char": "最",

"location": { "width": 21, "top": 240, "left": 535, "height": 36 }

}

],

"location": { "width": 33, "top": 240, "left": 535, "height": 36 },

"words": "最"

},

{

"chars": [

{

"char": "亲",

"location": { "width": 26, "top": 233, "left": 922, "height": 42 }

},

{

"char": "爱",

"location": { "width": 26, "top": 233, "left": 959, "height": 42 }

},

{

"char": "的",

"location": { "width": 26, "top": 233, "left": 986, "height": 42 }

},

{

"char": "觉",

"location": { "width": 26, "top": 233, "left": 1024, "height": 42 }

},

{

"char": "醒",

"location": { "width": 26, "top": 233, "left": 1063, "height": 42 }

},

{

"char": "勇",

"location": { "width": 26, "top": 233, "left": 1088, "height": 42 }

},

{

"char": "士",

"location": { "width": 25, "top": 233, "left": 1127, "height": 42 }

},

{

"char": ":",

"location": { "width": 21, "top": 233, "left": 1148, "height": 42 }

}

],

"location": { "width": 253, "top": 233, "left": 922, "height": 42 },

"words": "亲爱的觉醒勇士:"

},

{

"chars": [

{

"char": "新",

"location": { "width": 26, "top": 266, "left": 535, "height": 44 }

},

{

"char": "新",

"location": { "width": 27, "top": 266, "left": 588, "height": 44 }

},

{

"char": "服",

"location": { "width": 26, "top": 266, "left": 628, "height": 44 }

},

{

"char": "公",

"location": { "width": 27, "top": 266, "left": 668, "height": 44 }

},

{

"char": "告",

"location": { "width": 26, "top": 266, "left": 709, "height": 44 }

}

],

"location": { "width": 201, "top": 266, "left": 535, "height": 44 },

"words": "新新服公告"

},

{

"chars": [

{

"char": "承",

"location": { "width": 26, "top": 282, "left": 984, "height": 43 }

},

{

"char": "载",

"location": { "width": 26, "top": 282, "left": 1023, "height": 43 }

},

{

"char": "六",

"location": { "width": 26, "top": 282, "left": 1063, "height": 43 }

},

{

"char": "大",

"location": { "width": 26, "top": 282, "left": 1090, "height": 43 }

},

{

"char": "种",

"location": { "width": 26, "top": 282, "left": 1116, "height": 43 }

},

{

"char": "族",

"location": { "width": 26, "top": 282, "left": 1155, "height": 43 }

},

{

"char": "的",

"location": { "width": 26, "top": 282, "left": 1181, "height": 43 }

},

{

"char": "重",

"location": { "width": 26, "top": 282, "left": 1220, "height": 43 }

},

{

"char": "生",

"location": { "width": 26, "top": 282, "left": 1260, "height": 43 }

},

{

"char": "之",

"location": { "width": 26, "top": 282, "left": 1286, "height": 43 }

},

{

"char": "使",

"location": { "width": 27, "top": 282, "left": 1325, "height": 43 }

},

{

"char": "命",

"location": { "width": 26, "top": 282, "left": 1351, "height": 43 }

},

{

"char": ",",

"location": { "width": 21, "top": 282, "left": 1385, "height": 43 }

},

{

"char": "超",

"location": { "width": 26, "top": 282, "left": 1416, "height": 43 }

},

{

"char": "现",

"location": { "width": 26, "top": 282, "left": 1456, "height": 43 }

},

{

"char": "实",

"location": { "width": 26, "top": 282, "left": 1483, "height": 43 }

},

{

"char": "3",

"location": { "width": 22, "top": 282, "left": 1517, "height": 43 }

},

{

"char": "D",

"location": { "width": 21, "top": 282, "left": 1531, "height": 43 }

},

{

"char": "魔",

"location": { "width": 26, "top": 282, "left": 1560, "height": 43 }

},

{

"char": "M",

"location": { "width": 61, "top": 284, "left": 1583, "height": 40 }

},

{

"char": "M",

"location": { "width": 35, "top": 284, "left": 1639, "height": 40 }

},

{

"char": "O",

"location": { "width": 31, "top": 284, "left": 1669, "height": 40 }

},

{

"char": "A",

"location": { "width": 25, "top": 284, "left": 1696, "height": 40 }

},

{

"char": "R",

"location": { "width": 25, "top": 284, "left": 1716, "height": 40 }

},

{

"char": "P",

"location": { "width": 25, "top": 284, "left": 1735, "height": 40 }

},

{

"char": "G",

"location": { "width": 25, "top": 284, "left": 1755, "height": 40 }

},

{

"char": "幻",

"location": { "width": 26, "top": 282, "left": 1588, "height": 43 }

},

{

"char": "手",

"location": { "width": 26, "top": 282, "left": 1784, "height": 43 }

},

{

"char": "游",

"location": { "width": 26, "top": 282, "left": 1823, "height": 43 }

}

],

"location": { "width": 867, "top": 282, "left": 984, "height": 43 },

"words": "承载六大种族的重生之使命,超现实3D魔 MMOARPG幻手游"

},

{

"chars": [

{

"char": "不",

"location": { "width": 37, "top": 337, "left": 923, "height": 40 }

},

{

"char": "负",

"location": { "width": 23, "top": 337, "left": 959, "height": 40 }

},

{

"char": "觉",

"location": { "width": 25, "top": 337, "left": 996, "height": 40 }

},

{

"char": "醒",

"location": { "width": 23, "top": 337, "left": 1021, "height": 40 }

},

{

"char": "勇",

"location": { "width": 23, "top": 337, "left": 1058, "height": 40 }

},

{

"char": "士",

"location": { "width": 25, "top": 337, "left": 1094, "height": 40 }

},

{

"char": "们",

"location": { "width": 25, "top": 337, "left": 1119, "height": 40 }

},

{

"char": "的",

"location": { "width": 25, "top": 337, "left": 1156, "height": 40 }

},

{

"char": "使",

"location": { "width": 25, "top": 337, "left": 1192, "height": 40 }

},

{

"char": "命",

"location": { "width": 25, "top": 337, "left": 1230, "height": 40 }

},

{

"char": "之",

"location": { "width": 25, "top": 337, "left": 1254, "height": 40 }

},

{

"char": "约",

"location": { "width": 25, "top": 337, "left": 1292, "height": 40 }

},

{

"char": ",",

"location": { "width": 20, "top": 337, "left": 1324, "height": 40 }

},

{

"char": "震",

"location": { "width": 25, "top": 337, "left": 1353, "height": 40 }

},

{

"char": "撼",

"location": { "width": 25, "top": 337, "left": 1390, "height": 40 }

},

{

"char": "来",

"location": { "width": 23, "top": 337, "left": 1428, "height": 40 }

},

{

"char": "袭",

"location": { "width": 25, "top": 337, "left": 1452, "height": 40 }

},

{

"char": "。",

"location": { "width": 18, "top": 337, "left": 1485, "height": 40 }

}

],

"location": { "width": 580, "top": 337, "left": 923, "height": 40 },

"words": "不负觉醒勇士们的使命之约,震撼来袭。"

},

{

"chars": [

{

"char": "最",

"location": { "width": 20, "top": 364, "left": 535, "height": 33 }

}

],

"location": { "width": 33, "top": 364, "left": 535, "height": 33 },

"words": "最"

},

{

"chars": [

{

"char": "新",

"location": { "width": 26, "top": 389, "left": 535, "height": 43 }

},

{

"char": "违",

"location": { "width": 26, "top": 389, "left": 588, "height": 43 }

},

{

"char": "规",

"location": { "width": 25, "top": 389, "left": 627, "height": 43 }

},

{

"char": "发",

"location": { "width": 26, "top": 389, "left": 666, "height": 43 }

},

{

"char": "言",

"location": { "width": 26, "top": 389, "left": 704, "height": 43 }

},

{

"char": "处",

"location": { "width": 26, "top": 389, "left": 743, "height": 43 }

},

{

"char": "理",

"location": { "width": 26, "top": 389, "left": 770, "height": 43 }

},

{

"char": "机",

"location": { "width": 26, "top": 389, "left": 808, "height": 43 }

},

{

"char": "制",

"location": { "width": 26, "top": 389, "left": 848, "height": 43 }

}

],

"location": { "width": 343, "top": 389, "left": 535, "height": 43 },

"words": "新违规发言处理机制"

},

{

"chars": [

{

"char": "【",

"location": { "width": 21, "top": 387, "left": 997, "height": 43 }

},

{

"char": "普",

"location": { "width": 25, "top": 387, "left": 1023, "height": 43 }

},

{

"char": "纳",

"location": { "width": 26, "top": 387, "left": 1061, "height": 43 }

},

{

"char": "山",

"location": { "width": 25, "top": 387, "left": 1087, "height": 43 }

},

{

"char": "谷",

"location": { "width": 26, "top": 387, "left": 1126, "height": 43 }

},

{

"char": "1",

"location": { "width": 21, "top": 387, "left": 1148, "height": 43 }

},

{

"char": "2",

"location": { "width": 21, "top": 387, "left": 1160, "height": 43 }

},

{

"char": "0",

"location": { "width": 21, "top": 387, "left": 1187, "height": 43 }

},

{

"char": "服",

"location": { "width": 26, "top": 387, "left": 1204, "height": 43 }

},

{

"char": "】",

"location": { "width": 21, "top": 387, "left": 1238, "height": 43 }

},

{

"char": "将",

"location": { "width": 25, "top": 387, "left": 1269, "height": 43 }

},

{

"char": "于",

"location": { "width": 26, "top": 387, "left": 1308, "height": 43 }

},

{

"char": "0",

"location": { "width": 20, "top": 387, "left": 1329, "height": 43 }

},

{

"char": "7",

"location": { "width": 21, "top": 387, "left": 1355, "height": 43 }

},

{

"char": "月",

"location": { "width": 25, "top": 387, "left": 1385, "height": 43 }

},

{

"char": "0",

"location": { "width": 21, "top": 387, "left": 1406, "height": 43 }

},

{

"char": "8",

"location": { "width": 21, "top": 387, "left": 1420, "height": 43 }

},

{

"char": "日",

"location": { "width": 25, "top": 387, "left": 1451, "height": 43 }

},

{

"char": "0",

"location": { "width": 21, "top": 387, "left": 1471, "height": 43 }

},

{

"char": "0",

"location": { "width": 21, "top": 387, "left": 1497, "height": 43 }

},

{

"char": ":",

"location": { "width": 21, "top": 387, "left": 1510, "height": 43 }

},

{

"char": "1",

"location": { "width": 21, "top": 387, "left": 1523, "height": 43 }

},

{

"char": "5",

"location": { "width": 21, "top": 387, "left": 1535, "height": 43 }

},

{

"char": "震",

"location": { "width": 25, "top": 387, "left": 1567, "height": 43 }

},

{

"char": "撼",

"location": { "width": 26, "top": 387, "left": 1592, "height": 43 }

},

{

"char": "开",

"location": { "width": 25, "top": 387, "left": 1631, "height": 43 }

},

{

"char": "启",

"location": { "width": 26, "top": 387, "left": 1656, "height": 43 }

},

{

"char": "。",

"location": { "width": 21, "top": 387, "left": 1678, "height": 43 }

}

],

"location": { "width": 711, "top": 387, "left": 997, "height": 43 },

"words": "【普纳山谷120服】将于07月08日00:15震撼开启。"

},

{

"chars": [

{

"char": "各",

"location": { "width": 23, "top": 438, "left": 987, "height": 40 }

},

{

"char": "位",

"location": { "width": 25, "top": 438, "left": 1022, "height": 40 }

},

{

"char": "勇",

"location": { "width": 25, "top": 438, "left": 1059, "height": 40 }

},

{

"char": "士",

"location": { "width": 25, "top": 438, "left": 1095, "height": 40 }

},

{

"char": "请",

"location": { "width": 25, "top": 438, "left": 1120, "height": 40 }

},

{

"char": "拿",

"location": { "width": 23, "top": 438, "left": 1157, "height": 40 }

},

{

"char": "起",

"location": { "width": 25, "top": 438, "left": 1181, "height": 40 }

},

{

"char": "手",

"location": { "width": 23, "top": 438, "left": 1230, "height": 40 }

},

{

"char": "中",

"location": { "width": 23, "top": 438, "left": 1255, "height": 40 }

},

{

"char": "武",

"location": { "width": 25, "top": 438, "left": 1291, "height": 40 }

},

{

"char": "器",

"location": { "width": 23, "top": 438, "left": 1316, "height": 40 }

},

{

"char": ",",

"location": { "width": 20, "top": 438, "left": 1348, "height": 40 }

},

{

"char": "与",

"location": { "width": 25, "top": 438, "left": 1389, "height": 40 }

},

{

"char": "我",

"location": { "width": 23, "top": 438, "left": 1414, "height": 40 }

},

{

"char": "们",

"location": { "width": 25, "top": 438, "left": 1449, "height": 40 }

},

{

"char": "一",

"location": { "width": 23, "top": 438, "left": 1487, "height": 40 }

},

{

"char": "同",

"location": { "width": 23, "top": 438, "left": 1524, "height": 40 }

},

{

"char": "踏",

"location": { "width": 25, "top": 438, "left": 1548, "height": 40 }

},

{

"char": "上",

"location": { "width": 23, "top": 438, "left": 1584, "height": 40 }

},

{

"char": "王",

"location": { "width": 25, "top": 438, "left": 1621, "height": 40 }

},

{

"char": "者",

"location": { "width": 25, "top": 438, "left": 1645, "height": 40 }

},

{

"char": "觉",

"location": { "width": 23, "top": 438, "left": 1683, "height": 40 }

},

{

"char": "醒",

"location": { "width": 25, "top": 438, "left": 1718, "height": 40 }

},

{

"char": "之",

"location": { "width": 23, "top": 438, "left": 1756, "height": 40 }

},

{

"char": "路",

"location": { "width": 25, "top": 438, "left": 1780, "height": 40 }

},

{

"char": "!",

"location": { "width": 20, "top": 438, "left": 1813, "height": 40 }

}

],

"location": { "width": 853, "top": 438, "left": 987, "height": 40 },

"words": "各位勇士请拿起手中武器,与我们一同踏上王者觉醒之路!"

},

{

"chars": [

{

"char": "限",

"location": { "width": 21, "top": 486, "left": 535, "height": 35 }

}

],

"location": { "width": 35, "top": 486, "left": 535, "height": 35 },

"words": "限"

},

{

"chars": [

{

"char": "时",

"location": { "width": 26, "top": 512, "left": 534, "height": 42 }

},

{

"char": "新",

"location": { "width": 26, "top": 512, "left": 597, "height": 42 }

},

{

"char": "服",

"location": { "width": 26, "top": 512, "left": 622, "height": 42 }

},

{

"char": "活",

"location": { "width": 25, "top": 512, "left": 661, "height": 42 }

},

{

"char": "动",

"location": { "width": 26, "top": 512, "left": 699, "height": 42 }

}

],

"location": { "width": 202, "top": 512, "left": 534, "height": 42 },

"words": "时新服活动"

},

{

"chars": [

{

"char": "【",

"location": { "width": 22, "top": 538, "left": 927, "height": 43 }

},

{

"char": "游",

"location": { "width": 26, "top": 538, "left": 955, "height": 43 }

},

{

"char": "戏",

"location": { "width": 26, "top": 538, "left": 994, "height": 43 }

},

{

"char": "特",

"location": { "width": 26, "top": 538, "left": 1020, "height": 43 }

},

{

"char": "色",

"location": { "width": 27, "top": 538, "left": 1060, "height": 43 }

},

{

"char": "】",

"location": { "width": 19, "top": 538, "left": 1095, "height": 43 }

}

],

"location": { "width": 187, "top": 538, "left": 927, "height": 43 },

"words": "【游戏特色】"

},

{

"chars": [

{

"char": "火",

"location": { "width": 20, "top": 612, "left": 537, "height": 33 }

}

],

"location": { "width": 33, "top": 612, "left": 537, "height": 33 },

"words": "火"

},

{

"chars": [

{

"char": "1",

"location": { "width": 21, "top": 588, "left": 992, "height": 43 }

},

{

"char": ".",

"location": { "width": 21, "top": 588, "left": 1005, "height": 43 }

},

{

"char": "超",

"location": { "width": 26, "top": 588, "left": 1036, "height": 43 }

},

{

"char": "宏",

"location": { "width": 26, "top": 588, "left": 1076, "height": 43 }

},

{

"char": "伟",

"location": { "width": 26, "top": 588, "left": 1101, "height": 43 }

},

{

"char": "世",

"location": { "width": 26, "top": 588, "left": 1141, "height": 43 }

},

{

"char": "界",

"location": { "width": 26, "top": 588, "left": 1167, "height": 43 }

},

{

"char": "观",

"location": { "width": 26, "top": 588, "left": 1206, "height": 43 }

},

{

"char": "、",

"location": { "width": 21, "top": 588, "left": 1228, "height": 43 }

},

{

"char": "六",

"location": { "width": 26, "top": 588, "left": 1271, "height": 43 }

},

{

"char": "大",

"location": { "width": 26, "top": 588, "left": 1297, "height": 43 }

},

{

"char": "种",

"location": { "width": 26, "top": 588, "left": 1337, "height": 43 }

},

{

"char": "族",

"location": { "width": 26, "top": 588, "left": 1363, "height": 43 }

},

{

"char": "秘",

"location": { "width": 26, "top": 588, "left": 1403, "height": 43 }

},

{

"char": "密",

"location": { "width": 26, "top": 588, "left": 1429, "height": 43 }

},

{

"char": "等",

"location": { "width": 26, "top": 588, "left": 1468, "height": 43 }

},

{

"char": "你",

"location": { "width": 26, "top": 588, "left": 1493, "height": 43 }

},

{

"char": "探",

"location": { "width": 26, "top": 588, "left": 1533, "height": 43 }

},

{

"char": "索",

"location": { "width": 26, "top": 588, "left": 1559, "height": 43 }

},

{

"char": ";",

"location": { "width": 18, "top": 588, "left": 1595, "height": 43 }

}

],

"location": { "width": 642, "top": 588, "left": 971, "height": 43 },

"words": "1.超宏伟世界观、六大种族秘密等你探索;"

},

{

"chars": [

{

"char": "爆",

"location": { "width": 27, "top": 635, "left": 534, "height": 44 }

},

{

"char": "严",

"location": { "width": 27, "top": 635, "left": 588, "height": 44 }

},

{

"char": "禁",

"location": { "width": 26, "top": 635, "left": 628, "height": 44 }

},

{

"char": "代",

"location": { "width": 27, "top": 635, "left": 668, "height": 44 }

},

{

"char": "充",

"location": { "width": 27, "top": 635, "left": 708, "height": 44 }

},

{

"char": "公",

"location": { "width": 26, "top": 635, "left": 735, "height": 44 }

},

{

"char": "告",

"location": { "width": 27, "top": 635, "left": 774, "height": 44 }

}

],

"location": { "width": 273, "top": 635, "left": 534, "height": 44 },

"words": "爆严禁代充公告"

},

{

"chars": [

{

"char": "2",

"location": { "width": 22, "top": 642, "left": 992, "height": 43 }

},

{

"char": ".",

"location": { "width": 21, "top": 642, "left": 1015, "height": 43 }

},

{

"char": "全",

"location": { "width": 26, "top": 642, "left": 1032, "height": 43 }

},

{

"char": "民",

"location": { "width": 26, "top": 642, "left": 1072, "height": 43 }

},

{

"char": "打",

"location": { "width": 26, "top": 642, "left": 1099, "height": 43 }

},

{

"char": "宝",

"location": { "width": 27, "top": 642, "left": 1138, "height": 43 }

},

{

"char": "得",

"location": { "width": 26, "top": 642, "left": 1165, "height": 43 }

},

{

"char": "神",

"location": { "width": 27, "top": 642, "left": 1204, "height": 43 }

},

{

"char": "装",

"location": { "width": 26, "top": 642, "left": 1231, "height": 43 }

},

{

"char": ",",

"location": { "width": 21, "top": 642, "left": 1267, "height": 43 }

},

{

"char": "自",

"location": { "width": 26, "top": 642, "left": 1297, "height": 43 }

},

{

"char": "由",

"location": { "width": 26, "top": 642, "left": 1337, "height": 43 }

},

{

"char": "交",

"location": { "width": 26, "top": 642, "left": 1364, "height": 43 }

},

{

"char": "易",

"location": { "width": 27, "top": 642, "left": 1403, "height": 43 }

},

{

"char": "换",

"location": { "width": 27, "top": 642, "left": 1429, "height": 43 }

},

{

"char": "货",

"location": { "width": 27, "top": 642, "left": 1469, "height": 43 }

},

{

"char": "币",

"location": { "width": 26, "top": 642, "left": 1496, "height": 43 }

},

{

"char": ",",

"location": { "width": 21, "top": 642, "left": 1532, "height": 43 }

},

{

"char": "极",

"location": { "width": 26, "top": 642, "left": 1563, "height": 43 }

},

{

"char": "速",

"location": { "width": 26, "top": 642, "left": 1589, "height": 43 }

},

{

"char": "致",

"location": { "width": 26, "top": 642, "left": 1629, "height": 43 }

},

{

"char": "富",

"location": { "width": 27, "top": 642, "left": 1668, "height": 43 }

},

{

"char": ";",

"location": { "width": 22, "top": 642, "left": 1689, "height": 43 }

}

],

"location": { "width": 719, "top": 642, "left": 992, "height": 43 },

"words": "2.全民打宝得神装,自由交易换货币,极速致富;"

},

{

"chars": [

{

"char": "3",

"location": { "width": 21, "top": 693, "left": 994, "height": 42 }

},

{

"char": ".",

"location": { "width": 21, "top": 693, "left": 1014, "height": 42 }

},

{

"char": "神",

"location": { "width": 25, "top": 693, "left": 1031, "height": 42 }

},

{

"char": "装",

"location": { "width": 25, "top": 693, "left": 1069, "height": 42 }

},

{

"char": "极",

"location": { "width": 26, "top": 693, "left": 1094, "height": 42 }

},

{

"char": "速",

"location": { "width": 25, "top": 693, "left": 1133, "height": 42 }

},

{

"char": "进",

"location": { "width": 26, "top": 693, "left": 1170, "height": 42 }

},

{

"char": "阶",

"location": { "width": 26, "top": 693, "left": 1195, "height": 42 }

},

{

"char": ",",

"location": { "width": 20, "top": 693, "left": 1229, "height": 42 }

},

{

"char": "炼",

"location": { "width": 25, "top": 693, "left": 1259, "height": 42 }

},

{

"char": "化",

"location": { "width": 26, "top": 693, "left": 1296, "height": 42 }

},

{

"char": "玩",

"location": { "width": 25, "top": 693, "left": 1334, "height": 42 }

},

{

"char": "法",

"location": { "width": 25, "top": 693, "left": 1359, "height": 42 }

},

{

"char": "打",

"location": { "width": 26, "top": 693, "left": 1397, "height": 42 }

},

{

"char": "造",

"location": { "width": 25, "top": 693, "left": 1436, "height": 42 }

},

{

"char": "独",

"location": { "width": 25, "top": 693, "left": 1461, "height": 42 }

},

{

"char": "特",

"location": { "width": 26, "top": 693, "left": 1497, "height": 42 }

},

{

"char": "个",

"location": { "width": 25, "top": 693, "left": 1536, "height": 42 }

},

{

"char": "性",

"location": { "width": 25, "top": 693, "left": 1561, "height": 42 }

},

{

"char": "神",

"location": { "width": 26, "top": 693, "left": 1599, "height": 42 }

},

{

"char": "装",

"location": { "width": 26, "top": 693, "left": 1624, "height": 42 }

},

{

"char": ";",

"location": { "width": 21, "top": 693, "left": 1657, "height": 42 }

}

],

"location": { "width": 686, "top": 693, "left": 994, "height": 42 },

"words": "3.神装极速进阶,炼化玩法打造独特个性神装;"

},

{

"chars": [

{

"char": "4",

"location": { "width": 19, "top": 746, "left": 996, "height": 38 }

},

{

"char": ".",

"location": { "width": 19, "top": 746, "left": 1008, "height": 38 }

},

{

"char": "独",

"location": { "width": 22, "top": 746, "left": 1036, "height": 38 }

},

{

"char": "创",

"location": { "width": 23, "top": 746, "left": 1070, "height": 38 }

},

{

"char": "十",

"location": { "width": 23, "top": 746, "left": 1106, "height": 38 }

},

{

"char": "二",

"location": { "width": 23, "top": 746, "left": 1141, "height": 38 }

},

{

"char": "星",

"location": { "width": 23, "top": 746, "left": 1165, "height": 38 }

},

{

"char": "使",

"location": { "width": 23, "top": 746, "left": 1199, "height": 38 }

},

{

"char": "助",

"location": { "width": 23, "top": 746, "left": 1235, "height": 38 }

},

{

"char": "战",

"location": { "width": 23, "top": 746, "left": 1270, "height": 38 }

},

{

"char": "系",

"location": { "width": 23, "top": 746, "left": 1305, "height": 38 }

},

{

"char": "统",

"location": { "width": 23, "top": 746, "left": 1328, "height": 38 }

},

{

"char": ",",

"location": { "width": 19, "top": 746, "left": 1360, "height": 38 }

},

{

"char": "五",

"location": { "width": 22, "top": 746, "left": 1400, "height": 38 }

},

{

"char": "大",

"location": { "width": 23, "top": 746, "left": 1435, "height": 38 }

},

{

"char": "魔",

"location": { "width": 23, "top": 746, "left": 1457, "height": 38 }

},

{

"char": "幻",

"location": { "width": 22, "top": 746, "left": 1494, "height": 38 }

},

{

"char": "星",

"location": { "width": 23, "top": 746, "left": 1529, "height": 38 }

},

{

"char": "使",

"location": { "width": 23, "top": 746, "left": 1564, "height": 38 }

},

{

"char": "技",

"location": { "width": 23, "top": 746, "left": 1599, "height": 38 }

},

{

"char": "能",

"location": { "width": 23, "top": 746, "left": 1623, "height": 38 }

},

{

"char": "逆",

"location": { "width": 23, "top": 746, "left": 1659, "height": 38 }

},

{

"char": "转",

"location": { "width": 23, "top": 746, "left": 1693, "height": 38 }

},

{

"char": "战",

"location": { "width": 23, "top": 746, "left": 1728, "height": 38 }

},

{

"char": "局",

"location": { "width": 22, "top": 746, "left": 1765, "height": 38 }

},

{

"char": ";",

"location": { "width": 15, "top": 746, "left": 1794, "height": 38 }

}

],

"location": { "width": 821, "top": 746, "left": 989, "height": 38 },

"words": "4.独创十二星使助战系统,五大魔幻星使技能逆转战局;"

},

{

"chars": [

{

"char": "5",

"location": { "width": 21, "top": 795, "left": 992, "height": 41 }

},

{

"char": ".",

"location": { "width": 21, "top": 795, "left": 1013, "height": 41 }

},

{

"char": "六",

"location": { "width": 25, "top": 795, "left": 1043, "height": 41 }

},

{

"char": "大",

"location": { "width": 25, "top": 795, "left": 1067, "height": 41 }

},

{

"char": "化",

"location": { "width": 25, "top": 795, "left": 1103, "height": 41 }

},

{

"char": "神",

"location": { "width": 25, "top": 795, "left": 1128, "height": 41 }

},

{

"char": "任",

"location": { "width": 25, "top": 795, "left": 1166, "height": 41 }

},

{

"char": "意",

"location": { "width": 23, "top": 795, "left": 1204, "height": 41 }

},

{

"char": "搭",

"location": { "width": 25, "top": 795, "left": 1228, "height": 41 }

},

{

"char": "配",

"location": { "width": 25, "top": 795, "left": 1264, "height": 41 }

},

{

"char": ",",

"location": { "width": 20, "top": 795, "left": 1297, "height": 41 }

},

{

"char": "助",

"location": { "width": 23, "top": 795, "left": 1327, "height": 41 }

},

{

"char": "力",

"location": { "width": 25, "top": 795, "left": 1364, "height": 41 }

},

{

"char": "培",

"location": { "width": 25, "top": 795, "left": 1400, "height": 41 }

},

{

"char": "养",

"location": { "width": 25, "top": 795, "left": 1438, "height": 41 }

},

{

"char": "个",

"location": { "width": 25, "top": 795, "left": 1463, "height": 41 }

},

{

"char": "性",

"location": { "width": 23, "top": 795, "left": 1501, "height": 41 }

},

{

"char": "化",

"location": { "width": 25, "top": 795, "left": 1525, "height": 41 }

},

{

"char": "职",

"location": { "width": 25, "top": 795, "left": 1563, "height": 41 }

},

{

"char": "业",

"location": { "width": 25, "top": 795, "left": 1599, "height": 41 }

},

{

"char": "属",

"location": { "width": 23, "top": 795, "left": 1624, "height": 41 }

},

{

"char": "性",

"location": { "width": 25, "top": 795, "left": 1661, "height": 41 }

},

{

"char": ";",

"location": { "width": 18, "top": 795, "left": 1694, "height": 41 }

}

],

"location": { "width": 719, "top": 795, "left": 992, "height": 41 },

"words": "5.六大化神任意搭配,助力培养个性化职业属性;"

},

{

"chars": [

{

"char": "6",

"location": { "width": 20, "top": 844, "left": 994, "height": 42 }

},

{

"char": ".",

"location": { "width": 20, "top": 844, "left": 1014, "height": 42 }

},

{

"char": "勇",

"location": { "width": 25, "top": 844, "left": 1030, "height": 42 }

},

{

"char": "者",

"location": { "width": 26, "top": 844, "left": 1068, "height": 42 }

},

{

"char": "转",

"location": { "width": 25, "top": 844, "left": 1106, "height": 42 }

},

{

"char": "职",

"location": { "width": 25, "top": 844, "left": 1131, "height": 42 }

},

{

"char": "突",

"location": { "width": 26, "top": 844, "left": 1168, "height": 42 }

},

{

"char": "破",

"location": { "width": 26, "top": 844, "left": 1193, "height": 42 }

},

{

"char": "属",

"location": { "width": 25, "top": 844, "left": 1232, "height": 42 }

},

{

"char": "性",

"location": { "width": 25, "top": 844, "left": 1269, "height": 42 }

},

{

"char": ",",

"location": { "width": 21, "top": 844, "left": 1291, "height": 42 }

},

{

"char": "职",

"location": { "width": 25, "top": 844, "left": 1333, "height": 42 }

},

{

"char": "业",

"location": { "width": 25, "top": 844, "left": 1371, "height": 42 }

},

{

"char": "时",

"location": { "width": 25, "top": 844, "left": 1396, "height": 42 }

},

{

"char": "装",

"location": { "width": 25, "top": 844, "left": 1433, "height": 42 }

},

{

"char": "免",

"location": { "width": 38, "top": 844, "left": 1459, "height": 42 }

},

{

"char": "费",

"location": { "width": 25, "top": 844, "left": 1496, "height": 42 }

},

{

"char": "领",

"location": { "width": 25, "top": 844, "left": 1534, "height": 42 }

},

{

"char": ";",

"location": { "width": 21, "top": 844, "left": 1555, "height": 42 }

}

],

"location": { "width": 586, "top": 844, "left": 994, "height": 42 },

"words": "6.勇者转职突破属性,职业时装免费领;"

},

{

"chars": [

{

"char": "7",

"location": { "width": 23, "top": 893, "left": 992, "height": 46 }

},

{

"char": ".",

"location": { "width": 22, "top": 893, "left": 1002, "height": 46 }

},

{

"char": "B",

"location": { "width": 23, "top": 893, "left": 1029, "height": 46 }

},

{

"char": "O",

"location": { "width": 22, "top": 893, "left": 1044, "height": 46 }

},

{

"char": "S",

"location": { "width": 23, "top": 893, "left": 1071, "height": 46 }

},

{

"char": "S",

"location": { "width": 23, "top": 893, "left": 1100, "height": 46 }

},

{

"char": "爆",

"location": { "width": 28, "top": 893, "left": 1118, "height": 46 }

},

{

"char": "率",

"location": { "width": 27, "top": 893, "left": 1147, "height": 46 }

},

{

"char": "1",

"location": { "width": 22, "top": 893, "left": 1171, "height": 46 }

},

{

"char": "0",

"location": { "width": 22, "top": 893, "left": 1184, "height": 46 }

},

{

"char": "0",

"location": { "width": 23, "top": 893, "left": 1212, "height": 46 }

},

{

"char": "%",

"location": { "width": 28, "top": 893, "left": 1231, "height": 46 }

},

{

"char": ",",

"location": { "width": 22, "top": 893, "left": 1255, "height": 46 }

},

{

"char": "狂",

"location": { "width": 28, "top": 893, "left": 1287, "height": 46 }

},

{

"char": "爆",

"location": { "width": 27, "top": 893, "left": 1331, "height": 46 }

},

{

"char": "极",

"location": { "width": 28, "top": 893, "left": 1358, "height": 46 }

},

{

"char": "品",

"location": { "width": 27, "top": 893, "left": 1387, "height": 46 }

},

{

"char": "装",

"location": { "width": 28, "top": 893, "left": 1428, "height": 46 }

},

{

"char": "备",

"location": { "width": 27, "top": 893, "left": 1456, "height": 46 }

},

{

"char": ",",

"location": { "width": 22, "top": 893, "left": 1480, "height": 46 }

},

{

"char": "轻",

"location": { "width": 28, "top": 893, "left": 1526, "height": 46 }

},

{

"char": "松",

"location": { "width": 28, "top": 893, "left": 1555, "height": 46 }

},

{

"char": "集",

"location": { "width": 28, "top": 893, "left": 1597, "height": 46 }

},

{

"char": "齐",

"location": { "width": 28, "top": 893, "left": 1625, "height": 46 }

},

{

"char": "全",

"location": { "width": 28, "top": 893, "left": 1653, "height": 46 }

},

{

"char": "套",

"location": { "width": 27, "top": 893, "left": 1696, "height": 46 }

},

{

"char": "神",

"location": { "width": 28, "top": 893, "left": 1724, "height": 46 }

},

{

"char": "装",

"location": { "width": 28, "top": 893, "left": 1752, "height": 46 }

},

{

"char": ";",

"location": { "width": 23, "top": 893, "left": 1775, "height": 46 }

}

],

"location": { "width": 813, "top": 893, "left": 992, "height": 46 },

"words": "7.BOSS爆率100%,狂爆极品装备,轻松集齐全套神装;"

},

{

"chars": [

{

"char": "8",

"location": { "width": 25, "top": 945, "left": 994, "height": 49 }

},

{

"char": ".",

"location": { "width": 23, "top": 945, "left": 1008, "height": 49 }

},

{

"char": "首",

"location": { "width": 44, "top": 945, "left": 1029, "height": 49 }

},

{

"char": "日",

"location": { "width": 29, "top": 945, "left": 1072, "height": 49 }

},

{

"char": "直",

"location": { "width": 29, "top": 945, "left": 1102, "height": 49 }

},

{

"char": "升",

"location": { "width": 29, "top": 945, "left": 1132, "height": 49 }

},

{

"char": "1",

"location": { "width": 23, "top": 945, "left": 1158, "height": 49 }

},

{

"char": "5",

"location": { "width": 25, "top": 945, "left": 1172, "height": 49 }

},

{

"char": "0",

"location": { "width": 25, "top": 945, "left": 1201, "height": 49 }

},

{

"char": "级",

"location": { "width": 29, "top": 945, "left": 1222, "height": 49 }

},

{

"char": ",",

"location": { "width": 25, "top": 945, "left": 1247, "height": 49 }

},

{

"char": "登",

"location": { "width": 30, "top": 945, "left": 1281, "height": 49 }

},

{

"char": "录",

"location": { "width": 29, "top": 945, "left": 1327, "height": 49 }

},

{

"char": "即",

"location": { "width": 29, "top": 945, "left": 1357, "height": 49 }

},

{

"char": "送",

"location": { "width": 29, "top": 945, "left": 1387, "height": 49 }

},

{

"char": ":",

"location": { "width": 23, "top": 945, "left": 1412, "height": 49 }

},

{

"char": "兽",

"location": { "width": 29, "top": 945, "left": 1446, "height": 49 }

},

{

"char": "王",

"location": { "width": 29, "top": 945, "left": 1492, "height": 49 }

},

{

"char": "天",

"location": { "width": 29, "top": 945, "left": 1521, "height": 49 }

},

{

"char": "神",

"location": { "width": 29, "top": 945, "left": 1551, "height": 49 }

},

{

"char": "、",

"location": { "width": 25, "top": 945, "left": 1575, "height": 49 }

},

{

"char": "春",

"location": { "width": 29, "top": 945, "left": 1611, "height": 49 }

},

{

"char": "日",

"location": { "width": 30, "top": 945, "left": 1655, "height": 49 }

},

{

"char": "时",

"location": { "width": 29, "top": 945, "left": 1686, "height": 49 }

},

{

"char": "装",

"location": { "width": 29, "top": 945, "left": 1716, "height": 49 }

},

{

"char": "、",

"location": { "width": 25, "top": 945, "left": 1740, "height": 49 }

},

{

"char": "雪",

"location": { "width": 29, "top": 945, "left": 1789, "height": 49 }

},

{

"char": "豹",

"location": { "width": 28, "top": 945, "left": 1820, "height": 49 }

}

],

"location": { "width": 879, "top": 945, "left": 968, "height": 49 },

"words": "8.首日直升150级,登录即送:兽王天神、春日时装、雪豹"

}

]

}

计算文字的位置

背景：百度OCR返回的文字都是json字典，希望能从对应匹配到的单词，找到对应的位置坐标信息

代码：

def calcWordsLocation(self, wordStr, curWordsResult):

"""Calculate words location from result

Args:

wordStr (str): the words to check

curWordsResult (dict): the baidu OCR result of current words

Returns:

location, a tuple (x, y, width, height)

Raises:

Examples

wordStr="首充"

curWordsResult= {

"chars": [

{

"char": "寻",

"location": {

"width": 15,

"top": 51,

"left": 725,

"height": 24

}

},

...

{

"char": "首",

"location": {

"width": 15,

"top": 51,

"left": 971,

"height": 24

}

},

{

"char": "充",

"location": {

"width": 15,

"top": 51,

"left": 986,

"height": 24

}

}

],

"location": {

"width": 280,

"top": 51,

"left": 725,

"height": 24

},

"words": "寻宝福利大厅商城首充"

}

-> (971, 51, 30, 24)

"""

(x, y, width, height) = (0, 0, 0, 0)

matchedStr = curWordsResult["words"]

# Note: for special, contain multilple words, here only process firt words

foundWords = re.search(wordStr, matchedStr)

if foundWords:

logging.debug("foundWords=%s" % foundWords)

firstMatchedPos = foundWords.start()

lastMatchedPos = foundWords.end() - 1

matchedStrLen = len(matchedStr)

charResultList = curWordsResult["chars"]

charResultListLen = len(charResultList)

firstCharResult = None

lastCharResult = None

if matchedStrLen == charResultListLen:

firstCharResult = charResultList[firstMatchedPos]

lastCharResult = charResultList[lastMatchedPos]

else:

# Special: for 'Loading' matched ' Loading', but charResultList not include first space ' ', but from fisrt='L' to end='g'

# so using find the corresponding char, then got its location

# Note: following method not work for regex str, like '^游戏公告$'

firtToMatchChar = wordStr[0]

lastToMatchChar = wordStr[-1]

for eachCharResult in charResultList:

if firstCharResult and lastCharResult:

break

eachChar = eachCharResult["char"]

if firtToMatchChar == eachChar:

firstCharResult = eachCharResult

elif lastToMatchChar == eachChar:

lastCharResult = eachCharResult

# Note: follow no need check words, to support input ^游戏公告$ to match "游戏公告"

# firstLocation = None

# lastLocation = None

# if firstCharResult["char"] == firtToMatchChar:

#     firstLocation = firstCharResult["location"]

# if lastCharResult["char"] == lastToMatchChar:

#     lastLocation = lastCharResult["location"]

firstLocation = firstCharResult["location"]

lastLocation = lastCharResult["location"]

# if firstLocation and lastLocation:

# support both horizontal and vertical words

firstLeft = firstLocation["left"]

lastLeft = lastLocation["left"]

minLeft = min(firstLeft, lastLeft)

x = minLeft

firstHorizontalEnd = firstLeft + firstLocation["width"]

lastHorizontalEnd = lastLeft + lastLocation["width"]

maxHorizontalEnd = max(firstHorizontalEnd, lastHorizontalEnd)

width = maxHorizontalEnd - x

lastTop = lastLocation["top"]

minTop = min(firstLocation["top"], lastTop)

y = minTop

lastVerticalEnd = lastTop + lastLocation["height"]

height = lastVerticalEnd - y

return x, y, width, height

调用：

calculatedLocation = self.calcWordsLocation(eachInputWords, eachWordsMatchedResult)

把坐标位置转成中间坐标值

作用：用于后续点击按钮中间坐标值

代码：

def locationToCenterPos(self, wordslocation):

"""Convert location of normal button to center position

Args:

wordslocation (tuple): words location, (x, y, width, height)

Example: (267, 567, 140, 39)

Returns:

tuple, (x, y), the location's center position, normal used later to click it

Example: (337.0, 586.5)

Raises:

"""

x, y, width, height = wordslocation

centerX = x + width/2

centerY = y + height/2

centerPosition = (centerX, centerY)

return centerPosition

调用：

curCenterX, curCenterY = self.locationToCenterPos(eachLocation)

检测文字是否在结果中

代码：

def isWordsInResult(self, respJson, wordsOrWordsList, isMatchMultiple=False):

"""Check words is in result or not

Args:

respJson (dict): Baidu OCR responsed json

wordsOrWordsList (str/list): single input str or str list

isMatchMultiple (bool): for each single str, to match multiple output or only match one output

Returns:

dict, matched result

Raises:

"""

# Note: use OrderedDict instead dict to keep order, for later get first match result to process

orderedMatchedResultDict = OrderedDict()

inputWordsList = wordsOrWordsList

if isinstance(wordsOrWordsList, str):

inputWords = str(wordsOrWordsList)

inputWordsList = [inputWords]

wordsResultList = respJson["words_result"]

for curInputWords in inputWordsList:

curMatchedResultList = []

for eachWordsResult in wordsResultList:

eachWords = eachWordsResult["words"]

foundCurWords = re.search(curInputWords, eachWords)

if foundCurWords:

curMatchedResultList.append(eachWordsResult)

if not isMatchMultiple:

break

orderedMatchedResultDict[curInputWords] = curMatchedResultList

return orderedMatchedResultDict

调用：

matchedResultDict = self.isWordsInResult(wordsResultJson, wordsOrWordsList, isMatchMultiple)

检测当前屏幕中是否包含对应文字

def isWordsInCurScreen(self, wordsOrWordsList, imgPath=None, isMatchMultiple=False, isRespShortInfo=False):

"""Found words in current screen

Args:

wordsOrWordsList (str/list): single input str or str list

imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image

isMatchMultiple (bool): for each single str, to match multiple output or only match one output; default=False

isRespShortInfo (bool): return simple=short=nomarlly bool or list[bool] info or return full info which contain imgPath and full matched result.

Returns:

matched result, type=bool/list[bool]/dict/tuple, depends on diffrent condition

Raises:

"""

retValue = None

if not imgPath:

# do screenshot

imgPath = self.getCurScreenshot()

wordsResultJson = self.baiduImageToWords(imgPath)

isMultipleInput = False

inputWords = None

inputWordsList = []

if isinstance(wordsOrWordsList, list):

isMultipleInput = True

inputWordsList = list(wordsOrWordsList)

elif isinstance(wordsOrWordsList, str):

isMultipleInput = False

inputWords = str(wordsOrWordsList)

inputWordsList = [inputWords]

matchedResultDict = self.isWordsInResult(wordsResultJson, wordsOrWordsList, isMatchMultiple)

# add caclulated location and words

# Note: use OrderedDict instead dict to keep order, for later get first match result to process

processedResultDict = OrderedDict()

for eachInputWords in inputWordsList:

isCurFound = False

# curLocatoinList = []

# curWordsList = []

curResultList = []

curWordsMatchedResultList = matchedResultDict[eachInputWords]

if curWordsMatchedResultList:

isCurFound = True

for curIdx, eachWordsMatchedResult in enumerate(curWordsMatchedResultList):

curMatchedWords = eachWordsMatchedResult["words"]

calculatedLocation = self.calcWordsLocation(eachInputWords, eachWordsMatchedResult)

# curLocatoinList.append(calculatedLocation)

# curWordsList.append(curMatchedWords)

curResult = (curMatchedWords, calculatedLocation)

curResultList.append(curResult)

# processedResultDict[eachInputWords] = (isCurFound, curLocatoinList, curWordsList)

processedResultDict[eachInputWords] = (isCurFound, curResultList)

logging.debug("For %s, matchedResult=%s from imgPath=%s", wordsOrWordsList, processedResultDict, imgPath)

if isMultipleInput:

if isRespShortInfo:

isFoundList = []

for eachInputWords in processedResultDict.keys():

isCurFound, noUse = processedResultDict[eachInputWords]

isFoundList.append(isCurFound)

# Note: no mattter isMatchMultiple, both only return single boolean for each input words

retBoolList = isFoundList

retValue = retBoolList

else:

if isMatchMultiple:

retTuple = processedResultDict, imgPath

retValue = retTuple

else:

# Note: use OrderedDict instead dict to keep order, for later get first match result to process

respResultDict = OrderedDict()

for eachInputWords in processedResultDict.keys():

# isCurFound, curLocatoinList, curWordsList = processedResultDict[eachInputWords]

isCurFound, curResultList = processedResultDict[eachInputWords]

# singleLocation = None

# singleWords = None

singleResult = (None, None)

if isCurFound:

# singleLocation = curLocatoinList[0]

# singleWords = curWordsList[0]

singleResult = curResultList[0]

# respResultDict[eachInputWords] = (isCurFound, singleLocation, singleWords)

respResultDict[eachInputWords] = (isCurFound, singleResult)

retTuple = respResultDict, imgPath

retValue = retTuple

else:

singleInputResult = processedResultDict[inputWords]

# isCurFound, curLocatoinList, curWordsList = singleInputResult

isCurFound, curResultList = singleInputResult

if isRespShortInfo:

# Note: no mattter isMatchMultiple, both only return single boolean for each input words

retBool = isCurFound

retValue = retBool

else:

if isMatchMultiple:

# retTuple = isCurFound, curLocatoinList, curWordsList, imgPath

retTuple = isCurFound, curResultList, imgPath

retValue = retTuple

else:

singleResult = (None, None)

# singleLocation = None

# singleWords = None

if isCurFound:

# singleLocation = curLocatoinList[0]

# singleWords = curWordsList[0]

singleResult = curResultList[0]

# retTuple = isCurFound, singleLocation, singleWords, imgPath

retTuple = isCurFound, singleResult, imgPath

retValue = retTuple

logging.debug("Input: %s, output=%s", wordsOrWordsList, retValue)

return retValue

调用：

allResultDict, _ = self.isWordsInCurScreen(allStrList, imgPath, isMatchMultiple=True)

获取当前屏幕中的文字

def getWordsInCurScreen(self):

"""get words in current screenshot"""

screenImgPath = self.getCurScreenshot()

wordsResultJson = self.baiduImageToWords(screenImgPath)

return wordsResultJson

调用：

curScreenWords = self.getWordsInCurScreen()

检测当前屏幕中是否存在某些信息

def checkExistInScreen(self,

imgPath=None,

mandatoryStrList=[],

mandatoryMinMatchCount=0,

optionalStrList=[],

# optionalMinMatchCount=2,

optionalMinMatchCount=1,

isRespFullInfo=False

):

"""Check whether mandatory and optional str list in current screen or not

Args:

imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image

mandatoryStrList (list): mandatory str, at least match `mandatoryMinMatchCount`, or all must match if `mandatoryMinMatchCount`=0

mandatoryMinMatchCount (int): minimal match count for mandatory list

optionalStrList (list): optional str, some may match

optionalMinMatchCount (int): for `optionalStrList`, the minimal match count, consider to match or not

isRespFullInfo (bool): return full info or not, full info means match location result and imgPath

Returns:

matched result, type=bool/tuple, depends on `isRespFullInfo`

Raises:

"""

if not imgPath:

imgPath = self.getCurScreenshot()

logging.debug("imgPath=%s", imgPath)

isExist = False

# Note: use OrderedDict instead dict to keep order, for later get first match result to process

respMatchLocation = OrderedDict()

isMandatoryMatch = True

isMandatoryShouldMatchAll = (mandatoryMinMatchCount <= 0)

isOptionalMatch = True

allStrList = []

allStrList.extend(mandatoryStrList)

allStrList.extend(optionalStrList)

optionalMatchCount = 0

mandatoryMatchCount = 0

allResultDict, _ = self.isWordsInCurScreen(allStrList, imgPath, isMatchMultiple=True)

for eachStr, (isFoundCur, curResultList) in allResultDict.items():

if eachStr in mandatoryStrList:

if isFoundCur:

mandatoryMatchCount += 1

respMatchLocation[eachStr] = curResultList

else:

if isMandatoryShouldMatchAll:

isMandatoryMatch = False

break

elif eachStr in optionalStrList:

if isFoundCur:

optionalMatchCount += 1

respMatchLocation[eachStr] = curResultList

if mandatoryStrList:

if not isMandatoryShouldMatchAll:

if mandatoryMatchCount >= mandatoryMinMatchCount:

isMandatoryMatch = True

else:

isMandatoryMatch = False

if optionalStrList:

if optionalMatchCount >= optionalMinMatchCount:

isOptionalMatch = True

else:

isOptionalMatch = False

isExist = isMandatoryMatch and isOptionalMatch

logging.debug("isMandatoryMatch=%s, isOptionalMatch=%s -> isExist=%s", isMandatoryMatch, isOptionalMatch, isExist)

if isRespFullInfo:

logging.debug("mandatoryStrList=%s, optionalStrList=%s -> isExist=%s, respMatchLocation=%s, imgPath=%s",

mandatoryStrList, optionalStrList, isExist, respMatchLocation, imgPath)

return (isExist, respMatchLocation, imgPath)

else:

logging.debug("mandatoryStrList=%s, optionalStrList=%s -> isExist=%s",

mandatoryStrList, optionalStrList, isExist)

return isExist

调用：

checkResult = self.checkExistInScreen(

imgPath=imgPath,

optionalStrList=strList,

optionalMinMatchCount=1,

isRespFullInfo=isRespFullInfo,

)

和：

minOptionalMatchCount = 2

mandatoryList = [

# "公告",

"^公告$",

'^强力推荐$', # 王城英雄, 不规则弹框

。。。

]

possibleTitleList = [

"^游戏公告$",

]

optionalList = []

otherOptionalList = [

"新增(内容)?",

"游戏特色",

"(主要)?更新(内容)?",

。。。

"登录即送",

]

optionalList.extend(possibleTitleList)

optionalList.extend(otherOptionalList)

checkResult = self.checkExistInScreen(

imgPath=imgPath,

mandatoryStrList=mandatoryList,

mandatoryMinMatchCount=1,

optionalStrList=optionalList,

optionalMinMatchCount=minOptionalMatchCount,

isRespFullInfo=isRespFullInfo,

)

和：

mandatoryList = [

"^购买$", # 造梦西游：'购买'

"^￥\d+元?", # 剑玲珑, 至尊屠龙

"^\d+元", # 造梦西游：'6元券3元券3’

"^充值$", # 剑玲珑, 至尊屠龙

]

optionalList = [

# common

"元宝",

"月卡",

。。。

# 剑玲珑

"赠",

]

isRealPay, matchResult, imgPath  = self.checkExistInScreen(

mandatoryStrList=mandatoryList,

mandatoryMinMatchCount=2,

optionalStrList=optionalList,

optionalMinMatchCount=2,

isRespFullInfo=True,

)

和：

mandatoryList = [

"^((继续)|(结束))$", # 暗黑觉醒：'继续' or '结束'

"^\d秒$", # 暗黑觉醒：'5秒', '1秒'

]

isAutoConverstion, matchResult, imgPath  = self.checkExistInScreen(

imgPath=imgPath,

mandatoryStrList=mandatoryList,

mandatoryMinMatchCount=2,

isRespFullInfo=True,

)

和：

mandatoryList = [

"^充值", # 造梦西游： "充值战神榜邮件各但,挑战竞技好友"

"首充$", # 剑玲珑，至尊屠龙

。。。

]

optionalList = [

# common

"组队", # 至尊屠龙, 剑玲珑

"任务", # 至尊屠龙, 剑玲珑

# "商城", # 剑玲珑

"寻宝", # 剑玲珑

"福利大厅", # 剑玲珑

。。。

"背包",

]

isHome, matchResult, imgPath  = self.checkExistInScreen(

mandatoryStrList=mandatoryList,

mandatoryMinMatchCount=1,

optionalStrList=optionalList,

isRespFullInfo=True,

)

和：

mandatoryList = [

"立即登录", # 剑玲珑, 至尊屠龙

。。。

"^登录$", # 暗黑觉醒

]

optionalList = [

# common

"一键((注册)|(试玩))",

"忘记密码", # 至尊屠龙, 青云诀

"((用户)|(账号)|(手机))登录", # 剑玲珑, 至尊屠龙,

"点击选服", # 青云诀，暗黑觉醒

。。。

# 造梦西游

"游客登录", #更新：不能用游客登录，否则后续无法弹出支付页面

]

respUserLogin = self.checkExistInScreen(

mandatoryStrList=mandatoryList,

mandatoryMinMatchCount=1,

optionalStrList=optionalList,

isRespFullInfo=isRespFullInfo

)

是否存在任意一个词组

def isExistAnyStr(self, strList, imgPath=None, isRespFullInfo=False):

"""Is any str exist or not

Args:

strList (list): str list to check exist or not

imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image

isRespFullInfo (bool): return full info or not, full info means match location result and imgPath

Returns:

matched result, type=bool/tuple, depends on `isRespFullInfo`

Raises:

"""

if not imgPath:

imgPath = self.getCurScreenshot()

checkResult = self.checkExistInScreen(

imgPath=imgPath,

optionalStrList=strList,

optionalMinMatchCount=1,

isRespFullInfo=isRespFullInfo,

)

if isRespFullInfo:

isExistAny, matchResult, imgPath = checkResult

logging.debug("isExistAny=%s, matchResult=%s, imgPath=%s for %s", isExistAny, matchResult, imgPath, strList)

return (isExistAny, matchResult, imgPath)

else:

isExistAny = checkResult

logging.debug("isExistAny=%s, for %s", isExistAny, strList)

return isExistAny

调用：

isExist, matchResult, imgPath = self.isExistAnyStr(buttonStrList, imgPath=imgPath, isRespFullInfo=True)

和：

mandatoryList = [

# 御剑仙缘

"""^(请)?点击\s?[“”"'][^“”"',]{1,6}[“”"']?""", # '请点击“战骑'，'请点击“魂兽”'，'请点击“人物”'，'请点击“领取”'，'请点击“仙盟”'，'点击“+”,放入吞噬'，'点击“进阶”,提升战'，请点击“每日必做”按

]

respResult = self.isExistAnyStr(mandatoryList, imgPath=imgPath, isRespFullInfo=isRespFullInfo)

和：

requireManualOperationList = [

"完成指定操作", # speical：造梦西游 的 '(请完成指定操作)梦口'  的顶部弹框

]

isRequireManual, _, imgPath = self.isExistAnyStr(requireManualOperationList, isRespFullInfo=True)

和：

lanuchStrList = [

"^4399手机游戏$", # 剑玲珑

"^西瓜游戏$", # 青云诀

]

isLaunch, _, imgPath = self.isExistAnyStr(lanuchStrList, isRespFullInfo=True)

和：

loadingStrList = [

# 登录类

"正在登录", # 正在登录

"logging",

。。。

"游戏资源", # 青云诀:本地游戏资源已是最新

。。。

]

isLoadingSth, _, imgPath = self.isExistAnyStr(loadingStrList, isRespFullInfo=True)

和：

gotoPayStrList = [

"^前往充值$", # 剑玲珑

"^立即充值$", # 至尊屠龙

。。。

]

respBoolOrTuple = self.isExistAnyStr(gotoPayStrList, isRespFullInfo=isRespLocation)

判断是否所有的字符都存在

def isExistAllStr(self, strList, imgPath=None, isRespFullInfo=False):

"""Is all str exist or not

Args:

strList (list): str list to check exist or not

imgPath (str): current screen image file path; default=None; if None, will auto get current scrren image

isRespFullInfo (bool): return full info or not, full info means match location result and imgPath

Returns:

matched result, type=bool/tuple, depends on `isRespFullInfo`

Raises:

"""

if not imgPath:

imgPath = self.getCurScreenshot()

checkResult = self.checkExistInScreen(imgPath=imgPath, mandatoryStrList=strList, isRespFullInfo=isRespFullInfo)

if isRespFullInfo:

isExistAll, matchResult, imgPath = checkResult

logging.debug("isExistAll=%s, matchResult=%s, imgPath=%s for %s", isExistAll, matchResult, imgPath, strList)

return (isExistAll, matchResult, imgPath)

else:

isExistAll = checkResult

logging.debug("isExistAll=%s, for %s", isExistAll, strList)

return isExistAll

调用：

realPayStrList = [

"^￥\d+元?", # 剑玲珑, 至尊屠龙

"^充值$", # 剑玲珑, 至尊屠龙

]

return self.isExistAllStr(realPayStrList, isRespFullInfo=isRespLocation)

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 10:17:30

音频

播放音频

树莓派中用python播放音频

前提：

树莓派中，先去安装vlc：

sudo apt-get install vlc

代码：

import vlc

instance = vlc.Instance('--aout=alsa')

p = instance.media_player_new()

m = instance.media_new('/home/pi/Music/lizhongsheng_massif_live.mp3')

p.set_media(m)

p.play()

即可播放音频。

实现设置音量，暂停，继续播放等操作的代码是：

p.pause()

vlc.libvlc_audio_set_volume(p, 40)

p.play()

vlc.libvlc_audio_set_volume(p, 90)

Mac中调用mpv播放音频

播放音频：

cmdPlayer = "mpv"

cmdParaFilePath = tmpAudioFileFullPath

cmdArgList = [cmdPlayer, cmdParaFilePath]

if gCurSubProcess:

gCurSubProcess.terminate()

gCurSubProcess = subprocess.Popen(cmdArgList)

log.debug("gCurSubProcess=%s", gCurSubProcess)

停止播放：

if audioControl == "stop":

if gCurSubProcess:

gCurSubProcess.terminate()

respData = {

audioControl: "ok"

}

else:

respData = {

audioControl: "Unsupport command"

}

获取播放效果：

if gCurSubProcess:

isTerminated = gCurSubProcess.poll() # None

# stdout_data, stderr_data = gCurSubProcess.communicate()

# stdoutStr = str(stdout_data)

# stderrStr = str(stderr_data)

respData = {

"isTerminated": isTerminated,

# "stdout_data": stdoutStr,

# "stderr_data": stderrStr,

}

返回结果：

- 正在播放：返回isTerminated为 null

- 被终止后，返回isTerminated为 4

播放的效果：

mac系统中播放音频

PyCharm的console中输出当前播放的信息 -》 如果是mac的terminal中，则是覆盖式的，不会这么多行 同时弹框GUI窗口

mp3

解析mp3等音频文件得到时长信息

用库：

- audioread

- GitHub

- beetbox/audioread: cross-library (GStreamer + Core Audio + MAD + FFmpeg) audio decoding for Python

import audioread

try:

audioFullFilePath = "/your/input/audio/file.mp3"

with audioread.audio_open(audioFullFilePath) as audioFp:

audioInfo["duration"] = audioFp.duration

audioInfo["channels"] = audioFp.channels

audioInfo["sampleRate"] = audioFp.samplerate

except OSError as osErr:

logging.error("OSError when open %s error %s", audioFullFilePath, osErr)

except EOFError as eofErr:

logging.error("EOFError when open %s error %s", audioFullFilePath, eofErr)

except audioread.DecodeError as decodeErr:

logging.error("Decode audio %s error %s", audioFullFilePath, decodeErr)

后经整理成函数：

import audioread

def detectAudioMetaInfo(audioFullPath):

"""

detect audio meta info: duration, channels, sampleRate

"""

isOk = False

errMsg = ""

audioMetaInfo = {

"duration": 0,

"channels": 0,

"sampleRate": 0,

}

try:

with audioread.audio_open(audioFullPath) as audioFp:

audioMetaInfo["duration"] = audioFp.duration

audioMetaInfo["channels"] = audioFp.channels

audioMetaInfo["sampleRate"] = audioFp.samplerate

isOk = True

except OSError as osErr:

errMsg = "detect audio info error: %s" % str(osErr)

except EOFError as eofErr:

errMsg = "detect audio info error: %s" % str(eofErr)

except audioread.DecodeError as decodeErr:

errMsg = "detect audio info error: %s" % str(decodeErr)

if isOk:

return isOk, audioMetaInfo

else:

return isOk, errMsg

调用：

def demoDetectAudioMeta():

curPath = os.path.dirname(__file__)

inputAudioList = [

"input/audio/actual_aac_but_suffix_mp3.mp3",

"input/audio/real_mp3_format.mp3",

"not_exist_audio.wav",

"input/audio/fake_audio_actual_image.wav",

]

for eachAudioPath in inputAudioList:

eachAudioFullPath = os.path.join(curPath, eachAudioPath)

isOk, errOrInfo = detectAudioMetaInfo(eachAudioFullPath)

print("isOk=%s, errOrInfo=%s" % (isOk, errOrInfo))

if __name__ == "__main__":

demoDetectAudioMeta()

对应的音频文件，用MediaInfo检测出的信息：

- 正常mp3

- 异常mp3：

- 故意把png图片改成mp3

输出：

# isOk=True, errOrInfo={'duration': 637.8, 'channels': 2, 'sampleRate': 44100}

# isOk=True, errOrInfo={'duration': 2.3510204081632655, 'channels': 2, 'sampleRate': 44100}

# 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'

# isOk=False, errOrInfo=detect audio info error:

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:45:02

视频

从视频中提取出音频mp3文件

import os

import logging

import subprocess

videoFullpath = "show_157712932_video.mp4"

startTimeStr = "00:00:11.270"

# startTimeStr = "%02d:%02d:%02d.%03d" % (startTime.hours, startTime.minutes, startTime.seconds, startTime.milliseconds)

endTimeStr = "00:00:14.550"

# endTimeStr = "%02d:%02d:%02d.%03d" % (endTime.hours, endTime.minutes, endTime.seconds, endTime.milliseconds)

outputAudioFullpath = "show_157712932_audio_000011270_000014550.mp3"

# extract audio segment from video

# 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

if not os.path.exists(outputAudioFullpath):

ffmpegCmd = "ffmpeg -i %s -ss %s -to %s -b:a 128k %s" % (videoFullpath, startTimeStr, endTimeStr, outputAudioFullpath)

subprocess.call(ffmpegCmd, shell=True)

logging.info("Complete use ffmpeg extract audio: %s", ffmpegCmd)

可以从mp4中提取出mp3音频：

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:45:32

网络相关

此处整理和网络相关的一些常用代码段。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:47:42

BeautifulSoup

用网络库下载到页面源码后，就是去解析（HTML等）内容了。

Python中最常用的HTML（和XML）解析库之一就是： BeautifulSoup

- 最新代码详见：https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/thirdParty/crifanBeautifulsoup.py

从html转soup

from bs4 import BeautifulSoup

def htmlToSoup(curHtml):

"""convert html to soup

Args:

curHtml (str): html str

Returns:

soup

Raises:

"""

soup = BeautifulSoup(curHtml, 'html.parser')

return soup

从xml转换出soup

背景：

iOS自动化期间，常会涉及到，获取到当前页面源码，是xml字符串，需要转换为soup，才能后续操作

所以整理出通用转换逻辑

def xmlToSoup(xmlStr):

"""convert to xml string to soup

Note: xml is tag case sensitive -> retain tag upper case -> NOT convert tag to lowercase

Args:

xmlStr (str): xml str, normally page source

Returns:

soup

Raises:

"""

# HtmlParser = 'html.parser'

# XmlParser = 'xml'

XmlParser = 'lxml-xml'

curParser = XmlParser

soup = BeautifulSoup(xmlStr, curParser)

return soup

举例：

（1）

curPageXml = self.get_page_source()

soup = CommonUtils.xmlToSoup(curPageXml)

获取到xml字符串后，去转换为soup

soup转html

def soupToHtml(soup, isFormat=True):

"""Convert soup to html string

Args:

soup (Soup): BeautifulSoup soup

isFormat (bool): use prettify to format html

Returns:

html (str)

Raises:

"""

if isFormat:

curHtml = soup.prettify() # formatted html

else:

curHtml = str(soup) # not formatted html

return curHtml

获取soup节点所有的文字内容

def getAllContents(curNode, isStripped=False):

"""Get all contents of current and children nodes

Args:

curNode (soup node): current Beautifulsoup node

isStripped (bool): return stripped string or not

Returns:

str

Raises:

"""

# codeSnippetStr = curNode.prettify()

# codeSnippetStr = curNode.string

# codeSnippetStr = curNode.contents

codeSnippetStr = ""

stringList = []

if isStripped:

stringGenerator = curNode.stripped_strings

else:

stringGenerator = curNode.strings

# stringGenerator = curNode.strings

for eachStr in stringGenerator:

# logging.debug("eachStr=%s", eachStr)

stringList.append(eachStr)

codeSnippetStr = "\n".join(stringList)

logging.debug("codeSnippetStr=%s", codeSnippetStr)

return codeSnippetStr

从html中提取title值

def extractHtmlTitle_BeautifulSoup(htmlStr):

"""

Extract title from html, use BeautifulSoup

Args:

htmlStr (str): html string

Returns:

str

Raises:

Examples:

"""

curTitle = ""

soup = BeautifulSoup(htmlStr, "html.parser")

if soup:

if soup.title and soup.title.string:

curTitle = soup.title.string

curTitle = curTitle.strip()

else:

# logging.warning("Empty title for html: %s", htmlStr)

logging.debug("Empty title for html: %s", htmlStr)

# Empty title for html: <script type="text/javascript">top.location.href='https://login.zhongan.com/passport/login.htm?sourceApp=1&target=http://www.zhongan.com/open/member/loginJump?logincallback=%2Fahita';</script>

# for debug

if "<title>" not in htmlStr:

logging.warning("Special not incldue <title> html: %s", htmlStr)

# 'Illegal access address!\n'

# <script type="text/javascript">top.location.href='https://login.zhongan.com/passport/login.htm?sourceApp=1&target=http://www.zhongan.com/open/member/loginJump?logincallback=%2Fahita';</script>

#

else:

logging.error("Failed to convert to soup for html: %s", htmlStr)

#

return curTitle

是否包含符合特定条件的soup节点

def isContainSpecificSoup(soupList, elementName, isSizeValidCallback, matchNum=1):

"""

判断BeautifulSoup的soup的list中，是否包含符合条件的特定的元素：

只匹配指定个数的元素才视为找到了

元素名相同

面积大小是否符合条件

Args:

elementName (str): element name

isSizeValidCallback (function): callback function to check whether element size(width * height) is valid or not

matchNum (int): sould only matched specific number consider as valid

Returns:

bool

Raises:

"""

isFound = False

matchedSoupList = []

for eachSoup in soupList:

# if hasattr(eachSoup, "tag"):

if hasattr(eachSoup, "name"):

# curSoupTag = eachSoup.tag

curSoupTag = eachSoup.name

if curSoupTag == elementName:

if hasattr(eachSoup, "attrs"):

soupAttr = eachSoup.attrs

soupWidth = int(soupAttr["width"])

soupHeight = int(soupAttr["height"])

curSoupSize = soupWidth * soupHeight # 326 * 270

isSizeValid = isSizeValidCallback(curSoupSize)

if isSizeValid:

matchedSoupList.append(eachSoup)

matchedSoupNum = len(matchedSoupList)

if matchNum == 0:

isFound = True

else:

if matchedSoupNum == matchNum:

isFound = True

return isFound

说明：

判断soup内，是否有符合特定条件的soup

举例：

（1）iOS的弹框，有上角带关闭按钮时，去判断一个弹框，是否符合对应条件，以便于判断是否可能是弹框

nextSiblingeSoupGenerator = possibleCloseSoup.next_siblings

nextSiblingeSoupList = list(nextSiblingeSoupGenerator)

hasLargeImage = CommonUtils.isContainSpecificSoup(nextSiblingeSoupList, "XCUIElementTypeImage", self.isPopupWindowSize)

isPossibleClose = hasLargeImage

相关函数

def isPopupWindowSize(self, curSize):

"""判断一个soup的宽高大小是否是弹框类窗口(Image,Other等）的大小"""

# global FullScreenSize

FullScreenSize = self.X * self.totalY

curSizeRatio = curSize / FullScreenSize # 0.289

PopupWindowSizeMinRatio = 0.25

# PopupWindowSizeMaxRatio = 0.9

PopupWindowSizeMaxRatio = 0.8

# isSizeValid = curSizeRatio >= MinPopupWindowSizeRatio

# is popup like window, size should large enough, but should not full screen

isSizeValid = PopupWindowSizeMinRatio <= curSizeRatio <= PopupWindowSizeMaxRatio

return isSizeValid

（2）

hasNormalButton = CommonUtils.isContainSpecificSoup(nextSiblingeSoupList, "XCUIElementTypeButton", self.isNormalButtonSize)

相关函数：

def isNormalButtonSize(self, curSize):

"""判断一个soup的宽高大小是否是普通的按钮大小"""

NormalButtonSizeMin = 30*30

NormalButtonSizeMax = 100*100

isNormalSize = NormalButtonSizeMin <= curSize <= NormalButtonSizeMax

return isNormalSize

查找元素，限定条件是符合对应的几级的父元素的条件

背景：

很多时候，需要对于iOS的app的页面的源码，即xml中，查找符合特定情况的的元素

这些特定情况，往往是parent或者前几层级的parent中，元素符合一定条件，往往是type，以及宽度是屏幕宽度，高度是屏幕高度等等

所以提取出公共函数，用于bs的find查找元素

def bsChainFind(curLevelSoup, queryChainList):

"""BeautifulSoup find with query chain

Args:

curLevelSoup (soup): BeautifulSoup

queryChainList (list): str list of all level query dict

Returns:

soup

Raises:

Examples:

input:

[

{

"tag": "XCUIElementTypeWindow",

"attrs": {"visible":"true", "enabled":"true", "width": "%s" % ScreenX, "height": "%s" % ScreenY}

},

{

"tag": "XCUIElementTypeButton",

"attrs": {"visible":"true", "enabled":"true", "width": "%s" % ScreenX, "height": "%s" % ScreenY}

},

{

"tag": "XCUIElementTypeStaticText",

"attrs": {"visible":"true", "enabled":"true", "value":"可能离开微信，打开第三方应用"}

},

]

output:

soup node of

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="可能离开微信，打开第三方应用" name="可能离开微信，打开第三方应用" label="可能离开微信，打开第三方应用" enabled="true" visible="true" x="71" y="331" width="272" height="18"/>

in :

<XCUIElementTypeWindow type="XCUIElementTypeWindow" enabled="true" visible="true" x="0" y="0" width="414" height="736">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="414" height="736">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="414" height="736">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="414" height="736">

<XCUIElementTypeButton type="XCUIElementTypeButton" enabled="true" visible="true" x="0" y="0" width="414" height="736">

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" enabled="true" visible="false" x="47" y="288" width="0" height="0"/>

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="可能离开微信，打开第三方应用" name="可能离开微信，打开第三方应用" label="可能离开微信，打开第三方应用" enabled="true" visible="true" x="71" y="331" width="272" height="18"/>

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="取消" name="取消" label="取消" enabled="true" visible="true" x="109" y="409" width="36" height="22"/>

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="继续" name="继续" label="继续" enabled="true" visible="true" x="269" y="409" width="36" height="22"/>

</XCUIElementTypeButton>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeWindow>

"""

foundSoup = None

if queryChainList:

chainListLen = len(queryChainList)

if chainListLen == 1:

# last one

curLevelFindDict = queryChainList[0]

curTag = curLevelFindDict["tag"]

curAttrs = curLevelFindDict["attrs"]

foundSoup = curLevelSoup.find(curTag, attrs=curAttrs)

else:

highestLevelFindDict = queryChainList[0]

curTag = highestLevelFindDict["tag"]

curAttrs = highestLevelFindDict["attrs"]

foundSoupList = curLevelSoup.find_all(curTag, attrs=curAttrs)

if foundSoupList:

childrenChainList = queryChainList[1:]

for eachSoup in foundSoupList:

eachSoupResult = CommonUtils.bsChainFind(eachSoup, childrenChainList)

if eachSoupResult:

foundSoup = eachSoupResult

break

return foundSoup

举例：

（1）

"""

微信-小程序 弹框 警告 尚未进行授权：

<XCUIElementTypeButton type="XCUIElementTypeButton" enabled="true" visible="true" x="0" y="0" width="375" height="667">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="32" y="240" width="311" height="187">

<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="32" y="240" width="311" height="187"/>

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="警告" name="警告" label="警告" enabled="true" visible="true" x="52" y="265" width="271" height="21"/>

<XCUIElementTypeTextView type="XCUIElementTypeTextView" value="尚未进行授权，请点击确定跳转到授权页面进行授权。" enabled="true" visible="true" x="60" y="300" width="255" height="57"/>

<XCUIElementTypeButton type="XCUIElementTypeButton" name="取消" label="取消" enabled="true" visible="true" x="32" y="376" width="156" height="51"/>

<XCUIElementTypeButton type="XCUIElementTypeButton" name="确定" label="确定" enabled="true" visible="true" x="187" y="376" width="156" height="51"/>

</XCUIElementTypeOther>

</XCUIElementTypeButton>

"""

warningChainList = [

{

"tag": "XCUIElementTypeButton",

"attrs": {"visible":"true", "enabled":"true", "width": "%s" % self.X, "height": "%s" % self.totalY}

},

{

"tag": "XCUIElementTypeOther",

"attrs": {"visible":"true", "enabled":"true"}

},

{

"tag": "XCUIElementTypeStaticText",

"attrs": {"visible":"true", "enabled":"true", "value":"警告"}

},

]

warningSoup = CommonUtils.bsChainFind(soup, warningChainList)

相关：

找到元素后，再去点击：

if warningSoup:

parentOtherSoup = warningSoup.parent

confirmSoup = parentOtherSoup.find(

"XCUIElementTypeButton",

attrs={"visible":"true", "enabled":"true", "name": "确定"}

)

if confirmSoup:

self.clickElementCenterPosition(confirmSoup)

foundAndProcessedPopup = True

（2）

"""

系统弹框 拍照或录像：

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="530" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="530" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="530" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="0" y="0" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="0" width="398" height="133">

<XCUIElementTypeTable type="XCUIElementTypeTable" enabled="true" visible="true" x="0" y="0" width="398" height="133">

<XCUIElementTypeCell type="XCUIElementTypeCell" enabled="true" visible="true" x="0" y="0" width="398" height="45">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="43" width="398" height="2"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="13" y="6" width="1" height="32"/>

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="拍照或录像" name="拍照或录像" label="拍照或录像" enabled="true" visible="true" x="15" y="11" width="87" height="22"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="351" y="6" width="32" height="32"/>

</XCUIElementTypeCell>

<XCUIElementTypeCell type="XCUIElementTypeCell" enabled="true" visible="true" x="0" y="44" width="398" height="45">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="88" width="398" height="1"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="13" y="50" width="1" height="33"/>

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="照片图库" name="照片图库" label="照片图库" enabled="true" visible="true" x="15" y="56" width="70" height="21"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="351" y="50" width="32" height="33"/>

</XCUIElementTypeCell>

<XCUIElementTypeCell type="XCUIElementTypeCell" enabled="true" visible="true" x="0" y="88" width="398" height="45">

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="0" y="132" width="398" height="1"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" visible="false" x="13" y="94" width="1" height="33"/>

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="浏览" name="浏览" label="浏览" enabled="true" visible="true" x="14" y="100" width="36" height="21"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" name="UIDocumentPicker-more" enabled="true" visible="false" x="351" y="94" width="32" height="33"/>

</XCUIElementTypeCell>

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="132" width="398" height="1"/>

</XCUIElementTypeTable>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

</XCUIElementTypeOther>

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="671" width="398" height="57">

<XCUIElementTypeButton type="XCUIElementTypeButton" name="取消" label="取消" enabled="true" visible="true" x="8" y="671" width="398" height="57"/>

</XCUIElementTypeOther>

"""

photoCameraChainList = [

{

"tag": "XCUIElementTypeOther",

"attrs": {"enabled":"true", "visible":"true"}

},

{

"tag": "XCUIElementTypeTable",

"attrs": {"enabled":"true", "visible":"true", "x":"0", "y":"0"}

},

{

"tag": "XCUIElementTypeStaticText",

"attrs": {"enabled":"true", "visible":"true", "value":"拍照或录像"}

},

]

photoCameraSoup = CommonUtils.bsChainFind(soup, photoCameraChainList)

（3）iOS 设置 无线局域网 列表页 找 当前已连接的WiFi，特征是带蓝色✅的：

"""

设置 无线局域网 列表页：

<XCUIElementTypeTable type="XCUIElementTypeTable" enabled="true" visible="true" x="0" y="0" width="414" height="736">

。。。

<XCUIElementTypeCell type="XCUIElementTypeCell" name=“xxx_guest_5G, 安全网络, 信号强度 3 格，共 3 格" label=“xxx_guest_5G, 安全网络, 信号强度 3 格，共 3 格" enabled="true" visible="true" x="0" y="144" width="414" height="43">

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value=“xxx_guest_5G" name=“xxx_guest_5G" label=“xxx_guest_5G" enabled="true" visible="true" x="40" y="155" width="278" height="21"/>

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="0" y="186" width="414" height="1"/>

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" x="8" y="151" width="28" height="29">

<XCUIElementTypeImage type="XCUIElementTypeImage" name="UIPreferencesBlueCheck" enabled="true" visible="false" x="8" y="151" width="28" height="29"/>

</XCUIElementTypeOther>

<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="false" x="15" y="187" width="245" height="1"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" name="Lock" enabled="true" visible="false" x="326" y="159" width="8" height="12"/>

<XCUIElementTypeImage type="XCUIElementTypeImage" name="WifiBars3" enabled="true" visible="false" x="346" y="153" width="16" height="25"/>

<XCUIElementTypeButton type="XCUIElementTypeButton" name="更多信息" label="更多信息" enabled="true" visible="true" x="372" y="154" width="22" height="22"/>

</XCUIElementTypeCell>

"""

curPageXml = self.get_page_source()

soup = CommonUtils.xmlToSoup(curPageXml)

blueCheckChainList = [

{

"tag": "XCUIElementTypeCell",

"attrs": {"enabled":"true", "visible":"true", "x":"0", "width":"%s" % self.X}

},

{

"tag": "XCUIElementTypeOther",

"attrs": {"enabled":"true", "visible":"true"}

},

{

"tag": "XCUIElementTypeImage",

# "attrs": {"enabled":"true", "visible":"true", "name": "UIPreferencesBlueCheck"}

"attrs": {"enabled":"true", "name": "UIPreferencesBlueCheck"}

},

]

blueCheckSoup = CommonUtils.bsChainFind(soup, blueCheckChainList)

if blueCheckSoup:

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-08-17 17:30:57

requests

下载图片，保存为二进制文件

import requests

resp = requests.get(pictureUrl)

with open(saveFullPath, 'wb') as saveFp:

saveFp.write(resp.content)

详见：

【已解决】Python的requests中如何下载二进制数据保存为图片文件

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-07 17:11:49

数学

详见：

https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanMath.py

md5

md5计算

- md5

- Python 3 中已改名 hashlib

- 且update参数只允许 bytes

- 不允许 str

的md5代码：

from hashlib import md5 # only for python 3.x

def generateMd5(strToMd5) :

"""

generate md5 string from input string

eg:

xxxxxxxx -> af0230c7fcc75b34cbb268b9bf64da79

:param strToMd5: input string

:return: md5 string of 32 chars

"""

encrptedMd5 = ""

md5Instance = md5()

# print("type(md5Instance)=%s" % type(md5Instance)) # type(md5Instance)=<class '_hashlib.HASH'>

# print("type(strToMd5)=%s" % type(strToMd5)) # type(strToMd5)=<class 'str'>

bytesToMd5 = bytes(strToMd5, "UTF-8")

# print("type(bytesToMd5)=%s" % type(bytesToMd5)) # type(bytesToMd5)=<class 'bytes'>

md5Instance.update(bytesToMd5)

encrptedMd5 = md5Instance.hexdigest()

# print("type(encrptedMd5)=%s" % type(encrptedMd5)) # type(encrptedMd5)=<class 'str'>

# print("encrptedMd5=%s" % encrptedMd5) # encrptedMd5=3a821616bec2e86e3e232d0c7f392cf5

return encrptedMd5

之前旧版本的 Python 2 （ <= 2.7 ）版本：

- md5还是个独立模块

- 还没有并入 hashlib

- 注：

- 好像 python 2.7 中已将md5并入 hashlib

- 但是 update 参数还允许 str （而不是 bytes ）

- update参数允许str

的md5代码：

try:

import md5

except ImportError:

from hashlib import md5

def generateMd5(strToMd5) :

encrptedMd5 = ""

md5Instance = md5.new()

#md5Instance=<md5 HASH object @ 0x1062af738>

md5Instance.update(strToMd5)

encrptedMd5 = md5Instance.hexdigest()

#encrptedMd5=af0230c7fcc75b34cbb268b9bf64da79

return encrptedMd5

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 09:49:42

邮件

发送邮件

函数：

def sendEmail(  sender, senderPassword, receiverList,

senderName="", receiverNameList= "",

smtpServer = "", smtpPort = None, useSSL=False,

type = "plain", title = "", body = ""):

"""

send email

:param sender:

:param senderPassword:

:param receiverList:

:param senderName:

:param receiverNameList:

:param smtpServer:

:param smtpPort:

:param type: html/plain

:param title:

:param body:

:return:

"""

logging.debug("sender=%s, senderName=%s, smtpServer=%s, smtpPort=%s, useSSL=%s, type=%s, title=%s, body=%s",

sender, senderName, smtpServer, smtpPort, useSSL, type, title, body)

logging.debug("receiverList=%s, receiverNameList=%s", receiverList, receiverNameList)

defaultPort = None

SMTP_PORT_NO_SSL = 25

SMTP_PORT_SSL = 465

if useSSL:

defaultPort = SMTP_PORT_SSL

else:

defaultPort = SMTP_PORT_NO_SSL

if not smtpPort:

smtpPort = defaultPort

# init smtp server if necessary

if not smtpServer:

# extract domain from sender email

# crifan2003@163.com -> 163.com

atIdx = sender.index('@')

afterAtIdx = atIdx + 1

lastDomain = sender[afterAtIdx:]

smtpServer = 'smtp.' + lastDomain

# smtpServer = "smtp.163.com"

# smtpPort = 25

# RECEIVER_SEPERATOR = '; '

RECEIVER_SEPERATOR = ', '

senderNameAddr = "%s <%s>" % (senderName, sender)

receiversAddr = RECEIVER_SEPERATOR.join(receiverList)

receiverNameAddrList = []

formatedReceiverNameAddrList = []

for curIdx, eachReceiver in enumerate(receiverList):

eachReceiverName = receiverNameList[curIdx]

eachNameAddr = "%s <%s>" % (eachReceiverName, eachReceiver)

eachFormatedNameAddr = formatEmailNameAddrHeader(eachNameAddr)

receiverNameAddrList.append(eachNameAddr)

formatedReceiverNameAddrList.append(eachFormatedNameAddr)

formatedReceiversNameAddr = RECEIVER_SEPERATOR.join(formatedReceiverNameAddrList) # '=?utf-8?b?57u/6Imy5Z6D5Zy+?= <green-waste@163.com>, =?utf-8?b?5YWL55Ge6Iqs?= <admin@crifan.com>'

mergedReceiversNameAddr = RECEIVER_SEPERATOR.join(receiverNameAddrList) # u'Crifan2003 <crifan2003@163.com>, 克瑞芬 <admin@crifan.com>'

# formatedReceiversNameAddr = formatEmailHeader(mergedReceiversNameAddr) #=?utf-8?b?Q3JpZmFuMjAwMyA8Y3JpZmFuMjAwM0AxNjMuY29tPiwg5YWL55Ge6IqsIDxh?=

# =?utf-8?q?dmin=40crifan=2Ecom=3E?=

msg = MIMEText(body, _subtype=type, _charset="utf-8")

# msg["From"] = _format_addr(senderNameAddr)

# msg["To"] = _format_addr(receiversNameAddr)

msg["From"] = formatEmailHeader(senderNameAddr)

# msg["From"] = senderNameAddr

# msg["To"] = formatEmailHeader(formatedReceiversNameAddr)

# msg["To"] = formatedReceiversNameAddr

# msg["To"] = mergedReceiversNameAddr

# msg["To"] = formatEmailHeader(receiversAddr)

msg["To"] = formatEmailHeader(mergedReceiversNameAddr)

# titleHeader = Header(title, "utf-8")

# encodedTitleHeader = titleHeader.encode()

# msg['Subject'] = encodedTitleHeader

msg['Subject'] = formatEmailHeader(title)

# msg['Subject'] = title

msgStr = msg.as_string()

# try:

# smtpObj = smtplib.SMTP('localhost')

smtpObj = None

if useSSL:

smtpObj = smtplib.SMTP_SSL(smtpServer, smtpPort)

else:

smtpObj = smtplib.SMTP(smtpServer, smtpPort)

# start TLS for security

# smtpObj.starttls()

# smtpObj.set_debuglevel(1)

smtpObj.login(sender, senderPassword)

# smtpObj.sendmail(sender, receiversAddr, msgStr)

smtpObj.sendmail(sender, receiverList, msgStr)

logging.info("Successfully sent email: message=%s", msgStr)

# except smtplib.SMTPException:

#     logging.error("Fail to sent email: message=%s", message)

return

调用：

productName = "First 163 then crifan. Dell XPS 13 XPS9360-5797SLV-PUS Laptop"

productUrl = "https://www.microsoft.com/en-us/store/d/dell-xps-13-xps-9360-laptop-pc/8q17384grz37/GV5D?activetab=pivot%253aoverviewtab"

notifType = "HighPrice"

title = "[%s] %s" % (notifType, productName)

notifContent = """

<html>

<body>

<h1>%s</h1>

<p>Not buy <a href="%s">%s</a> for current price <b>$699.00</b> &gt; expected price <b>$599.00</b></p>

<p>So save for later process</p>

</body>

</html>

""" % (title, productUrl, productName)

receiversDictList = gCfg["notification"]["receivers"]

receiverList = []

receiverNameList = []

for eachReceiverDict in receiversDictList:

receiverList.append(eachReceiverDict["email"])

receiverNameList.append(eachReceiverDict["username"])

sendEmail(

sender = gCfg["notification"]["sender"]["email"],

senderPassword = gCfg["notification"]["sender"]["password"],

receiverList = receiverList,

senderName = gCfg["notification"]["sender"]["username"],

receiverNameList= receiverNameList,

type = "html",

title = title,

body = notifContent

)

附录：

- 最新代码详见：

- https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanEmail.py

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-04-13 20:17:10

csv和Excel

用Python操作 csv 和 excel ，详见独立教程：

Python表格处理：CSV和Excel

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-04-13 20:16:53

常见语法

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-26 11:41:52

函数参数

可变参数

之前一个用到了可变参数的函数是：

def multipleRetry(self, functionInfoDict, maxRetryNum=5, sleepInterval=0.1):

"""

do something, retry mutiple time if fail

Args:

functionInfoDict (dict): function info dict contain functionCallback and [optional] functionParaDict

maxRetryNum (int): max retry number

sleepInterval (float): sleep time of each interval when fail

Returns:

bool

Raises:

"""

doSuccess = False

functionCallback = functionInfoDict["functionCallback"]

functionParaDict = functionInfoDict.get("functionParaDict", None)

curRetryNum = maxRetryNum

while curRetryNum > 0:

if functionParaDict:

doSuccess = functionCallback(**functionParaDict)

else:

doSuccess = functionCallback()

if doSuccess:

break

time.sleep(sleepInterval)

curRetryNum -= 1

if not doSuccess:

functionName = str(functionCallback)

# '<bound method DevicesMethods.switchToAppStoreSearchTab of <src.AppCrawler.AppCrawler object at 0x1053abe80>>'

logging.error("Still fail after %d retry for %s", functionName)

return doSuccess

其中的：

functionCallback(**functionParaDict)

中的：

**functionParaDict

表示，dict类型的参数，内部包含多个key和value，用**去展开后，传入真正要执行的函数

几种调用中带参数的例子是：

searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"}

isInputOk = self.multipleRetry(

{

"functionCallback": self.wait_element_setText,

"functionParaDict": {

"locator": searchInputQuery,

"text": appName,

}

}

)

之前原始写法：

searchInputQuery = {"type":"XCUIElementTypeSearchField", "name":"App Store"}

isInputOk = self.wait_element_setText(searchInputQuery, appName)

其中wait_element_setText的定义是：

def wait_element_setText(self, locator, text):

对应着之前传入时的：

"functionParaDict": {

"locator": searchInputQuery,

"text": appName,

}

即可，给出上述细节，便于理解，传入的参数是如何用 ** 展开的。

详见：

【已解决】Python中如何实现函数调用时多个可变数量的参数传递

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-26 11:44:46

dict字典

删除dict中某个键（和值）

- 常见写法：

del yourDict["keyToDelete"]

- 更加Pythonic的写法：

yourDict.pop("keyToDelete")

注意：

为了防止出现 KeyError ，注意确保要删除的key都是存在的，否则就要先判断存在，再去删除。

OrderedDict

想要获取OrderedDict的最后一个item（的key和value）

next(reversed(someOrderedDict.items()))

另外，只需要获取最后一个元素的key，则可以：

next(reversed(someOrderedDict.keys()))

或：

next(reversed(someOrderedDict))

详见：

【已解决】Python中获取OrderedDict中最后一个元素

合并2个dict的值

（1）如果无需保留原有（第一个dict）的值，则用update即可：

firstDict.update(secondDict)

支持： Python >=3.5

（2）如果要保留之前的dict的值，则用**展开

thirdDict = (**firstDict, **secondDict)

支持： Python 2 和  Python <=3.4

详见：

【已解决】Python中如何合并2个dict字典变量的值

有序字典OrderedDict的初始化

from collections import OrderedDict

orderedDict = OrderedDict()

后续正常作为普通dict使用

>>> from collections import OrderedDict

>>> orderedDict = OrderedDict()

>>> orderedDict["key2"] = "value2"

>>> orderedDict["key1"] = "value1"

>>> orderedDict["key3"] = "value3"

>>> orderedDict

OrderedDict([('key2', 'value2'), ('key1', 'value1'), ('key3', 'value3')])

dict的递归的合并更新

def recursiveMergeDict(aDict, bDict):

"""

Recursively merge dict a to b, return merged dict b

Note: Sub dict and sub list's won't be overwritten but also updated/merged

example:

(1) input and output example:

input:

{

"keyStr": "strValueA",

"keyInt": 1,

"keyBool": true,

"keyList": [

{

"index0Item1": "index0Item1",

"index0Item2": "index0Item2"

},

{

"index1Item1": "index1Item1"

},

{

"index2Item1": "index2Item1"

}

]

}

and

{

"keyStr": "strValueB",

"keyInt": 2,

"keyList": [

{

"index0Item1": "index0Item1_b"

},

{

"index1Item1": "index1Item1_b"

}

]

}

output:

{

"keyStr": "strValueB",

"keyBool": true,

"keyInt": 2,

"keyList": [

{

"index0Item1": "index0Item1_b",

"index0Item2": "index0Item2"

},

{

"index1Item1": "index1Item1_b"

},

{

"index2Item1": "index2Item1"

}

]

}

(2) code usage example:

import copy

cDict = recursiveMergeDict(aDict, copy.deepcopy(bDict))

Note:

bDict should use deepcopy, otherwise will be altered after call this function !!!

"""

aDictItems = None

if (sys.version_info[0] == 2): # is python 2

aDictItems = aDict.iteritems()

else: # is python 3

aDictItems = aDict.items()

for aKey, aValue in aDictItems:

# print("------ [%s]=%s" % (aKey, aValue))

if aKey not in bDict:

bDict[aKey] = aValue

else:

bValue = bDict[aKey]

# print("aValue=%s" % aValue)

# print("bValue=%s" % bValue)

if isinstance(aValue, dict):

recursiveMergeDict(aValue, bValue)

elif isinstance(aValue, list):

aValueListLen = len(aValue)

bValueListLen = len(bValue)

bValueListMaxIdx = bValueListLen - 1

for aListIdx in range(aValueListLen):

# print("---[%d]" % aListIdx)

aListItem = aValue[aListIdx]

# print("aListItem=%s" % aListItem)

if aListIdx <= bValueListMaxIdx:

bListItem = bValue[aListIdx]

# print("bListItem=%s" % bListItem)

recursiveMergeDict(aListItem, bListItem)

else:

# print("bDict=%s" % bDict)

# print("aKey=%s" % aKey)

# print("aListItem=%s" % aListItem)

bDict[aKey].append(aListItem)

return bDict

调用举例：

templateJson = {

"author": "Crifan Li <admin@crifan.com>",

"description": "gitbook书的描述",

"gitbook": "3.2.3",

"language": "zh-hans",

"links": { "sidebar": { "主页": "http://www.crifan.com" } },

"plugins": [

"theme-comscore",

"anchors",

"-lunr",

"-search",

"search-plus",

"disqus",

"-highlight",

"prism",

"prism-themes",

"github-buttons",

"splitter",

"-sharing",

"sharing-plus",

"tbfed-pagefooter",

"expandable-chapters-small",

"ga",

"donate",

"sitemap-general",

"copy-code-button",

"callouts",

"toolbar-button"

],

"pluginsConfig": {

"callouts": { "showTypeInHeader": false },

"disqus": { "shortName": "crifan" },

"donate": {

"alipay": "https://www.crifan.com/files/res/crifan_com/crifan_alipay_pay.jpg",

"alipayText": "支付宝打赏给Crifan",

"button": "打赏",

"title": "",

"wechat": "https://www.crifan.com/files/res/crifan_com/crifan_wechat_pay.jpg",

"wechatText": "微信打赏给Crifan"

},

"ga": { "token": "UA-28297199-1" },

"github-buttons": {

"buttons": [

{

"count": true,

"repo": "gitbook_name",

"size": "small",

"type": "star",

"user": "crifan"

},

{

"count": false,

"size": "small",

"type": "follow",

"user": "crifan",

"width": "120"

}

]

},

"prism": { "css": ["prism-themes/themes/prism-atom-dark.css"] },

"sharing": {

"all": [

"douban",

"facebook",

"google",

"instapaper",

"line",

"linkedin",

"messenger",

"pocket",

"qq",

"qzone",

"stumbleupon",

"twitter",

"viber",

"vk",

"weibo",

"whatsapp"

],

"douban": false,

"facebook": true,

"google": false,

"hatenaBookmark": false,

"instapaper": false,

"line": false,

"linkedin": false,

"messenger": false,

"pocket": false,

"qq": true,

"qzone": false,

"stumbleupon": false,

"twitter": true,

"viber": false,

"vk": false,

"weibo": true,

"whatsapp": false

},

"sitemap-general": {

"prefix": "https://book.crifan.com/gitbook/gitbook_name/website/"

},

"tbfed-pagefooter": {

"copyright": "crifan.com，使用<a \"href=\"https://creativecommons.org/licenses/by/4.0/deed.zh\">署名4.0国际(CC \"BY 4.0)协议</a>发布",

"modify_format": "YYYY-MM-DD HH:mm:ss",

"modify_label": "最后更新："

},

"theme-default": { "showLevel": true },

"toolbar-button": {

"icon": "fa-file-pdf-o",

"label": "下载PDF",

"url": "http://book.crifan.com/books/gitbook_name/pdf/gitbook_name.pdf"

}

},

"root": "./src",

"title": "Gitbook的书名"

}

currentJson = {

"description": "crifan整理的Python各个方面常用的代码段，供需要的参考。",

"pluginsConfig": {

"github-buttons": { "buttons": [{ "repo": "python_common_code_snippet" }] },

"sitemap-general": {

"prefix": "https://book.crifan.com/gitbook/python_common_code_snippet/website/"

},

"toolbar-button": {

"url": "http://book.crifan.com/books/python_common_code_snippet/pdf/python_common_code_snippet.pdf"

}

},

"title": "Python常用代码段"

}

bookJson = recursiveMergeDict(templateJson, copy.deepcopy(currentJson))

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-07-11 10:37:49

list列表和set集合

list vs set

- set

- 适用于检测某元素是否在集合内、对集合进行一定的数学操作

- 不支持indexing，slicing

- list

- 普通的数组

- 支持indexing，slicing

把list换成set

someSet = set([])

for eachItem in someList:

someSet.add(eachItem)

set集合转换为字符串

someSetStr = ", ".join(someSet)

把列表转为python正则中的group中可能出现的选项

def listToPatternGroup(curList):

"""Convert list to pattern group"""

patternGroupList = list(map(lambda curType: "(%s)" % curType, curList)) # ['(aaa)', '(bbb)', '(ccc)', '(zzz)', '(eee)', '(yyy)', '(ddd)', '(xxx)']

groupP = "|".join(patternGroupList) # '(aaa)|(bbb)|(ccc)|(zzz)|(eee)|(yyy)|(ddd)|(xxx)'

return groupP

调用：

ValidPlatformTypeList = ["iOS", "Android"]

ValidPlatformRule = listToPatternGroup(ValidPlatformTypeList) # '(iOS)|(Android)'

目的是用于后续的正则判断

TaskFilenamePattern = "(?P<taskDate>\d+)_(?P<businessType>%s)_(?P<taskName>[a-zA-Z\d]+)_(?P<crawlerType>%s)(_(?P<platform>%s))?" % (ValidBusinessTypeRule, ValidCrawlerTypeRule, ValidPlatformRule)

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-12-31 17:47:33

sort排序

详见：

- https://github.com/crifan/crifanLibPython/blob/master/crifanLib/crifanDict.py

- https://github.com/crifan/crifanLibPython/blob/master/crifanLib/demo/crifanDictDemo.py

对字典根据key去排序

from collections import OrderedDict

def sortDictByKey(originDict):

"""

Sort dict by key

"""

originItems = originDict.items()

sortedOriginItems = sorted(originItems)

sortedOrderedDict = OrderedDict(sortedOriginItems)

return sortedOrderedDict

调用：

def demoSortDictByKey():

originDict = {

"c": "abc",

"a": 1,

"b": 22

}

print("originDict=%s" % originDict)

# originDict={'c': 'abc', 'a': 1, 'b': 22}

sortedOrderedDict = sortDictByKey(originDict)

print("sortedOrderedDict=%s" % sortedOrderedDict)

# sortedOrderedDict=OrderedDict([('a', 1), ('b', 22), ('c', 'abc')])

sort和sorted

# Function: Demo sorted

#   mainly refer official doc:

#       排序指南 — Python 3.8.2 文档

#       https://docs.python.org/zh-cn/3/howto/sorting.html

# Author: Crifan Li

# Update: 20200304

from operator import itemgetter, attrgetter

print("%s %s %s" % ('='*40, "sort", '='*40))

originIntList = [5, 2, 3, 1, 4]

originIntList.sort()

sortedSelfIntList = originIntList

print("sortedSelfIntList=%s" % sortedSelfIntList)

# sortedSelfIntList=[1, 2, 3, 4, 5]

print("%s %s %s" % ('='*40, "sorted", '='*40))

intList = [5, 2, 3, 1, 4]

sortedIntList = sorted(intList)

print("sortedIntList=%s" % sortedIntList)

# sortedIntList=[1, 2, 3, 4, 5]

reversedSortIntList = sorted(intList, reverse=True)

print("reversedSortIntList=%s" % reversedSortIntList)

# reversedSortIntList=[5, 4, 3, 2, 1]

intStrDict = {5: 'A', 1: 'D', 2: 'B',  4: 'E', 3: 'B'}

dictSortedIntList = sorted(intStrDict)

print("dictSortedIntList=%s" % dictSortedIntList)

# dictSortedIntList=[1, 2, 3, 4, 5]

normalStr = "Crifan Li best love language is Python"

strList = normalStr.split()

print("strList=%s" % strList)

sortedStrList = sorted(strList, key=str.lower)

print("sortedStrList=%s" % sortedStrList)

# strList=['Crifan', 'Li', 'best', 'love', 'language', 'is', 'Python']

# sortedStrList=['best', 'Crifan', 'is', 'language', 'Li', 'love', 'Python']

studentTupleList = [

# name, grade, age

('Cindy', 'A', 15),

('Crifan', 'B', 12),

('Tony', 'B', 10),

]

sortedTupleList_lambda = sorted(studentTupleList, key=lambda student: student[2]) # [2] is age

print("sortedTupleList_lambda=%s" % sortedTupleList_lambda)

# sortedTupleList_lambda=[('Tony', 'B', 10), ('Crifan', 'B', 12), ('Cindy', 'A', 15)]

# same as single function:

def getStudentAge(curStudentTuple):

return curStudentTuple[2] # [2] is age

sortedTupleList_singleFunction = sorted(studentTupleList, key=getStudentAge)

print("sortedTupleList_singleFunction=%s" % sortedTupleList_singleFunction)

# sortedTupleList_singleFunction=[('Tony', 'B', 10), ('Crifan', 'B', 12), ('Cindy', 'A', 15)]

# same as operator itemgetter:

sortedTupleList_operator = sorted(studentTupleList, key=itemgetter(2))

print("sortedTupleList_operator=%s" % sortedTupleList_operator)

# sortedTupleList_operator=[('Tony', 'B', 10), ('Crifan', 'B', 12), ('Cindy', 'A', 15)]

class Student:

def __init__(self, name, grade, age):

self.name = name

self.grade = grade

self.age = age

def __repr__(self):

return repr((self.name, self.grade, self.age))

studentObjectList = [

Student('john', 'A', 15),

Student('jane', 'A', 15),

Student('dave', 'A', 15),

]

sortedObjectList = sorted(studentObjectList, key=lambda student: student.age)

print("sortedObjectList=%s" % sortedObjectList)

# sortedObjectList=[('john', 'A', 15), ('jane', 'A', 15), ('dave', 'A', 15)]

# same as operator attrgetter:

sortedObjectList_operator = sorted(sortedObjectList, key=attrgetter('age'))

print("sortedObjectList_operator=%s" % sortedObjectList_operator)

# sortedObjectList_operator=[('john', 'A', 15), ('jane', 'A', 15), ('dave', 'A', 15)]

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-27 13:17:24

enum枚举

枚举基本用法

枚举定义

举例1：

from enum import Enum

class BatteryState(Enum):

Unknown = 0

Unplugged = 1

Charging = 2

Full = 3

举例2：

import enum

class ScreenshotQuality(enum.Enum):

Original = 0

Medium = 1

Low = 2

举例3：

class SentenceInvalidReason(Enum):

NONE = "none"

UNKNOWN = "unknown"

EMPTY = "empty

TOO_SHORT = "too short"

TOO_LONG = "too long"

TOO_MANY_INVALID_WORD = "contain too many invalid words"

初始化创建枚举值

直接传入对应的（此处是int）值即可：

batteryStateInt = 2

curBattryStateEnum = BatteryState(batteryStateInt)

log输出是：

curBattryStateEnum=BatteryState.Charging

获取枚举的名称

curBattryStateName = curBattryStateEnum.name

输出： 'Charging'

获取枚举的值

curBattryStateValue = curBattryStateEnum.value

输出： 2

类似，直接从定义中获取值：

gScreenQuality = ScreenshotQuality.Low.value # 2

枚举高级用法

给枚举中添加函数

class TipType(enum.Enum):

NoTip = "NoTip"

TenPercent = "TenPercent"

FifthPercent = "FifthPercent"

TwentyPercent = "TwentyPercent"

# @property

def getTipPercent(self):

tipPercent = 0.0

if self == TipType.NoTip:

tipPercent = 0.0

elif self == TipType.TenPercent:

tipPercent = 0.10

elif self == TipType.FifthPercent:

tipPercent = 0.15

elif self == TipType.TwentyPercent:

tipPercent = 0.20

gLog.debug("self=%s -> tipPercent=%s", self, tipPercent)

return tipPercent

调用：

tipPercent = initiatorTipType.getTipPercent()

# tipPercent=0.1

注意事项

字符串枚举定义最后不要加逗号

enum定义期间不要加（多余的）逗号：

class ScreenshotQuality(enum.Enum):

Original = 0,

Medium = 1,

Low = 2,

否则 value 就是 tuple 元祖了：

gScreenQuality = ScreenshotQuality.Low.value # 实际上是 (2,)

print("gScreenQuality=%s" % gScreenQuality) # gScreenQuality=2

print("type(gScreenQuality)=%s" % type(gScreenQuality)) # type(gScreenQuality)=<class 'tuple'>

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-27 12:56:53

collections集合

根据官网：

collections --- 容器数据类型 — Python 3.8.1 文档

介绍，集合有很多种，列出供了解：

- namedtuple() ：创建命名元组子类的工厂函数

- deque ：类似列表(list)的容器，实现了在两端快速添加(append)和弹出(pop)

- ChainMap ：类似字典(dict)的容器类，将多个映射集合到一个视图里面

- Counter ：字典的子类，提供了可哈希对象的计数功能

- OrderedDict ：字典的子类，保存了他们被添加的顺序

- defaultdict ：字典的子类，提供了一个工厂函数，为字典查询提供一个默认值

- UserDict ：封装了字典对象，简化了字典子类化

- UserList ：封装了列表对象，简化了列表子类化

- UserString ：封装了列表对象，简化了字符串子类化

待以后用到了，再详细总结。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-06-27 13:07:42

logging日志

彩色日志+日志初始化

自己的库：crifanLogging.py

已实现常用的功能，包括：

- 彩色日志

- 初始化

使用方式 = 典型调用代码：

先下载我的库：

- crifanLogging.py

- https://github.com/crifan/crifanLibPython/blob/master/python3/crifanLib/crifanLogging.py

对于文件： somePythonFile.py

调用和初始化代码：

import crifanLogging

CurFilePath = os.path.abspath(__file__)

# print("CurFilePath=%s" % CurFilePath)

CurFilename = os.path.basename(CurFilePath)

# 'autoSearchGame_YingYongBao.py'

CurFileNoSuffix, pointSuffix = os.path.splitext(CurFilename)

CurFolder = os.path.dirname(CurFilePath)

# print("CurFolder=%s" % CurFolder)

LogFolder = os.path.join(CurFolder, "logs")

def initLog():

curDatetimeStr = utils.getCurDatetimeStr() # '20200316_155954'

utils.createFolder(LogFolder)

curLogFile = "%s_%s.log" % (CurFileNoSuffix, curDatetimeStr)

logFullPath = os.path.join(LogFolder, curLogFile)

crifanLogging.loggingInit(logFullPath)

def main():

initLog()

即可生成log文件： logs/somePythonFile.log

注：相关函数：

- createFolder

- 新建文件夹

- getCurDatetimeStr

- getCurDatetimeStr 生成当前日期时间字符串

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-04-13 20:17:15

附录

下面列出相关参考资料。

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2020-03-17 09:11:34

参考资料

- 【已解决】AppStore自动安装iOS的app：逻辑优化加等待和多试几次

- 【已解决】Python中如何实现函数调用时多个可变数量的参数传递

- 【已解决】Python中如何合并2个dict字典变量的值

- 【已解决】Python中给Mac中文件加上可执行权限

- 【已解决】Python如何从二进制数据中生成Pillow的Image

- 【已解决】Python的Pillow如何从二进制数据中读取图像数据

- 【已解决】Python的requests中如何下载二进制数据保存为图片文件

- 【已解决】Python中用Pillow去缩小分辨率以及保持画质同时最大程度压缩图片

- 【已解决】Python中实现二进制数据的图片的压缩

- 【已解决】Python中如何解析mp3等音频文件得到时长信息

- 【已解决】用Python代码从视频中提取出音频mp3文件

- 【已解决】Python 3中通过二进制生成文件类型对象

- 【已解决】用ffmpeg从mp4视频中提取出整个mp3以及根据时间段去分割mp3

- 【已解决】python中从文件名后缀推断出MIME类型

- 【已解决】Python中从int值生成Enum枚举和获取枚举值的字符串或名字

- 【未解决】python的wda中调整appium的settings参数实现功能优化

- 【已解决】Python中实现类似touch创建一个空文件

- 【已解决】Python中根据key去对字典排序

- 【已解决】Python 3中如何把字符串str转换成字节码bytes

- 【已解决】Python的md5运行出错：发生异常AttributeError builtin_function_or_method object has no attribute new

- 【已解决】Python 3中md5报错：Unicode-objects must be encoded before hashing

- 【已解决】Python 3中判断变量类型

- 【已解决】Python中删除字典dict中的键值

- 【已解决】Python中获取文件最后更新时间

- 【已解决】Python中获取OrderedDict中最后一个元素

- 【记录】python中smtp发送gmail邮箱

- 【已解决】Python中smtp如何发送多个收件人地址且带名字的且可以被格式化

- 【已解决】python中判断单个或多个单词是否是全部小写或首字母小写

- 【已解决】Python中如何让Enum的字符串输出字段的值而不带类型的前缀 – 在路上

- 【已解决】Python中给枚举添加内置函数或属性

- 【基本解决】Python中把wma、wav等格式音频转换为mp3 – 在路上

- 【已解决】Python中如何格式化大小为人类易读的效果

- 【已解决】Python中获取带毫秒的时间戳

- 【已解决】Python中实现dict的递归的合并更新

- 【已解决】Python中把list换成set

- 【整理】python中一次性创建多级文件夹，判断一个文件夹是否已经存在 – 在路上

- 【已解决】Python中如何递归的删除整个非空文件夹 – 在路上

- Python表格处理：CSV和Excel

- python - Correct way to write line to file? - Stack Overflow

- How do you do a simple "chmod +x" from within python? - Stack Overflow

- io.BytesIO.getvalue

- 排序指南 — Python 3.8.2 文档

- Python 常用指引 — Python 3.8.2 文档

- Built-in Functions — Python 3.8.2 documentation

- operator — Standard operators as functions — Python 3.8.2 documentation

- Python 常用指引 — Python 3.8.2 文档

- 3.8.2 Documentation

- 编程常见问题 — Python 3.8.2 文档

- 术语对照表 — Python 3.8.2 文档

- enum — Support for enumerations — Python 3.8.2 documentation

- enum --- 对枚举的支持 — Python 3.9.0a4 文档

- collections --- 容器数据类型 — Python 3.8.1 文档

- md5 not support in python 3.6 and django 1.10 - Stack Overflow

- list - Python: how to join entries in a set into one string? - Stack Overflow

crifan.com，使用署名4.0国际(CC BY 4.0)协议发布 all right reserved，powered by Gitbook最后更新： 2021-04-13 20:16:49

Table of Contents

前言

背景

常用代码段

通用逻辑

变量

系统

日期时间

字符和字符串

文件系统

文件

文件夹

多媒体

图像

音频

视频

网络相关

BeautifulSoup

requests

数学

邮件

csv和Excel

常见语法

函数参数

dict字典

list列表和set集合

sort排序

enum枚举

collections集合

logging日志

附录

参考资料