Python | 多进程具体实现

1 背景

在之前的博客中,笔者尝试介绍了多进程和多线程的原理,以及相关的实现,详情见 : Python | 多线程和多进程 。不过当时跑的都是示例性的代码,而最近刚好做一个项目的时候真刀真枪的用到了Python多进程!于是特此总结一下

2 Python实现多进程案例1

2.1 我们想要干嘛?

现在已经有了15万的商户编号(在mids.csv文件中),希望从数据库的商户表中查询到这15万商户的商户信息!怎么办呢?

  • 思路1:直接用SQL语句查询具体见下方
select * from table where mer_id in minds_merchant

但存在以下的问题:

  • 需要对15万商户的商户号改成SQL的形成,即括号括起来,里面每一个元素都是字符型!
  • 直接查询速度会很慢!!!速度很慢!速度很慢!

所以有了第二种思路:

  • 采用多进程的方式,同时开20个进程,每个进程同步的去取数据
  • 每个进程当中又同步的去开很多个小块,这样做的目的是为了减小内存的压力,进一步提升效率!

2.2 Python实现

2.2.1 主进程代码

  • 主要使用到的库是multiprocessing的Process函数

知识点总结:

  • 显示主进程PID是为了进行监控任务的情况,可以在linux中输入 top 查看!相当于任务管理器!
  • 记录了主进程PID也可以方便后续的kill掉任务(一旦不需要继续进行这个任务的时候)
  • 那kill掉任务为什么不直接control+C呢?因为涉及到庞大的任务的时候,一般我们在后台运行,这时候可以加一个nohup,具体用法见下面:
nohup python cmd.py &

即 nohup + Python + 要执行的Python脚本 + &

  • 因此一旦程序后台执行了,使用control+C就无法让其停止,那我们怎么停止任务呢?
  • 需要强制关闭子进程时,需要用linux的kill命令。由于我们开启了多个子进程,一个进程一个进程地kill费时费力,所以我们利用刚才记录进程号的文件,读取其中所有pid,并用linux shell脚本直接依次kill掉
- 步骤1:输入vim stop.sh
- 步骤2:复制粘贴下面的代码
for i  in `cat pid`  
do  
echo $i  
kill -9 $i
done  
- 步骤3:保存脚本。(依次按 [ESC][:],然后输入[wq],回车)
- 步骤4:停止脚本赋执行权限。chmod +x stop.sh
- 步骤5:运行stop.sh脚本。./stop.sh

知识点补充:

  • 可以直接import一个py脚本文件 比如 import score_mer
  • import之后如果想用其中的一个函数,可以这么干:score_mer.concat() concat就是score_mer.py文件中的一个函数
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
控制台
"""

from multiprocessing import Process
import pandas as pd
import os
import math
import shutil
import time

import score_mer

def log(rec):
    with open("log", 'a') as f:
        f.write(rec+'\n')

def cmd():
    # 显示主进程PID 这是为了监控
    main_pid = os.getpid() 
    with open("pid", "w") as f:
        f.write(str(main_pid)+'\n')

    # 控制台
    ur_time0 = time.time()

    # 确定目标商户总数量 mids.csv是需要自己提前准备好的!
    mid_csv = pd.read_csv('mids.csv')
    # 商户号的列表
    mids = list(mid_csv['MERC_ID'])
    n = len(mids)

    # 初始化日志
    with open("log", 'w') as f:
        f.write("====[Start: %d]====\n" % n)
    with open('error_log', 'w') as f:
    	f.write("errors:\n" )

    # 清空计算指标目录
    index_fold = "index_res/"
    '''
    知识点1:判断一个文件夹/文件是否存在:os.path.exists()
    知识点2:删除一个文件夹:shutil.rmtree()【其中要先导入 import shutil】
    知识点3:创建一个文件夹:os.mkdir()
    '''
    if os.path.exists(index_fold):
        shutil.rmtree(index_fold)
    os.mkdir(index_fold)

    # [1].多进程-计算交易特征
    # 每个进程计算结果作为一小块放在index_res目录下
    log("*** index computation start.")
    time_all0 = time.time()
    # 进程列表
    ps = []
    # 进程数
    n_thread = 20
    # 单个进程处理数据量
    block = int(n / n_thread)
    # 补充一个进程补完数据
    n_thread = n_thread + 1

    log("    block size: %d" % block)

    # 设置子进程
    for i in range(0, n_thread):
        p = Process(target = score_mer.generate_index,
            args=(i, i*block, (1+i)*block,))
        ps.append(p)

    # 启动子进程
    for i in range(0, len(ps)):
        time.sleep(2)
        ps[i].daemon = True
        ps[i].start()  # 通过调用start方法,来启动进程
        log("    [ ] thread [%d] launched." % i)      

    # 等待所有进程结束
    for i in range(0, len(ps)):
        ps[i].join()  # 阻塞当前的进程,直到调用join方法的那个进程执行完毕
        #thread_t1 = time.time()
        #log("    [*] thread [%d] finished. (%.2fh)" % (i, (float(thread_t1 - time_all0)/ 3600)))

    time_all1 = time.time()
    log("    index computation finished. (%.2fs)" % (time_all1-time_all0))
    score_mer.concat(index_fold, 'complete_index.csv')

cmd()

2.2.2 target代码

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
从商户表提取商户信息
"""

