Category Archive : python相关

API测试:通过faker生成测试数据,通过schema检查返回结果

需求

假定有如主图相同的http请求。我们一般的做法是,用postman去抓取http请求,然后修改request的body或者header里的数据,点击send按钮,检查返回的response的body是否正确。
对于输入。一般来说,我们会纯手工,或者半自动的,设计测试用例。例如使用边界值分析,等价类划分等方法,用在我们的输入参数中。比如我参数中的configname最多200个参数,我测试输入201个参数。
对于输出。一般来说,我们大部分时候是肉眼检查,或者写代码,通过jsonpath取参数,然后判断是否存在来检查。
这里我打算用一个新的方法来降低测试的手工特性,让他更自动化一点。以下想法还处于调试阶段,用于大规模使用,暂时不行。

设计

输入修改方案:引入faker库和jsonschema库。通过这两个库,我们可以产生随机的json串
faker是我无意之间发现的,能按照规律产生随机字的库,例如

fake.name()

是产生一个随机的名字,只要加入适当的providers,就能按照需要的规则产生随机字
jsonschema这个用的人很多,这里就不介绍了,下面推荐一个网站,能把json请求转换为schema格式
https://jsonschema.net/
schema中会注明每个字段的规则,例如是string类型还是integer。
输出修改方案:使用jsonschma的validate方法来检查(这种检查方法目前有一些检查不充分,但是已经可以让测试人员减少一些工作量了)

jsonschema.validate(response, schema)

使用方案

1.去postman抓取http请求,并且记录下所需要的输入json和输出json

2.打开https://jsonschema.net/ 把输入json和输入json 转换成jsonschema

3.把输入jsonschema文件,输出文件jsonschema放入相应的目录,自己写一个用于生成随机requestbody的provider和一个测试用的主函数

4.运行测试主入口文件,打印一下发送的json文件,看是不是随机化了,结果是确实随机化了。

代码

测试主入口test_json_from_schema.py

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import json
import faker
import jsonschema
import requests
from jsonschema.exceptions import ValidationError
import jsonprovider
def generate_request(request_json_schema):
    '''
    通过schema生成随机测试数据
    :param request_json_schema:
    :return:
    '''
    fake = faker.Faker()
    fake.add_provider(jsonprovider.JSONProvider)
    request_body = fake.json(json.load(open(request_json_schema)))
    print(request_body)
    return request_body
def check_json_schema(response, schema):
    '''
    通过json_schema检查返回的json串
    :param response:
    :param schema:
    :return:
    '''
    result = True
    try:
        jsonschema.validate(response, schema)
    except ValidationError, e:
        print("fail")
        result = False
    return result
if __name__ == '__main__':
    # 生成request body
    body = generate_request("schema_file/create_config_request_schemas.json")
    # 使用request库发送post请求
    url = "https://dev.honcloud.honeywell.com.cn:8080/dashboard/clustercentre/configmng/newconfig/addconfig"
    headers = {"Content-Type": "application/json", "authorization": "48a5eb61-914e-4b3a-a7a3-0b25f72d06d7"}
    response = requests.post(url, data=body, headers=headers)
    print(response.json())
    response_json=response.json()
    response_schema="schema_file/create_config_response_schemas.json"
    # 用生成的response的schema来检查
    result=check_json_schema(response_json,response_schema)
    print(result)

jsonprovider.py可以自行百度一个faker的provider的方案,我这里做的也不好,随机出来的值只遵循了字符类型,后面会考虑融合我们的边界值分析,等价类划分的方案进来,完善这个jsonprovider.py之后再放出来

python读取excel内容再转变成html添加到outlook中

需求

读取excel里的表格里的内容,然后打开本机的outlook。把excel里的内容添加到正文里,注意。这里是要添加到正文!正文!正文!而不是添加到附件里

设计思路

1.excel处理

打开excel的方法有很多,但是在不知道excel里,行和列的大小的情况下,就能获得excel里的非空值行列的办法不多。我这边采用的是xlwings这个库,用的方法是range.current_region这个方法。这个方法会选择当前range下,有值的区域(非空区域)
通过配置options选项,可以指定excel获得的值的格式,int或者string,或者空值返回N/A

2.打开outlook

打开outlook在windows上只能用win32模块了,通过下面方法可以打开outlook并且创建一个空的email

olook = win32com.client.Dispatch("Outlook.Application")
mail = olook.CreateItem(0)

然后配置邮件的htmlbody和outlook邮件悬停(可以手动更改邮件内容,手动发送),可以用以下方法

mail.HTMLBody = body_html
mail.Display(True)

完整代码

下面是完整代码,读者修改一下excel的路径,就可以自己去试试啦

