PythonETL课程笔记

PythonETL背景介绍

一、背景:

1 、数据孤岛-现象

各业务数据不互通,无法发挥数据的利用价值。

2、数据仓库-解决

通过工具将零散数据集中存储(Hadoop的hdfs组件工具)、分析(mapreduce或spark),最大限度挖掘数据价值。

3、 ETL的引出-具体

ETL:Extract-Transform-Load,数据从源头抽取(输入)、转换(处理)、加载(输出)到目的地。即对数据进行高效收集。常在数仓中用,不仅限用在数仓。

4、 ETL实现

ETL实现工具(简单型):Kettle、Datax、Sqoop

工具优势:简单易用,配置方便,无需代码鼠标拖拽

工具劣势:不灵活,特定任务难以实现

代码Python、Java、Scala完成ETL开发(复杂型)

优势:自由处理

劣势:开发时间长

二、常见数据格式:

CSV、JSON、XML

1、CSV格式(Comma-Separated Values)逗号分隔值,又名字符分隔值,常用半角逗号(,)或制表符(/t),也可用其他符号。

将txt文件改为csv(修改文件属性后缀:计算机-查看-文件扩展名)本质上仍是文本文件,但格式上是csv(文件类型不只取决于后缀),ctrl+h(替换符号)。

csv可以有表头Header(关系到后续处理),有则将首行加载为列名,否则直接处理。

id,name,age#可有可无
1,天,99
2,地,100

2、JSON格式(JavaScriptObjectNotation)JavaScript对象表示法。Key-value型,与Python中的字典格式一一对应,是字典的静态格式,是字典的字符串形态。

JSON类似XML,存储和交换文本的语法,比XML更小、快、解析。

{
    "sites":{
    {"name":"菜鸟教程","url":"www.bird.com"},
    {"name":"微博","url":"www.weibo.com"}
    }
}
{
    "name":"lisa",`
    "age":10`,
    "like":{"music","run"}`
`}`

3、XML格式(eXtensibleMarkupLanguage)可扩展标记语言。

XML所有标签成对出现,有始有终。

XML传输和存储数据(如用在Hadoop、spark和Hive的配置文件格式),注意把修改的配置项放在特定范围内。

#xml通过作开头,作为结束的配置项格式,note项目有4个子项
<?xml version="1.0" encoding="UTF-8"?>#XML必带
<note>
    <to>Beijing</to>
    <from>Qinhuangdao</from>
    <heading>book</heading>
    <body>Remember me!</body>
<note>
#xml无缝转为Json
{
    "note":{
        "to":"Beijing",
        "from":"Qinhuangdao",
        "heading":"book",
        "body":"Remember me!"
    }
}

采集JSON,生成CSV。

三、 数据结构类型:

常见数据:非结构、半结构、结构化数据,数据结构VS数据类型

1、结构化数据:

判断:可按行列存入到MySQL/Excel中。数据有表结构(schema,对一种数据结构的描述,数据结构即规范化,定义列名和列类型)。

information
name(string)age(int)address(string)
Lisa20Shanghai

常见结构化数据:可用表结构、MySQL/Excel/CSV中的数据。JSON不一定是结构化,根据内容判断,如简单的Key-Value是,复杂嵌套不是(无法用二维表表示)。

20230815,0101-0104(63min),3h。


2、半结构化数据:

判断:数据本身有特定描述形式/组织形式,但不能二维表达。如XML无法无损转化为二维形式。

<!--xml-->
<configuration>
    <property>
        <name>dfs.datanode.data.dir.perm</name>
        <value>700</value>
    </property>
​
  <property>
    <name>dfs.namenode.name.dir</name>
    <value>/data/nn</value>
  </property>
<configuration>
<!--YAML-->
companies:
    -
        id:1
        name:company1
        price:200w
    -
        id:2
        name:company2
        price:300w
<!--ini-->
[win32**.mapi]
dsp=*
cl=*
libs=*
[win32**.exchange]
#重复以上

JSON:获取数据之前是不知道表结构(因为key:value中的value可以是字符串、数字等,也可以是嵌套)。有的JSON无法用固定schema描述,只能描述部分,不能描述全部。

# 获取的数据
{"name": "zhangsan", "age": 10, "like": ["football", "music"]}
{"string": "string", "string": int, "string": Array[String]} # 表结构
# 以下数据的表结构和上面的数据的表结构同,但表结构能力有限,不能完全约束数据,可能产生歧义?
{"title": "developer", "level": 10, "like": ["football", "music"]}

3、非结构化数据:

判断:完全无法用schema定义

举例:代码、程序日志、协议、二进制、音图视频、PPT

4、数据结构和数据类型的区别

数据类型:一个具体的数据类型

数据结构:一堆数据组合一起形成的数据整体

数据结构可以包含各种各样的数据类型。

四、 需求分析:

背景说明,需采集的数据,在哪,输到哪?

1、背景说明:

#JSON格式化软件https://cn.bing.com/-搜json格式化-找线上

零售公司的数据案例

 #,JSON数据文件,收银机里的一个订单记录
 "discountRate": 1,
  "storeShopNo": "None",
  "dayOrderSeq": 37,
  "storeDistrict": "芙蓉区",
  "isSigned": 0,
  "storeProvince": "湖南省",
  "origin": 0,
  "storeGPSLongitude": "undefined",
  "discount": 0,
  "storeID": 1766,
  "productCount": 1,
  "operatorName": "OperatorName",
  "operator": "NameStr",
  "storeStatus": "open",
  "storeOwnUserTel": 12345678910,
  "payType": "cash",
  "discountType": 2,
  "storeName": "亿户超市郭一一食品店",
  "storeOwnUserName": "OwnUserNameStr",
  "dateTS": 1542436490000,
  "smallChange": 0,
  "storeGPSName": "None",
  "erase": 0,
  "product": [
    {
      "count": 1,
      "name": "南京特醇",
      "unitID": 8,
      "barcode": "6901028300056",
      "pricePer": 12,
      "retailPrice": 12,
      "tradePrice": 11,
      "categoryID": 1
    }
  ],
  "storeGPSAddress": "None",
  "orderID": "154243648991517662217",
  "moneyBeforeWholeDiscount": 12,
  "storeCategory": "normal",
  "receivable": 12,
  "faceID": "",
  "storeOwnUserId": 1694,
  "paymentChannel": 0,
  "paymentScenarios": "OTHER",
  "storeAddress": "StoreAddress",
  "totalNoDiscount": 12,
  "payedTotal": 12,
  "storeGPSLatitude": "undefined",
  "storeCreateDateTS": 1540793134000,
  "storeCity": "长沙市",
  "memberID": "0"
}

2、需采集的数据:

三份数据源(以零售业务为例):

(1)JSON:订单业务

在哪:通过网络发到后台的订单信息,存JSON中。

(2)后台MySQL:商品信息

在哪:存储商品的条码信息,存后台MySQL中。

(3)后台文本文件:log日志的服务业务数据(服务器运行性能指标)

在哪:记录后台的被访问信息,存在后台日志文件中。(启动数据产生的模拟器来产生日志数据)

三份数据都输出到MySQL(数仓技术)和CSV中。

2023.8.16,0105-0107(34min),1.5h


项目需求1:JSON数据处理I

一、JSON数据文件

1、生成逻辑

后端服务器定期写一个JSON文件(每次生成1M左右)。

我们任务-定期读取并处理源源不断的JSON(源数据-待采集-内存)。

tip-不重复处理已处理过的文件。方法-已处理的文件存文件或数据库表中,存数据库好处(把文件名存数据库的表中)-安全不易受损有专业运维+便捷(存储或修改只需改SQL)。

科普区分:内存(!=内部存储),正确的内存=计算机的Memory。hdfs分布式文件系统大小取决于机器数量。

2、JSON数据格式

3、采集和处理方式

建立元数据库存储元数据(元数据metadata即描述数据的数据,如属性,数本身据属于业务数据)-用来存储是否被采集过的状态信息,避免重复采集。在案例中,被处理文件的文件名就是元数据。

二、项目开发-初始化

1、项目中使用MySQL

在项目中接触三个数据库:在企业中三个数据库分别在不同服务器(电脑)上操作,模拟学习时暂时在一个MySQL中使用。

  • 提供数据的数据源库

  • 元数据管理的库

  • 数据处理的目的地库(通过ETL转化处理后的数据)

    右键-New-Query Console?安装数据库软件

    导入源数据:通过datagroup?打开数据库-自动在数据库显示-右键run-Edit Configuration-Target data source+-python-etl下的source_data-apply-run。

2、创建项目的PyCharm工程

Pure Python-选择目录(python-etl)-Previously configred interpreter解释器-创建4个package(其下有init—py文件,Directory下无)

  • config(记录ETL工程的配置信息)

  • model(记录数据模型)

  • test(做单元测试)

  • util(记录工具方法)

三、项目开发-日志模块

1、日志介绍

日志就是程序在运行时,程序当前状态,通过对外输出信息来确定。对外输出的这个信息,就是`程序的运行日志。

2、logging模块

logging是Python自带的日志输出工具,类似于print语句,可控制输出级别。先编写基本日志模块代码,供业务逻辑代码使用。

2.1日志级别

logging:标准的日志模块,支持日志的级别控制( logging每个具体级别都有一个数字代码)。Directory-Python file。

import logging#不需要导入包,python自带
#复杂计算
a=100
logging.debug("你好,我是debug,计算结果是:"+str(a))
logging.info("你好,我是info")
logging.warning("你好,我是warning")
logging.error("你好,我是error")
logging.fatal("你好,我是fatal")
#输出时默认输出waring及以上级别(warn,30),可灵活设置级别,但小于被设置级别的日志不会输出
#快捷键:settings-Editor-Plugins-IdeaVim插件
#输出
WARNING:root:你好 我是 warning
ERROR:root:你好 我是 error
CRITICAL:root:你好 我是 fatal
  • 50CRITICAL(FATAL):灾难级别,挂了

  • 40ERROR:错误级别,有错但不一样影响运行

  • 30WARN:警告级别,有可能有隐患

  • 20INFO:信息级别,输出普通信息

  • 10DEBUG:调试、话痨级别,输出非常详细的日志

  • NOTSRT=0

2023.8.17,0108-0111(83min),3h。


2.2logging基本使用

可以控制允许哪个级别的日志被输出。

logging的常规使用方式(以下代码在新创建的learning目录下的test_logging完成。注释一段文字的方法?)

#logging的基本使用
#1.获取logger对象
logger=logging.getLogger('test')#获取一个logging对象
​
fmt=logging.Formatter("%(asctime)s-[%(levelname)s]-%(filename)s[%(lineno)d]:%(message)s")#格式化
​
#2.设置logger对象属性
#2.1把日志输出到终端
stream_handler=logging.StreamHandler()#获取一个将日志输出到控制台的Handler对象
stream_handler.setFormatter(fmt)
logger.addHandler(stream_handler)#给logger对象添加了将日志输出到控制台的功能
​
#2.2把日志输出到文件
file_handler=logging.FileHandler(
filename="D:\1.notebook\ETL\logs\test.log",mode="a",encoding="utf-8")#路径必须存在
file_handler.setFormatter(fmt)
logger.addHandler(file_handler)
​
3.使用对象属性
logger.setLevel(10)#控制输出级别
logger.debug("你好,我是debug")
logger.info("你好,我是info")
logger.warning("你好,我是warning")
logger.error("你好,我是error")
logger.fatal("你好,我是fatal")
#for i in range(10):
    #logger.info("你好,我是info "+str(i))

#文件的操作模式:r,w,a,rb,wb

2.3logging输出格式控制

问题引入:若日志输出在终端,不小心把终端关闭,如何调试。把日志写入到日志文件。 #?终端输出后报错,别人调试时自己把窗口关了,再复现错误需运行很久。

详见以上代码的把日志输出到文件,设置文件格式,运行完成后,Filename的路径里产生日志文件

logging中的formatter,提前设定好日志中要展示的信息。#print("%s", % "abc")

import logging
logger=logging.gerlogger("test")
​
#fmt=logging.Formatter("%(name)s")
#终端输出,test
fmt=logging.Formatter("%(name)s-%(levelname)s-%(message)s")
#构造formatter
stream_handler=logging.StreamHandler()
stream_handler.setFormatter(fmt)
logger.addHandler(stream_handler)
​
logger.setlevel(20)
logger.info("你好,我是info")
附录:Formatter参数列表
%(name)s            Name of the logger (logging channel)
%(levelno)s         日志的等级数字
%(levelname)s       日志的等级字符串
%(pathname)s        日志的路径
%(filename)s        输出日志的文件名称
%(module)s          Module (name portion of filename)
%(lineno)d          输出日志的代码的行数
%(funcName)s        方法名称
%(created)f         Time when the LogRecord was created (time.time()
                    return value)
%(asctime)s         输出日志的时间
%(msecs)d           Millisecond portion of the creation time
%(relativeCreated)d Time in milliseconds when the LogRecord was created,
                    relative to the time the logging module was loaded
                    (typically at application startup time)
