作者: 李镇伟

electron实例(一个例子教你入门)

总览

electron是目前最流行的桌面级开发框架。作为一个老测开,不能停下学习的脚本。下面我用一个electron制作了一个登陆的例子,用到的功能包含简单的ipc通信,页面跳转,引入jquery,bootstrap。下面开始我们的分解动作。

文件目录


该工程是我下载了github上的quick-start的工程,自己修改了main.js和index.html。添加了一个web-content/welcome.html页面。

  • main.js是后端主进程文件
  • index.html是首页
  • welcome.html是登录成功之后的页面

main.js(主进程)

main.js也就是主进程。这里我贴出用处最大的ipc前后端通信的代码。
讲解:后端用的是ipcMain。前端用的是ipcRender,这个开发模式是发布/订阅模式。ipc.on()发布一个‘login’的监听消息。当前端发起‘login’的请求时,触发后台的funtion()。mainWindow控制页面跳转。mainWindow.loadFile(‘./web-content/welcome.html’)用当前窗口打开一个welcome页面。

let mainWindow
const ipc = require('electron').ipcMain
ipc.on('login', function(event, user_name,user_password) {
  result='登录失败';
  console.log(user_name,user_password);  // 打印 "用户名密码"
  if(user_name=="1" && user_password=="2"){
     result='登录成功';
     console.log(result);
     //登录成功之后打开welcome页面
     mainWindow.loadFile('./web-content/welcome.html')
  }else{
    console.log(result);
  }
  //因为是同步策略,所以用event.returnValue
  event.returnValue = result;
  console.log("end");
});

index.html(首页)


首页效果如上图,用户名输入1,密码输入2,点击submit按钮开始登陆。
为了引用bootstrap,我们在最前面的<head>下加入bootstrap和jquery

<head>
    <title>测试开发笔记</title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
    <script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
    <script>if (window.module) module = window.module;</script>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
</head>
ipcRenderer连接后端,通过sendSync来同步发送消息,发送用户名和密码到后端,如果为真,新窗口打开github首页。用了jquery的$定位元素。用了.val()获得元素里填写的值,.afrer()在指定位置后添加元素。
<script>
  function connectMain() {
    const ipcRenderer = require('electron').ipcRenderer;
    var user_name=$("#input_username").val()
    var user_password=$("#input_userpassword").val()
    result=ipcRenderer.sendSync('login',user_name,user_password)
    if(result=="登录成功"){
      const remote = require('electron').remote;
      const BrowserWindow = remote.BrowserWindow;
      var win = new BrowserWindow({ width: 800, height: 600 });
      win.loadURL('https://github.com');
      // BrowserWindow.loadURL('https://github.com');
    }
    else{
      console.log($("#input_username"))
      var txt1="<div>密码错误</div>";
      $("#input_userpassword").after(txt1)
    }
    console.log(result)
  }
</script>

完整代码