# -*- coding: UTF-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import win32com.client
import xlwings
def get_excel_date(filename):
    '''
    获得excel里的所有内容,返回list
    :param filename:  excel路径
    :return: list[list[]]
    '''
    app = xlwings.App(visible=False, add_book=True)
    app.display_alerts = False
    app.screen_updating = False
    wb = app.books.open(filename)
    sht = wb.sheets[0]
    rng = sht.range('A1')
    # 把excel里的数据读取成 年-月-日 时:分:秒的格式
    my_date_handler = lambda year, month, day, hour, minute, second, **kwargs: "%04i-%02i-%02i %02i:%02i:%02i" % (
    year, month, day, hour, minute, second)
    # 取出所有内容,这里用ig这个变量,是为了庆祝I.G获得LOL S8赛季总冠军
    ig = rng.current_region.options(index=False, numbers=int, empty='N/A', dates=my_date_handler)
    result = ig.value
    wb.close()
    app.quit()
    return result
if __name__ == '__main__':
    olook = win32com.client.Dispatch("Outlook.Application")
    mail = olook.CreateItem(0)
    mail.Recipients.Add("357244849@qq.com")
    mail.Subject = "test report"
    body_html = ""
    body_html = body_html + '<body>Hi all:<br/>以下是XXXXX项目今天的测试情况:<br/><br/>明天的测试计划:<br/><br/>目前的bug:'
    body_html = body_html + '<table width="1" border="1" cellspacing="1" cellpadding="1" height="100">'
    # 这里用rng 是因为这一次rng止步8强!
    rng_list = get_excel_date("C:\lzw_programming\resource\reports\CurrentVersionAllDefectTable.xlsx")
    # 表头
    for tr_list in rng_list[:1]:
        body_html = body_html + "<tr>"
        for td_list in tr_list:
            # 这里也是奇葩需求,因为要求表头不能换行,所以用了nowrap
            body_html = body_html + '<th bgcolor="#C3C3C3" nowrap="nowrap">' + td_list + '</th>'
        body_html = body_html + "</tr>"
    # 表内容
    for tr_list in rng_list[1:]:
        body_html = body_html + "<tr>"
        for td_list in tr_list:
            body_html = body_html + "<td>" + td_list + "</td>"
        body_html = body_html + "</tr>"
    body_html = body_html + '</table>'
    body_html = body_html + "</body>"
    mail.HTMLBody = body_html
    mail.Display(True)

解决pyinstaller不兼容python-docx的方法

需求

python-docx是一个python的读写word的库,可以用来读写word文档,向word文档里插入表格。例如如下的操作docx的代码:

from docx import Document
document = Document()
document.add_heading('Document Title', 0)
p = document.add_paragraph('A plain paragraph having some ')
p.add_run('bold').bold = True
p.add_run(' and some ')
p.add_run('italic.').italic = True
document.add_heading('Heading, level 1', level=1)
document.add_paragraph('Intense quote', style='Intense Quote')
document.add_paragraph(
    'first item in unordered list', style='List Bullet'
)
document.add_paragraph(
    'first item in ordered list', style='List Number'
)
records = (
    (3, '101', 'Spam'),
    (7, '422', 'Eggs'),
    (4, '631', 'Spam, spam, eggs, and spam')
)
table = document.add_table(rows=1, cols=3,style='Light Grid Accent 1')
hdr_cells = table.rows[0].cells
hdr_cells[0].text = 'Qty'
hdr_cells[1].text = 'Id'
hdr_cells[2].text = 'Desc'
for qty, id, desc in records:
    row_cells = table.add_row().cells
    row_cells[0].text = str(qty)
    row_cells[1].text = id
    row_cells[2].text = desc
document.add_page_break()
document.save('demo.docx')

 
pyinstaller是python打包成exe的工具。
当我们要把编写好的使用了python-docx的程序打包时,问题来了。
首先,命令行打包

pyinstaller -D word_generate.py

这个没问题,word_generate.py是我的主程序文件。这里打包也不报错。但是下一步,运行的时候,duang~报错了,报错如下:

C:lzw_programmingjira_testdistword_generate>word_generate.exe
Traceback (most recent call last):
  File "word_generate.py", line 4, in <module>
  File "site-packagesdocxapi.py", line 25, in Document
  File "site-packagesdocxopcpackage.py", line 116, in open
  File "site-packagesdocxopcpkgreader.py", line 32, in from_file
  File "site-packagesdocxopcphys_pkg.py", line 31, in __new__
docx.opc.exceptions.PackageNotFoundError: Package not found at 'C:LZW_PR~1JIRA_T~1distWORD_G~1docxtemplatesdefault.docx'
[4232] Failed to execute script word_generate

解决方法

在翻了很多地方之后,终于找到了解决方法。很简单。增加一个hook-docx.py文件在PyInstallerhooks目录下就可以了。下面是文件内容以及路径

#-----------------------------------------------------------------------------
# Copyright (c) 2018-2018, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License with exception
# for distributing bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------
from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files("docx")

路径:

python根据excel的一列数据产生加权随机数

需求

