目录

前言 1.1

背景 1.2

常用代码段 1.3

通用逻辑 1.3.1

变量 1.3.2

系统 1.3.3

日期时间 1.3.4

字符和字符串 1.3.5

文件系统 1.3.6

文件 1.3.6.1

文件夹 1.3.6.2

多媒体 1.3.7

图像 1.3.7.1

Pillow 1.3.7.1.1

百度OCR 1.3.7.1.2

音频 1.3.7.2

视频 1.3.7.3

网络相关 1.3.8

BeautifulSoup 1.3.8.1

requests 1.3.8.2

数学 1.3.9

邮件 1.3.10

csv和Excel 1.3.11

常见语法 1.4

函数参数 1.4.1

dict字典 1.4.2

list列表和set集合 1.4.3

sort排序 1.4.4

enum枚举 1.4.5

collections集合 1.4.6

logging日志 1.4.7

附录 1.5

参考资料 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

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

百度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

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

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

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字典

删除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列表和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排序

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枚举

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集合

根据官网：

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日志

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