import pandas as pd
import time
import pymysql
import copy
import re
import os

# 最新时间点
now_year = 2018
now_mon = 12
# 最新时间(根据不同数据的时间节点然后进行相应的修改)
zjsj = '20181231235959'

def log(rec):
    with open("log", 'a') as f:
        f.write(rec+'\n')

def generate_index(tid, st, ed):
    # 三个参数分别为 进程号;起始商户序号;终止商户序号
    # 读取数据,计算特征指标
    # 输出参数,st,ed,指定对商户号列表的[st, ed]部分进行计算,即对特定子块计算

    # 记录PID
    with open("pid", "a") as f:
        f.write(str(os.getpid())+'\n')

    all_t0 = time.time()
    print("*** [score]-[%d] start %d - %d" % (tid, st, ed))
    tablename = 'V_WSXD_MERC' # 商户表

    # 数据库连接
    a = pymysql.connect(host='你的host',port=3306,
                        user='XXXX',passwd='XXXX',db='XX',charset="utf8")

    # [0].获取数据
    # 读取需要计算的商户号列表
    mid_csv = pd.read_csv('mids.csv')
    mids = list(mid_csv['MERC_ID'])
    real_ed = min(ed, len(mids))

    # 目标:抽取指定部分
    #print(st, real_ed)
    mids = mids[st:real_ed]
    n = len(mids)
    print("*** [score]-[%d] %d - %d: %d merchants." % (tid, st, real_ed, n))


    # 分块大小,每块有多少商户
    block = 50
    end = int( n / block ) + 1
    #end = 4
    print("*** [score]-[%d] Block size: %d | Block amount: %d" % (tid, block, end))

    # 全局结果
    global_res = []
    # 上一次存储位置
    last_pos = 0
    for pos in range(0, end):
        try:
            t_0 = time.time()
            #print("    [score] Block [%d]" % pos)
            # 组装sql语句
            merchants = "('"
            # 检索商户号时的偏移量
            offset = pos*block
            right_pos = min(offset+block, n)
            for i in range(offset, right_pos-1):
                 merchants += str(mids[i]) + "','"
            merchants += str(mids[right_pos-1]) + "')"
            #print merchants
            sql = 'select * from %s where %s in %s' %\
                     (tablename, 'MERC_ID', merchants)
            # 获取mysql数据
            t0 = time.time()
            d = pd.read_sql(sql, con=a)
            t1 = time.time()
            #print("    [score] get %d merchants. (%.2fs)" % (d['MERC_ID'].nunique(), t1-t0))
        
            d.to_csv('index_res/thread_'+str(tid)+'_'+str(st+last_pos)+'_'+str(st+right_pos)+'.csv', index=False, encoding = 'gbk')

            # 记录当前进程进度
            log("        thread [%d] block[%d] finished: %d - %d" % (tid, pos, (st+last_pos), (st+right_pos)))

            # 清空结果缓存
            last_pos = right_pos

                
        except Exception as e:
            print(e)
            with open('error_log', 'a') as f:
                f.write('[%d]-block %d error \n' % (tid, pos))

    
    all_t1 = time.time()
    a.close()
    rec = "Thread over - [%d] %.2f h" % (tid, (float(all_t1-all_t0)/3600))
    print(rec)

    # 记录计算时间
    log(rec)
    
def concat(res_fold, final_name):
    # 合并所有计算指标文件
    #res_fold = 'index_res/'
    ds = []
    t0 = time.time()
    for filename in os.listdir(res_fold):
        if 'csv' in filename:
            d = pd.read_csv(res_fold+filename, encoding = 'gbk')
            ds.append(d)
    data = pd.concat(ds, axis=0, sort=True)
    t1 = time.time()
    log("*** [concat scores starting] %d merchants." % (len(data)))
    # 去重
    data.drop_duplicates('MERC_ID', 'first', inplace=True) 
    # first 表示 删除重复项并保留第一次出现的项
    # inplace=True 表示直接在原来的DataFrame上删除重复项
    data.to_csv(final_name, index=False, encoding = 'gbk')
    log("*** [concat scores finished] %d merchants (%d). %.2fs" % (data['MERC_ID'].nunique(), len(data), t1-t0))

