建立在多进程的基础之上,使用模块进行优化
介绍
优化上一个挑战中完成的计算器,完善下述需求:

  • 使用 getopt 模块处理命令行参数
  • 使用 Python3 中的 configparser 模块读取配置文件
  • 使用 datetime模块写入工资单生成时间

计算器执行中包含下面的参数:

  • -h 或 --help,打印当前计算器的使用方法,内容为:
Usage: calculator.py -C cityname -c configfile -d userdata -o resultdata
  • -C 城市名称 指定使用某个城市的社保配置信息,如果没有使用该参数,则使用配置文件中 [DEFAULT] 栏目中的数据,城市名称不区分大小写,比如配置文件中写的是 [CHENGDU],这里参数可以写 -C Chengdu,仍然可以匹配
  • -c 配置文件 配置文件,由于各地的社保比例稍有不同,我们将多个城市的不同配置信息写入一个配置文件
  • -d 员工工资数据文件 指定员工工资数据文件,文件中包含两列内容,分别为员工工号和工资金额
  • -o 员工工资单数据文件 输出内容,将员工缴纳的社保、税前、税后工资等详细信息输出到文件中

配置文件格式如下,数字不一定非常准确,仅供参考:

[DEFAULT]
JiShuL = 2193.00
JiShuH = 16446.00
YangLao = 0.08
YiLiao = 0.02
ShiYe = 0.005
GongShang = 0
ShengYu = 0
GongJiJin = 0.06

[CHENGDU]
JiShuL = 2193.00
JiShuH = 16446.00
YangLao = 0.08
YiLiao = 0.02
ShiYe = 0.005
GongShang = 0
ShengYu = 0
GongJiJin = 0.06

[BEIJING]
JiShuL = 4251.00
JiShuH = 21258.00
YangLao = 0.08
YiLiao = 0.02
ShiYe = 0.002
GongShang = 0
ShengYu = 0
GongJiJin = 0.12

员工工资数据文件格式每行为 工号,税前工资,举例如下:

101,5000
203,6500
309,15000

输出的员工工资单数据文件每行格式为 工号,税前工资,社保金额,个税金额,税后工资,计算时间如下:

101,5000,825.00,0.00,4175.00,2019-02-01 12:09:32
203,6500,1072.50,12.82,5414.68,2019-02-01 12:09:32
309,15000,2475.00,542.50,11982.50,2019-02-01 12:09:32

计算时间为上一挑战中实现的多进程代码中的进程2计算的时间,格式为 年-月-日 小时:分钟:秒。

程序的执行过程如下,注意配置文件和输入的员工数据文件需要你自己创建并填入数据,可以参考上述的内容示例:

$ ./calculator.py -C Chengdu -c test.cfg -d user.csv -o gongzi.csv

执行成功不需要输出信息到屏幕,执行失败或有异常出现则将错误信息输出到屏幕
系统检测说明
后台有多个脚本对程序文件的路径及运行结果进行检测,如果严格按照实验楼楼赛的标准只给出是否准确的反馈则非常不利于新手排错调试,这里将后台使用的部分测试用例提供出来,大家可以在遇到错误的时候先自行进行排错,提供的部分测试用例仅供参考,如果有任何疑问可以在 QQ 讨论组里与同学和助教进行交流。

如果 /home/shiyanlou/calculator.py 已经完成,点击 提交结果 后遇到报错,那么测试用例的临时文件都会被保留,可以进入到测试文件夹中进行排错。

首先,测试脚本会将 /home/shiyanlou/calculator.py 拷贝到 /tmp/test.py,然后会下载以下测试需要的文件:

/tmp/c5.cfg 配置文件
/tmp/c5user.csv 员工工资数据文件
执行的测试命令:

$ python3 /tmp/test.py -C chengdu -c /tmp/c5.cfg -d /tmp/c5user.csv -o /tmp/c5gongzi.csv

排错的时候可以进入到 /tmp 目录,先检查下输出的文件 /tmp/c5gongzi.csv(员工工资单数据文件)是否存在,如果存在,重点检查下里面的工号为 207 的员工税后工资是否为 14467.77,这个地方是最容易出错的地方,通常都是由于社保基数计算的问题导致的,可以确认下。

另外一个容易出错的地方就是 -C chengdu 这个参数,需要注意以下几点:

chengdu 大小写都应该支持
可以准确的找到并从配置文件中加载 [CHENGDU] 这一个 section 的配置信息
如果挑战 PASS 了,那么 /tmp 目录下的测试文件都会被全部删除

# -*- coding: utf-8 -*-
import sys
import csv
import configparser
from getopt import getopt, GetoptError
from datetime import datetime
from collections import namedtuple
import queue
from multiprocessing import Queue, Process