%(thread)d          Thread ID (if available)
%(threadName)s      Thread name (if available)
%(process)d         Process ID (if available)
%(message)s         日志的正文信息
"""

2.4logging输出到文件中

输出到文件,只需把Handler替换成文件Handle。说明如下:logging有2个常用Handler

  • StreamHandlre:logging.StreamHandler

  • FileHandler:logging.FileHandler(参数1,参数2,参数3)

  • 参数1:文件路径

  • 参数2:模式,“a”是追加,“w”是覆盖

  • 参数3:编码,一般UTF-8

问题引入:每次都在文件中打开,比较繁琐,可以同时在终端和文件中输出。

日志模块梳理:

20230818,0112-0114(59min),3h


待复习3、准备logging工具

4、logging工具单元测试

4.1单元测试

4.2结果验证方法

四、项目开发-处理文件工具

梳理ETL业务逻辑:

  • 查看文件列表:在存放被采集JSON数据的文件夹中

  • 筛选新文件:对比MySQL已记录的信息

  • 读取JSON文件:执行ETL操作(打开读取文件,提取文件信息,写入目的地)

  • 记录已处理文件:将已被处理JSON记录到MySQL数据库

1、创建处理文件工具

做测试learning-test_os.py

import os
#os库
#输出目录中所有文件和文件夹
files=os.listdir("D:/2.project/data/pyetl-data-logs/json/"))#不支持子目录查询
​
#python判断文件、目录,os.path.isdir()
for file in files:
    print(file,os.path.isdir("D:/2.project/data/pyetl-data-logs/json/"+file))#如果不加绝对路径,判断错误?
    #json父目录:相对路径..,绝对路径D:/2.project/data/pyetl-data-logs
    

需求:让工具支持查询目录中的文件功能(预判能力,预判后面的日志存储方式变化,只是有一定扩展性和健壮性,不能过度设计)

思路:递归查询目录文件:获取当前目录下文件名,如果是具体文件,append;如果是目录,传给函数,继续获取文件。介绍斐波那契:做测试learning-fib.py

def fib(num):
    if num==1 or num==2:
        return 1;
    else:
        return fib(num-1)+fib(num-2)
print(fib(20))  

2、常见处理文件单元测试

#coding:utf8
import os
form util.logging-util import init_logger
logger=init_logger()#加日志工具
def get_dir_files_list(path="./",recursive=False):
    """获取path目录中所有文件名称
    #path为被判断文件夹的路径,默认当前路径
    #recursive是否递归,根据需求设定
    #return:list对象,存储文件的路径
    """
    #1.获取制定目录中的文件名称
    #API返回指定path下的文件和文件夹,只接收绝对路径
    dir_names=os.listdir(path)
    logger.debug(dir_names)#加日志工具
    #2.判断是文件还是文件夹,循环取每个名称判断
    files=[]#定义一个list,记录文件
    for dir_name in dir_names:
        logger.debug(dir_name)#加日志工具
        absolute_path=f"{path}/{dir_name}"#f的意思是?
        if not os.path.isdir(absolute_path):
            files.append(absolute)
            logger.debug(files)#加日志工具
        else:
            logger.debug(absolute_path)#加日志工具
            if recursive:
            files+=get_dir_file_list(absolute_path,true)#拼接内层的返回list列,到当前层的list,或者absolute_path用path+dir_name+"/"表示
    return files
​
print(get_dir_files_list("D:/2.project/data/pyetl-data-logs/json/"))#反斜线,最后是反斜线

#for i in range(1,10):a+=[i]

2、创建处理文件单元测试

耦合:不同事情紧密联系在一起(比如代码放的测试老师电脑的路径)

解耦合:让不同事情不紧密联系(如在工程根目录下test_dir文件下建立)#directory

在项目的test目录下创建
        test_dir/
            inner1/
                3
                4
                inner2/
                    5
            1
            2
#动态获取项目目录,一开始不要写死
os.getcwd()获取当前代码所在的目录
os.dirname() 获取指定文件/目录的上一级目录
print(os.path.dirname(os.getcwd())) 即可获取到项目目录D:\project\bigdata\python-etl
​
#os.path.basename
import os
file_name="D:/2.project/data/pyetl-data-logs/json/x01.log"
print(os.path.basename(file_name))
#输出x01.log
​
#dirname+basename
"D:/logs/1.log"
os.dirname("D:/logs/1.log") 得到的是 "D:/logs/"
os.basename("D:/logs/1.log")得到的是 "1.log"

单元测试:在test包中,创建test_file_util.py

#coding:utf8
#python3.0之后默认都是coding:utf8
form unittest import TestCase
from util.file_util import get_dir_files_list
​
class TestFileUtil(TestCase):#自己起名字
    def setUp(self)->None:?
        self.project_root_path=os.dirname(os.getcwd())
        pass#?
    def test_get_dir-files_list(self):
        test_path=f"{self.project_root_path}/test/test_dir/"
        #1.测试不递归,最后加/
        result=get_dir_files_list(test_path,recursive=False)
        files=[]
        for i in result:
            files.appened(os,basename(i))
            files.sort()#排序以便与以下一致
        self.assertEqual(["1","2"],files)
        #2.测试递归
       result=get_dir_files_list(test_path,recursive=True)
        files=[]
        for i in result:
            files.appened(os,basename(i))
            files.sort()#排序以便与以下一致
        self.assertEqual(["1","2","3","4","5"],files)    
        pass
    

五、项目开发-MySQL工具

功能描述:和MySQL内记录的信息做对比,判断哪些文件是新文件,需要采集的。

1、准备配置信息

2、编写MySQL工具类

util包下创建mysql_util

把工具函数定义在类中,可以减少建立类似连接的耗时操作,代码比较规范项目涉及的数据库:元数据库,数据源库,目的地库。

将创建的工具类总需实现以下函数。

  • 1 建立连接和关闭连接

  • 2 执行查询操作

  • 3 执行sql语句,不关心返回结果(一个是带提交,不带提交)

  • 4 查询表是否存在

  • 5 查询表是否存在,不存在创建表

实现的两种方式:

  • 1.不写类,将功能写成方法,重复编写

  • 2.写一个class类,将方法写到类里面

  • 选第二种理由:要维护链接,而class有一个成员变量就是链接,只要class对象没有被销毁,链接成功后,可以持续使用。而直接使用无类的方法的链接,是一次性的。

演示pymysql基本使用,learing下的test_mysql

![微信截图_20230820084534](C:\Users\tianwany\Desktop\微信截图_20230820084534.png)import pymsql
conn=pymysql.Connection(host="localhost",port=306,user="root",password="mysql",charset="utf8",autocommit=False)
#用DataGrip连接数据库,测试操作
#conn切换数据库
conn.select_db("test")
#获取游标对象
cursor=conn.cursor()
#执行SQL语句
cursor.execute("select * from tb_stu")
result=cursor.fetchall()
print(result)
#执行插入语句
cursot.execute("insert into tb_stu values(3,"李四")")#直接打印此句返回的是影响的行数,1
conn.commit()#加上此句是因为设置autocommit为False后,执行插入语句后,只能放在缓存中,无法插入数据库中
cursor.execute("select * from tb_stu")
print("fetchall",cursor.fetchall())
​
#获取数据库表
cursor.execute("SHOW TABLES")
result=cursor.fetchall()
print result
if ("tb_stu",) in result:#注意result存放的元组形式,所以带查询的内容需要有,
    print("存在")
 else:
    print("不存在")

​
​#上图演示如何插入数据库数据,以及用cmd连接数据库

pymysql中Connection有一个autocommit的参数,设置为True后,就不必手动提交commit;否则需手动提交,不提交的话,数据写在缓存中,没有写入数据库。

正式编写MySQL工具类,util下的mysql_util。

![创建新数据库](D:\1.notebook\ETL\未解决插图\创建新数据库.png)"""与mysql操作相关的工具都在此文件"""
import pymysql
from util.logging_util import init_logger
from config import project_config as pc#导包的方式?
​
logger=init_logger()
​
class MySQLUtil:
    def _init_(self,
              host=pc.metadata_host,
              port=pc.metadata_port,
              user=pc.metadata_user,
              password=pc.metadata_password,
              charset=pc.mysql_charset,):#初始化,优化传参,设置默认值
        self.conne=pymysql.Connection(
        host=host,#host="localhost",#主机IP?
           
        port=port,#port=3306,#端口号?
        user=user,#user="root",
        password=password,#password="mysql",
        charset=charset,#charset="utf8",#字符集
        autocommit=False)#不自动提交性能更优
       logger.info(f"构建完成到{pc.metadata_host}:{pc.metadata_port}的数据库连接")#加入日志{conf.metadata}:{conf.metadata_port}是不是活动的?
      
    def close_conn(self):#建立连接
        if self.conn:
            self.conn.close()
    def select_db(self,db):#切换数据库?
        self.conn.select_db(db)
    def query(self,sql):#查询
        cursor=self.conn.cursor()
        cursor.execute(sql)
        result=cursor.fetchall()
        cursor.close()
        logger.info(f"执行查询的SQL语句完成,查询结果有{len(result)}条,执行的查询SQL是:{sql})#加入日志
        return result
    def execute(self,sql):#执行SQL语句,不关心返回结果,(自动更新数据库)。
        #确保修改的数据会提交到数据库中。
        cursor=self.conn.cursor()
        cursor.execute(sql)#此句调用了execute函数,但不确定是否写入数据库
        #如果autocommit=False,还需执行commit
        logger.debug(f"执行了一条SQL:{sql}")#加入日志                    
        if self.conn.get_autocommit():
            pass
        else:
            self.conn.commit()
        cursor.close()
    def execute_without_commit(self,sql):#直接执行SQL语句(手动更新数据库),不关心是否提交,如遇到大量数据,可以批量提交
        cursor=self.conn.cursor()
        cursot.execute(sql)
        logger.debug(f"执行了一条SQL:{sql}")#加入日志           
        cursor.close()
    def check_table_exists(self,db_name,table_name):#查询表是否存在
        self.select_db(db_name)#添加切换数据库,更严谨
        #cursor=self.conn.cursor()
        #cursor.execute("SHOW TABLES")
        #result=cursor.fetchall()#后加入此句
        result=self.query("SHOW TABLES")#此句可以替代原来的4句
        return (table_name,) in result#最简洁版
        #cursor.close()
    def check_table_exists_and_create(self,db_name,table_name,create_cols):#查询表是否存在,不存在则创建
        if not self.check_table_exists(db_name,table_name):
            create_sql=f"CREATE TABLE {table_name}({creat_cols})"
            self.select_db(db_name)#切换数据库
            self.execute( create_sql)
            logger.info(f"在数据库:{db_name}中创建了表,{table_name}完成,建表语句是:{create_sql}")#加入日志  
      else:      
            logger.info(f"数据库:{db_name}中,表{table_name}已经存在,创建表的操作跳过")#加入日志
                    
 if _name_=="_main_"#直接输入main,有无此行都行
    mysql_util=MySQLUtil()
    mysql_util.select_db("test")
    print(mysql_util.query("select * from tb_stu"))#养成好习惯,写入函数时适时做测试
    mysql_util.execute("insert into tb_stu values(5,"王五")")
    mysql_util.execute——without_autocommit("insert into tb_stu values(6,"刘七")")#不显示插入数据库
    mysql_util.check-table_exists("test",)
    print(mysqlutil.check_table_exists_and_create("test","tb_stu5","name varchar(20),age int"))
​

#上图演示mysql中列举所有数据库表的方法

20230820,0211-0216(75min),3.5h


实现init函数的不足与需求:前面代码把连接的数据库写死了。需求是可以通过这个MySQLUtil连接到三个数据库,需将这些设置在config中配置。

重构init(在config包下的project_config)

import time
#日志相关配置,开始部分,?
log_root_path="D:/logs/"
log_name=f"pyetl-{time.strftime("Y%m%d-%H",time.localtime(time.time()))}.log"
log_level=10
log_fommatter="%(asctime)s-[%(levelnam)s]-%(filename)s[%(lineno)d]:%(message)s"
#元数据库相关的配置,开始
mysql_charset="utf8"
#元数据管理库的配置,可通过更改配置,连接任何一个数据库
metadata_host="localhost"
metadata_user="root"
metadata_password="123456"
metadata_port=3306
#之后在mysql_util中导包,详见以上代码

3、MySQL工具类单元测试

测试分析:1.工具的一个特性是复用性,编完后注意检查bug,所以会做测试;2.解耦合思想(如测试时自创表格,数据库不动);3.以最少测试内容覆盖全部函数,若一个函数包含多个函数,可以在测试该函数时,省略测试其包含的函数。

#语句,truncate与delete删除区别,前者是ddl语句(dql,dml,dcl),表级删除,保留表结构先删除再重建,?执行trucate是为防止之前已创建此表。

在test包下,建立test_mysql_util.py

#先写需求,再写框架

"""mysql_util单元测试"""
from unittest import TestCase#?
form util.mysql_util import MySQLUtil
class MySQLUtilTest(TestCase):
    def setUp(self)->None:
        self.db_util=MySQLUtil()
        
    def test_query(self):#一次性测试多个函数
        #确保解耦合,先创建一张表,查询数据,从而进行判断
        self.db_util.select_db("test")#先写死
        self.db_util.check_table_exists_and_create(
            "test",                      
            "for_unit_test".
            "id int primary key,name varchar(255)")
        self.db_util.execute("TRUNCATE for_unit_test")
        self.db_util.execute("INSERT INTO for_unit_test VALUES(1,"天天"),(2,“地地)")
        #数据库已准备好,准备测试
        result=self.db_util.query("SELECT * FROM for_unit_test ORDER BY id")#接收
        expected=((1,"天天"),(2,“地地))#写入预期值
        self.asserEqual(ecpected,result)
        #测试完后清理表
        self.db_util.execute("DROP TABLE for_unit_test")
        self.db_util.close_conn()
       
    def test_execute_without_autocommit(self):
        #先设置autocommit为true,函数行为和前面execute行为一致
        self.db_util.conn.autocommit(True)
        #与前面测试重复开始
             self.db_util.select_db("test")#先写死
             self.db_util.execute("drop table for_unit-test2")#代码优化,防止已存在此表,但字段不一致
        self.db_util.check_table_exists_and_create(
            "test",                      
            "for_unit_test2",
            "id int primary key,name varchar(255)")
        self.db_util.execute("TRUNCATE for_unit_test2)
        self.db_util.execute("INSERT INTO for_unit_test2 VALUES(1,"天天")")
        #数据已准备好,准备测试
        result=self.db_util.query("SELECT * FROM for_unit_test2 ORDER BY id")#接收
        expected=((1,"天天"))#写入预期值
        self.asserEqual(ecpected,result)      
        #测试完后没清理表
        self.db_util.close_conn()#必须关掉,重新创建新的连接,防止缓存,否则后面测试能查询到。
        #与前面测试重复结束
        
        #专为插入数据建立连接,设置autocommit为False
        new_util=MySQLUtil()
        new_util.select_db("test")             new_util.conn.autocommit(False)         new_util.execute_without_autocommit("INSERT INTO for_unit_test2 VALUES(2,"地地")")
        new_util.close.conn()                   #专为查询数据建立连接   
        new_util2=MySLQUtil()
        new_util2.select_db("test")
        new_util                     
        result=new_util2.query("SELECT * FROM for_unit_test2 ORDER BY id)")
        expected=((1,"天天"),)#猜刚才的非autocommit并没有写入
        self.assertEqual(expected,result)  
        #清理单元测试残留
        new_util2.execute("DROP TABLE for_unit_test2")                    
         new_util2.close.conn()         

20230821,0217-0220(59min),3.5h


六、项目开发-对比工具方法

1、读取处理过的文件

数据库认知:元数据库、数据源库、目的地库三个数据库指代的是三台不同的电脑服务器,在每个服务器下可以创建MySQL数据库

对比思路:从文件夹中读取待处理文件VS从MySQL中可以读取到已经处理过的文件。

代码思路(切换数据库;查询是否存在工作表,不存在则创建;查询工作表中的文件,将元组形式转化为列表形式)

在util下的mysql_util.py中添加以下代码(与MySQLUtil不是一个类)

def get processed_files(
    #配置信息已在之前导入
#from config import project_config as pc
    db_util,#实例化MSQL对象
    db_name=pc.metadata_db_name,#默认固定值,设置为metadata
    table_name=pc.metadata_file_monitor_table_name,#默认固定
    create_cols=pc.metadata_file_mointor_table_create_cols#默认固定
):
    db_util.select_db(db_name)
    db_util.check_table_exists_and_create(db_name,table_name,create_cols)
    result=db_util.query(f"SELECT file_name from {table_name}")#每行取一个元素
    logger.debug(result)
    processed_files=[]
    for r in result:
        processed_files.append(r[0])#拿到文件名
        logger.debug(processed_files)
        return processed_files
        
在main里
mysql_util=MySQLUtil()#默认连接元数据库
get_processed_files(mysql_util)#这里只用传入util,其他参数默认
#运行后可在metadata中看到相关表格,演示时手动添加数据,其中时间戳自动默认
#常见的约束:主键约束(非空、唯一)
#判断日志输出等级
#debug(不重要,仅仅为演示,打印),info(开发时使用,按执行过程,输出每一步执行情况),warning(如做了一个强制类型转型,虽然不匹配但没报错),error(发生异常),fatal(异常无法处理)
#产品开发有不同阶段,开发(level级别低,方便调试)-发布(level设置高,万一出现异常可以调试)。
#配置文件,在config包下的project_config中,元数据库相关的配置
metadata_db_name="metadata"
metadata_file_monitor_table_name="file_monitor"
metadata_file_mointor_table_create_cols="""#三个双引号一般用作引入多行字符串,与""、''类似,前者换行时不会自动出现\
    id INT PRIMARY KEY AUTO_INCREMENT,
    file_name VARCHAR(255) UNIQUE NOT NULL COMMENT "被处理的文件名称"
    process_lines INT COMMENT"本文件中有多少数据被处理"
    process_times TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT "处理时间"