##################################################################################################

总结:

  • 确定目标。首先要明确我们用多进程来干吗!比如说这个例子是多进程来获取商户表的信息。多进程具体体现在对商户号分多个进程去取!

  • 搭建多进程。使用multiprocessing的process函数来搭建。

  • 准备好日志函数。在多进程的过程中做好日志的记录

  • 启动子进程

  • 待所有进程全部结束后(join函数判断),记录时间,评估效率

  • 其中在第二步搭建多进程的时候,就需要确定target了!即我们到底想要干嘛的落实工作!

  • 这时候需要进一步去分block!进一步的提高效率

  • 所以总的来说就是多个进程同时启动,每一个进程下面的block按顺序来进行处理的!

  • 上述过程中主要耗时的地方在于表的查询!

3 Python实现多进程案例2

需求:根据原始交易流水,计算每个商户最近6个月中以3个月为维度进行移动加权平均,并取其中的最大值和最小值!

cmd代码思路保持一致。

核心的score代码思路

  • 首先把最近6个月的总交易金额分别都算出来,然后存到一个list中
  • 然后用一个for循环,3个月为维度,权重为3 2 1 结果存到一个list
  • 最后取list的最大值和最小值。同时返回的结果也可以把每个月的总交易金额返回,以防后面有其余的需求!

3.1 cmd主代码

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
控制台
"""

from multiprocessing import Process
import pandas as pd
import os
import math
import shutil
import time

import score_wa
# import rank

def log(rec):
    with open("log", 'a') as f:
        f.write(rec+'\n')

def cmd():
    # 显示主进程PID
    main_pid = os.getpid() 
    with open("pid", "w") as f:
        f.write(str(main_pid)+'\n')

    # 控制台
    ur_time0 = time.time()

    # 确定目标商户总数量
    mid_csv = pd.read_csv('mids.csv')
    # 商户号的列表
    mids = list(mid_csv['MERC_ID'])
    n = len(mids)

    # 初始化日志
    with open("log", 'w') as f:
        f.write("====[Start: %d]====\n" % n)
    with open('error_log', 'w') as f:
    	f.write("errors:\n" )

    # 清空计算指标目录
    index_fold = "index_res/"
    if os.path.exists(index_fold):
        shutil.rmtree(index_fold)
    os.mkdir(index_fold)

    # [1].多进程-计算交易特征
    # 每个进程计算结果作为一小块放在index_res目录下
    log("*** index computation start.")
    time_all0 = time.time()
    # 进程列表
    ps = []
    # 进程数
    n_thread = 20
    # 单个进程处理数据量
    block = int(n / n_thread)
    # 补充一个进程补完数据
    n_thread = n_thread + 1

    log("    block size: %d" % block)
    # block = 50
    # n_thread = 2

    # 设置子进程
    for i in range(0, n_thread):
        p = Process(target = score_wa.generate_index,
            args=(i, i*block, (1+i)*block,))
        ps.append(p)

    # 启动子进程
    for i in range(0, len(ps)):
        time.sleep(2)
        ps[i].daemon = True
        ps[i].start()  
        log("    [ ] thread [%d] launched." % i)      

    # 等待所有进程结束
    for i in range(0, len(ps)):
        ps[i].join() # 通信 等待结束
        #thread_t1 = time.time()
        #log("    [*] thread [%d] finished. (%.2fh)" % (i, (float(thread_t1 - time_all0)/ 3600)))

    time_all1 = time.time()
    log("    index computation finished. (%.2fs)" % (time_all1-time_all0))
    score_wa.concat('index_res/', 'wa_trans_amt.csv')

cmd()

3.2 score计算代码

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
基于交易记录计算特征指标
"""

import pandas as pd
import time
import pymysql
import copy
import re
import os

# 最新时间点
now_year = 2019
now_mon = 4
# 最新时间(根据不同数据的时间节点然后进行相应的修改)
# zjsj = '20181231235959'

# 根据最新的年份、月份,得出预设时间 
def get_mon_set(now_year, now_mon, l):
    mons = []
    tp_mon = now_mon+1
    tp_year = now_year
    for i in range(0, l):
        next_year = tp_year
        next_mon = tp_mon-1
        if next_mon == 0:
            next_mon = 12
            next_year = tp_year-1
        tp_mon = next_mon
        tp_year = next_year
        if next_mon<10:
            mons.append(str(next_year)+'-0'+str(next_mon))
        else:
            mons.append(str(next_year)+'-'+str(next_mon))
    return mons