# 税率表条目类,该类由 namedtuple 动态创建,代表一个命名元组
IncomeTaxQuickLookupItem = namedtuple(
    'IncomeTaxQuickLookupItem',
    ['start_point', 'tax_rate', 'quick_subtractor']
)

# 起征点常量
INCOME_TAX_START_POINT = 5000

# 税率表,里面的元素类型为前面创建的 IncomeTaxQuickLookupItem
INCOME_TAX_QUICK_LOOKUP_TABLE = [
    IncomeTaxQuickLookupItem(80000, 0.45, 15160),
    IncomeTaxQuickLookupItem(55000, 0.35, 7160),
    IncomeTaxQuickLookupItem(35000, 0.30, 4410),
    IncomeTaxQuickLookupItem(25000, 0.25, 2660),
    IncomeTaxQuickLookupItem(12000, 0.2, 1410),
    IncomeTaxQuickLookupItem(3000, 0.1, 210),
    IncomeTaxQuickLookupItem(0, 0.03, 0)
]

class Args(object):
    """
    命令行参数处理类
    """

    def __init__(self):
        # 解析命令行选项
        self.options = self._options()

    def _options(self):
        """
        内部函数,用来解析命令行选项,返回保存了所有选项及其取值的字典
        """

        try:
            # 解析命令行选项和参数,本程序只支持选项,因此忽略返回结果里的参数列表
            opts, _ = getopt(sys.argv[1:], 'hC:c:d:o:', ['help'])
        except GetoptError:
            print('Parameter Error')
            exit()
        options = dict(opts)

        # 处理 -h 或 --help 选项
        if len(options) == 1 and ('-h' in options or '--help' in options):
            print(
                'Usage: calculator.py -C cityname -c configfile -d userdata -o resultdata')
            exit()

        return options

    def _value_after_option(self, option):
        """
        内部函数,用来获取跟在选项后面的值
        """

        value = self.options.get(option)

        # 城市参数可选,其它参数必须提供
        if value is None and option != '-C':
            print('Parameter Error')
            exit()

        return value

    @property
    def city(self):
        """
        城市
        """

        return self._value_after_option('-C')

    @property
    def config_path(self):
        """
        配置文件路径
        """

        return self._value_after_option('-c')

    @property
    def userdata_path(self):
        """
        用户工资文件路径
        """

        return self._value_after_option('-d')

    @property
    def export_path(self):
        """
        税后工资文件路径
        """

        return self._value_after_option('-o')


# 创建一个全局参数类对象供后续使用
args = Args()


class Config(object):
    """
    配置文件处理类
    """

    def __init__(self):
        # 读取配置文件
        self.config = self._read_config()

    def _read_config(self):
        """
        内部函数,用来读取配置文件中指定城市的配置
        """

        config = configparser.ConfigParser()
        config.read(args.config_path)
        # 如果指定了城市并且该城市在配置文件中,返回该城市的配置,否则返回默认的配置
        if args.city and args.city.upper() in config.sections():
            return config[args.city.upper()]
        else:
            return config['DEFAULT']

    def _get_config(self, key):
        """
        内部函数,用来获得配置项的值
        """

        try:
            return float(self.config[key])
        except (ValueError, KeyError):
            print('Parameter Error')
            exit()

    @property
    def social_insurance_baseline_low(self):
        """
        获取社保基数下限
        """

        return self._get_config('JiShuL')

    @property
    def social_insurance_baseline_high(self):
        """
        获取社保基数上限
        """

        return self._get_config('JiShuH')

    @property
    def social_insurance_total_rate(self):
        """
        获取社保总费率
        """

        return sum([
            self._get_config('YangLao'),
            self._get_config('YiLiao'),
            self._get_config('ShiYe'),
            self._get_config('GongShang'),
            self._get_config('ShengYu'),
            self._get_config('GongJiJin')
        ])


# 创建一个全局的配置文件处理对象供后续使用
config = Config()


class UserData(Process):
    """
    用户工资文件处理进程
    """

    def __init__(self, userdata_queue):
        super().__init__()

        # 用户数据队列
        self.userdata_queue = userdata_queue

    def _read_users_data(self):
        """
        内部函数,用来读取用户工资文件
        """

        userdata = []
        with open(args.userdata_path) as f:
            # 依次读取用户工资文件中的每一行并解析得到用户 ID 和工资
            for line in f.readlines():
                employee_id, income_string = line.strip().split(',')
                try:
                    income = int(income_string)
                except ValueError:
                    print('Parameter Error')
                    exit()
                userdata.append((employee_id, income))

        return userdata

    def run(self):
        """
        进程入口方法
        """

        # 从用户数据文件依次读取每条用户数据并写入到队列
        for item in self._read_users_data():
            self.userdata_queue.put(item)