"""

20230822,0303-0306(48min),2h


2、创建对应代码单元测试

在test包下的test_mysql_util中

from util.mysqlutil import MySQLUtil.get_processed_files
from config import project_config as pc
def test_get_processed_files(self):
    self.db_util.select_db("test")#根据报错提示添加,此句与下句搭配
    self.db_util.execute("drop table if exists "test_file_monitor"")
    self.db_util.check_table_exists_and_create(
    "test",
    "test_file_monitor",
        pc.metadata_file_monitor_table_create_cols   
    )
    self.db_util.execute("""
    INSERT INTO test_file_monitor VALUES("e:/data.log",1024)""")
  result=get_processed_files(self.db_util,"test","test_file_monitor")
    self.assertEqual(["e:/data.log"].result)
    self.db_util.execute("drop table if exists 'test_file_monitor'")
    self.db_util.close_conn()

3、创建对比工具

在util包下的file_util中

def get_new_by_compare_lists(a_list,b_list):
    new_list=[]
    for item in b_list:
        if item not in a_list:
            new_list.append(item)
     return new_list
#实现方式2:两个集合相减,先将列表转集合,结果是在第一个集合中且不在第二个集合中的元素
def get_new_by_compare_list1(a_list,b_list):
    a_set=set(a_list)
    b_set=set(b_list)
    result=list(b_set-a_set)#最后得转列表.有个隐患,集合转为列表后,顺序不一定与对比的列表同
    result.sort()#直接在原表上排序,返回值为空,不能直接return
    return result
​

4、创建对比工具单元测试

在test包下的file_util中

def test_new_by_comparelists(self):
    a_list= ['e:/a.txt', 'e:/b.txt']
    b_list=['e:/a.txt', 'e:/b.txt', 'e:/c.txt', 'e:/d.txt']
    result1=get_new_by_compare_lists(a_list,b_list)
   self.assertEqual(['e:/c.txt', 'e:/d.txt'],result1) result2=get_new_by_compare_lists(a_list,b_list)
    self.assertEqual(['e:/c.txt', 'e:/d.txt'],result2)

七、项目开发-业务主逻辑

思路:已知全部源文件名,和已处理并存入元数据库的名字,找未处理的文件名

1.业主主逻辑代码实现

在项目根目录下创建json_service.py文件,具体思路:

  • 1)读取json目录下文件,生成一个列表b,

  • 2)读取元数据库file_monitor中已经处理过的文件,生成一个列表a,

  • 3)将两个列表相减,得到需要处理的

"""处理json数据的主要逻辑代码"""
from util.file_util import get_dir_files_list,get_new_by_compare_lists
from util.mysql_util import MySQLUtil.get_processed_files
from util.logging_util import init_logger
from config import project_config as pc
​
if _name_=="_main_":#有没有都可
    logger=init_logger()
    logger.info("读取JSON数据处理,程序开始执行了......")
    #1.通过file_util中的函数获取json目录中的文件
    files=get_dir_files_list(pc.json_data_root_path)
    logger.info(f"判断json的文件夹,发现有如下文件“{files}")
    #2读取元数据库中file_monitor中已经处理过的文件,生成一个列表
    meta_util=MySQLUtil()
    processed_files=get_processed_files(meta_util)
    logger.info(f"查询MySQL,找到有如下文件已经被处理过了:{processed_files}")
    #3 这两个列表相减,得到需要的文件
    need_to_process_files=get_new_by_compare_lists(processed_files,files)
    logger.info(f"经过对比mysql元数据库,找出如下文件供我们处理:{need_to_process_files}")
    
    
#在config包下的project_config中配置json
json_data_root_path="D:/2.project/data/pyetl-data-logs/json/"#注意斜线,路径中的“\t要写成r"\t"或“\\t”。改成自己的路径。路径结尾一定加/,否则会出现拼接错误

project-etl代码梳理

20230823,0307-0311(76min),3h


项目需求1:JSON数据处理II

目的:处理JSON文件内部信息

八、项目开发-扩展知识点

1.用模型记录数据

处理json数据,希望存储到csv mysql中。处理时需要先把json数据加载到计算机内存,需要有一个容器承载这些数据,再把这些数据写入到csv mysql

实现方式有两种:

  • python字典存储(虽然简单快捷但处理数据量有限)。

  • 使用模型(类、class)存储(效率高,结构清晰)。

在learning包下建立test_model.py

class Person:
    def _init_(self,name,age,address,sep=","):
        self.name=name
        self.age=age
        self.address=address
        self.sep=sep
    def to_csv(self):
        return f"{self.name}{self.sep}{self.age}{self.sep}{self.address}"
    def generate_insert_sql(self):
        return f"INSERT INTO person VALUES('{self.name}',{self.age},'{self.address}')"#加''因为是字符串
print(p1, p1.name, p1.age, p1.address)
print(p2, p2.name, p2.age, p2.address)
print(p1.to_csv())
print(p1.generate_insert_sql())
"""自己测试输出的插入语句可行性(在数据库中测试)
CREATE TABLE person(
    name VARCHAR(20),
    age INT,
    address VARCHAR(100)
)
INSERT INTO person VALUES("张三",20."深圳")
"""

2.时间戳的概念(timestamp)

用一个数字来记录时间,这个数字是从UTC时间:1970-01-01 00:00:00开始,到某个时刻过了多少秒(毫秒)。

python里处理时间戳的库:time模块,(还有datatime、pandas等了解即可)。

time.localtime

在learning包下的test-time.py中

import time
print(time.strftime("%Y-%m-%d% H:%M:%S"))
print(time.strftime("%Y-%m-%d% H:%M:%S"),time.localtime(time.time()))#与上句结果一样,多了个转换
print(time.time())#获取秒级时间戳,10位
print(time.time()*1000)#获取毫级时间戳,13位
print(time.localtime(time.time()))#打印详细时间信息,还包括季度、一年中第几天等

3.Python中时间戳转换

在util包下的time_util.py中,把输入ts按制定格式转成字符串

#和时间戳相关代码
import time
def ts10_to_date_str(ts,format="%Y-%m-%d %H:%M:%S"):
    #输入10位时间戳
    #文件名中不能有
    #time.strftime两种用法
    #直接获取当前的时间戳转成的字符串,time.strftime("%Y-%m-%d %H:%M:%S")
    #获取指定时间戳转成的字符串time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(ts))
    return time.strftime(format,time.localtime(ts))
def ts13_to_data_str(ts,format="%Y-%m-%d %H:%M:%S"):
    #输入13位时间戳
    #return time.strftime(format,int(ts/1000))
    return ts10_to_date_str(int(ts/1000),format=format)

创建对应代码单元测试

在test包下的test_time_util中

from unittest import TestCase#?
from util.time_util import ts10_to_date_str,ts12_to_date_str
class TimeUtilTest(TestCase):
    def setUp(self)->None:
        pass
    def test_ts10_to_date_str(self):
        #1692847583
        #2023-08-24 11:26:23
        ts=1692847583
        result=ts10_to_date_str(ts)
        self.assertEqual("2023-08-24 11:26:23",result)
    def test_ts13_to_date_str(self):
        #1692847583000
        #2023-08-24 11:26:23
        ts=1692847583000
        result=ts13_to_date_str(ts)
        self.assertEqual("2023-08-24 11:26:23",result)
        
 
    
    

20230824,0312-0314(55min),2h(至少)


九、项目开发-模型设计

订单模型构造思路

1)一条订单数据示例,针对JSON数据我们设计2个模型存储数据:订单模型(OrdersModel)和产品模型(OrdersDetailModel)。

2)最终写入数据库时,需创建两张表:

订单表可以作为主键的: "orderID": "154245064025224198149"

订单详情作为主键的:联合主键,用订单表的orderID和barcode一起作为主键

{
    "discountRate": 1,
    "storeShopNo": "None",
    "dayOrderSeq": 10,
    "storeDistrict": "芙蓉区",
    "isSigned": 0,
    "storeProvince": "湖南省",
    "origin": 0,
    "storeGPSLongitude": "112.97851",
    "discount": 0,
    "storeID": 2419,
    "productCount": 7,
    "operatorName": "OperatorName",
    "operator": "NameStr",
    "storeStatus": "open",
    "storeOwnUserTel": 12345678910,
    "payType": "cash",
    "discountType": 2,
    "storeName": "肖之艳南食店",
    "storeOwnUserName": "OwnUserNameStr",
    "dateTS": 1542450646000,
    "smallChange": 0,
    "storeGPSName": "None",
    "erase": 0,
    "product": [{
        "count": 1,
        "name": "伊利安慕希黄桃燕麦味酸奶200g",
        "unitID": 2,
        "barcode": "6907992513560",
        "pricePer": 7,
        "retailPrice": 7,
        "tradePrice": 0,
        "categoryID": 11
    }, {
        "count": 1,
        "name": "京牛香素食烤鸭90g",
        "unitID": 7,
        "barcode": "6935814600112",
        "pricePer": 2.5,
        "retailPrice": 2.5,
        "tradePrice": 0,
        "categoryID": 10
    }, {
        "count": 1,
        "name": "QQ糖双色可乐味70g",
        "unitID": 0,
        "barcode": "6920952712254",
        "pricePer": 3.5,
        "retailPrice": 3.5,
        "tradePrice": 0,
        "categoryID": 1
    }, {
        "count": 1,
        "name": "奥利奥夹心原味高原装116g",
        "unitID": 0,
        "barcode": "6901668005717",
        "pricePer": 6,
        "retailPrice": 6,
        "tradePrice": 0,
        "categoryID": 1
    }, {
        "count": 1,
        "name": "阿胶枣",
        "unitID": 4,
        "barcode": "6959435300091",
        "pricePer": 4,
        "retailPrice": 4,
        "tradePrice": 0,
        "categoryID": 1
    }, {
        "count": 1,
        "name": "润口香甜王米风味香肠",
        "unitID": 0,
        "barcode": "6902890237754",
        "pricePer": 7,
        "retailPrice": 7,
        "tradePrice": 0,
        "categoryID": 1
    }, {
        "count": 1,
        "name": "好丽友派巧克力味6枚204g",
        "unitID": 3,
        "barcode": "6920907800944",
        "pricePer": 9,
        "retailPrice": 9,
        "tradePrice": 0,
        "categoryID": 10
    }],
    "storeGPSAddress": "None",
    "orderID": "154245064025224198149",
    "moneyBeforeWholeDiscount": 39,
    "storeCategory": "normal",
    "receivable": 39,
    "faceID": "",
    "storeOwnUserId": 2364,
    "paymentChannel": 0,
    "paymentScenarios": "OTHER",
    "storeAddress": "StoreAddress",
    "totalNoDiscount": 39,
    "payedTotal": 39,
    "storeGPSLatitude": "28.187935",
    "storeCreateDateTS": 1542358458000,
    "storeCity": "长沙市",
    "memberID": "0"
}

1.新增MySQL表的相关配置

在在project_config.py中

#在目的地数据库的相关配置
target_host=metadata_host
target_user=metadata_user
target_password=metadata_
target_port=metadata_port
target_db_name="retail"
​
# JSON数据采集后,写入MySQL,存储订单相关的表,表名是:
target_orders_table_name = "orders"
# orders表的建表语句的列信息
target_orders_table_create_cols = \
    f"order_id VARCHAR(255) PRIMARY KEY, " \
    f"store_id INT COMMENT '店铺ID', " \
    f"store_name VARCHAR(30) COMMENT '店铺名称', " \
    f"store_status VARCHAR(10) COMMENT '店铺状态(open,close)', " \
    f"store_own_user_id INT COMMENT '店主id', " \
    f"store_own_user_name VARCHAR(50) COMMENT '店主名称', " \
    f"store_own_user_tel VARCHAR(15) COMMENT '店主手机号', " \
    f"store_category VARCHAR(10) COMMENT '店铺类型(normal,test)', " \
    f"store_address VARCHAR(255) COMMENT '店铺地址', " \
    f"store_shop_no VARCHAR(255) COMMENT '店铺第三方支付id号', " \
    f"store_province VARCHAR(10) COMMENT '店铺所在省', " \
    f"store_city VARCHAR(10) COMMENT '店铺所在市', " \
    f"store_district VARCHAR(10) COMMENT '店铺所在行政区', " \
    f"store_gps_name VARCHAR(255) COMMENT '店铺gps名称', " \
    f"store_gps_address VARCHAR(255) COMMENT '店铺gps地址', " \
    f"store_gps_longitude VARCHAR(255) COMMENT '店铺gps经度', " \
    f"store_gps_latitude VARCHAR(255) COMMENT '店铺gps纬度', " \
    f"is_signed TINYINT COMMENT '是否第三方支付签约(0,1)', " \
    f"operator VARCHAR(10) COMMENT '操作员', " \
    f"operator_name VARCHAR(50) COMMENT '操作员名称', " \
    f"face_id VARCHAR(255) COMMENT '顾客面部识别ID', " \
    f"member_id VARCHAR(255) COMMENT '顾客会员ID', " \
    f"store_create_date_ts TIMESTAMP COMMENT '店铺创建时间', " \
    f"origin VARCHAR(255) COMMENT '原始信息(无用)', " \
    f"day_order_seq INT COMMENT '本订单是当日第几单', " \
    f"discount_rate DECIMAL(10, 5) COMMENT '折扣率', " \
    f"discount_type TINYINT COMMENT '折扣类型', " \
    f"discount DECIMAL(10, 5) COMMENT '折扣金额', " \
    f"money_before_whole_discount DECIMAL(10, 5) COMMENT '折扣前总金额', " \
    f"receivable DECIMAL(10, 5) COMMENT '应收金额', " \
    f"erase DECIMAL(10, 5) COMMENT '抹零金额', " \
    f"small_change DECIMAL(10, 5) COMMENT '找零金额', " \
    f"total_no_discount DECIMAL(10, 5) COMMENT '总价格(无折扣)', " \
    f"pay_total DECIMAL(10, 5) COMMENT '付款金额', " \
    f"pay_type VARCHAR(10) COMMENT '付款类型', " \
    f"payment_channel TINYINT COMMENT '付款通道', " \
    f"payment_scenarios VARCHAR(15) COMMENT '付款描述(无用)', " \
    f"product_count INT COMMENT '本单卖出多少商品', " \
    f"date_ts TIMESTAMP COMMENT '订单时间', " \
    f"INDEX (receivable), INDEX (date_ts)"
​
# JSON数据采集后,写入MySQL,存储订单详情(带商品信息的)相关的表,表名是:
target_orders_detail_table_name = "orders_detail"
# orders_detail表的建表语句的列信息
target_orders_detail_table_create_cols = \
    f"order_id VARCHAR(255) COMMENT '订单ID', " \
    f"barcode VARCHAR(255) COMMENT '商品条码', " \
    f"name VARCHAR(255) COMMENT '商品名称', " \
    f"count INT COMMENT '本单此商品卖出数量', " \
    f"price_per DECIMAL(10, 5) COMMENT '实际售卖单价', " \
    f"retail_price DECIMAL(10, 5) COMMENT '零售建议价', " \
    f"trade_price DECIMAL(10, 5) COMMENT '贸易价格(进货价)', " \
    f"category_id INT COMMENT '商品类别ID', " \
    f"unit_id INT COMMENT '商品单位ID(包、袋、箱、等)', " \
    f"PRIMARY KEY (order_id, barcode)"
​

2.OrdersModel

2.1构建模型

用json库加载字符串演示(在learning包中的test_model.py中)。最繁琐的是输入字段,易出错。里面不含product的model。

import json
data="""
{
  "orderID":"1233123123123312",#注意别少个逗号
  "storeProvince": "湖南省",
  "payType": "wechat",
  "storeName": "湖南平价特产总汇",
  "dateTS": 1542458768000,
  "product": [
    {
      "count": 3,
      "name": "伊利优酸乳草莓味250ml",
      "unitID": 3,
      "barcode": "6907992100012",
      "pricePer": 2.5,
      "retailPrice": 2.5,
      "tradePrice": 1.8,
      "categoryID": 11
    },
    {
      "count": 3,
      "name": "巧克力面包",
      "unitID": 4,
      "barcode": "6971518660038",
      "pricePer": 3,
      "retailPrice": 3,
      "tradePrice": 1.5,
      "categoryID": 1
    }
  ]
}
"""
print(type(data))#字符串
print(type(json.loads(data)))#字典,json字符串时Python字典的静态表示方式
json_dict=json.loads(data)#字符串转字典的方法
print(json_dict["orderID"])
print(json_dict["product"])
​
部分输出结果
<class 'str'>
<class 'dict'>
1233123123123312
[{'count': 3, 'name': '伊利优酸乳草莓味250ml', ···}, {'count': 3, 'name': '巧克力面包', ···}]
​

2.2构建订单类

在model包下建立retail_orders_model.py

class OrderModel:
    def _init_(self,data:str):#data:str意思是设定输入格式,是建议类型,不是强制。python是动态类型的语言,输入int可能也不会报错
        data=json.loads(data)
                self.discount_rate = data['discountRate']                   # 折扣率
        self.store_shop_no = data['storeShopNo']                    # 店铺店号(无用列)
        self.day_order_seq = data['dayOrderSeq']                    # 本单为当日第几单
        self.store_district = data['storeDistrict']                 # 店铺所在行政区
        self.is_signed = data['isSigned']                           # 是否签约店铺(签约第三方支付体系)
        self.store_province = data['storeProvince']                 # 店铺所在省份
        self.origin = data['origin']                                # 原始信息(无用)
        self.store_gps_longitude = data['storeGPSLongitude']        # 店铺GPS经度
        self.discount = data['discount']                            # 折扣金额
        self.store_id = data['storeID']                             # 店铺ID
        self.product_count = data['productCount']                   # 本单售卖商品数量
        self.operator_name = data['operatorName']                   # 操作员姓名
        self.operator = data['operator']                            # 操作员ID
        self.store_status = data['storeStatus']                     # 店铺状态
        self.store_own_user_tel = data['storeOwnUserTel']           # 店铺店主电话
        self.pay_type = data['payType']                             # 支付类型
        self.discount_type = data['discountType']                   # 折扣类型
        self.store_name = data['storeName']                         # 店铺名称
        self.store_own_user_name = data['storeOwnUserName']         # 店铺店主名称
        self.date_ts = data['dateTS']                               # 订单时间
        self.small_change = data['smallChange']                     # 找零金额
        self.store_gps_name = data['storeGPSName']                  # 店铺GPS名称
        self.erase = data['erase']                                  # 是否抹零
        self.store_gps_address = data['storeGPSAddress']            # 店铺GPS地址
        self.order_id = data['orderID']                             # 订单ID
        self.money_before_whole_discount = data['moneyBeforeWholeDiscount']  # 折扣前金额
        self.store_category = data['storeCategory']                 # 店铺类别
        self.receivable = data['receivable']                        # 应收金额
        self.face_id = data['faceID']                               # 面部识别ID
        self.store_own_user_id = data['storeOwnUserId']             # 店铺店主ID
        self.payment_channel = data['paymentChannel']               # 付款通道
        self.payment_scenarios = data['paymentScenarios']           # 付款情况(无用)
        self.store_address = data['storeAddress']                   # 店铺地址
        self.total_no_discount = data['totalNoDiscount']            # 整体价格(无折扣)
        self.payed_total = data['payedTotal']                       # 已付款金额
        self.store_gps_latitude = data['storeGPSLatitude']          # 店铺GPS纬度
        self.store_create_date_ts = data['storeCreateDateTS']       # 店铺创建时间
        self.store_city = data['storeCity']                         # 店铺所在城市
        self.member_id = data['memberID']                           # 会员ID
​
2.3添加to_csv功能:

在model包下添加retail_orders_model.py

from json
from util import time_util
def to_csv(self,sep=","):
    self.check_and_transform_area()#将省市区无意义内容转化为未知。此句不需在generate_insert_sql中调用,因为转换函数改的是类的成员变量,属性已经修改,后续先执行to_csv后执行generate——insert_sql
     csv_line = \#换行符,不会别识别
            f"{self.order_id}{sep}" \
            f"{self.store_id}{sep}" \
            f"{self.store_name}{sep}" \
            f"{self.store_status}{sep}" \
            f"{self.store_own_user_id}{sep}" \
            f"{self.store_own_user_name}{sep}" \
            f"{self.store_own_user_tel}{sep}" \
            f"{self.store_category}{sep}" \
            f"{self.store_address}{sep}" \
            f"{self.store_shop_no}{sep}" \
            f"{self.store_province}{sep}" \
            f"{self.store_city}{sep}" \
            f"{self.store_district}{sep}" \
            f"{self.store_gps_name}{sep}" \
            f"{self.store_gps_address}{sep}" \
            f"{self.store_gps_longitude}{sep}" \
            f"{self.store_gps_latitude}{sep}" \
            f"{self.is_signed}{sep}" \
            f"{self.operator}{sep}" \
            f"{self.operator_name}{sep}" \
            f"{self.face_id}{sep}" \
            f"{self.member_id}{sep}" \
            f"{time_util.ts13_to_date_str(self.store_create_date_ts)}{sep}" \
            f"{self.origin}{sep}" \
            f"{self.day_order_seq}{sep}" \
            f"{self.discount_rate}{sep}" \
            f"{self.discount_type}{sep}" \
            f"{self.discount}{sep}" \
            f"{self.money_before_whole_discount}{sep}" \
            f"{self.receivable}{sep}" \
            f"{self.erase}{sep}" \
            f"{self.small_change}{sep}" \
            f"{self.total_no_discount}{sep}" \
            f"{self.payed_total}{sep}" \
            f"{self.pay_type}{sep}" \
            f"{self.payment_channel}{sep}" \
            f"{self.payment_scenarios}{sep}" \
            f"{self.product_count}{sep}" \
            f"{time_util.ts13_to_date_str(self.date_ts)}"
        return csv_line
    
if __name__ == '__main__':    #自测
    data="""
        {"discountRate": 1, "storeShopNo": "None", "dayOrderSeq": 10, "storeDistrict": "芙蓉区", "isSigned": 0, "storeProvince": "湖南省", "origin": 0, "storeGPSLongitude": "112.97851", "discount": 0, "storeID": 2419, "productCount": 7, "operatorName": "OperatorName", "operator": "NameStr", "storeStatus": "open", "storeOwnUserTel": 12345678910, "payType": "cash", "discountType": 2, "storeName": "肖之艳南食店", "storeOwnUserName": "OwnUserNameStr", "dateTS": 1542450646000, "smallChange": 0, "storeGPSName": "None", "erase": 0, "product": [{"count": 1, "name": "伊利安慕希黄桃燕麦味酸奶200g", "unitID": 2, "barcode": "6907992513560", "pricePer": 7, "retailPrice": 7, "tradePrice": 0, "categoryID": 11}, {"count": 1, "name": "京牛香素食烤鸭90g", "unitID": 7, "barcode": "6935814600112", "pricePer": 2.5, "retailPrice": 2.5, "tradePrice": 0, "categoryID": 10}, {"count": 1, "name": "QQ糖双色可乐味70g", "unitID": 0, "barcode": "6920952712254", "pricePer": 3.5, "retailPrice": 3.5, "tradePrice": 0, "categoryID": 1}, {"count": 1, "name": "奥利奥夹心原味高原装116g", "unitID": 0, "barcode": "6901668005717", "pricePer": 6, "retailPrice": 6, "tradePrice": 0, "categoryID": 1}, {"count": 1, "name": "阿胶枣", "unitID": 4, "barcode": "6959435300091", "pricePer": 4, "retailPrice": 4, "tradePrice": 0, "categoryID": 1}, {"count": 1, "name": "润口香甜王米风味香肠", "unitID": 0, "barcode": "6902890237754", "pricePer": 7, "retailPrice": 7, "tradePrice": 0, "categoryID": 1}, {"count": 1, "name": "好丽友派巧克力味6枚204g", "unitID": 3, "barcode": "6920907800944", "pricePer": 9, "retailPrice": 9, "tradePrice": 0, "categoryID": 10}], "storeGPSAddress": "None", "orderID": "154245064025224198149", "moneyBeforeWholeDiscount": 39, "storeCategory": "normal", "receivable": 39, "faceID": "", "storeOwnUserId": 2364, "paymentChannel": 0, "paymentScenarios": "OTHER", "storeAddress": "StoreAddress", "totalNoDiscount": 39, "payedTotal": 39, "storeGPSLatitude": "28.187935", "storeCreateDateTS": 1542358458000, "storeCity": "长沙市", "memberID": "0"}
    """
    o=OrderModel(data=data)
    print(o.to_csv())
​
​

20230825,0315-0316(46min),1.5h


2.4添加转换无意义字符串功能

存在问题:原始数据中很多空字段需判断:

检查字符串是否是空内容的方法

检查字符串是否是空内容,是则返回“NULL",否则非空返回字符串的方法。

#在类里定义函数,至少有一个参数self。

在util包下创建str_util.py

"""字符串处理相关的代码都在这个文件中"""
def check_null(data):#如果data有意义,返回False,无意义返回True。
    #无意义的定义:空串""、“none"、"null"、"undefined"
    if not data:
        return True
    #代码能跑到此,说明不是空串
    data=data.lower()
    if data=="none" or data="null" or data="undefined":#根据实际情况判断
        return True
    return False
    
    

创建单元测试:在test包下的test_str_util.py中

from unittest import TestCase
from util.str_util import check_null
class StrUtilTest(TestCase):#?
    def setUp(self)->None:#相当于init(用pycharm框架时,先调用setUp),但不是(当仅仅将其当成类,使用函数里面对象时,对象是不存在的,需要先手动调用这个函数)
        """self.a=1#做测试用
        pass
    sut=StrUtilTest()
    sut.setUp()#加上此句,后面才能打印出a。
    print(sut.a)"""
        pass
    def test_check_null(self):
        s=None
        result=check_null(s)
        self.assertTrue(result)
     def test_check_null(self):
        s="None"
        result=check_null(s)
        self.assertTrue(result)
     def test_check_null(self):
        s="NONE"
        result=check_null(s)
        self.assertTrue(result)  
     def test_check_null(self):
        s="null"
        result=check_null(s)
        self.assertTrue(result)
      def test_check_null(self):
        s=""
        result=check_null(s)
        self.assertTrue(result)
      def test_check_null(self):
        s="undefined"
        result=check_null(s)
        self.assertTrue(result)
      def test_check_null(self):
        s="有意义的字符串"
        result=check_null(s)
        self.assertFales(result)
​
添加转换无意义字符串功能

在retail_order_model.py文件中,

#转换无意义的省市区
def check_and_transform_area(self):
    if str_util.check_null(self.store_province):
        self.store_province="未知省份"
     if str_util.check_null(self.store_city):
        self.store_province="未知城市"   
     if str_util.check_null(self.store_district):
        self.store_province="未知县区"
​

2.5添加写入MySQL功能

在model包下的retail_orders_model.py中,

from util import str_util
def generate_insert_sql(self):
    """
        将模型转化为一条insert sql语句
        return 字符串
    """
    #ignore意思是如果有此数据,可忽略,不报错
    sql = f"INSERT IGNORE INTO {pc.target_orders_table_name}(" \
              f"order_id,store_id,store_name,store_status,store_own_user_id," \
              f"store_own_user_name,store_own_user_tel,store_category," \
              f"store_address,store_shop_no,store_province,store_city," \
              f"store_district,store_gps_name,store_gps_address," \
              f"store_gps_longitude,store_gps_latitude,is_signed," \
              f"operator,operator_name,face_id,member_id,store_create_date_ts," \
              f"origin,day_order_seq,discount_rate,discount_type,discount," \
              f"money_before_whole_discount,receivable,erase,small_change," \
              f"total_no_discount,pay_total,pay_type,payment_channel," \
              f"payment_scenarios,product_count,date_ts" \
              f") VALUES(" \
              f"'{self.order_id}', " \#加上‘’因为是int类型
        
              f"{self.store_id}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_name)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_status)}, " \
              f"{self.store_own_user_id}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_own_user_name)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_own_user_tel)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_category)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_address)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_shop_no)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_province)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_city)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_district)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_gps_name)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_gps_address)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_gps_longitude)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.store_gps_latitude)}, " \
              f"{self.is_signed}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.operator)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.operator_name)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.face_id)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.member_id)}, " \
              f"'{time_util.ts13_to_date_str(self.store_create_date_ts)}', " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.origin)}, " \
              f"{self.day_order_seq}, " \
              f"{self.discount_rate}, " \
              f"{self.discount_type}, " \
              f"{self.discount}, " \
              f"{self.money_before_whole_discount}, " \
              f"{self.receivable}, " \
              f"{self.erase}, " \
              f"{self.small_change}, " \
              f"{self.total_no_discount}, " \
              f"{self.payed_total}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.pay_type)}, " \
              f"{self.payment_channel}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.payment_scenarios)}, " \
              f"{self.product_count}, " \
              f"'{time_util.ts13_to_date_str(self.date_ts)}')"
​
        return sql

解析insert ignore,作用:防止重复插入同一数据时报错

USE metadata;
INSERT INTO file_monitor(file_name,process_lines)VALUES("x09",20);
INSERT IGNORE INTO file_monitor(file_name,process_lines)VALUES("x09",20);
#第一条改为x08,正确运行(说明已经入库),再改为x08报错,而第二条重复插入不报错

在数据库查看某个表的详细列的方式

use 数据库名字

show CREATE TABLE 某个表

#插入数据库的语句中值部分,如果是string类型,加' '

2.6添加返回SQL中NULL功能

在util包下的str_util.py中

def check_str_null_and_transform_to_sql_null(data):
    """判断传入的字符串是否是无意义的,如果是返回MySQL的NULL
    否则返回格式化的字符串
    """
    if check_null(str(data)):#字符串化
        return "NULL"
    else:# 内容有意义,返回'内容本身'
        return f"'{data}'"

在test包下的test_str_util.py下测试

import util.str_util import check_null,check_str_null_and_transform_to_sql_null
def test_check_str_null_and_transform_to_sql_null(self):
    s=""
    result=check_str_null_and_transform_to_sql_null(s)
    self.assertEqual("NULL",result)
    s="None"
    result=check_str_null_and_transform_to_sql_null(s)
    self.assertEqual("NULL",result)
    s="null"
    result=check_str_null_and_transform_to_sql_null(s)
    self.assertEqual("NULL",result)
    s="undefined"
    result=check_str_null_and_transform_to_sql_null(s)
    self.assertEqual("NULL",result)
    s="张三"
    result=check_str_null_and_transform_to_sql_null(s)
    self.assertEqual("'张三'",result)

2.7产生结果演示

(检查数据是否可以正常运行)

在model包下的retail_orders_model.py中

import config.project_config as pc
json_str='{}'
model=OrderModel(json_str)
print(model.to_csv())
print(model.genetate_insert_sql())

20230826,0402-0408(82min),3.1h


3.OrdersDertailModel

创建数据表三范式:有主键,无依赖,?

3.1构建模型

订单详情模型,包含订单ID+商品信息

在retail_orders_model.py中

class OrderDetailModel:
    def _init_(self,data:str):
        data=json.loads(data)
        self.order_id=data["orderID"]#为后续结合barcode做主键
        self.products_detail=[]
        order_detail_product=data["product"]
        for p in order_detail_product:
            self.products_detail.append(SingleProductSoldModel(self,order_id,product_detail_dict))    
    def generate_insert_sql(self):
        #sql语句的前半部分
        sql=f"INSERT IGNORE INTO {pc.target_orders_detail_table_name}("\
        f"order_id,barcode,name,count,price_per,retail_price,trade_price,category_id,unit_id)VALUES"
        #sql语句的后半部分
        for model in self.products_detail:#机器怎么识别model是SingleProductSoldModel,通过for循环?
            sql+="("
            sql += f"'{model.order_id}', " \
                   f"{str_util.check_str_null_and_transform_to_sql_null(model.barcode)}, " \#判断非空
                   f"{str_util.check_str_null_and_transform_to_sql_null(model.name)}, " \
                   f"{model.count}, " \
                   f"{model.price_per}, " \
                   f"{model.retail_price}, " \
                   f"{model.trade_price}, " \
                   f"{model.category_id}, " \
                   f"{model.unit_id}"
            sql+="), "#)后有个空格
       sql=sql[:-2]#切去)和空格
    def to_csv(self):
        csv_line=""
        for model in self.products_detail:
            csv_line+=model.to_csv()
            csv_line+="\n"
        return csv_line
class SingleProductSoldModel:
    def init_(self,order_id,product_detail_dict):#product_detail_dict是什么?怎么调用的?
        self.order_id = order_id                                    # 订单ID
        self.name = product_detail_dict["name"]                     # 商品名称
        self.count = product_detail_dict["count"]                   # 商品售卖数量
        self.unit_id = product_detail_dict["unitID"]                # 单位ID
        self.barcode = product_detail_dict["barcode"]               # 商品的条码
        self.price_per = product_detail_dict["pricePer"]            # 商品卖出的单价
        self.retail_price = product_detail_dict["retailPrice"]      # 商品建议零售价
        self.trade_price = product_detail_dict["tradePrice"]        # 商品建议成本价
        self.category_id = product_detail_dict["categoryID"]        # 商品类别ID  
        
    def to_csv(self,sep=","):
         """生成一条csv数据,分隔符默认逗号"""
        csv_line = \
            f"{self.order_id}{sep}" \
            f"{self.barcode}{sep}" \
            f"{self.name}{sep}" \
            f"{self.count}{sep}" \
            f"{self.price_per}{sep}" \
            f"{self.retail_price}{sep}" \
            f"{self.trade_price}{sep}" \
            f"{self.category_id}{sep}" \
            f"{self.unit_id}"
​
        return csv_line
    
main
json_str='{}'#参照之前写入的
order_detal_model=OrderDetailModel(json_str)
print(order_detail_model.to_csv())
print(order_detail_model.generate_insert_sql())
    

十、项目开发-业务主逻辑

1.填充主逻辑代码,写出csv

两种读取文件方式区别演示

with open("D:/2.project/data/pyetl-data-logs/json/x01",encoding="UTF-8") as f:
    lines=f.readlines()#全部读取,占较大内存
    for line in lines[:2]:
        print(line)
​
for line in open("D:/2.project/data/pyetl-data-logs/json/x01",encoding="UTF-8"):#迭代读取,不会一次性读完,节约内存
    print(line)
    break

主逻辑梳理

每次处理时:

会处理若干个json文件:x09、x10、x11

每个json文件有1024笔订单

每笔订单有若干个商品

20230827,0409-0412(37min),2.6h


在独立的json_service.py中继续添加

from config import project_config as pc
from model.retail_orders_model import OrderModel,OrderDetailModel
#步骤二
#1.通过for循环取文件
for file in need_to_process_files:
    logger.debug(f"当前处理{file}")
    order_Model_list=[]
    order_detail_model_list=[]
  
    #2.获取文件中每行的订单信息,构造两个订单模型
    for line in open(file,encoding="utf-8"):
        #logger.debug(f"正在处理{file},内容{line}")
        #替换掉行尾的换行符
        line=line.replace("\n","")#具体替换哪里?不替换又有什么差别?
        order_Model=OrderModel(line)#先导入包
        order_Model_list.appened(order_Model)
        order_detail_model=OrderDetailModel(line)
        order_Model_detail_list.appened(order_detail_Model) 
    #增加数据转换功能,即对数据进行过滤
    reserved_model=[]
    for model in order_model_list:
            if model.receivable<=10000:
                reserved_model.appened(model)
            else:
                logger.debug(f"该订单异常:order_id:{model.order_id}{model.to_csv()}")
    #3.创建两个CSV文件,准备写入
    order_csv_write_f=open(
        file=pc.retail_output_csv_root_path+pc.retail_orders_output_csv_file_name,mode="a",encoding="utf-8"
    )#处理文件的handler。mode由原来的w改为a,因为前者会产生覆盖现象,即一个csv文件中最多1024行
    order_detail_csv_write_f=open(
        file=pc.retail_output_csv_root_path+pc.retail_orders_detail_output_csv_file_name,mode="a",encoding="utf-8"
    )#file_name怎么自动替换
    #4.写入CSV文件
    #4.1写入订单表,从过滤后的结果中取
    for model in reserved_model:
        line=model.to_csv()
        line+="\n"
        order_csv_write_f.write(line)
    order_csv_write_f.close()
    #4.2写入订单详情表
    for model in order_detail_model_list:
        for single_product_model in model.products_detail:
            line=sing_product_model.to_csv()
            line+="\n"
            order_detail_csv_write_f.write(line)
    order_detail_csv_write_f.close()
logger.info(f"完成了CSV备份文件的写出,写出到了:{conf.retail_outut_csv_root_path}")

在project.config.py中,

csv相关的配置
# 写出CSV的根目录配置
retail_output_csv_root_path = "D:/2.project/data/pyetl-data-logs/output/csv/"
#TODO,考虑扩展代码,自动创建目录
# 写出订单模型,CSV的文件名字配置
retail_orders_output_csv_file_name = f'orders-{time.strftime("%Y%m%d-%H%M%S", time.localtime(time.time()))}.csv'
# 写出订单详情(带商品信息)模型,CSV的文件名字配置
retail_orders_detail_output_csv_file_name = f'orders-detail-{time.strftime("%Y%m%d-%H%M%S", time.localtime(time.time()))}.csv'

主逻辑代码重构

一般默认规则:主代码一屏20行,一行最多80字符

重构目的:在main中按照功能分成一个个小函数,能抓住主要逻辑,把握全局。

重构:得到待处理文件

在json_service.py中的步骤一

  • 选中代码块,右键Refactor-Extract Method进行函数封装

  • def部分:get_process_file函数中有global全局变量(need_to_process_files),可以不定义全局变量,让与后续代码有关的变量(meta_util,need_to_process_files)以return返回

  • main部分:替换为meta_util,need_to_process_files=get_process_file()

    #重构之后
    def get_process_file(meta_util):#新增参数,受以下“写入MySQL”部分中重构影响
        #logger=init_logger()
        #logger.info("读取JSON数据处理,程序开始执行了......")
        #1.通过file_util中的函数获取json目录中的文件
        files=get_dir_files_list(pc.json_data_root_path)
        logger.info(f"判断json的文件夹,发现有如下文件“{files}")
        #2读取元数据库中file_monitor中已经处理过的文件,生成一个列表
        #meta_util=MySQLUtil()
        processed_files=get_processed_files(meta_util)
        logger.info(f"查询MySQL,找到有如下文件已经被处理过了:{processed_files}")
        #3 这两个列表相减,得到需要的文件
        need_to_process_files=get_new_by_compare_lists(processed_files,files)
        logger.info(f"经过对比mysql元数据库,找出如下文件供我们处理:{need_to_process_files}")

重构:建立订单和详情的模型

在json_service.py中的步骤二重构build_models(file)

#原文
    order_Model_list=[]
    order_detail_model_list=[]
  
    #2.获取文件中每行的订单信息,构造两个订单模型
    for line in open(file,encoding="utf-8"):
        #logger.debug(f"正在处理{file},内容{line}")
        #替换掉行尾的换行符
        line=line.replace("\n","")#具体替换哪里?不替换又有什么差别?
        order_Model=OrderModel(line)#先导入包
        order_Model_list.appened(order_Model)
        order_detail_model=OrderDetailModel(line)
        order_Model_detail_list.appened(order_detail_Model) 
        
#重构build_models():
#函数中
    #参数:在def中build_models(file)#file为参数
    #全局变量:删去der_model_list,order_detail_model_list,line
    #返回值:order_model_list,order_detail_model_list
#代码中
    #替代:order_model_list,order_detail_model_list=build_models(file)
​
    
#在for file in need_to_process_files: 循环前新加以下两个全局变量,因为若只在for循环里体现,后续引用时可能会显示无定义。用处来源:写入MySQL部分
order_model_list=[],
order_detail_model_list=[]    
​
    
重构:订单异常数据处理

在json_service.py中的步骤二重构file_except_data(order_model_list)

#原文
#增加数据转换功能,即对数据进行过滤
    reserved_model=[]
    for model in order_model_list:
            if model.receivable<=10000:
                reserved_model.appened(model)
            else:
                logger.debug(f"该订单异常:order_id:{model.order_id}{model.to_csv()}")
                
#重构file_except_data() 
#函数中
    #参数:order_model_list
    #全局变量:删
    #返回值:reserved_model
#代码中
reserved_model=file_except_data(order_model_list) 
重构:打开订单和订单详情文件

在json_service.py中的步骤二重构open_order_file()

#原文
#3.创建两个CSV文件,准备写入
    order_csv_write_f=open(
        file=pc.retail_output_csv_root_path+pc.retail_orders_output_csv_file_name,mode="a",encoding="utf-8"
    )#处理文件的handler。mode由原来的w改为a,因为前者会产生覆盖现象,即一个csv文件中最多1024行
    order_detail_csv_write_f=open(
        file=pc.retail_output_csv_root_path+pc.retail_orders_detail_output_csv_file_name,mode="a",encoding="utf-8"
    )
#重构open_order_file()
#函数中
    #参数:
    #全局变量:删
    #返回值:order_csv_write_f,order_detail_csv_write_f
#代码中
order_csv_write_f,order_detail_csv_write_f=open_order_file()
重构:文件写入csv

在json_service.py中的步骤二重构write_data_to_csv(reserved_model,order_detail_model_list)

#原文
    #4.1写入订单表,从过滤后的结果中取
    for model in reserved_model:
        line=model.to_csv()
        line+="\n"
        order_csv_write_f.write(line)
    order_csv_write_f.close()
    #4.2写入订单详情表
    for model in order_detail_model_list:
        for single_product_model in model.products_detail:
            line=sing_product_model.to_csv()
            line+="\n"
            order_detail_csv_write_f.write(line)
    order_detail_csv_write_f.close()
#重构write_data_to_csv()
#函数中
    #参数:reserved_model,order_detail_model_list
    #全局变量:删
    #返回值:
#代码中
write_data_to_csv(reserved_model,order_detail_model_list)

2.填充主逻辑代码,完全写成

2.1重构MySQLUtil

之前修改链接已完成,思路:将连接数据库的信息从类代码处移到config配置文件中,初始化时,传递给参数。

2.2写入MySQL

在json_service.py中,继续

#步骤三:把数据写入MySQL中,(目的地数据库)
#1.建立MySQL连接
target_db_util=MySQLUtil(
    host=pc.target_host,
    port=pc.target_port,
    uesr=pc.target_user,
    password=pc.target_password
)
#2.判断order表orderdetail表是否存在,不存在则创建
target_db_util.check_table_exists_and_create(
    db_name=pc.target_db_name,
    table_name=pc.target_orders_table_name,
    create_cols=pc.target_orders_table_create_cols
)
target_db_util.check_table_exists_and_create(
    db_name=pc.target_db_name
    table_name=pc.target_orders_detail_table_name,
    create_cols=pc.target_orders_detail_table_create_cols
)
#3.把数据写入表中
for model in reserved_model:
    insert_sql=model.generate_insert_sql()
    target_db_util.select_db(pc.target_db_name)
    #target_db_util.execute(insert_sql)执行速度比下行慢,因为每次执行插入语句都需要提交一次
    target_db_util.execute_without_autocommit(insert_sql)#最后一起提交插入语句,效率高
    
for model in order_detail_model_list:
    insert_sql=model.generate_insert_sql()
    target_db_util.select_db(pc.target_db_name)
    #target_db_util.execute(insert_sql)速度比下一句慢
    target_db_util.execute_without_autocommit(insert_sql)
target_db_util.conn.commit()   
    #
#运行完成后,数据库中的两个表内容会补充
#运行后,数据库中的表只包含1024条数据,因为以上最后两段for循环对齐每个file的for循环内部
​

主逻辑代码重构

def build_db_util():
    meta_util=MySQLUtil()
    target_db_util=MySQLUtil(
        host=pc.target_host,
        port=pc.target_port,
        uesr=pc.target_user,
        password=pc.target_password
    )
    return meta_util, target_db_util
def check_target_table_and_create():
#2.判断order表orderdetail表是否存在,不存在则创建
        target_db_util.check_table_exists_and_create(
            db_name=pc.target_db_name,
            table_name=pc.target_orders_table_name,
            create_cols=pc.target_orders_table_create_cols
        )
        target_db_util.check_table_exists_and_create(
            db_name=pc.target_db_name
            table_name=pc.target_orders_detail_table_name,
            create_cols=pc.target_orders_detail_table_create_cols
        )
 def write_data_to_mysql(target_db_util,reserved_model,order_detail_model_list)
        for model in reserved_model:
            insert_sql=model.generate_insert_sql()
            target_db_util.select_db(pc.target_db_name)
            #target_db_util.execute(insert_sql)执行速度比下行慢,因为每次执行插入语句都需要提交一次
             target_db_util.execute_without_autocommit(insert_sql)#最后一起提交插入语句,效率高
    
        for model in order_detail_model_list:
            insert_sql=model.generate_insert_sql()
             target_db_util.select_db(pc.target_db_name)
            #target_db_util.execute(insert_sql)速度比下一句慢
            target_db_util.execute_without_autocommit(insert_sql)
        target_db_util.conn.commit()   
#以上代码为重构部分
if __name__ == '__main__':
    # 步骤一
    logger = init_logger()
    logger.info("读取JSON数据处理,程序开始执行了......")
​
    meta_util, target_db_util = build_db_util()
​
    need_to_process_files = get_process_file(meta_util)#改参数,同时改def
    
    # 步骤二 写csv文件
    # 1 通过for循环取出每个文件
    order_model_list = []
    order_detail_model_list = []
​
    # 定义一个变量,统计所有文件的行数
    global_count = 0
    # 定义一个字典,存储处理的文件相关数据
    processed_files_record_dict = {}
​
    for file in need_to_process_files:
        logger.debug(f'当前处理{file}')
​
        order_model_list, order_detail_model_list, file_processed_lines_count = build_models(file)
​
        # 这里时进行统计文件行数,作用为后续写入元数据库服务
        processed_files_record_dict[file] = file_processed_lines_count
        global_count += file_processed_lines_count
​
        # 这里时进行数据过滤
        reserved_model = filt_except_data(order_model_list)
​
        # 4 写入到csv文件中
        write_data_to_csv(reserved_model, order_detail_model_list)
​
        logger.info(f"完成了CSV备份文件的写出,写出到了:{pc.retail_output_csv_root_path}")
        
#自以下重构代码   
        #步骤三:把数据写入MySQL中,(目的地数据库)
    
        """原代码转移到步骤一及其函数中
        #1.建立MySQL连接#不必每个文件都连接一次
        target_db_util=MySQLUtil(
            host=pc.target_host,
            port=pc.target_port,
            uesr=pc.target_user,
            password=pc.target_password
        )
        """
        check_target_table_and_create()#重构以下       
    """
        #2.判断order表orderdetail表是否存在,不存在则创建
        target_db_util.check_table_exists_and_create(
            db_name=pc.target_db_name,
            table_name=pc.target_orders_table_name,
            create_cols=pc.target_orders_table_create_cols
        )
        target_db_util.check_table_exists_and_create(
            db_name=pc.target_db_name
            table_name=pc.target_orders_detail_table_name,
            create_cols=pc.target_orders_detail_table_create_cols
        )
        """
         write_data_to_mysql(target_db_util,reserved_model,order_detail_model_list)#重构以下
    """
        #3.把数据写入表中
        for model in reserved_model:
            insert_sql=model.generate_insert_sql()
            target_db_util.select_db(pc.target_db_name)
            #target_db_util.execute(insert_sql)执行速度比下行慢,因为每次执行插入语句都需要提交一次
             target_db_util.execute_without_autocommit(insert_sql)#最后一起提交插入语句,效率高
    
        for model in order_detail_model_list:
            insert_sql=model.generate_insert_sql()
             target_db_util.select_db(pc.target_db_name)
            #target_db_util.execute(insert_sql)速度比下一句慢
            target_db_util.execute_without_autocommit(insert_sql)
        target_db_util.conn.commit()   
        
    """

20230828,0413-0417(78min),4h


目的:将处理完的文件名写入元数据库

在json.service.py中继续

#步骤四
#将处理过的文件名写入到元数据库中
#需要file name和process_lines(在for lines 中每循环一次加一)
#修改步骤二封装的build_models
​
def build_models(file):
    
    order_Model_list=[]
    order_detail_model_list=[]
    file_processed_lines_count=0#新加计算行数
  
    #2.获取文件中每行的订单信息,构造两个订单模型
    for line in open(file,encoding="utf-8"):
        #logger.debug(f"正在处理{file},内容{line}")
        file_processed_lines_count+=0#新加,统计当前文件行数
    
        line=line.replace("\n","")
        order_Model=OrderModel(line)
        order_Model_list.appened(order_Model)
        order_detail_model=OrderDetailModel(line)
        order_Model_detail_list.appened(order_detail_Model) 
        return order_Model_list,order_Model_detail_list,file_processed_lines_count
    
#在main中,build_models返回值新增file_processed_lines_count
#在for file 循环前增加统计每个文件行数的字典变量,在循环下新加统计计数
main
global_count=0#定义一个变量,存储所有行数据
processd_files_record_dict={}#for file循环前定义一个字典,存储处理的文件相关数据
for file in need_to_process_files:
    file_processed_lines_count=0
    order_Model_list,order_Model_detail_list,file_processed_lines_count=build_models(file)
    processed_files_record_dict[file]=file_processed_lines_count
    global_count+=file_processed_lines_count
    #继续之前的reserved_model等
​
#步骤四
#将处理过的文件名写入到元数据库中,在for file循环外
​
"""
打印字典数据
a={"key1":1,"key2":2}
for k in a.keys():
    print(k)