main.js
// Modules to control application life and create native browser window
const {app, BrowserWindow,Menu,shell} = require('electron')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
const ipc = require('electron').ipcMain
ipc.on('login', function(event, user_name,user_password) {
  result='登录失败';
  console.log(user_name,user_password);  // 打印 "用户名密码"
  if(user_name=="1" && user_password=="2"){
     result='登录成功';
     console.log(result);
     //登录成功之后打开welcome页面
     mainWindow.loadFile('./web-content/welcome.html')
  }else{
    console.log(result);
  }
  //因为是同步策略,所以用event.returnValue
  event.returnValue = result;
  console.log("end");
});
function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 800, height: 600})
  // and load the index.html of the app.
  mainWindow.loadFile('index.html')
  // Open the DevTools.
  // mainWindow.webContents.openDevTools()
  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null
  })
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
  createWindow()
})
// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})
app.on('activate', function () {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow()
  }
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
index.html
<!DOCTYPE html>
<html>
<head>
    <title>测试开发笔记</title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
    <script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
    <script>if (window.module) module = window.module;</script>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
</head>
<body>
  <div class="container-fluid">
    <h1 class="page-header">electron-bootstrap实例</h1>
    <div class="row">
      <div class="col-md-offset-8 col-sm-offset-3 col-md-4 col-sm-6">
        <div class="panel panel-default">
          <div class="panel-body">
            <form>
              <div class="form-group">
                <label for="input_username">用户名</label>
                <input type="email" class="form-control" id="input_username" placeholder="Email">
              </div>
              <div class="form-group">
                <label for="input_userpassword">密码</label>
                <input type="password" class="form-control" id="input_userpassword" placeholder="Password">
              </div>
              <div class="checkbox">
                <label>
                  <input type="checkbox"> 记住密码
                </label>
              </div>
              <button type="submit" id="abc" class="btn btn-default" onclick="connectMain()">开始登陆</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
<script>
  function connectMain() {
    const ipcRenderer = require('electron').ipcRenderer;
    var user_name=$("#input_username").val()
    var user_password=$("#input_userpassword").val()
    result=ipcRenderer.sendSync('login',user_name,user_password)
    if(result=="登录成功"){
      const remote = require('electron').remote;
      const BrowserWindow = remote.BrowserWindow;
      var win = new BrowserWindow({ width: 800, height: 600 });
      win.loadURL('https://github.com');
      // BrowserWindow.loadURL('https://github.com');
    }
    else{
      console.log($("#input_username"))
      var txt1="<div>密码错误</div>";
      $("#input_userpassword").after(txt1)
    }
    console.log(result)
  }
</script>
</body>
    <script src="./renderer.js"></script>
</html>
welcome.html
<!DOCTYPE html>
<html>
<head>
    <title>es2105的写法</title>
    <meta charset="utf-8">
</head>
<body>
hello,world
</body>
</html>
renderer.js
function connectMain() {
    const ipcRenderer = require('electron').ipcRenderer;
    var user_name=$("#input_username").val()
    var user_password=$("#input_userpassword").val()
    result=ipcRenderer.sendSync('login',user_name,user_password)
    if(result=="登录成功"){
      const remote = require('electron').remote;
      const BrowserWindow = remote.BrowserWindow;
      var win = new BrowserWindow({ width: 800, height: 600 });
      win.loadURL('https://github.com');
      // BrowserWindow.loadURL('https://github.com');
    }
    else{
      console.log($("#input_username"))
      var txt1="<div>密码错误</div>";
      $("#input_userpassword").after(txt1)
    }
    console.log(result)
  }
package.json
{
  "name": "electron-quick-start",
  "version": "1.0.0",
  "description": "A minimal Electron application",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "repository": "https://github.com/electron/electron-quick-start",
  "keywords": [
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],
  "author": "GitHub",
  "license": "CC0-1.0",
  "devDependencies": {
    "electron": "^2.0.0"
  },
  "dependencies": {
    "bootstrap": "^4.1.3",
    "electron-json-storage": "^4.1.4",
    "jquery": "^1.12.4",
    "popper.js": "^1.14.4"
  }
}

参考

https://wizardforcel.gitbooks.io/electron-doc/content/api/ipc-main.html
https://electronjs.org/docs/tutorial/first-app

使用docker构建jenkins

1.拉取docker hub上的jenkins镜像,并且运行

参考https://jenkins.io/doc/book/installing/ 文章,使用的镜像是jenkinsci/blueocean
输入命令

docker pull jenkinsci/blueocean

然后输入运行docker镜像命令,这里我选择8081端口,和jenkins数据路径/root/docker/jenkins_home

docker run 
  -u root 
  --rm 
  -d 
  -p 8081:8080 
  -p 50000:50000 
  -v jenkins-data:/root/docker/jenkins_home 
  -v $PWD/allure-results:/allure-results 
  -v /var/run/docker.sock:/var/run/docker.sock 
  jenkinsci/blueocean

2.打开网页的8081端口,登录jenkins首页


发现需要输入jenkins密码。因为jenkins是安装在docker里的,所以要进入docker容器里去寻找密码

3.进入容器里,获取管理员密码

输入docker ps 获取容器的ID,290cbfd4bfe4

[root@VM_0_12_centos docker]# docker ps -a
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                           PORTS                                              NAMES
290cbfd4bfe4        jenkinsci/blueocean   "/sbin/tini -- /usr/…"   10 minutes ago      Up 10 minutes                    0.0.0.0:50000->50000/tcp, 0.0.0.0:8081->8080/tcp   sharp_fermat
de4a5ed40efd        friendlyhello         "python app.py"          47 hours ago        Exited (137) About an hour ago                                                      wizardly_kar

进入jenkins容器

docker exec -it 910d23b3bd10 /bin/bash

进入/var/jenkins_home/secrets/目录

cd /var/jenkins_home/secrets

查看initialAdminPassword

bash-4.4# cat initialAdminPassword
084b2fca0b434f189358123412312313113

把密码复制出来,用于登录jenkins

4.选择插件,安装jenkins,等待即可完成


完成之后,输入自己的系统管理员用户名密码就好啦~本次课程结束~

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文件会下载完成后解压至程序根目录

python-logging全局日志配置-滚动删除,只保存最近7天的日志,按级别存入不同文件

最近有这样一个需求,需要记录一下用户行为,和记下一些错误日志,放入指定文件夹里不同的文件下,方便后续debug。我决定采用python logging模块。并且使用配置文件,并做一个全局的globalLog.py来使用logging。 (关键词:logging,TimedRotatingFileHandler)

——————————————第一步——————————————

添加配置文件logger_config.ini。文件的后缀名其实无所谓,conf,ini或者别的都行。我把该文件放置在/{$项目名}/resource/路径下。

重点1:TimedRotatingFileHandler用来配置log的滚动删除,arg里的“midnight”, 1, 6,’utf-8′ 表示“每一天午夜12点将当天的日志转存到一份新的日志文件中,并且加上时间戳后缀,最多保存6个文件,编码格式UTF-8,支持中文”

重点2:如果要同时输出到2个文件和屏幕上,那么需要使用三个handler,2个为TimedRotatingFileHandler,还有1个是StreamHandler

重点3:level级别如下,级别越低,打印的日志越详细,例如级别为noset,则会打印出所有信息,如果级别为info,那么不会打印出debug信息。我们的例子中,级别为info,日志里不会打印出debug的信息

 下面是详细的配置文件: 
[loggers] keys=root 
[handlers] keys=rotatingFileHandler,streamHandler,errorHandler 
[formatters] keys=simpleFmt 
[logger_root] level=DEBUG 
handlers=rotatingFileHandler,streamHandler,errorHandler 
[handler_rotatingFileHandler] 
class=handlers.TimedRotatingFileHandler level=INFO formatter=simpleFmt args=(os.path.abspath(os.getcwd() + "/resource/ta_log/default.log"),"midnight", 1, 6,'utf-8') 
[handler_errorHandler] class=handlers.TimedRotatingFileHandler level=ERROR formatter=simpleFmt args=(os.path.abspath(os.getcwd() + "/resource/ta_log/error.log"), "midnight", 1, 6,'utf-8') 
[handler_streamHandler] level=INFO class=StreamHandler formatter=simpleFmt args=(sys.stdout,) 
[formatter_simpleFmt] format=%(asctime)s %(pathname)s(%(lineno)d): %(levelname)s %(message)s

——————————————第二步——————————————

编写globalLog.py 作为全局log的管理入口。

后续别的模块下的python文件需要使用日志功能时,需要导入该模块。该模块的原理也很简单,定位到文件路径,然后通过logger的名字获得要用的logger配置,例如我上面的配置文件中 [loggers] keys=root 所以这里就用了root.然后创建一个全局的logger对象,命名为ta_log

#globalLog.py
import logging
import logging.config
import os
def get_logger(name='root'):
    conf_log = os.path.abspath(os.getcwd() + "/resource/logger_config.ini")
    logging.config.fileConfig(conf_log)
    return logging.getLogger(name)
ta_log = get_logger(__name__)

——————————————第三步——————————————

在需要打印日志的地方使用该方法,例如在main.py里通过ta_log.info()打印日志。该方法会打印各个级别的信息进入两个日志文件中。我们在第四步来看一看日志文件的内容

import sys
from globalLog import ta_log
if __name__ == '__main__':
    ta_log.info("start")
    try:
        print(1 / 0)
    except Exception:
        ta_log.error("error:")
        ta_log.exception(sys.exc_info())
    ta_log.debug("end")

——————————————第四步——————————————

运行程序,检查日志文件。 这是default.log,级别是info.没有记录ta_log.debug()的信息

2018-08-31 13:09:34,928 C:/lzw_programming/test/main.py(6): INFO start
2018-08-31 13:09:34,928 C:/lzw_programming/test/main.py(10): ERROR error:
2018-08-31 13:09:34,930 C:/lzw_programming/test/main.py(11): ERROR (<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x0000000003B38888>)
Traceback (most recent call last):
  File "C:/lzw_programming/test/main.py", line 8, in <module>
    print(1 / 0)
ZeroDivisionError: integer division or modulo by zero

这是error.log。级别是error。没有记录ta_log.info()和ta_log.debug()的信息

2018-08-31 13:09:34,928 C:/lzw_programming/testassist/main.py(10): ERROR error:
2018-08-31 13:09:34,930 C:/lzw_programming/testassist/main.py(11): ERROR (<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x0000000003B38888>)
Traceback (most recent call last):
  File "C:/lzw_programming/testassist/main.py", line 8, in <module>
    print(1 / 0)
ZeroDivisionError: integer division or modulo by zero

——————————————第五步——————————————

运行了3天主程序,查看是否滚动产生了新的日志信息,结果是把历史日志信息滚动复制到了新的文件里,后缀名为日期,如图。

Jira-API的详细使用例子

下面是Jira-API的详细使用的例子,包含:

  • Jira的登陆,通过jql批量查询jira-issue,
  • 获得jira-project下的所有issue,assignee的详细信息,
  • 添加和更新defect
  • 下载和上传附件
  • 通过Jira登录的cookies搭配requsts库发送自定义的一些http请求
 
# -*- coding: UTF-8 -*-
import traceback
import requests
from atlassian import Jira
from jira import JIRA
import globalParam
jira = object
jira2 = object
#url是jira的地址
url = "https://www.Jira.com"
# 登录jira,成功为1,失败为0
def login(username, password):
    global jira, jira2
    result = 2
    try:
        jira = JIRA(server=url, basic_auth=(username, password), max_retries=1, timeout=400)
        jira2 = Jira(url=url, username=username, password=password)
        globalParam.reporter = get_current_user()
    except Exception:
        result = 0
        return result
    result = 1
    return result
# 获得一个项目的所有assignee的信息
def get_all_assignable_users_by_project_key(project_key):
    return jira2.get_all_assignable_users_for_project(project_key)
#下载附件到本地
def download_attachment(jira_attachment_id, filepath):
    attachment = jira.attachment(jira_attachment_id)
    image = attachment.get()
    with open(filepath, 'wb') as f:
        f.write(image)
#删除Jira上的附件
def delete_attachment(jira_attachment_id):
    jira.delete_attachment(jira_attachment_id)
#获得当前登陆的用户名
def get_current_user():
    return jira.myself()['displayName']
#获得当前登陆的用户的Key
def get_current_eid():
    return jira.myself()['key']
#根据jira-key获得开发人员
# key 可以是key'JIRA-1451'
def get_assignee(self, key):
    global jira
    issue = jira.issue(key)
    return issue.fields.assignee
#获得所有的Jira项目
def get_all_project():
    try:
        projects = jira.projects()
    except Exception:
        print(traceback.format_exc())
        result = 0
        return result
    return projects
# 根据jira-project-key参数获得所有的issue的各类信息
def get_issues_by_project_key(project_key):
    # 因为component从jira取出来是list,所以用逗号将它分割,重新组合成string
    jqlstring = "project = " + project_key + " AND issuetype = Defect ORDER BY updated DESC"
    try:
        query = jira.search_issues(
            jql_str=jqlstring,
            json_result=True,
            fields="key,summary,description,environment,comment,"
                   "components,labels,assignee,reporter,status,attachment,updated",
            maxResults=4000)
    except Exception:
        print(traceback.format_exc())
        result = 0
        return result
    issues = query["issues"]
    return issues
# 根据jira-project-key参数和时间获得所有的issue的各类信息
def get_issues_by_project_key_and_time(project_key, last_synctime):
    # 因为component从jira取出来是list,所以用逗号将它分割,重新组合成string
    jqlstring = "project = " + project_key + " AND updatedDate >='" + last_synctime + 
                "' AND issuetype = Defect ORDER BY updated DESC"
    try:
        query = jira.search_issues(
            jql_str=jqlstring,
            json_result=True,
            fields="key,summary,description,environment,comment,"
                   "components,labels,assignee,reporter,status,attachment,updated",
            maxResults=4000)
    except Exception:
        print(traceback.format_exc())
        result = 0
        return result
    issues = query["issues"]
    return issues
#根据jira-project-key参数获得所有的component信息
def get_components_by_project_key(project_key):
    try:
        components = jira.project_components(project_key)
    except Exception:
        print(traceback.format_exc())
        result = 0
        return result
    return components
#根据jira-project-key参数获得所有的version信息
def get_version_by_project_key(project_key):
    jira_project = jira.project(project_key)
    versions = jira_project.versions
    return versions
#添加一个defect
def add_defect(project_key, summary, description, steps_to_reproduce, labels, defect_severity, env, components,
               frequency, product_id, version_name, assignee_eid):
    jira_project = jira.project(project_key)
    label_list = labels.split(",")
    pid = jira_project.id
    if isinstance(product_id, int):
        product_id = str(product_id)
    components_list = []
    components_name = components.split(",")
    for item in components_name:
        components_list.append({"name": item})
    issue_dict = {
        'project': {'id': pid},
        'summary': summary,
        'description': description,
        'issuetype': {'name': 'Defect'},
        'labels': label_list,
        'customfield_10040': {"value": defect_severity},  # BUG的Defect Severity字段
        'customfield_10033': steps_to_reproduce,  # BUG的描述步骤
        'environment': env,
        'components': components_list,
        'customfield_10336': {"value": frequency},
        'customfield_13600': product_id,
        'customfield_11170': {"name": version_name},
        "assignee": {"name": assignee_eid}
    }
    if assignee_eid is None:
        del issue_dict['assignee']
    if env == "":
        del issue_dict['environment']
    if components == "":
        del issue_dict['components']
    if product_id == "" or product_id == "n":
        del issue_dict['customfield_13600']
    if version_name == "":
        del issue_dict['customfield_11170']
    new_issue = jira.create_issue(fields=issue_dict)
    return new_issue.key
#更新一个defect
def update_defect(issue_key, summary, description, steps_to_reproduce,
                  labels, defect_severity, env, components, frequency, product_id, version_name, assignee_eid):
    issue = jira.issue(issue_key)
    label_list = labels.split(",")
    components_list = []
    components_name = components.split(",")
    if components == "":
        pass
    for item in components_name:
        components_list.append({"name": item})
    issue_dict = {
        'summary': summary,
        'description': description,
        'issuetype': {'name': 'Defect'},
        'labels': label_list,
        'customfield_10040': {"value": defect_severity},  # BUG的Defect Severity字段
        'customfield_10033': steps_to_reproduce,  # BUG的描述步骤
        'environment': env,
        'components': components_list,
        'customfield_10336': {"value": frequency},
        'customfield_13600': str(product_id),
        'customfield_11170': {"name": version_name},
        "assignee": {"name": assignee_eid}
    }
    if assignee_eid is None:
        del issue_dict['assignee']
    if env == "":
        del issue_dict['environment']
    if components == "":
        del issue_dict['components']
    if version_name == "":
        del issue_dict['customfield_11170']
    update_issue = issue.update(fields=issue_dict)
    cccc = update_issue
    return issue_key
#将一个本地的附件上传到Jira上
def add_attachment(issue, path, filename):
    newpath = path.encode('utf-8')
    cc = unicode(newpath, "utf-8")
    result = jira.add_attachment(issue=issue, attachment=cc, filename=filename)
    return result
#根据jira-key获得该issue所有的附件
def get_attachment(issue_key):
    issue = jira.issue(issue_key)
    return issue.fields.attachment
#通过项目project_key值,抓取页面上的一些自定义参数,例如自定义参数13600,对应products参数
def get_products_by_project_key(project_key):
    product_map = {}
    jiraproject = jira.project(project_key)
    #jira._session.cookies可以用来搭配requests库,发送请求
    cookies = jira._session.cookies
    body = {"pid": jiraproject.id}
    url = "https://www.jira.com/secure/QuickCreateIssue!default.jspa?decorator=none"
    result = requests.post(url, data=body, cookies=cookies,
                           headers={"Content-type": "application/x-www-form-urlencoded"})
    html = result.text
    try:
        select1 = html.split('"id":"customfield_13600","label":"Product/s","required":false,')[1]
        select2 = select1.split('</select>')[0]
        if not select2.__contains__("\n\tNone\n\n"):
            select3 = select2.split('<option')
            c = select3
            for item in select3:
                if item.__contains__('value='):
                    patern_id = item.split('value=\"')[1]
                    id = patern_id.split('\"')[0]
                    patern_value = item.split("\n    ")
                    value = patern_value[1].strip()
                    product_map[id] = value
    except Exception:
        # print(traceback.format_exc())
        pass
    return product_map
#给一个jira-issue添加comment
def add_comment(issue_key, comment_body):
    comment = jira.add_comment(issue_key, comment_body)
    return comment.id

将Jira获取的UTC时间转本地时间yyyy-MM-dd’T’HH:mm:ss

通过API获得jira 备注(comment)的更新时间,发现获得的UTC时间格式为
2018-08-14T02:52:22.216+0000
这个时候,如果需要转换成datetime :yyyy-MM-dd'T'HH:mm:ss格式,
或者date:yyyy-MM-dd,可以用下面的代码:

#!/usr/bin/python
# -*- coding:utf-8 -*-
import re
import time
from datetime import datetime
def utc2local(utc_st):
    # UTC时间转本地时间(+8:00)
    now_stamp = time.time()
    local_time = datetime.fromtimestamp(now_stamp)
    utc_time = datetime.utcfromtimestamp(now_stamp)
    offset = local_time - utc_time
    local_st = utc_st + offset
    return local_st
def local2utc(local_st):
    # 本地时间转UTC时间(-8:00)
    time_struct = time.mktime(local_st.timetuple())
    utc_st = datetime.utcfromtimestamp(time_struct)
    return utc_st
def utc_format(utcstr):
    """
    将不标准的utc时间字符串转换成datetime格式
    :param utcstr: 不标准的utc字符串,如‘2018-04-23T03:43:35.000+0000’
    :return:
    """
    utcstr = re.sub('.\d\d\d\+0000', '', utcstr, count=1)
    try:
        UTC_FORMAT = "%Y-%m-%dT%H:%M:%S"
        d_time = datetime.strptime(utcstr, UTC_FORMAT)
    except Exception:
        UTC_FORMAT = "%Y-%m-%d %H:%M:%S"
        d_time = datetime.strptime(utcstr, UTC_FORMAT)
    return d_time
def get_local_date(utcstr):
    """
    将jira返回的str类型的时间,转换成datetime格式,并去除日期之后的内容(提供给SprintDailyIssuesBar使用的日期处理接口)
    :param utcstr: jira返回的time字符串
    :return:datetime格式
    """
    utc_st = utc_format(utcstr)
    # print utc_st
    local_st = utc2local(utc_st)
    # print local_st
    local_str = local_st.strftime('%Y-%m-%d')
    local_date = datetime.strptime(local_str, "%Y-%m-%d")
    # print local_date
    return local_date
def get_local_datetime_str(utcstr):
    """
    将jira返回的str类型的UTC时间,转换成str类型的local time,包含具体的时、分、秒(提供给tableData使用,转换时间)
    :param utcstr: jira返回的time字符串
    :return:string
    """
    utc_st = utc_format(utcstr)
    # print utc_st
    local_st = utc2local(utc_st)
    # print local_st
    local_str = local_st.strftime('%Y-%m-%d %H:%M:%S')
    return local_str
 if __name__ == '__main__':
     utcstr1 = '2018-08-14T02:52:22.216+0000'
     tt=get_local_datetime_str(utcstr1)
     print(tt)

苏ICP备18047533号-1