最近遇到一个奇葩的事,行政那边说,让估算一下明年的这些杂七杂八费,然后给了我一个excel,里面有200多个这样的费用。我没做过行政,也搞不清这个到底咋来,为什么要弄这玩意。一番交流,原来是上头要的,不一定看,但是东西得有,让我弄个数字和去年差不多的就行。于是变有了下面的故事

设计

1.需要读取excel. 用的库是xlwing。可以根据sheet名称定位sheet,然后用range()方法定位一行,一列,或者指定几个单元格。
例如:定位 A1到A3 ,range1=sheet.range("A1:A3")
具体用法很多,在xlwing的官网上有介绍。http://docs.xlwings.org/en/stable/api.html
2.需要产生随机数,用的库是numpy,产生一个加权的随机数,然后我用的加权随机数规则为:
【-10%,概率10%】【不变,概率30%】【+2%,概率30%】【+5%,概率20%】【+10%,概率10%】

代码实现

代码中分为两个function,一个是读取excel,一个是根据一个值,产生一个加权随机数。观众老爷可以参考,对有疑问的地方,可以再来与我沟通O(∩_∩)O

# -*- coding: UTF-8 -*-
import xlwings
import numpy as np
def import_excel(filename, sheet_name, source,target):
    '''
    导入一个excel,根据某一列的值,产生一个加权随机数,覆盖掉另一列的值
    :param filename: excel路径
    :param sheet_name: sheet的名字
    :param source: 源列的第一个值 如:A1,C1
    :param target: 目标列的第一个值,如B1,D2
    :return:
    '''
    app = xlwings.App(visible=False, add_book=True)
    app.display_alerts = False
    app.screen_updating = False
    wb = app.books.open(filename)
    sht = wb.sheets[sheet_name]
    rng1 = sht.range(source)
    rng2 = sht.range(source).end('down')
    rng3 = sht.range(rng1, rng2)
    all_rng = rng3.value
    list_dest = []
    for item in all_rng:
        dest_item = arithmetic(item)
        # 这里要把结果变成[[1],[2]]这样的形式,才是一列的数据
        # 如果是[1,2]这样,是一行的数据
        list_temp = []
        list_temp.append(dest_item)
        list_dest.append(list_temp)
    sht.range(target).value = list_dest
    wb.save(filename)
    wb.close()
    app.quit()
def arithmetic(source):
    '''
    加权随机算法
    :param source: 输入一个原值
    :return: 返回浮动后的值
    '''
    power = np.array([0.1, 0.3, 0.3, 0.2, 0.1])
    index = np.random.choice([-0.1, 0, 2, 5, 10], p=power.ravel())
    rand = source * index / 100
    dest = int(source + rand)
    return dest
if __name__ == '__main__':
    import_excel("Book1.xlsx", 'Sheet1','C2','D2')

运行结果

python使用request库发送上传zip文件的post请求

使用工具:

  • python(2.7)
  • requests(2.18.4)
  • zip文件一个
  • chrome浏览器

第一步,通过chrome浏览器的开发者工具,获得发送的参数。


第二步,编写python代码

使用request库的post方法。注意的是要添加files参数,例如:

files ={'app_filename':open('portal-1.0-SNAPSHOT-fat.jar.zip','rb')}

zip压缩包用的后缀是application/x-zip-compressed,其他的文件是application/octet-stream
其中,’app_filename’是F12工具里抓出来的from data里的标有{binary}这一行的参数名。
portal-1.0-SNAPSHOT-fat.jar.zip是我自己电脑本地的一个zip文件。
rb是读二进制文件。因为这个form data是以二进制形式上传文件的
其余的常规参数,放到data参数里。例如上图的image_name:fff就是常规参数。
在header里注意添加cookies值或者Authorization值,这里我测试的网站用的是Authorization。如果没有该参数,会返回401
完整python request体参数如下:

path = os.path.split(os.path.realpath(__file__))[0]
url = host + '/dashboard/cicd/images'
headers = {
    'Authorization':'6bae7b70-8dae-4f74-9631-680b9501b52',
    'cookie': "_ga=GA1.3.733851079.1534745675; Hm_lvt_dde6ba2851f3db0ddc415ce0f895822e=1537859803; _ga=GA1.3.733851079.1534745675; Hm_lvt_dde6ba2851f3db0ddc415ce0f895822e=1537859803",
}
datat = {'image_name': 'abcd',
         'image_description': 'ccccvcc',
         'image_label': '1cc1fcc',
         'basic_image': 'openjdk:10',
         'store_path': '/opt/app/lzw/'}
files = {'app_filename': (
    'portal-1.0-SNAPSHOT-fat.jar.zip', open(os.path.join(path, 'portal-1.0-SNAPSHOT-fat.jar.zip'), 'rb'),
    'application/x-zip-compressed')}
# files ={'app_filename':open('portal-1.0-SNAPSHOT-fat.jar.zip','rb')} 和上面的功能一样
result = requests.post(url, files=files, data=datat, headers=headers)
r1 = result.text
print(result.text)