for (key,value) in a.items():
    print(key,value)
"""
for filename,lines in processed_files_record_dict.itmes():
    insert_sql=f"INSERT INTO {pc.metadata_file_monitor_table_name} (file_name,processed_lines)"\
    f"VALUES('{filename}',{lines})"#字符串加''
    meta_util.execute(insert_sql)
meta_util_close.conn()
target_db_util_close_conn()

封装步骤四

#原代码
for filename,lines in processed_files_record_dict.itmes():
    insert_sql=f"INSERT INTO {pc.metadata_file_monitor_table_name} (file_name,processed_lines)"\
    f"VALUES('{filename}',{lines})"#字符串加''
    meta_util.execute(insert_sql)
#封装后
#函数中
def write_metadata_to_metadata(meta_util,processed_files_record_dict):
    for filename,lines in processed_files_record_dict.itmes():
    insert_sql=f"INSERT INTO {pc.metadata_file_monitor_table_name} (file_name,processed_lines)"\
    f"VALUES('{filename}',{lines})"
    meta_util.execute(insert_sql)
#main中
write_metadata_to_metadata(meta_util,processed_files_record_dict)

json主逻辑梳理(概念代码块)

main
初始化logger
1.build_db_util创建两个数据库连接(元数据库和目的地数据库),返回两个util对象
2.get_processe_file接收元数据util,读取指定JSON目录,读取元数据库中的元数据表,进行对比,返回待处理文件名称
3.定义订单列表和订单详情列表,总订单数,已处理过的文件的行数(字典形式)。统计行数作用是为写入元数据库准备。
4.for file循序
    5.build_models接受每个文件,
    从给定文件名中读取JSON每行数据,
    每行一笔订单,转成Order_model对象,
    每个订单多个商品,把订单中product转成Order_detail_model对象
    每个商品SingeProductSoldModel对象
    把Json数据分别存储到三个模型对象中
    返回订单列表、订单详情表和每个file的行数
    6.统计每个file行数,及所有file行数
    7.file_except_data进行数据过滤,
    返回reserved_model
    8.write_data_to_csv,接收reserved_model和order_detail_model_list,
    分别把订单信息写入csv,
    open_order_file,打开两个csv文件,等待写入
    #以上为步骤二写csv文件
    #步骤三,把数据写入mysql
    9.check_target_table_and_create():检查待写入的数据表是否存在,如果不存在则创建,
    两个表为目的数据库中存取订单和订单详情的表
    10.write_data_to_mysql:
    把两个模型的数据写入mysql.注意是选择每条信息提交一次,还是写完所有数据提交一次
    #步骤四:写入元数据库
    write_metadata_to_metadb,把处理过的文件名和行数写入数据库中
    11.关闭两个数据库连接
    
    
    
    
​

项目需求2:MySQL数据处理

一、前期准备

导入简化版sql到datagrip中:被采集的数据为barcode,将被采集的数据导入到MySQL数据源库source_data中

数据运行:拖进datagrip,右键run,弹出Edit Configuration-选择target data source里的+,打开source_data-点击run,数据库source_data自动生成一个sys_barcode。‘

了解表结构,尤其其中的updata_at,记录更新的商品及时间戳

二、采集的要求

采集json数据特点:不重复采集,会记录采集过的文件名称

MySQL数据采集:新增内容,更新的内容,同时在目的地中进行更新。即增量更新。

三、实现步骤

1.实现思路

基于被采集的表的字段:updata_at

这个字段存储是数据源数据库中每次修改(更新)或者插入(新增)的数据的时间戳,每次采集时记录当前采集时这个批次中最大的时间戳。

实现增量更新采集:

  • 下次采集时,SELECT SQL按照updata_at升序,

  • 采集完成后,将当前批次最大的时间记录在MySQL的元数据库中,

  • 下一次采集数据时,从元数据库的这个时间戳进行比较,只采集大于等这个时间戳的数据。

2.实现代码

2.1新增工具方法

目的:替换字符串中的特殊字符。因为这些字符串后续在构造sql句时使用,如果 ''、""存在可能会影响写的sql语句(不匹配)。导致代码执行时语法错误。

在util包下的str_util.py中

def clean_str(data:str):
    #去除字符串中的特殊字符
    if check_null(data):
        return data#后续对无意义字符串处理
    data=data.replace("'"."")
    data=data.replace("""."")
    data=data.replace("\\"."")
    data=data.replace(";"."")
    data=data.replace(","."")
    data=data.replace("@"."")
    
    return data