class IncomeTaxCalculator(Process):
    """
    税后工资计算进程
    """

    def __init__(self, userdata_queue, export_queue):
        super().__init__()

        # 用户数据队列
        self.userdata_queue = userdata_queue
        # 导出数据队列
        self.export_queue = export_queue

    @staticmethod
    def calc_social_insurance_money(income):
        """
        计算应纳税额
        """

        if income < config.social_insurance_baseline_low:
            return config.social_insurance_baseline_low * \
                config.social_insurance_total_rate
        elif income > config.social_insurance_baseline_high:
            return config.social_insurance_baseline_high * \
                config.social_insurance_total_rate
        else:
            return income * config.social_insurance_total_rate

    @classmethod
    def calc_income_tax_and_remain(cls, income):
        """
        计算税后工资
        """

        # 计算社保金额
        social_insurance_money = cls.calc_social_insurance_money(income)

        # 计算应纳税额
        real_income = income - social_insurance_money
        taxable_part = real_income - INCOME_TAX_START_POINT

        # 从高到低判断落入的税率区间,如果找到则用该区间的参数计算纳税额并返回结果
        for item in INCOME_TAX_QUICK_LOOKUP_TABLE:
            if taxable_part > item.start_point:
                tax = taxable_part * item.tax_rate - item.quick_subtractor
                return '{:.2f}'.format(tax), '{:.2f}'.format(real_income - tax)

        # 如果没有落入任何区间,则返回 0
        return '0.00', '{:.2f}'.format(real_income)

    def calculate(self, employee_id, income):
        """
        计算单个用户的税后工资
        """

        # 计算社保金额
        social_insurance_money = '{:.2f}'.format(
            self.calc_social_insurance_money(income))

        # 计算税后工资
        tax, remain = self.calc_income_tax_and_remain(income)

        return [employee_id, income, social_insurance_money, tax, remain,
                datetime.now().strftime('%Y-%m-%d %H:%M:%S')]

    def run(self):
        """
        进程入口方法
        """

        # 从用户数据队列读取用户数据,计算用户税后工资,然后写入到导出数据队列
        while True:
            # 获取下一个用户数据
            try:
                # 超时时间为 1 秒,如果超时则认为没有需要处理的数据,退出进程
                employee_id, income = self.userdata_queue.get(timeout=1)
            except queue.Empty:
                return

            # 计算税后工资
            result = self.calculate(employee_id, income)

            # 将结果写入到导出数据队列
            self.export_queue.put(result)


class IncomeTaxExporter(Process):
    """
    税后工资导出进程
    """

    def __init__(self, export_queue):
        super().__init__()

        # 导出数据队列
        self.export_queue = export_queue

        # 创建 CSV 写入器
        self.file = open(args.export_path, 'w', newline='')
        self.writer = csv.writer(self.file)

    def run(self):
        """
        进程入口方法
        """

        # 从导出数据队列读取导出数据,写入到导出文件中
        while True:
            # 获取下一个导出数据
            try:
                # 超时时间为 1 秒,如果超时则认为没有需要处理的数据,退出进程
                item = self.export_queue.get(timeout=1)
            except queue.Empty:
                # 退出时关闭文件
                self.file.close()
                return

            # 写入到导出文件
            self.writer.writerow(item)


if __name__ == '__main__':
    # 创建进程之间通信的队列
    userdata_queue = Queue()
    export_queue = Queue()

    # 用户数据进程
    userdata = UserData(userdata_queue)
    # 税后工资计算进程
    calculator = IncomeTaxCalculator(userdata_queue, export_queue)
    # 税后工资导出进程
    exporter = IncomeTaxExporter(export_queue)

    # 启动进程
    userdata.start()
    calculator.start()
    exporter.start()

    # 等待所有进程结束
    userdata.join()
    calculator.join()
    exporter.join()

2.getopt介绍
getopt这个函数 就是用来抽取 sys.argv 获得的用户输入来确定执行步骤。

getopt是个模块,而这个模块里面又有getopt 函数,所以getopt需要这样这样用。

getopt.getopt( [命令行参数列表], "短选项", [长选项列表] )

该函数返回两个值. opts 和args

opts 是一个存有所有选项及其输入值的元组.当输入确定后,这个值不能被修改了.

args 是去除有用的输入以后剩余的部分.

import getopt,sys
shortargs = 'f:t' #短选项
longargs = ['directory-prefix=', 'format', '--f_long='] #长选项
opts,args= getopt.getopt( sys.argv[1:], shortargs, longargs)
print 'opts=',opts
print 'args=',args

getopt函数的格式是getopt.getopt ( [命令行参数列表], “短选项”, [长选项列表] )
短选项名后的冒号(:)表示该选项必须有附加的参数。
长选项名后的等号(=)表示该选项必须有附加的参数。