注意:千万不要在head里加入 ‘Content-Type’:’multipart/form-data;参数。

docker官网实例,用Dockerfile构建你的第一个python应用

#备注:默认读者已经安装好了docker环境

第一步:创建一个应用目录(后面使用该目录为工作目录,存放应用文件以及Dockerfile):

mkdir /root/docker/lzwtestpython
cd /root/docker/lzwtestpython

第二步:为容器定义一个Dockerfile:

创建Dockerfile文件:

vi Dockerfile

输入Dockerfile文件内容:

# Use an official Python runtime as a parent image
FROM python:2.7-slim
# Set the working directory to /app
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt
# Make port 80 available to the world outside this container
EXPOSE 80
# Define environment variable
ENV NAME World
# Run app.py when the container launches
CMD ["python", "app.py"]

在文件内容中,我们可以看到,我们的python文件为app.py,下面我们需要去创建这个文件。一般来说,python项目中会包含一个 requirements.txt 文件,用于记录所有依赖包。

第三步:创建requirements.txt和app.py

创建requirements.txt文件:

vi requirements.txt

输入requirements.txt文件内容:

Flask
Redis
创建app.py文件:
vi app.py

输入app.py文件内容:

from flask import Flask
from redis import Redis, RedisError
import os
import socket
# Connect to Redis
redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)
app = Flask(__name__)
@app.route("/")
def hello():
    try:
        visits = redis.incr("counter")
    except RedisError:
        visits = "<i>cannot connect to Redis, counter disabled</i>"
    html = "<h3>Hello {name}!</h3>" 
           "<b>Hostname:</b> {hostname}<br/>" 
           "<b>Visits:</b> {visits}"
    return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)

到这里,我们可以发现,我们没有真正的在电脑上安装python,和requirements.txt里提到的flask或者redis。看起来,你并没有搭建好python+flask+redis的环境。但是,通过接下来的步骤,你即将拥有。

第四步:创建应用

检查工作文件夹下的三个文件是否都创建完成,三个文件如下:

$ ls
Dockerfile		app.py			requirements.txt

现在开始运行创建Docker镜像命令,我们加上-t命令来命名,这里我们起一个很友好的名字friendlyhello(^_^)

docker build -t friendlyhello .

我们可以看一看执行的命令的回显:

Sending build context to Docker daemon  4.608kB
Step 1/7 : FROM python:2.7-slim
2.7-slim: Pulling from library/python
802b00ed6f79: Pull complete
10b2d5f7ed73: Pull complete
1073a127cf89: Pull complete
90283f3dc1cd: Pull complete
Digest: sha256:0a43a6d7858af4a42427c792b682936d2cd34e183fb026627f53ddb556d4bf62
Status: Downloaded newer image for python:2.7-slim
 ---> c9cde4658340
Step 2/7 : WORKDIR /app
 ---> Running in 5b6e0800c538
Removing intermediate container 5b6e0800c538
 ---> 3ac183b809ce
Step 3/7 : COPY . /app
 ---> b05ac52c77de
Step 4/7 : RUN pip install --trusted-host pypi.python.org -r requirements.txt
 ---> Running in 58bd2a10311e