在util包下的str_util.py中,添加一个函数,

def check_number_null_and_transform_to_sql_null(data):
    """检查数据是否是空,空的则返回mysql中的NULL
    """
    if data and not check_null(str(data)):
        return data
    else:
        return "NULL"#返回sql意义上的NULL

2.2新增模型

单独创建一个文件,因为这里是一个新的数据源(之前采集的是json),在这个文件中按照数据特点构造模型。后续采集日志数据时可以类比进行编码。

从datagrip执行,可以快速获得建表字段(前提是表本身存在数据库中)

#右键source_data数据库-New-Query Console
use source_data;
show CREATE TABLE sys_barcode;
#直接复制相应字段的Create Table

在model包下新建barcode_model.py

#这个文件中存储的是从mysql中读取数据的模型
from util import str_util
from config import project_config as pc
class BarcodeModel:
    def _init_(self, code=None, name=None, spec=None, trademark=None,
                 addr=None, units=None, factory_name=None, trade_price=None,
                 retail_price=None, update_at=None, wholeunit=None,
                 wholenum=None, img=None, src=None):
        self.code = code
        self.name = str_util.clean_str(name)
        self.spec = str_util.clean_str(spec)
        self.trademark = str_util.clean_str(trademark)
        self.addr = str_util.clean_str(addr)
        self.units = str_util.clean_str(units)
        self.factory_name = str_util.clean_str(factory_name)
        self.trade_price = trade_price
        self.retail_price = retail_price
        self.update_at = update_at
        self.wholeunit = str_util.clean_str(wholeunit)
        self.wholenum = wholenum
        self.img = img
        self.src = src
    def to_csv():
        csv_line = \
            f"{self.code}{sep}" \
            f"{self.name}{sep}" \
            f"{self.spec}{sep}" \
            f"{self.trademark}{sep}" \
            f"{self.addr}{sep}" \
            f"{self.units}{sep}" \
            f"{self.factory_name}{sep}" \
            f"{self.trade_price}{sep}" \
            f"{self.retail_price}{sep}" \
            f"{self.update_at}{sep}" \
            f"{self.wholeunit}{sep}" \
            f"{self.wholenum}{sep}" \
            f"{self.img}{sep}" \
            f"{self.src}"