try:  
    opts, args = getopt.getopt(sys.argv[1:], "ho:", ["help", "output="])  
except getopt.GetoptError:  
    # print help information and exit:

解释如下:

  1. 处理所使用的函数叫getopt() ,因为是直接使用import 导入的getopt 模块,所以要加上限定getopt 才可以。
  2. 使用sys.argv[1:] 过滤掉第一个参数(它是执行脚本的名字,不应算作参数的一部分)。
  3. 使用短格式分析串"ho:" 。当一个选项只是表示开关状态时,即后面不带附加参数时,在分析串中写入选项字符。当选项后面是带一个附加参数时,在分析串中写入选项字符同时后面加一 个":" 号 。所以"ho:" 就表示"h" 是一个开关选项;“o:” 则表示后面应该带一个参数。
  4. 使用长格式分析串列表:[“help”, “output=”] 。长格式串也可以有开关状态,即后面不跟"=" 号。如果跟一个等号则表示后面还应有一个参数 。这个长格式表示"help" 是一个开关选项;“output=” 则表示后面应该带一个参数。
  5. 调用getopt 函数。函数返回两个列表:opts 和args 。opts 为分析出的格式信息。args 为不属于格式信息的剩余的命令行参数。opts 是一个两元组的列表。每个元素为:( 选项串, 附加参数) 。如果没有附加参数则为空串’’ 。
  6. 整个过程使用异常来包含,这样当分析出错时,就可以打印出使用信息来通知用户如何使用这个程序。

如上面解释的一个命令行例子为:
‘-h -o file --help --output=out file1 file2’
在分析完成后,opts 应该是:
[(’-h’, ‘’), (’-o’, ‘file’), (’–help’, ‘’), (’–output’, ‘out’)]
而args 则为:
[‘file1’, ‘file2’]
转载:

3.Python3中的configparser模块

configparser模块简介
该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)与java原先的配置文件相同的格式

看一下configparser生成的配置文件的格式

[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes
  
[bitbucket.org]
User = Atlan
  
[topsecret.server.com]
Port = 50022
ForwardX11 = no

现在看一下类似上方的配置文件是如何生成的

import configparser #引入模块

config = configparser.ConfigParser()    #类中一个方法 #实例化一个对象

config["DEFAULT"] = {'ServerAliveInterval': '45',
                      'Compression': 'yes',
                     'CompressionLevel': '9',
                     'ForwardX11':'yes'
                     }	#类似于操作字典的形式

config['bitbucket.org'] = {'User':'Atlan'} #类似于操作字典的形式

config['topsecret.server.com'] = {'Host Port':'50022','ForwardX11':'no'}

with open('example.ini', 'w') as configfile:

   config.write(configfile)	#将对象写入文件

解释一下,操作方式

config["DEFAULT"] = {'ServerAliveInterval': '45',
                      'Compression': 'yes',
                     'CompressionLevel': '9',
                     'ForwardX11':'yes'
                     }	#类似于操作字典的形式
#config后面跟的是一个section的名字,section的段的内容的创建类似于创建字典。类似与字典当然还有别的操作方式啦!
config['bitbucket.org'] = {'User':'Atlan'}  #类似与最经典的字典操作方式

和字典的操作方式相比,configparser模块的操作方式,无非是在实例化的对象后面,跟一个section,在紧跟着设置section的属性(类似字典的形式)

读文件内容
import configparser

config = configparser.ConfigParser()

#---------------------------查找文件内容,基于字典的形式

print(config.sections())        #  []

config.read('example.ini')

print(config.sections())        #   ['bitbucket.org', 'topsecret.server.com']

print('bytebong.com' in config) # False
print('bitbucket.org' in config) # True


print(config['bitbucket.org']["user"])  # Atlan

print(config['DEFAULT']['Compression']) #yes

print(config['topsecret.server.com']['ForwardX11'])  #no


print(config['bitbucket.org'])          #<Section: bitbucket.org>

for key in config['bitbucket.org']:     # 注意,有default会默认default的键
    print(key)

print(config.options('bitbucket.org'))  # 同for循环,找到'bitbucket.org'下所有键

print(config.items('bitbucket.org'))    #找到'bitbucket.org'下所有键值对

print(config.get('bitbucket.org','compression')) # yes       get方法Section下的key对应的value

修改

import configparser

config = configparser.ConfigParser()

config.read('example.ini')  #读文件

config.add_section('yuan')  #添加section



config.remove_section('bitbucket.org') #删除section
config.remove_option('topsecret.server.com',"forwardx11") #删除一个配置想


config.set('topsecret.server.com','k1','11111')
config.set('yuan','k2','22222')
with open('new2.ini','w') as f:
     config.write(f)