Collecting Flask (from -r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/7f/e7/08578774ed4536d3242b14dacb4696386634607af824ea997202cd0edb4b/Flask-1.0.2-py2.py3-none-any.whl (91kB)
Collecting Redis (from -r requirements.txt (line 2))
  Downloading https://files.pythonhosted.org/packages/3b/f6/7a76333cf0b9251ecf49efff635015171843d9b977e4ffcf59f9c4428052/redis-2.10.6-py2.py3-none-any.whl (64kB)
Collecting itsdangerous>=0.24 (from Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/dc/b4/a60bcdba945c00f6d608d8975131ab3f25b22f2bcfe1dab221165194b2d4/itsdangerous-0.24.tar.gz (46kB)
Collecting Jinja2>=2.10 (from Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/7f/ff/ae64bacdfc95f27a016a7bed8e8686763ba4d277a78ca76f32659220a731/Jinja2-2.10-py2.py3-none-any.whl (126kB)
Collecting Werkzeug>=0.14 (from Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/20/c4/12e3e56473e52375aa29c4764e70d1b8f3efa6682bef8d0aae04fe335243/Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
Collecting click>=5.1 (from Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl (81kB)
Collecting MarkupSafe>=0.23 (from Jinja2>=2.10->Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/4d/de/32d741db316d8fdb7680822dd37001ef7a448255de9699ab4bfcbdf4172b/MarkupSafe-1.0.tar.gz
Building wheels for collected packages: itsdangerous, MarkupSafe
  Running setup.py bdist_wheel for itsdangerous: started
  Running setup.py bdist_wheel for itsdangerous: finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/2c/4a/61/5599631c1554768c6290b08c02c72d7317910374ca602ff1e5
  Running setup.py bdist_wheel for MarkupSafe: started
  Running setup.py bdist_wheel for MarkupSafe: finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/33/56/20/ebe49a5c612fffe1c5a632146b16596f9e64676768661e4e46
Successfully built itsdangerous MarkupSafe
Installing collected packages: itsdangerous, MarkupSafe, Jinja2, Werkzeug, click, Flask, Redis
Successfully installed Flask-1.0.2 Jinja2-2.10 MarkupSafe-1.0 Redis-2.10.6 Werkzeug-0.14.1 click-7.0 itsdangerous-0.24
Removing intermediate container 58bd2a10311e
 ---> 20b7d92b6075
Step 5/7 : EXPOSE 80
 ---> Running in 45f7bfcee8c8
Removing intermediate container 45f7bfcee8c8
 ---> 0c99f24bb0ca
Step 6/7 : ENV NAME World
 ---> Running in 4d192a73ee76
Removing intermediate container 4d192a73ee76
 ---> da526dcf3514
Step 7/7 : CMD ["python", "app.py"]
 ---> Running in 50226d88c2d5
Removing intermediate container 50226d88c2d5
 ---> bb0d475e1b3c
Successfully built bb0d475e1b3c
Successfully tagged friendlyhello:latest

分析回显,我们可以看到执行的过程有7步:

Step 1/7 : FROM python:2.7-slim
Step 2/7 : WORKDIR /app
Step 3/7 : COPY . /app
Step 4/7 : RUN pip install --trusted-host pypi.python.org -r requirements.txt
Step 5/7 : EXPOSE 80
Step 6/7 : ENV NAME World
Step 7/7 : CMD ["python", "app.py"]

这7步,是在Dockerfile里指定的(所以Dockerfile最关键啊#_#)。
命令执行完后,我们可以输入如下命令来查看本机安装的docker 镜像

$ docker image ls
REPOSITORY            TAG                 IMAGE ID
friendlyhello         latest              326387cea398第五步

第五步:运行应用

运行应用,并且把你物理机(或者云服务器)的4000端口映射到我们容器的80端口,使用-p命令:

docker run -p 4000:80 friendlyhello

哈?你问为什么要这样做?因为你的app.py文件里指的是80端口,如果我们没有使用docker,直接是在自己电脑上运行该python程序,确实可以通过http://0.0.0.0:80进行访问。目前在我们的容器中,确实也是80端口启用了,但是要让我们的其他用户访问这台物理机地址的容器内的python应用,需要把物理机的端口4000与容器的80端口进行映射。现在,请用http://localhost:4000 来检查一下吧

如果你是布置在别的机器上而非本机的话,也可以用地址加端口号的方式来访问。例如http://192.168.99.100:4000/ 或者http://www.yinyubo.cn:4000/
没有浏览器的话,也可以通过curl命令工具来访问

$ curl http://localhost:4000
<h3>Hello World!</h3><b>Hostname:</b> 8fc990912a14<br/><b>Visits:</b> <i>cannot connect to Redis, counter disabled</i>

如果你需要关闭docker应用的话,可以用如下方法:

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED
1fa4ab2cf395        friendlyhello       "python app.py"     28 seconds ago

先获得docker 容器的ID,然后用docker container stop 命令来关闭它,例如:

docker container stop 1fa4ab2cf395

wxpython加pyinstaller加pyecharts(解决python打包exe不兼容问题)

简单介绍:wxpython和pyecharts可以搭配使用,在桌面程序中展示web形式的图表。但是在使用pyinstaller打包成exe文件后,可能会出现如下BUG:

BUG1:在用pyinstaller将wxpython程序打包成exe文件后,运行会发生TemplateNotFound: simple_chart.html的报错信息。

这时候,需要把pyechats 的在python环境里的html文件拷贝出来,目录用/pyecharts/templates/ 然后放置在pyinstall打包后的主程序文件夹目录下

BUG2.生成了HTML文件之后,没有加载JS

1.去echarts官网下载echarts.common.min.js放在resource目录下
2.修改Libsite-packagespyechartstemplates的simple_page.html和simple_chart.html 的{{ echarts_js_dependencies(chart) }} 变成<script type=”text/javascript” src=”../echarts.common.min.js”></script>,然后重新打包
(补充解释:把echarts.common.min.js 放在pyecharts生成的HTML文件的上级目录里,对应src=”../echarts.common.min.js”)
4.修改wxpython的代码,在frame里增加一个webview

self.frame = reportFrame.MyFrame(None, wx.ID_ANY, "")
self.wv = webview.WebView.New(self.frame.panel_1, size=(500, 600)) # 加了这行就能整体拖动了
name = os.path.abspath(os.getcwd() + './resource/reports/init_report.html')
self.wv.LoadURL("file:///"+name)

5.把pyecharts/template目录和resource目录一起拷贝到manage目录下(manage是因为我打包的时候用的命令是pyinstaller -D manage.py)
因为有读者不是很明白文件结构,下面附上我的resource目录和pyechart生成的结构html文件目录
resource目录


生成的html文件里的内容

jinja2不兼容pyinstaller打包exe程序的解决办法

jinja2是python下的一个根据html模板,产生html文件的库。在jinja2官方文档中,推荐使用PackageLoader的方式,产生html文件,但是这个方式却与pyinstaller冲突,所以我们需要修改成FileSystemLoader的方式来解决打包文件的问题。

不兼容pyinstall的老代码(PackageLoader产生HTML文件)
#使用PackageLoader产生html文件
def create_defect_html(defectlist, fileds, reportname=None):
    """
    create html defects report in "./reports" directory
    :param defectlist:传入问题单列表
    :param fileds:传入问题单关键字列表
    :param reportname:传入报告文件名称,若有需要可以指定生成的html报告名称;若不传,则默认为DefectList.html
    :return:无返回,在reports目录下生成html报告
    """
    mycolumnlist = col_transform(fileds)
    env = Environment(loader = PackageLoader('TAReport', 'templates'))
    template = env.get_template('DefectTemplate.html')
    if reportname:
        filestr = './reports/' + reportname + '.html'
    else:
        filestr = './reports/DefectList.html'
    with open(filestr, 'w+') as f:
        f.write(str(template.render(defectlist=defectlist, columnlist=mycolumnlist)))
使用FileSystemLoader,能够正确使用pyinstaller打包的新代码
from jinja2 import Environment, PackageLoader, FileSystemLoader
def create_defect_html(defectlist, fileds, reportname=None):
    """
    create html defects report in "./reports" directory
    :param defectlist:传入问题单列表
    :param fileds:传入问题单关键字列表
    :param reportname:传入报告文件名称,若有需要可以指定生成的html报告名称;若不传,则默认为DefectList.html
    :return:无返回,在reports目录下生成html报告
    """
    mycolumnlist = col_transform(fileds)
    template_file_name = "DefectTemplate.html"
    template_file_path = resource_path('resource/templates', template_file_name)
    template_file_directory = os.path.dirname(template_file_path)
    template_loader = FileSystemLoader(searchpath=template_file_directory)
    env = Environment(loader=template_loader)
    template = env.get_template(template_file_name)
    if reportname:
        filestr = os.path.abspath(os.getcwd() + "./resource/reports/" + reportname + '.html')
        # filestr = './resource/reports/' + reportname + '.html'
    else:
        filestr = os.path.abspath(os.getcwd() + "./resource/reports/DefectList.html")
        # filestr = './resource/reports/DefectList.html'
    html = template.render(defectlist=defectlist, columnlist=mycolumnlist)
    with open(filestr, 'w') as f:
            f.write(html)
def resource_path(relative_path, file_name):
    """ Get absolute path to resource, works for both in IDE and for PyInstaller """
    # PyInstaller creates a temp folder and stores path in sys._MEIPASS
    # In IDE, the path is os.path.join(base_path, relative_path, file_name)
    # Search in Dev path first, then MEIPASS
    base_path = os.path.abspath(".")
    dev_file_path = os.path.join(base_path, relative_path, file_name)
    if os.path.exists(dev_file_path):
        return dev_file_path
    else:
        base_path = sys._MEIPASS
        file_path = os.path.join(base_path, file_name)
        if not os.path.exists(file_path):
            msg = "nError finding resource in either {} or {}".format(dev_file_path, file_path)
            print(msg)
            return None
        return file_path

python-批量把文件和文件夹同时压缩成ZIP文件

  • 明确需求(压缩成ZIP文件,文件中既有普通文件,又有文件夹)

    1.通过某种方式获得一个文件(文件夹)列表作为一个list(例如wxpython的wx.FileDialog方法,在下面的代码中我们跳过文件夹列表的获取方法)。
    2.选择一个压缩文件的输出目录和压缩文件的输出名字(下面代码中选择输出默认路径为程序根目录)
    3.把文件list里的文件,先统一放在一个临时文件夹里,然后把该临时文件夹压缩成ZIP文件,最后删掉临时文件夹

  • 程序关键点

1.使用python的shutil模块。拷贝文件和拷贝文件夹用的是不同的方法,在填写目标文件夹时,有很大区别,copytree()如果目标文件夹路径已存在,会导致拷贝失败,所以copytree()的目标文件夹路径,我设计成了目标目录+原文件夹名称的路径

#拷贝文件,该方法第二个参数填写的是目标目录
shutil.copy2(srcfile, folder_name)
#拷贝文件夹,该方法的第二个参数,是目标目录+原文件夹名称,
#因为如果目标文件夹路径已存在会导致拷贝失败
last_name = os.path.basename(srcfile)
destination_name = folder_name + last_name
shutil.copytree(srcfile, destination_name)

2.打包文件夹和删除文件夹,使用shutil的make_archive()和rmtree()

shutil.make_archive(source, "zip", source)
shutil.rmtree(source)
  • 完整代码

运行测试时,请修改file的路径为你自己电脑里的文件路径

# -*- coding: UTF-8 -*-
import os
import shutil
def copy_and_zip(file_list, dst_folder_name):
    '''
    批量复制文件到指定文件夹,然后把指定文件夹的内容压缩成ZIP并且删掉该文件夹
    :param file_list: 文件或文件夹
    :param dst_folder_name: 目标压缩文件的名称
    :return:
    '''
    for item in file_list:
        copy_file(item, dst_folder_name)
    # 这里我把输出文件的路径选到了程序根目录下
    source = os.getcwd() + "\" + dst_folder_name
    shutil.make_archive(source, "zip", source)
    shutil.rmtree(source)
def copy_file(srcfile, filename):
    '''
    把文件或文件夹复制到指定目录中
    :param srcfile: 文件或者文件夹的绝对路径
    :param filename: 指定目录
    :return:
    '''
    dstfile = os.path.abspath(os.getcwd())
    # 这里我把输出文件的路径选到了程序根目录下
    folder_name = dstfile + "\" + filename + "\"
    if not os.path.isfile(srcfile):
        last_name = os.path.basename(srcfile)
        destination_name = folder_name + last_name
        shutil.copytree(srcfile, destination_name)
        print("copy %s -> %s" % (srcfile, destination_name))
    else:
        fpath, fname = os.path.split(folder_name)  # 分离文件名和路径
        if not os.path.exists(fpath):
            os.makedirs(fpath)  # 创建路径
        shutil.copy2(srcfile, folder_name)  # 移动文件
        print("copy %s -> %s" % (srcfile, folder_name))
if __name__ == '__main__':
    file1 = "C:/Users/Pictures/1/1.jpg"
    file2 = "C:/Users/Pictures/1/sitemap.xml"
    file3 = "C:/lzw_programming/resource/"
    file_list = [file1, file2, file3]
    # 目标压缩包名
    folder_name = "1234567"
    copy_and_zip(file_list, folder_name)

wxpython-通过request远程下载网络zip文件,并解压安装文件

  • 明确需求(升级程序)

1.通过wxpython,产生一个窗体,窗体上有一段[文字标签],一个[进度条],一个[开始按钮]。
2.点击【开始按钮】,下载网络资源文件http://example.cn/test.zip。进度条和文字标签同时显示百分比
3.下载完成后,解压到指定目录。如果指定目录下有文件,则覆盖掉。

  • 设计界面

  • 程序关键点

1.wxpython用的进度条控件是wx.guage.定义如下:

self.gauge_1 = wx.Gauge(self, wx.ID_ANY, 100, style=wx.GA_HORIZONTAL | wx.GA_SMOOTH)

设置进度条的方法如下:msg填的是数字

self.download_gauge.SetValue(msg)

2.因为下载时间长,所以需要在主线程外再启用一个线程下载,避免程序假死。
3.通过request.Session().get方法下载比request.get下载要快

requests.Session().get(url, verify=False, stream=True)

4.通过stream的形式可以获得下载进度。如下,message是下载进度,例如10%,message为10

for chunk in response.iter_content(chunk_size=512):
    if chunk:
        code.write(chunk)
        code.flush()
    i = i + 512
    number = int(i)
    message = number * 100 / total_length

5.Zipfile解压文件,如果解压目录下有同名文件,则会直接覆盖掉

azip = zipfile.ZipFile(zip_file_path)
azip.extractall(path=unzip_to_path)
  • 完整代码(frame窗体和event事件)

updateOTAFrame.py(窗体文件,用wxglade创建)

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# generated by wxGlade 0.9.0pre on Thu Sep 06 09:41:05 2018
#
import wx
# begin wxGlade: dependencies
# end wxGlade
# begin wxGlade: extracode
# end wxGlade
class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((475, 137))
        self.label_1 = wx.StaticText(self, wx.ID_ANY, "start")
        self.gauge_1 = wx.Gauge(self, wx.ID_ANY, 100, style=wx.GA_HORIZONTAL | wx.GA_SMOOTH)
        self.button_1 = wx.Button(self, wx.ID_ANY, "start")
        self.__set_properties()
        self.__do_layout()
        # end wxGlade
    def __set_properties(self):
        # begin wxGlade: MyFrame.__set_properties
        self.SetTitle(u"u5347u7ea7u7a0bu5e8f")
        # end wxGlade
    def __do_layout(self):
        # begin wxGlade: MyFrame.__do_layout
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_2 = wx.BoxSizer(wx.VERTICAL)
        sizer_2.Add(self.label_1, 1, 0, 0)
        sizer_2.Add(self.gauge_1, 1, wx.EXPAND, 0)
        sizer_2.Add(self.button_1, 1, wx.ALIGN_CENTER, 0)
        sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_1)
        self.Layout()
        # end wxGlade
# end of class MyFrame

updateOTAevent.py(事件文件,和窗体文件分开)

# -*- coding: UTF-8 -*-
import os
import zipfile
from threading import Thread
import requests
import wx
from wx.lib.pubsub import pub
from view.window import updateOTAFrame
class MyApp(wx.App):
    def OnInit(self):
        self.frame = updateOTAFrame.MyFrame(None, wx.ID_ANY, "")
        self.frame.CenterOnScreen()
        self.download_gauge = self.frame.gauge_1
        self.start_button = self.frame.button_1
        self.frame.timer = wx.Timer(self.frame)  # 创建定时器
        # 绑定一个定时器事件,wxpython存在bug,不设定定时器,pub功能不会正常启用
        self.frame.Bind(wx.EVT_TIMER, self.on_timer, self.frame.timer)
        self.frame.Show()
        self.start_button.Bind(wx.EVT_BUTTON, self.start_event)
        pub.subscribe(self.update_ota, "ota_topic")
        pub.subscribe(self.download_text_topic, "download_text_topic")
        self.download_text = self.frame.label_1
        self.download_text.SetLabelText("点击开始升级按钮,即刻开始升级")
        self.start_button.SetLabelText("开始升级")
        pub.subscribe(self.close_frame, "close_download_topic")
        return True
    # 下载完成后,关闭窗口
    def close_frame(self, msg):
        self.frame.Destroy()
    # 开始下载按钮事件
    def start_event(self, event):
        self.start_button.SetLabelText("正在升级")
        self.download_text.SetLabelText("正在下载升级包,请不要关闭程序,目前进度:0%")
        self.start_button.Disable()
        GuageThread()
        event.Skip()
    # 控制下载的时候的文字
    def download_text_topic(self, msg):
        if msg < 100:
            self.download_text.SetLabelText("正在下载升级包,请不要关闭程序,目前进度:" + str(msg) + "%")
        else:
            self.download_text.SetLabelText('下载成功,现在开始解压,请耐心等待大于10秒')
            self.start_button.SetLabelText('正在解压')
    # 控制下载的进度条
    def update_ota(self, msg):
        if msg < 100:  # 如果是数字,说明线程正在执行,显示数字
            self.download_gauge.SetValue(msg)
        else:
            self.download_gauge.SetValue(msg)
    def on_timer(self, evt):  # 定时执行检查网络状态
        pass
# 另外启动一个线程来控制进度条,不然程序会假死
class GuageThread(Thread):
    def __init__(self):
        # 线程实例化时立即启动
        Thread.__init__(self)
        self.start()
    def run(self):
        # 线程执行的代码
        self.download_file()
    def download_file(self):
        # url = "http://example.cn/test.zip" 是网络上的zip压缩包文件
        url = "http://example.cn/test.zip"
        # 通过Session来下载,速度比直接requests.get快了大约百分之30
        response = requests.Session().get(url, verify=False, stream=True)
        total_length = int(response.headers.get("Content-Length"))
        with open(os.path.abspath(os.getcwd() + "/resource/download/new.zip"), "wb") as code:
            i = 0
            temp = 0
            # 用chunk_size的方法来下载,可以知道当前的下载进度。chunk_size影响每次写入的内存大小
            for chunk in response.iter_content(chunk_size=512):
                if chunk:
                    code.write(chunk)
                    code.flush()
                i = i + 512
                number = int(i)
                # 因为进度条的长度设置成了100,所以这里要乘以100
                message = number * 100 / total_length
                wx.CallAfter(pub.sendMessage, "ota_topic", msg=message)
                if temp != message:
                    temp = message
                    wx.CallAfter(pub.sendMessage, "download_text_topic", msg=message)
        filepath = os.path.abspath(os.getcwd() + "/resource/download/new.zip")
        # 直接放在程序根目录下了
        foldpath = os.path.abspath(os.getcwd())
        self.unzip(filepath, foldpath)
        wx.CallAfter(pub.sendMessage, "close_download_topic", msg=1)
    # 解压文件用的zipfile,将解压的文件,放置到指定路径下(覆盖复制)
    def unzip(self, zip_file_path, unzip_to_path):
        unzip_flag = False
        try:
            check_zip_flag = zipfile.is_zipfile(zip_file_path)
            if check_zip_flag:
                azip = zipfile.ZipFile(zip_file_path)
                azip.extractall(path=unzip_to_path)
                unzip_flag = True
        except Exception as e:
            print e.message
        finally:
            print "unzip_flag==========", unzip_flag
            return unzip_flag
# end of class MyApp
if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

注:  把 url = “http://example.cn/test.zip” 修改为自己要下载的网络zip压缩包文件。然后在updateOTAevent.py中运行即可。zip文件会下载完成后解压至程序根目录


苏ICP备18047533号-1