​
        return csv_line
    
    def generate_insert_sql(self):
        #replace与insert区别:如果数据存在,前者替换,后者报错
        sql = f"REPLACE INTO {pc.target_barcode_table_name}(" \
              f"code,name,spec,trademark,addr,units,factory_name,trade_price," \
              f"retail_price,update_at,wholeunit,wholenum,img,src) VALUES(" \
              f"'{self.code}', " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.name)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.spec)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.trademark)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.addr)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.units)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.factory_name)}, " \
              f"{str_util.check_number_null_and_transform_to_sql_null(self.trade_price)}, " \
              f"{str_util.check_number_null_and_transform_to_sql_null(self.retail_price)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.update_at)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.wholeunit)}, " \
              f"{str_util.check_number_null_and_transform_to_sql_null(self.wholenum)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.img)}, " \
              f"{str_util.check_str_null_and_transform_to_sql_null(self.src)}" \
              f")"
        return sql

2.3修改配置文件

在config包下的project_config中

# 采集barcode业务,写入MySQL的表名
target_barcode_table_name = "barcode"
target_barcode_table_create_cols = """
    `code` varchar(50) PRIMARY KEY COMMENT '商品条码',
    `name` varchar(200) DEFAULT '' COMMENT '商品名称',
    `spec` varchar(200) DEFAULT '' COMMENT '商品规格',
    `trademark` varchar(100) DEFAULT '' COMMENT '商品商标',
    `addr` varchar(200) DEFAULT '' COMMENT '商品产地',
    `units` varchar(50) DEFAULT '' COMMENT '商品单位(个、杯、箱、等)',
    `factory_name` varchar(200) DEFAULT '' COMMENT '生产厂家',
    `trade_price` DECIMAL(50, 5) DEFAULT 0.0 COMMENT '贸易价格(指导进价)',
    `retail_price` DECIMAL(50, 5) DEFAULT 0.0 COMMENT '零售价格(建议卖价)',
    `update_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `wholeunit` varchar(50) DEFAULT NULL COMMENT '大包装单位',
    `wholenum` int(11) DEFAULT NULL COMMENT '大包装内装数量',
    `img` varchar(500) DEFAULT NULL COMMENT '商品图片',
    `src` varchar(20) DEFAULT NULL COMMENT '源信息', 
    INDEX (update_at)
"""
# 数据源数据库的配置
source_host = metadata_host
source_user = metadata_user
source_password = metadata_password
source_port = metadata_port
source_db_name = "source_data"
source_barcode_data_table_name = "sys_barcode"
#演示exit
import sys#?
main:
    print("Hello World")
    sys.exit(1)#目的是显示到达此步时,不正常退出
    