def time_check_mer(recent_time):  
    recent_stamp = time.mktime(time.strptime(recent_time, '%Y%m%d'))
    return int(recent_stamp)

# 提取月份
def extract_month(time_stamp):
    time_tuple = time.localtime(time_stamp)
    mon_num = time_tuple.tm_mon
    if mon_num<10:
        mon_str = '0'+str(mon_num)
    else:
        mon_str = str(mon_num)
    return str(time_tuple.tm_year)+'-'+mon_str

def log(rec):
    with open("log", 'a') as f:
        f.write(rec+'\n')

def check_mer(name, odf):
    # 对指定商户的交易流水提特征
    global now_year
    global now_mon

    # 原始数据,包含非正常交易的
    ndf = copy.deepcopy(odf)

    # 筛选数据,只包含正常交易的
    df = copy.deepcopy(odf)
    df = df[(df['TRANS_STATUS']==u'成功') & (df['TXN_AMT']>0)]

    if len(df)>0:
        # 转为字符型
        df['AC_DT'] = df['AC_DT'].map(str)
        # 转为时间戳
        df['ts'] = df['AC_DT'].map(time_check_mer)
        # 转为年份-月份
        df['month'] = df['ts'].map(extract_month)

    else:
        df = pd.DataFrame({}, columns =
            (list(odf.columns)+['ts', 'month']))

    # 所有月份
    all_mon = get_mon_set(now_year, now_mon, 6)

    # 加权移动平均处理
    # 设置一个空list来存储每个月交易金额
    amt_m = []
    for i in range(len(all_mon)):
        df_m = df[df['month']==all_mon[i]]
        amt_tmp = df_m['TXN_AMT'].sum()
        amt_m.append(amt_tmp)

    # 2019-4月总交易金额
    amt_m4 = amt_m[0]

    # 2019-3月总交易金额
    amt_m3 = amt_m[1]

    # 2019-2月总交易金额
    amt_m2 = amt_m[2]

    # 2019-1月总交易金额
    amt_m1 = amt_m[3]

    # 2018-12月总交易金额
    amt_m12 = amt_m[4]

    # 2018-11月总交易金额
    amt_m11 = amt_m[5]

    # 每个月的总交易金额组成的list amt_m

    # 确定权重
    w = [3/6, 2/6, 1/6]
    # 空的list 存储加权移动平均值
    wa = []
    for i in range(len(amt_m)-2):
        wa.append(w[0] * amt_m[i] + w[1] * amt_m[i+1] + w[2] * amt_m[i+2])
    # 加权移动平均的最大值
    wa_max = max(wa)
    # 加权移动平均的最小值
    wa_min = min(wa)
    

    # 汇总结果
    res = [name,
    # 1-加权移动平均的最大值
    wa_max,
    # 2-加权移动平均的最小值
    wa_min,
    amt_m4, # 2019-4月总交易金额
    amt_m3, # 2019-3月总交易金额
    amt_m2, # 2019-2月总交易金额
    amt_m1, # 2019-1月总交易金额
    amt_m12, # 2018-12月总交易金额
    amt_m11 # 2018-11月总交易金额
    ]
    return res