#输出结果
"Hello World"
Process finished with exit code 1
#一般正常执行的exit是0,不正常是1

20230829,0418-0507(84min),4.1h


2.4主业务逻辑代码

在project_config.py中,元数据库中

#barcode业务,updata_at字段的监控表的名称
metadata_barcode_table_name="barcode_monitor"
#barcode业务,updata_at字段的监控表的建表语句的列信息
#time_record字段,存储的是采集的批次数据中updata_at字段的最大值,而不是采集行为发生的时间戳
metadata_barcode_table_create_cols="id INT PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID',"\
"time_record TIMESTAMP NOT NULL COMMENT '本次采集记录的最大时间',"
"gather_line_count INT NULL COMMENT '本次采集条数'"

在工程根目录下,新建mysql_service.py

#采集mysql数据的主业务逻辑
from util.logging_util import init_logger
from util.mysql_util import MySQLUtil
from model.barcode_model import BarcodeModel
from config import project_config as pc
import sys
​
def build_db_util():
    metadata_db_util=MySQLUtil()#元数据库
    source_db_util=MySQLUtil(#数据源数据库
        host=pc.source_host,
        port=pc.source_port,
        user=pc.source_user,
        password=pc.source_password
)
    target_db_util=MySQLUtil(#目的地数据库
        host=pc.target_host,
        port=pc.target_port,
        user=pc.target_user,
        password=pc.target_password
)
    return metadata_db_util,source_db_util,target_db_util
​
def check_table_exists_or_exit(source_db_util):
    source_db_util.select_db(pc.source_db_name)
    if not      source_db_util.check_table_exists(pc.source_db_name,pc.source_barcode_data_table_name):
        logger.errot(":数据源库:{pc.source_db_name}中不存在数据源表:{pc.source_barcode_data_table_name},"f"无法采集,程序退出,请找人要去")
        sys.exit(1)#表示程序停止运行,传入0表示是正常停止,非0是异常停止
​
def check_target_table_exists(target_db_util):
    target_db_util.select_db(pc.target_db_name)
    target_db_util.check_table_exists_and-create(
        db_name=target_db_name,
        table_name=pc.target_barcode_table_name,
        create_cols=pc.target_barcode_table_create_cols
    )
    
def get_last_updata_time(metadata_db_util):
    metadata_db_util.select_db(pc.metadata_db_name)
    #取updata_at最大时间有两种情况
    #取到,后续根据最大时间过滤数据源数据库中的数据
    #取不到,说明是第一次运行,同时创建元数据表,取出数据源数据库中的所有数据
    last_updata_time=None
    if not metadata_db_util.check_table_exists(pc.metadata_db_name,pc.metadata_barcode_table_name):
    #进入这个分支说明不存在
        metadata_db_util.check_table_exists_and_create(
        db_name=pc.metadata_db_name,
        table_name=pc.metadata_barcode_table_name,
        create_cols=pc.metadata_barcode_table_create_cols
    )
    else:
        query_sql=f"SELECT time_record FROM {pc.metadata_barcode_table_name} ORDER BY time_record DESC LIMIT 1"
        result=metadata_db_util.query(
            query_sql
    )#重复判断是否存在,后续可以优化为一个exist表和create表
    #result二维元组(("具体时间"),),,每个小元组都是一行数据,并且这个元组中只有一个值,也就是时间
        if len(result)!=0:
            last_updata_time=result[0][0]
     return last_updata_time
​
def get_source_data_by_last_updata_time(source_db_util,last_updata_time):
    if last_updata_time:
        sql=f"SELECT * FROM {pc.source_barcode_data_table_name} WHERE updataAT          >='{last_updata_time}'"\
        f"ORDER BY updataAT"#updataAT是什么?
    else:
        sql=f"SELECT * FROM {pc.source_barcode_data_table_name} ORDER BY updataAT"
    source_db_util.select_db(pc.source_db_name)
    result=source_db_util.query(sql)
    return result 
​
def build_models(result):
    barcode_models = []
    for single_line_result in result:
        # single_line_result是元组,存储了一条结果的列
        # 查询是SELECT * 查询,结果中的第一个列 第二个列 第N个列是谁,和数据库的表结构顺序一致
        code = single_line_result[0]
        name = single_line_result[1]
        spec = single_line_result[2]
        trademark = single_line_result[3]
        addr = single_line_result[4]
        units = single_line_result[5]
        factory_name = single_line_result[6]
        trade_price = single_line_result[7]
        retail_price = single_line_result[8]
        update_at = str(single_line_result[9])  # single_line_result[9]是读取的updateAt时间,类型是datetime,转换成字符串
        wholeunit = single_line_result[10]
        wholenum = single_line_result[11]
        img = single_line_result[12]
        src = single_line_result[13]
​
        # 构建BarcodeModel
        model = BarcodeModel(
            code=code,
            name=name,
            spec=spec,
            trademark=trademark,
            addr=addr,
            units=units,
            factory_name=factory_name,
            trade_price=trade_price,
            retail_price=retail_price,
            update_at=update_at,
            wholeunit=wholeunit,
            wholenum=wholenum,
            img=img,
            src=src
        )
        barcode_models.append(model)
​
    return barcode_models
​
def write_data_to_mysql(target_db_util,barcode_models):
    max_last_updata_time="2023-08-31  00:00:00"
    target_db_util_select_db(pc.target_db_name)
    count=0#定义计数器,目的是每隔1000条数据提交一次
    for model in barcode_models:
        if model.updata_at>max_last_updata_time:
            max_last_updata_time=model.updata_at
            insert_sql=model.generate_insert_sql()
        target_db_util.execute_without_autocommit(insert_sql)#可以测试删去without
        #批量提交,保证提交的数目,又不会因为内存有限导致代码崩溃
        count+=1
        if count % 1000==0:
            target_db_util.conn.commit()
            logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source.barcode_data_table_name}",f"当前写入目标表:{conf.target_barcode_table_name}数据有:{count}行")
    target_db_util.conn.commit()#避免遗漏数据,最后提交一批不满1000的整数倍的数据#可以测试删去without,此句删除。并且删去数据库中retail的barcode,metadata的barcode_monitor
    logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source.barcode_data_table_name}",f"当前写入目标表:{conf.target_barcode_table_name}数据有:{count}行")
    return max_last_updata_time
def write_data_to_csv(barcode_models):
    barcode_csv_write_f=open(
        file=pc.barcode_out_csv_root_path+pc.barcode_orders_output_csv_file_name,
        mode="a"
        encoding+"utf8"   
)
    count=0
    for model in barcode_models:
        csv_line=model.to_csv()
        barcode_csv_write_f.write(csv_line+'\n')
        count+=1
        if count % 1000 ==0:
            barcode_csv_write_f.flush()
            logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source_barcode_data_table_name},"
                        f"写出CSV到:{barcode_csv_write_f.name}, 当前写出:{count}行。")
    barcode_csv_write_f.close()
    logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source_barcode_data_table_name},"
                        f"写出CSV到:{barcode_csv_write_f.name}, 当前写出:{count}行。")   
​
​
main
#步骤一:初始化
#1.1 logger
logger=init_logger()
#1.2 构造数据库工具对象,三个
​
"""重构
metadata_db_util=MySQLUtil()#元数据库
source_db_util=MySQLUtil(#数据源数据库
    host=pc.source_host,
    port=pc.source_port,
    user=pc.source_user,
    password=pc.source_password
)
target_db_util=MySQLUtil(#目的地数据库
    host=pc.target_host,
    port=pc.target_port,
    user=pc.target_user,
    password=pc.target_password
)
"""
metadata_db_util,source_db_util,target_db_util=build_db_util()
​
#步骤二:从数据源数据库读取数据
#2.1判断数据源表是否存在,不存在则退出
​
"""重构
source_db_util.select_db(pc.source_db_name)
if not source_db_util.check_table_exists(pc.source_db_name,pc.source_barcode_data_table_name):
    logger.errot(":数据源库:{pc.source_db_name}中不存在数据源表:{pc.source_barcode_data_table_name},"f"无法采集,程序退出,请找人要去")
    sys.exit(1)#表示程序停止运行,传入0表示是正常停止,非0是异常停止
"""
check_table_exists_or_exit(source_db_util)
​
#2.2判断目的地表是否存在,不存在则创建
"""
target_db_util.select_db(pc.target_db_name)
target_db_util.check_table_exists_and-create(
    db_name=target_db_name,
    table_name=pc.target_barcode_table_name,
    create_cols=pc.target_barcode_table_create_cols
)
"""
check_target_table_exists(target_db_util)
​
#2.3.1读取元数据库存储的上一批次updata_at最大时间
"""
metadata_db_util.select_db(pc.metadata_db_name)
#取updata_at最大时间有两种情况
#取到,后续根据最大时间过滤数据源数据库中的数据
#取不到,说明是第一次运行,同时创建元数据表,取出数据源数据库中的所有数据
last_updata_time=None
if not metadata_db_util.check_table_exists(pc.metadata_db_name,pc.metadata_barcode_table_name):
    #进入这个分支说明不存在
    metadata_db_util.check_table_exists_and_create(
    db_name=pc.metadata_db_name,
    table_name=pc.metadata_barcode_table_name,
        create_cols=pc.metadata_barcode_table_create_cols
    )
else:
    query_sql=f"SELECT time_record FROM {pc.metadata_barcode_table_name} ORDER BY time_record DESC LIMIT 1"
    result=metadata_db_util.query(
        query_sql
    )#重复判断是否存在,后续可以优化为一个exist表和create表
    #result二维元组(("具体时间"),),,每个小元组都是一行数据,并且这个元组中只有一个值,也就是时间
    if len(result)!=0:
        last_updata_time=result[0][0]
"""
last_updata_time=get_last_updata_time(metadata_db_util)
​
#2.3.2根据时间读取数据源数据中在范围内的数据
"""
if last_updata_time:
    sql=f"SELECT * FROM {pc.source_barcode_data_table_name} WHERE updataAT >='{last_updata_time}'"\
    f"ORDER BY updataAT"
 else:
    sql=f"SELECT * FROM {pc.source_barcode_data_table_name} ORDER BY updataAT"
 source_db_util.select_db(pc.source_db_name)
 result=source_db_util.query(sql)
"""
result=get_source_data_by_last_updata_time(source_db_util,last_updata_time)
#测试result里面有无数据
print(len(result))
for r in result:
    print(r)
    break
    
#步骤三:构造模型
"""
barcode_models=[]
for single_line_result in result:
    code = single_line_result[0]
    name = single_line_result[1]
    spec = single_line_result[2]
    trademark = single_line_result[3]
    addr = single_line_result[4]
    units = single_line_result[5]
    factory_name = single_line_result[6]
    trade_price = single_line_result[7]
    retail_price = single_line_result[8]
    update_at = str(single_line_result[9])  # single_line_result[9]是读取的updateAt时间,类型是datetime,转换成字符串.#时间戳转化为字符串,如何比较字符串大小
    wholeunit = single_line_result[10]
    wholenum = single_line_result[11]
    img = single_line_result[12]
    src = single_line_result[13]
​
        # 构建BarcodeModel
    model = BarcodeModel(
        code=code,
        name=name,
        spec=spec,
        trademark=trademark,
        addr=addr,
        units=units,
        factory_name=factory_name,
        trade_price=trade_price,
        retail_price=retail_price,
        update_at=update_at,
        wholeunit=wholeunit,
        wholenum=wholenum,
        img=img,
        src=src
    )
    barcode_models.append(model)
return barcode_models
"""
barcode_models=build_models(result)
    #single_line_result是元组,存储了一条结果的列? 
#循环取出每一行数据,每一行数据内构造一个模型对象,每一行数据都存储在这个对象中
#步骤四:写入数据
#4.1写入数据到目的地数据库
#每条数据都需判断并记录最大时间
"""重构
max_last_updata_time="2023-08-31  00:00:00"
target_db_util_select_db(pc.target_db_name)
count=0#定义计数器,目的是每隔1000条数据提交一次
for model in barcode_models:
    if model.updata_at>max_last_updata_time:
        max_last_updata_time=model.updata_at
        insert_sql=model.generate_insert_sql()
    target_db_util.execute_without_autocommit(insert_sql)
    #批量提交,保证提交的数目,又不会因为内存有限导致代码崩溃
    count+=1
    if count % 1000==0:
        target_db_util.conn.commit()
        logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source.barcode_data_table_name}",f"当前写入目标表:{conf.target_barcode_table_name}数据有:{count}行")
target_db_util.conn.commit()#避免遗漏数据,最后提交一批不满1000的整数倍的数据
logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source.barcode_data_table_name}",f"当前写入目标表:{conf.target_barcode_table_name}数据有:{count}行")
 """
max_last_updata_time=write_data_to_mysql(target_db_util,barcode_models)#与json.service.py内的函数同名,不在同一文件,无妨
    
#4.2写入数据到csv
"""重构
barcode_csv_write_f=open(
    file=pc.barcode_out_csv_root_path+pc.barcode_orders_output_csv_file_name,
    mode="a"
    encoding+"utf8"   
)
count=0
for model in barcode_models:
    csv_line=model.to_csv()
    barcode_csv_write_f.write(csv_line+'\n')
    count+=1
    if count % 1000 ==0:
        barcode_csv_write_f.flush()
        logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source_barcode_data_table_name},"
                        f"写出CSV到:{barcode_csv_write_f.name}, 当前写出:{count}行。")
barcode_csv_write_f.close()
logger.info(f"从数据源:{pc.source_db_name}库,读取表:{pc.source_barcode_data_table_name},"
                        f"写出CSV到:{barcode_csv_write_f.name}, 当前写出:{count}行。")
"""
write_data_to_csv(barcode_models)
​
#步骤五:记录MySQL元数据(这批次的最大时间),到元数据库中
metadata_util.select_db(pc.metadata_db_name)#是否是metadata_db_util
sql = f"INSERT INTO {pc.metadata_barcode_table_name}(" \
      f"time_record, gather_line_count) VALUES(" \
      f"'{max_last_update_time}', " \
      f"{result_len}" \
      f")"
metadata_util.execute(sql)
metadata_util.close_conn()
target_util.close_conn()
source_util.close_conn()
logger.info("读取MySQL数据,写入目标MySQL和CSV程序执行完成")  

20230830,0508-0511(64min),3h


将数据源采集到目的地目的:解决数据孤岛

在4.2写入数据到csv时,配置project_config.py中

#Barcode业务的相关配置
#生成Csv的路径,
barcode_output_csv_root_path="D:/2.project/data/pyetl-data-logs/output/csv/"
#写出订单模型,CSV的文件名字配置
barcode_orders_output_csv_file_name=f"barcode-{time.strftime{"%Y%m%d-%H%M%S",time.localtime}}"

将时间戳转化为字符串后,字符串按字典序比较大小

#按字典序比较,即挨个字符比较,比较对应位置的ASCII
#从大到小排列
"1" "2" "11" "`12" "24" "3" "200" "50"
"1" "11" "12" "2" "24" "200" "3" "50"

flush演示:flush目的在于刷新缓存,确认数据不在内存中,而是写进磁盘

#with自带关闭功能,在关闭时自动调用flush
with open("a.txt","w") as f:
    f.write('abc\n')
    
f=open("a1.txt","w")
f.write("def/n")
f.close()
​
f=open("a2.txt","w")
f.write("def/n")
f.flush()
​
import time#设置不让文件自动停止运行
f=open("a3.txt","w")
f.write("def/n")
time.sleep(30)

项目需求3:后台日志数据处理

一、日志数据介绍

在工程下建立目录Directory,名为simulator,在里面建立backend_logs_simlulaor.py

# coding:utf8
"""
后端服务写出log日志的模拟数据生成器
"""
import datetime
import random
import time
​
single_log_lines = 1024  # 一个logs文件生成多少行数据
generate_files = 5  # 一次运行生成多少个文件
​
output_path = "D:/2.project/data/pyetl-data-logs/backend_logs/"
log_level_array = ['WARN', 'WARN', 'WARN', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO',
                   'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO', 'INFO',
                   'ERROR']
​
backend_files_name = ['barcode_service.py', 'barcode_service.py', 'barcode_service.py',
                      'orders_service.py', 'orders_service.py', 'orders_service.py', 'orders_service.py',
                      'orders_service.py', 'orders_service.py',
                      'shop_manager.py', 'shop_manager.py',
                      'user_manager.py', 'user_manager.py', 'user_manager.py',
                      'goods_manager.py', 'goods_manager.py', 'goods_manager.py', 'goods_manager.py',
                      'goods_manager.py', 'goods_manager.py',
                      'base_network.py', 'base_network.py',
                      'event.py', 'event.py', 'event.py', 'event.py', 'event.py', 'event.py', 'event.py']
​
visitor_areas = {
    '北京市': ['海淀区', '大兴区', '丰台区', '朝阳区', '昌平区', '海淀区', '怀柔区'],
    '上海市': ['静安区', '黄浦区', '徐汇区', '普陀区', '杨浦区', '宝山区', '浦东新区', '浦东新区'],
    '重庆市': ['万州区', '万州区', '涪陵区', '渝中区', '沙坪坝区', '九龙坡区', '南岸区'],
    '江苏省': ['南京市', '南京市', '南京市', '苏州市', '苏州市', '无锡市', '常州市', '宿迁市', '张家港市'],
    '安徽省': ['阜阳市', '阜阳市', '六安市', '合肥市', '合肥市', '合肥市', '池州市', '铜陵市', '芜湖市'],
    '山东省': ['济南市', '济南市', '青岛市', '青岛市', '青岛市', '菏泽市'],
    '湖北省': ['武汉市', '武汉市', '武汉市', '十堰市', '荆州市', '恩施土家族苗族自治州'],
    '广东省': ['广州市', '广州市', '广州市', '深圳市', '深圳市', '深圳市', '珠海市'],
    '天津市': ['和平区', '河东区', '河西区', '武清区', '宝坻区'],
    '湖南省': ['长沙市', '长沙市', '长沙市', '长沙市', '长沙市', '长沙市', '长沙市', '株洲市', '张家界市', '常德市', '益阳市'],
    '浙江省': ['杭州市', '杭州市', '湖州市', '绍兴市', '舟山市', '金华市', '嘉兴市', '丽水市']
}
visitor_province = ['北京市', '上海市', '重庆市', '江苏省', '安徽省', '山东省', '湖北省', '广东省', '天津市', '湖南省', '浙江省']
​
response_flag = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
response_for_error_flag = [1, 1, 1, 1, 1, 0]
​
for j in range(0, generate_files):
    write_file_path = f'{output_path}{datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}.log'
    with open(write_file_path, 'w', encoding="UTF-8") as f:
        for i in range(single_log_lines):
            date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
            log_level = log_level_array[random.randint(0, len(log_level_array) - 1)]
            file_name = backend_files_name[random.randint(0, len(backend_files_name) - 1)]
            if not log_level == "ERROR":
                if response_flag[random.randint(0, len(response_flag) - 1)] == 1:
                    response_time = random.randint(0, 1000)
                else:
                    response_time = random.randint(1000, 9999)
            else:
                if response_for_error_flag[random.randint(0, len(response_for_error_flag) - 1)] == 1:
                    response_time = random.randint(0, 1000)
                else:
                    response_time = random.randint(1000, 9999)
​
            province = visitor_province[random.randint(0, len(visitor_province) - 1)]
            city = visitor_areas[province][random.randint(0, len(visitor_areas[province]) - 1)]
​
            log_str = f"{date_str}\t[{log_level}]\t{file_name}\t响应时间:{response_time}ms\t{province}\t{city}\t" \
                      f"这里是日志信息......"
​
            f.write(log_str)
            f.write("\n")
    print(f"本次写出第: {j + 1}个文件完成, 文件为: {write_file_path}, 行数:{single_log_lines}")
    time.sleep(1)

结果

数据读出后,写入csv和目的地数据库

构造模型(注意字段):字符串转时间戳,取日志级别,文件名,相应时间只要毫秒,省,市

一行数据存一个对象,写csv,目的地数据,

写元数据,即被记录的数据

1.数据模拟器

2.数据结构

二、实现步骤

1.步骤

2.实现代码

数据可视化

一、什么是BI

1.商业智能

BI(BusinessIntelligence)即商业智能,它是一套完整的解决方案,用来将企业中现有的数据进行有效的整合,快速准确的提供报表并提出决策依据,帮助企业做出明智的业务经营决策。

通过指标的展示助力决策

2.FineBI

使用FineBI进行数据分析的一般步骤 即:

  • 数据准备

  • 数据加工(数据处理)

  • 可视化分析(生成各种图表)

  • 仪表板(把各种图表整合在一起)

  • 分享仪表板

3.功能简介

二、FineBI下载安装

1.FineBI下载

jdk:java development kit,安装jdk-8u241-windows-x64,添加环境变量(复制jdk目录,Windows设置)。打开cmd:win+r,根据输出界面判断配置成功。选择内置数据库三。

2.FineBI安装

3.FineBI的启动

4.使用流程

仪表板-我的自助数据表

系统管理-配置工作

数据准备-添加业务包(可重命名)-点进去添加表

三、连接数据

1.数据来源

2.连接数据的基本步骤

管理系统-数据连接-数据连接管理-新建数据连接-MySQL-数据连接名称-数据库名称(retail)-主机localhost-端口3306-用户名密码-编码自动-测试连接-

数据准备-右击数据列表-抽取数据-更取消-返回-业务包(finebl使用)-点进去-添加表-数据库表-自动出现3个已有数据库表

四、创建仪表盘

1.每日销售

在仪表板-新建仪表板(finebi使用)-组件-抽取数据-finebi使用-选表-确定-维度(字符串类型)指标(数值类型)-图表类型-首排第四个自定义-选择图形属性(如柱状图)-将订单时间拖到横轴,总价格拖到纵轴-右上角进入仪表盘-

图表美化(右上角编辑)-改名称(编辑标题)-订单日期拖到编辑样式中的颜色-纵轴可以多拖指标-图表类型改为折线图-组件样式(选择是否展示标题、图例等,背景改透明,图片)

2.热销产品词云图

添加组件-选表-图表类型(abcd)-文本(商品名称)-大小(数量)-颜色(商品名称)-结果过滤器(商品名称)过滤不合适的文本-过滤-添加条件-选择字段-商品名称-不属于-(选择不想要的名称)-

3.地图

添加组件-图形类型自定义-填充地图-维度里的店铺所在省(地理角色-省市自治区)-经纬度分别拖到横轴纵轴-图形属性-标签(店铺所在省?记录数,相对位置决定图表上展示位置)-颜色(店铺所在省)-组件样式-都不显示-背景。

课程来源:【程序员四次元ポケット的个人空间-哔哩哔哩】 https://b23.tv/dgnWYRc

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您可以在以下地址找到langchain的课程笔记:https://learn.deeplearning.ai/langchain-chat-with-your-data/lesson/1/introduction [1。在开始之前,您需要创建一个名为langchain的虚拟环境,并安装langchain和openai的包。首先,使用以下命令创建虚拟环境:conda create -n langchain python。然后激活该虚拟环境:conda activate langchain。接下来,安装langchain和openai的包:pip install langchain和pip install openai -i https://pypi.tuna.tsinghua.edu.cn/simple [2。在笔记中,您还需要调用llm的api。您可以使用以下代码进行调用: from langchain.agents import load_tools from langchain.agents import initialize_agent from langchain.agents import AgentType from langchain.llms import OpenAI from langchain.chat_models import ChatOpenAI from langchain.chains.conversation.memory import ConversationBufferWindowMemory OPENAI_API_KEY = '***********' llm = ChatOpenAI(openai_api_key=OPENAI_API_KEY, temperature=0, model_name="gpt-3.5-turbo") [3。 通过这些步骤,您将能够开始学习langchain课程并使用llm的api进行聊天。祝您学习愉快!<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [吴恩达ChatGPT《LangChain Chat with Your Data》笔记](https://blog.csdn.net/weixin_39653948/article/details/131874862)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Langchain学习笔记](https://blog.csdn.net/weixin_38226321/article/details/131062424)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值