def generate_index(tid, st, ed):
    # 三个参数分别为 进程号;起始商户序号;终止商户序号
    # 读取数据,计算特征指标
    # 输出参数,st,ed,指定对商户号列表的[st, ed]部分进行计算,即对特定子块计算

    # 记录PID
    with open("pid", "a") as f:
        f.write(str(os.getpid())+'\n')

    all_t0 = time.time()
    print("*** [score]-[%d] start %d - %d" % (tid, st, ed))
    tablename = 'V_WSXD_TRANS' # 交易表

    # 数据库连接
    a = pymysql.connect(host='your host',port=3306,
                        user='XXXX',passwd='XXXX',db='XX',charset="utf8")

    # [0].获取数据
    # 读取需要计算的商户号列表
    mid_csv = pd.read_csv('mids.csv')
    mids = list(mid_csv['MERC_ID'])
    real_ed = min(ed, len(mids))

    # 目标:抽取指定部分
    #print(st, real_ed)
    mids = mids[st:real_ed]
    n = len(mids)
    print("*** [score]-[%d] %d - %d: %d merchants." % (tid, st, real_ed, n))


    # 分块大小,每块有多少商户
    block = 50
    end = int( n / block ) + 1
    #end = 4
    print("*** [score]-[%d] Block size: %d | Block amount: %d" % (tid, block, end))

    # 全局结果
    global_res = []
    # 上一次存储位置
    last_pos = 0
    for pos in range(0, end):
        try:
            t_0 = time.time()
            #print("    [score] Block [%d]" % pos)
            # 组装sql语句
            merchants = "('"
            # 检索商户号时的偏移量
            offset = pos*block
            right_pos = min(offset+block, n)
            for i in range(offset, right_pos-1):
                 merchants += str(mids[i]) + "','"
            merchants += str(mids[right_pos-1]) + "')"
            #print merchants
            sql = 'select * from %s where %s in %s' %\
                     (tablename, 'MERC_ID', merchants)
            # 获取mysql数据
            t0 = time.time()
            d = pd.read_sql(sql, con=a)
            t1 = time.time()
            #print("    [score] get %d merchants. (%.2fs)" % (d['MERC_ID'].nunique(), t1-t0))

            # [1].按商户分组,计算特征
            groups = d.groupby(d['MERC_ID'])
            res = []
            # 对每一个商户计算相应的交易流水特征
            for group in groups:
                try:
                    res.append(check_mer(group[0], group[1]))
                except Exception as e:
                    with open('error_log', 'a') as f:
                        f.write('[%d]-block %d local error. (sample[%s], -1) \n' % (tid, pos, group[0]))

            global_res = global_res + res
            t_1 = time.time()
            print("    [score]-[%d] Block[%d]. %d merc. Request: %.2fs. Processing: %.2fs" % (tid, pos, d['MERC_ID'].nunique(), t1-t0, t_1-t_0))

            if (pos % 40 == 0) or (pos == (end-1)):
                # 每40个块输出一次数据
                # 40X50 = 2000个商户
                # [2].输出结果
                header = ['MERC_ID',
                # 1-加权移动平均的最大值
                'wa_max', 
                # 2-加权移动平均的最小值
                'wa_min',
                'amt_m4', # 2019-4月总交易金额
                'amt_m3', # 2019-3月总交易金额
                'amt_m2', # 2019-2月总交易金额
                'amt_m1', # 2019-1月总交易金额
                'amt_m12', # 2018-12月总交易金额
                'amt_m11' # 2018-11月总交易金额
                ]

                rd = pd.DataFrame(global_res, columns=header)
                rd = rd.fillna(0)
                rd.to_csv('index_res/thread_'+str(tid)+'_'+str(st+last_pos)+'_'+str(st+right_pos)+'.csv', index=False, encoding = 'gbk')

                # 记录当前进程进度
                log("        thread [%d] block[%d] finished: %d - %d" % (tid, pos, (st+last_pos), (st+right_pos)))

                # 清空结果缓存
                global_res = []
                last_pos = right_pos

                
        except Exception as e:
            print(e)
            with open('error_log', 'a') as f:
                f.write('[%d]-block %d error \n' % (tid, pos))

    
    all_t1 = time.time()
    a.close()
    rec = "Thread over - [%d] %.2f h" % (tid, (float(all_t1-all_t0)/3600))
    print(rec)

    # 记录计算时间
    log(rec)
    
def concat(res_fold, final_name):
    # 合并所有计算指标文件
    #res_fold = 'index_res/'
    ds = []
    t0 = time.time()
    for filename in os.listdir(res_fold):
        if 'csv' in filename:
            d = pd.read_csv(res_fold+filename, encoding = 'gbk')
            ds.append(d)
    data = pd.concat(ds, axis=0, sort=True)
    t1 = time.time()
    log("*** [concat scores starting] %d merchants." % (len(data)))
    # 去重
    data.drop_duplicates('MERC_ID', 'first', inplace=True)
    data.to_csv(final_name, index=False, encoding = 'gbk')
    log("*** [concat scores finished] %d merchants (%d). %.2fs" % (data['MERC_ID'].nunique(), len(data), t1-t0))

##################################################################################################

4 知识点补充

4.1 为什么要给MySQL加索引

  • 提高查询的效率!索引可以大大提高MySQL的检索速度
  • 索引分单列索引组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。

4.2 如何给MySQL加索引

  • 创建索引时,你需要确保该索引是应用在 SQL 查询语句的条件(一般作为 WHERE 子句的条件)。
  • 具体的实现有很多种方式,比如建表的时候可以加,在已有的表上可以直接添加等等。下面是在已有的表格上进行添加索引!

添加索引:

ALTER table tableName ADD INDEX indexName(columnName)

4.3 加索引的缺点

  • 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
  • 建立索引会占用磁盘空间的索引文件
  • 即用空间换时间!

猜你喜欢

转载自blog.csdn.net/qq_27782503/article/details/90611655