mdbq 4.0.6__py3-none-any.whl → 4.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdbq/__version__.py CHANGED
@@ -1 +1 @@
1
- VERSION = '4.0.6'
1
+ VERSION = '4.0.8'
@@ -180,21 +180,18 @@ class MysqlDatasQuery:
180
180
  从数据库中下载数据
181
181
  """
182
182
  def __init__(self, download_manager):
183
- # target_service 从哪个服务器下载数据
184
183
  self.months = 0 # 下载几个月数据, 0 表示当月, 1 是上月 1 号至今
185
184
  self.download_manager = download_manager
186
- self.update_service = True # 调试时加,true: 将数据写入 mysql 服务器
187
185
  self.pf_datas = []
188
- self.pf_datas_jd = [] # 京东聚合销售表
189
186
 
190
187
  @staticmethod
191
- def try_except(func): # 在类内部定义一个异常处理方法
188
+ def try_except(func):
192
189
  @wraps(func)
193
190
  def wrapper(*args, **kwargs):
194
191
  try:
195
192
  return func(*args, **kwargs)
196
193
  except Exception as e:
197
- logger.info('函数执行错误', {'函数': func.__name__, '错误': str(e)}) # 将异常信息返回
194
+ logger.info('函数执行错误', {'函数': func.__name__, '错误': str(e), 'args': args, 'kwargs': kwargs})
198
195
 
199
196
  return wrapper
200
197
 
@@ -227,7 +224,6 @@ class MysqlDatasQuery:
227
224
  )
228
225
  __res.append(df)
229
226
  df = pd.concat(__res, ignore_index=True)
230
-
231
227
  df.rename(columns={
232
228
  '场景名字': '营销场景',
233
229
  '主体id': '商品id',
@@ -235,7 +231,6 @@ class MysqlDatasQuery:
235
231
  '总成交笔数': '成交笔数',
236
232
  '总成交金额': '成交金额'
237
233
  }, inplace=True)
238
-
239
234
  df = df.astype({
240
235
  '商品id': str,
241
236
  '花费': 'float64',
@@ -273,7 +268,7 @@ class MysqlDatasQuery:
273
268
  '直接成交金额': ('直接成交金额', np.max)
274
269
  }
275
270
  )
276
- df.insert(loc=1, column='推广渠道', value='万相台无界版') # df中插入新列
271
+ df.insert(loc=1, column='推广渠道', value='万相台无界版')
277
272
  set_typ = {
278
273
  '日期': 'date',
279
274
  '推广渠道': 'varchar(100)',
@@ -290,7 +285,7 @@ class MysqlDatasQuery:
290
285
  '直接成交笔数': 'int',
291
286
  '直接成交金额': 'decimal(12,2)',
292
287
  }
293
-
288
+ # 制作其他聚合表
294
289
  self.pf_datas.append(
295
290
  {
296
291
  '集合名称': '天猫汇总表调用',
@@ -298,7 +293,7 @@ class MysqlDatasQuery:
298
293
  ['日期', '店铺名称', '推广渠道', '营销场景', '商品id', '花费', '展现量', '点击量', '加购量',
299
294
  '成交笔数', '成交金额', '直接成交笔数', '直接成交金额', '自然流量曝光量']]
300
295
  }
301
- ) # 制作其他聚合表
296
+ )
302
297
  logger.info('正在更新数据库', {'主机': f'{host}:{port}', '库': db_name, '表': table_name})
303
298
  uld.upload_data(
304
299
  db_name=db_name,
@@ -720,23 +715,11 @@ class MysqlDatasQuery:
720
715
  __res.append(df)
721
716
  df = pd.concat(__res, ignore_index=True)
722
717
  df['宝贝id'] = df['宝贝id'].astype(str)
723
- # df = df.groupby(['日期', '店铺名称', '宝贝id', '行业类目'], as_index=False).agg(
724
- # **{
725
- # '销售额': ('销售额', np.min),
726
- # '销售量': ('销售量', np.min),
727
- # '订单数': ('订单数', np.min),
728
- # '退货量': ('退货量', np.max),
729
- # '退款额': ('退款额', np.max),
730
- # '退款额_发货后': ('退款额_发货后', np.max),
731
- # '退货量_发货后': ('退货量_发货后', np.max),
732
- # }
733
- # )
734
718
  # 仅保留最新日期的数据
735
719
  idx = df.groupby(['日期', '店铺名称', '宝贝id'])['更新时间'].idxmax()
736
720
  df = df.loc[idx]
737
721
  df = df[['日期', '店铺名称', '宝贝id', '行业类目', '销售额', '销售量', '订单数', '退货量', '退款额', '退款额_发货后', '退货量_发货后']]
738
- df['件均价'] = df.apply(lambda x: x['销售额'] / x['销售量'] if x['销售量'] > 0 else 0, axis=1).round(
739
- 0) # 两列运算, 避免除以0
722
+ df['件均价'] = np.where(df['销售量'] > 0, df['销售额'] / df['销售量'], 0).round(0)
740
723
  df['价格带'] = df['件均价'].apply(
741
724
  lambda x: '2000+' if x >= 2000
742
725
  else '1000+' if x >= 1000
@@ -850,8 +833,7 @@ class MysqlDatasQuery:
850
833
  '直接成交金额': ('直接成交金额', np.max)
851
834
  }
852
835
  )
853
- df.insert(loc=1, column='推广渠道', value='万相台无界版') # df中插入新列
854
-
836
+ df.insert(loc=1, column='推广渠道', value='万相台无界版')
855
837
  # 开始处理用户特征
856
838
  df_sx = self.download_manager.data_to_df(
857
839
  db_name='达摩盘3',
@@ -860,7 +842,7 @@ class MysqlDatasQuery:
860
842
  end_date=end_date,
861
843
  projection={'人群名称': 1, '消费能力等级': 1, '用户年龄': 1},
862
844
  )
863
- df_sx['人群名称'] = df_sx['人群名称'].apply(lambda x: f'达摩盘:{x}')
845
+ df_sx['人群名称'] = '达摩盘:' + df_sx['人群名称']
864
846
  df_sx.rename(columns={'消费能力等级': '消费力层级'}, inplace=True)
865
847
  df = pd.merge(df, df_sx, left_on=['人群名字'], right_on=['人群名称'], how='left')
866
848
  df.pop('人群名称')
@@ -869,10 +851,8 @@ class MysqlDatasQuery:
869
851
  df['用户年龄'] = df['用户年龄'].apply(
870
852
  lambda x: "~".join(re.findall(r'(\d{2})\D.*(\d{2})岁', str(x))[0])
871
853
  if str(x) != 'nan' and re.findall(r'(\d{2})\D.*(\d{2})岁', str(x)) else x)
872
-
873
854
  # 1. 匹配 L后面接 2 个或以上数字,不区分大小写,示例:L345
874
855
  # 2. 其余情况,L 后面接多个数字的都会被第一条 if 命中,不区分大小写
875
-
876
856
  df['消费力层级'] = df.apply(
877
857
  lambda x:
878
858
  ''.join(re.findall(r'(l\d+)', x['人群名字'].upper(), re.IGNORECASE))
@@ -883,12 +863,10 @@ class MysqlDatasQuery:
883
863
  else 'L2' if re.findall(r'(l\d*2)', x['人群名字'], re.IGNORECASE) and str(x['消费力层级']) == 'nan'
884
864
  else 'L1' if re.findall(r'(l\d*1)', x['人群名字'], re.IGNORECASE) and str(x['消费力层级']) == 'nan'
885
865
  else x['消费力层级'], axis=1)
886
-
887
866
  # 1. 匹配连续的 4 个数字且后面不能接数字或"元"或汉字,筛掉的人群示例:月均消费6000元|受众20240729175213|xxx2024真皮公文包
888
867
  # 2. 匹配 2数字_2数字且前面不能是数字,合法匹配:人群_30_50_促; 非法示例:L345_3040 避免识别出 35~20 岁用户的情况
889
868
  # pattern = r'(\d{4})(?!\d|[\u4e00-\u9fa5])' # 匹配 4 个数字,后面不能接数字或汉字
890
869
  # pattern = r'(?<![\d\u4e00-\u9fa5])(\d{4})' # 匹配前面不是数字或汉字的 4 个连续数字
891
-
892
870
  # 匹配 4 个数字,前面和后面都不能是数字或汉字
893
871
  pattern1 = r'(?<![\d\u4e00-\u9fa5])(\d{4})(?!\d|[\u4e00-\u9fa5])'
894
872
  # 匹配指定字符,前面不能是数字或 l 或 L 开头
@@ -909,8 +887,7 @@ class MysqlDatasQuery:
909
887
  else x
910
888
  )
911
889
  # 年龄层不能是 0 开头
912
- df['用户年龄'] = df['用户年龄'].apply(
913
- lambda x: '' if str(x).startswith('0') else x)
890
+ df['用户年龄'] = np.where(df['用户年龄'].astype(str).str.startswith('0'), '', df['用户年龄'])
914
891
  df['用户年龄'] = df['用户年龄'].apply(
915
892
  lambda x:
916
893
  re.sub(f'~50', '~49' ,str(x)) if '~50' in str(x) else
@@ -919,10 +896,6 @@ class MysqlDatasQuery:
919
896
  re.sub(r'\d{4}~', '', str(x)) if str(x) != 'nan' else
920
897
  x
921
898
  )
922
- # df = df.head(1000)
923
- # df.to_csv('/Users/xigua/Downloads/test.csv', index=False, header=True, encoding='utf-8_sig')
924
- # breakpoint()
925
-
926
899
  # 下面是添加人群 AIPL 分类
927
900
  dir_file = f'\\\\192.168.1.198\\时尚事业部\\01.运营部\\0-电商周报-每周五更新\\分类配置文件.xlsx'
928
901
  dir_file2 = '/Volumes/时尚事业部/01.运营部/0-电商周报-每周五更新/分类配置文件.xlsx'
@@ -1080,7 +1053,6 @@ class MysqlDatasQuery:
1080
1053
  dir_file = dir_file2
1081
1054
  if os.path.isfile(dir_file):
1082
1055
  df_fl = pd.read_excel(dir_file, sheet_name='关键词分类', header=0)
1083
- # df_fl.rename(columns={'分类1': '词分类'}, inplace=True)
1084
1056
  df_fl = df_fl[['关键词', '词分类']]
1085
1057
  # 合并并获取词分类信息
1086
1058
  df = pd.merge(df, df_fl, left_on=['词名字_词包名字'], right_on=['关键词'], how='left')
@@ -1159,7 +1131,6 @@ class MysqlDatasQuery:
1159
1131
  return pd.DataFrame()
1160
1132
  return df
1161
1133
 
1162
-
1163
1134
  @try_except
1164
1135
  @upload_data_decorator()
1165
1136
  def tg_cjzb(self, db_name='聚合数据', table_name='天猫_超级直播', is_maximize=True):
@@ -1207,7 +1178,6 @@ class MysqlDatasQuery:
1207
1178
  if col not in cjzb_qzt.columns.tolist():
1208
1179
  cjzb_qzt[col] = 0
1209
1180
  df = pd.concat([df, cjzb_qzt], ignore_index=True)
1210
-
1211
1181
  df.rename(columns={
1212
1182
  '观看次数': '观看次数',
1213
1183
  '总购物车数': '加购量',
@@ -1283,7 +1253,6 @@ class MysqlDatasQuery:
1283
1253
  '直接成交金额': 'decimal(12,2)',
1284
1254
  }
1285
1255
  logger.info('正在更新数据库', {'主机': f'{host}:{port}', '库': db_name, '表': table_name})
1286
-
1287
1256
  return df, {
1288
1257
  'db_name': db_name,
1289
1258
  'table_name': table_name,
@@ -1406,57 +1375,7 @@ class MysqlDatasQuery:
1406
1375
  'partition_date_column': '日期', # 用于分表的日期列名,默认为'日期'
1407
1376
  'indexes': [], # 普通索引列
1408
1377
  'transaction_mode': 'batch', # 事务模式
1409
- 'unique_keys': [['日期', '推广渠道', '店铺名称', '营销场景', '报表类型']], # 唯一约束列表
1410
- }
1411
-
1412
- @try_except
1413
- @upload_data_decorator()
1414
- def idbm_bak(self, db_name='聚合数据', table_name='商品id编码表'):
1415
- """ 用生意经日数据制作商品 id 和编码对照表 """
1416
- year = datetime.datetime.today().year
1417
- data_values = []
1418
- for year in range(2022, year+1):
1419
- data_values += self.download_manager.columns_to_list(
1420
- db_name='生意经3',
1421
- table_name=f'宝贝指标_{year}',
1422
- columns_name=['宝贝id', '商家编码', '行业类目'],
1423
- )
1424
- df = pd.DataFrame(data=data_values)
1425
- df['宝贝id'] = df['宝贝id'].astype(str)
1426
- df.drop_duplicates(subset='宝贝id', keep='last', inplace=True, ignore_index=True)
1427
- # df['行业类目'] = df['行业类目'].apply(lambda x: re.sub(' ', '', x))
1428
- try:
1429
- df[['一级类目', '二级类目', '三级类目']] = df['行业类目'].str.split(' -> ', expand=True).loc[:, 0:2]
1430
- except:
1431
- try:
1432
- df[['一级类目', '二级类目']] = df['行业类目'].str.split(' -> ', expand=True).loc[:, 0:1]
1433
- except:
1434
- df['一级类目'] = df['行业类目']
1435
- df.drop('行业类目', axis=1, inplace=True)
1436
- df.sort_values('宝贝id', ascending=False, inplace=True)
1437
- df = df[(df['宝贝id'] != '973') & (df['宝贝id'] != 973) & (df['宝贝id'] != '0')]
1438
- set_typ = {
1439
- '宝贝id': 'bigint',
1440
- '商家编码': 'varchar(100)',
1441
- '一级类目': 'varchar(100)',
1442
- '二级类目': 'varchar(100)',
1443
- '三级类目': 'varchar(100)',
1444
- }
1445
- logger.info('正在更新数据库', {'主机': f'{host}:{port}', '库': db_name, '表': table_name})
1446
- return df, {
1447
- 'db_name': db_name,
1448
- 'table_name': table_name,
1449
- 'set_typ': set_typ,
1450
- 'primary_keys': [], # 创建唯一主键
1451
- 'check_duplicate': False, # 检查重复数据
1452
- 'duplicate_columns': [], # 指定排重的组合键
1453
- 'update_on_duplicate': True, # 更新旧数据
1454
- 'allow_null': False, # 允许插入空值
1455
- 'partition_by': None, # 分表方式
1456
- 'partition_date_column': '日期', # 用于分表的日期列名,默认为'日期'
1457
- 'indexes': [], # 普通索引列
1458
- 'transaction_mode': 'batch', # 事务模式
1459
- 'unique_keys': [['宝贝id']], # 唯一约束列表
1378
+ 'unique_keys': [['日期', '推广渠道', '店铺名称', '营销场景', '报表类型', '花费', '展现量']], # 唯一约束列表
1460
1379
  }
1461
1380
 
1462
1381
  @try_except
@@ -1650,9 +1569,6 @@ class MysqlDatasQuery:
1650
1569
  end_date=end_date,
1651
1570
  projection=projection,
1652
1571
  )
1653
- # df.drop_duplicates(
1654
- # subset=['日期', '店铺名称', '商品id', '商品访客数'], keep='last',
1655
- # inplace=True, ignore_index=True)
1656
1572
  # 保留最新日期的数据
1657
1573
  idx = df.groupby(['日期', '店铺名称', '商品id'])['更新时间'].idxmax()
1658
1574
  df = df.loc[idx]
@@ -1691,7 +1607,6 @@ class MysqlDatasQuery:
1691
1607
  df['上市季节'] = df['上市年月'].apply(lambda x: check_jijie(x))
1692
1608
  p = df.pop('上市季节')
1693
1609
  df.insert(loc=9, column='上市季节', value=p)
1694
-
1695
1610
  set_typ = {
1696
1611
  '商品id': 'BIGINT',
1697
1612
  '店铺名称': 'varchar(100)',
@@ -1784,7 +1699,6 @@ class MysqlDatasQuery:
1784
1699
  )
1785
1700
  __res.append(df)
1786
1701
  df = pd.concat(__res, ignore_index=True)
1787
- # df['日期'] = pd.to_datetime(df['日期'], format='%Y-%m-%d', errors='ignore') # 转换日期列
1788
1702
  df = df.astype({'访客数': 'int64'}, errors='ignore')
1789
1703
  df = df[df['访客数'] > 0]
1790
1704
  df.drop_duplicates(subset=['日期', '店铺名称', '类别', '来源构成', '一级来源', '二级来源', '三级来源', '访客数'], keep='last', inplace=True, ignore_index=True)
@@ -1800,7 +1714,6 @@ class MysqlDatasQuery:
1800
1714
  df_visitor3['index'] = df_visitor3['index'] + 100
1801
1715
  df_visitor3.rename(columns={'index': '三级来源索引'}, inplace=True)
1802
1716
  df_visitor3 = df_visitor3[['三级来源', '三级来源索引']]
1803
-
1804
1717
  # 包含二级来源名称和预设索引值列
1805
1718
  df_visitor2 = df[df['日期'] >= pd.to_datetime(last_month)]
1806
1719
  df_visitor2 = df_visitor2[(df_visitor2['二级来源'] != '汇总') & (df_visitor2['二级来源'] != '0')]
@@ -1810,7 +1723,6 @@ class MysqlDatasQuery:
1810
1723
  df_visitor2['index'] = df_visitor2['index'] + 100
1811
1724
  df_visitor2.rename(columns={'index': '二级来源索引'}, inplace=True)
1812
1725
  df_visitor2 = df_visitor2[['二级来源', '二级来源索引']]
1813
-
1814
1726
  # 包含一级来源名称和预设索引值列
1815
1727
  df_visitor1 = df[df['日期'] >= pd.to_datetime(last_month)]
1816
1728
  df_visitor1 = df_visitor1[(df_visitor1['一级来源'] != '汇总') & (df_visitor1['一级来源'] != '0')]
@@ -1939,7 +1851,6 @@ class MysqlDatasQuery:
1939
1851
  projection=projection,
1940
1852
  )
1941
1853
  __res.append(df)
1942
-
1943
1854
  df = pd.concat(__res, ignore_index=True)
1944
1855
  df = df.groupby(
1945
1856
  ['日期', '店铺名称', '产品线', '触发sku_id', '跟单sku_id', 'spu_id', '花费', '展现数', '点击数'],
@@ -2201,13 +2112,6 @@ class MysqlDatasQuery:
2201
2112
  __res.append(df)
2202
2113
  df = pd.concat(__res, ignore_index=True)
2203
2114
  df = df[df['商品id'] != '合计']
2204
- # df = df.groupby(['日期', '店铺名称', '商品id', '货号', '访客数', '成交客户数', '加购商品件数', '加购人数'],
2205
- # as_index=False).agg(
2206
- # **{
2207
- # '成交单量': ('成交单量', np.max),
2208
- # '成交金额': ('成交金额', np.max),
2209
- # }
2210
- # )
2211
2115
  # 仅保留最新日期的数据
2212
2116
  idx = df.groupby(['日期', '店铺名称', '商品id', '货号', '访客数', '成交客户数', '加购商品件数', '加购人数'])['更新时间'].idxmax()
2213
2117
  df = df.loc[idx]
@@ -2271,13 +2175,6 @@ class MysqlDatasQuery:
2271
2175
  __res.append(df)
2272
2176
  df = pd.concat(__res, ignore_index=True)
2273
2177
  df = df[df['商品id'] != '合计']
2274
- # df = df.groupby(['日期', '店铺名称', '商品id', '货号', '访客数', '成交客户数', '加购商品件数', '加购人数'],
2275
- # as_index=False).agg(
2276
- # **{
2277
- # '成交单量': ('成交单量', np.max),
2278
- # '成交金额': ('成交金额', np.max),
2279
- # }
2280
- # )
2281
2178
  # 仅保留最新日期的数据
2282
2179
  idx = df.groupby(['日期', '店铺名称', '商品id', '货号', '访客数', '成交客户数', '加购商品件数', '加购人数'])['更新时间'].idxmax()
2283
2180
  df = df.loc[idx]
@@ -2544,7 +2441,6 @@ class MysqlDatasQuery:
2544
2441
  df_jd = pd.DataFrame() # 京东推广
2545
2442
  df_jd_qzyx = pd.DataFrame() # 京东全站推广
2546
2443
  df_jd_ziying = pd.DataFrame() # 京东推广
2547
-
2548
2444
  start_date, end_date = self.months_data(num=self.months)
2549
2445
  projection = {
2550
2446
  '日期': 1,
@@ -2659,7 +2555,6 @@ class MysqlDatasQuery:
2659
2555
  '成交金额': ('总成交金额', np.max)
2660
2556
  }
2661
2557
  )
2662
-
2663
2558
  # 天猫的全站推广包含在营销场景报表中,淘宝店不包含
2664
2559
  df_tb_qzt = pd.DataFrame()
2665
2560
  if '全站推广' not in df_tb['营销场景'].tolist():
@@ -2711,7 +2606,6 @@ class MysqlDatasQuery:
2711
2606
  }
2712
2607
  )
2713
2608
  df_tb_qzt['营销场景'] = '全站推广'
2714
-
2715
2609
  # 品销宝报表
2716
2610
  projection = {
2717
2611
  '日期': 1,
@@ -2750,7 +2644,6 @@ class MysqlDatasQuery:
2750
2644
  )
2751
2645
  df_tm_pxb.rename(columns={'报表类型': '营销场景', '消耗': '花费'}, inplace=True)
2752
2646
  df_tm_pxb['营销场景'] = '品销宝'
2753
-
2754
2647
  # 因为 2024.04.16及之前的营销场景报表不含超级直播,所以在此添加
2755
2648
  if start_date < pd.to_datetime('2024-04-17'):
2756
2649
  projection = {
@@ -2788,7 +2681,7 @@ class MysqlDatasQuery:
2788
2681
  '成交金额': ('总成交金额', np.max)
2789
2682
  }
2790
2683
  )
2791
-
2684
+ # 京东数据
2792
2685
  projection = {
2793
2686
  '日期': 1,
2794
2687
  '产品线': 1,
@@ -2832,7 +2725,6 @@ class MysqlDatasQuery:
2832
2725
  df_jd = df_jd[['日期', '店铺名称', '产品线', '花费', '展现数', '点击数', '加购量', '成交笔数', '成交金额']]
2833
2726
  df_jd.rename(columns={'产品线': '营销场景', '展现数': '展现量', '点击数': '点击量'}, inplace=True)
2834
2727
  df_jd = df_jd[df_jd['花费'] > 0]
2835
-
2836
2728
  projection = {
2837
2729
  '日期': 1,
2838
2730
  '产品线': 1,
@@ -2867,7 +2759,7 @@ class MysqlDatasQuery:
2867
2759
  df_jd_qzyx.rename(columns={'产品线': '营销场景'}, inplace=True)
2868
2760
  df_jd_qzyx = df_jd_qzyx[['日期', '店铺名称', '营销场景', '花费', '展现量', '点击量', '成交笔数', '成交金额']]
2869
2761
  df_jd_qzyx = df_jd_qzyx[df_jd_qzyx['花费'] > 0]
2870
-
2762
+ # 京东自营店数据
2871
2763
  projection = {
2872
2764
  '日期': 1,
2873
2765
  '产品线': 1,
@@ -3045,7 +2937,6 @@ class MysqlDatasQuery:
3045
2937
  )
3046
2938
  idx = df.groupby(['日期', '店铺名称', 'spuid'])['更新时间'].idxmax()
3047
2939
  df = df.loc[idx]
3048
-
3049
2940
  # 调整列顺序, 定义需要前置的列
3050
2941
  cols_to_move = ['日期','平台','店铺名称','品牌名','商品名称', '商品款号','spuid', '一级类目名称', '二级类目名称', '三级类目名称']
3051
2942
  # 生成新的列顺序:前置列 + 剩余列(保持原顺序)
@@ -3146,7 +3037,6 @@ class MysqlDatasQuery:
3146
3037
  '消费能力等级': 1,
3147
3038
  '用户性别': 1,
3148
3039
  }
3149
- # projection = {}
3150
3040
  df_crowd = self.download_manager.data_to_df(
3151
3041
  db_name='达摩盘3',
3152
3042
  table_name='我的人群属性',
@@ -3158,7 +3048,6 @@ class MysqlDatasQuery:
3158
3048
  df_crowd.drop_duplicates(subset=['人群id',], keep='last', inplace=True, ignore_index=True)
3159
3049
  df_crowd.pop('日期')
3160
3050
  df_crowd = df_crowd.astype({'人群id': 'int64'}, errors='ignore')
3161
-
3162
3051
  projection = {}
3163
3052
  __res = []
3164
3053
  for year in range(2024, datetime.datetime.today().year + 1):
@@ -3178,8 +3067,6 @@ class MysqlDatasQuery:
3178
3067
  # 清除一些不必要的字符
3179
3068
  df['用户年龄'] = df['用户年龄'].apply(lambda x: '~'.join(re.findall(r'^(\d+).*-(\d+)岁$', str(x))[0]) if '岁' in str(x) else x)
3180
3069
  df['消费能力等级'] = df['消费能力等级'].apply(lambda x: f'L{''.join(re.findall(r'(\d)', str(x)))}' if '购买力' in str(x) else x)
3181
- # df.to_csv('/Users/xigua/Downloads/test3.csv', index=False, header=True, encoding='utf-8_sig')
3182
- # breakpoint()
3183
3070
  df.rename(columns={'消耗_元': '消耗'}, inplace=True)
3184
3071
  set_typ = {
3185
3072
  '日期': 'date',
@@ -3242,7 +3129,7 @@ class MysqlDatasQuery:
3242
3129
  'partition_date_column': '日期', # 用于分表的日期列名,默认为'日期'
3243
3130
  'indexes': [], # 普通索引列
3244
3131
  'transaction_mode': 'batch', # 事务模式
3245
- 'unique_keys': [['日期', '店铺名称', '人群id', '营销渠道', '计划基础信息']], # 唯一约束列表
3132
+ 'unique_keys': [['日期', '店铺名称', '人群id', '营销渠道', '计划基础信息', '推广单元信息']], # 唯一约束列表
3246
3133
  }
3247
3134
 
3248
3135
  @try_except
@@ -3416,7 +3303,6 @@ class MysqlDatasQuery:
3416
3303
  result_i = re.findall('_i$|_i_|^i_', str(keyword), re.IGNORECASE)
3417
3304
  result_p = re.findall('_p$|_p_|_pl|^p_||^pl_', str(keyword), re.IGNORECASE)
3418
3305
  result_l = re.findall('_l$|_l_|^l_', str(keyword), re.IGNORECASE)
3419
-
3420
3306
  datas = [
3421
3307
  {
3422
3308
  '类别': 'A',
@@ -3435,7 +3321,6 @@ class MysqlDatasQuery:
3435
3321
  '值': result_l,
3436
3322
  }
3437
3323
  ]
3438
-
3439
3324
  is_res = False
3440
3325
  for data in datas:
3441
3326
  if data['值']:
@@ -3568,7 +3453,6 @@ class MysqlDatasQuery:
3568
3453
  '成交金额': ('成交金额', np.sum)
3569
3454
  }
3570
3455
  )
3571
-
3572
3456
  zb.rename(columns={
3573
3457
  '观看次数': '点击量',
3574
3458
  }, inplace=True)
@@ -3596,16 +3480,13 @@ class MysqlDatasQuery:
3596
3480
  '直接成交金额': 'float64',
3597
3481
  '自然流量曝光量': 'int64',
3598
3482
  }, errors='raise')
3599
-
3600
3483
  df = pd.concat([tg, zb, pxb], axis=0, ignore_index=True)
3601
3484
  df.fillna(0, inplace=True) # concat 之后要填充空值
3602
- df = df.astype(
3603
- {
3604
- '商品id': str,
3605
- '自然流量曝光量': 'int64',
3606
- }
3607
- )
3608
- [df[col].apply(lambda x: '0' if str(x) == '' else x) for col in df.columns.tolist()]
3485
+ df = df.astype({
3486
+ '商品id': str,
3487
+ '自然流量曝光量': 'int64',
3488
+ })
3489
+ df.replace(to_replace='', value=0, inplace=True)
3609
3490
  set_typ = {
3610
3491
  '日期': 'date',
3611
3492
  '店铺名称': 'varchar(100)',
@@ -3650,7 +3531,6 @@ def get_day_of_month(num):
3650
3531
  _, _lastDay = calendar.monthrange(months_ago.year, months_ago.month) # 返回月的第一天的星期和当月总天数
3651
3532
  _firstDay = datetime.date(months_ago.year, months_ago.month, day=1).strftime('%Y-%m-%d')
3652
3533
  _lastDay = datetime.date(months_ago.year, months_ago.month, day=_lastDay).strftime('%Y-%m-%d')
3653
-
3654
3534
  return _firstDay, _lastDay
3655
3535
 
3656
3536
 
@@ -3690,9 +3570,7 @@ def date_table():
3690
3570
  group['第n周_new'] = f'第{num}周'
3691
3571
  num += 1
3692
3572
  __res.append(group.copy())
3693
- # break
3694
3573
  df = pd.concat(__res, ignore_index=True)
3695
- # df['日期'] = df['日期'].apply(lambda x: pd.to_datetime(x))
3696
3574
  df['weekname'] = df['日期'].dt.day_name()
3697
3575
  dict_dt = {
3698
3576
  'Monday': '星期一',
@@ -3746,7 +3624,6 @@ def date_table():
3746
3624
  def query1(months=1, download_manager=None):
3747
3625
  sdq = MysqlDatasQuery(download_manager=download_manager) # 实例化数据处理类
3748
3626
  sdq.months = months # 设置数据周期, 1 表示近 2 个月
3749
-
3750
3627
  # 依赖表 -- >>
3751
3628
  sdq.tg_wxt(db_name='聚合数据', table_name='天猫_主体报表')
3752
3629
  sdq.tg_cjzb(db_name='聚合数据', table_name='天猫_超级直播')
@@ -3792,7 +3669,6 @@ def query3(months=1, download_manager=None):
3792
3669
  def main(months=3):
3793
3670
  # 1. 更新日期表 更新货品年份基准表, 属性设置 3 - 货品年份基准
3794
3671
  date_table()
3795
-
3796
3672
  # 2. 数据聚合
3797
3673
  download_manager = s_query.QueryDatas(
3798
3674
  username=username,
@@ -3807,6 +3683,5 @@ def main(months=3):
3807
3683
 
3808
3684
 
3809
3685
  if __name__ == '__main__':
3810
- # main(months=3)
3811
-
3686
+ main(months=3)
3812
3687
  pass
mdbq/log/mylogger.py CHANGED
@@ -11,18 +11,13 @@ from typing import Optional, Dict, Any, List, Callable
11
11
  import atexit
12
12
  import traceback
13
13
  import inspect
14
+ import psutil
14
15
 
15
- try:
16
- import psutil
17
- HAS_PSUTIL = True
18
- except ImportError:
19
- HAS_PSUTIL = False
20
16
 
21
17
  def get_caller_filename(default='mylogger'):
22
18
  stack = inspect.stack()
23
19
  for frame_info in stack:
24
20
  filename = frame_info.filename
25
- # 跳过本日志库自身
26
21
  if not filename.endswith('mylogger.py'):
27
22
  return os.path.splitext(os.path.basename(filename))[0]
28
23
  return default
@@ -102,7 +97,6 @@ class MyLogger:
102
97
  self.name = name
103
98
  self.logging_mode = logging_mode.lower()
104
99
  self.log_level = log_level.upper()
105
- # log_file 自动为 name+'.log'
106
100
  if log_file is None:
107
101
  self.log_file = os.path.join(log_path, f"{self.name}.log")
108
102
  else:
@@ -117,7 +111,7 @@ class MyLogger:
117
111
  self.buffer_size = buffer_size
118
112
  self.sample_rate = max(0.0, min(1.0, sample_rate))
119
113
  self.filters = filters or []
120
- self.enable_metrics = enable_metrics and HAS_PSUTIL
114
+ self.enable_metrics = enable_metrics
121
115
  self.metrics_interval = metrics_interval
122
116
  self.message_limited = message_limited
123
117
  self.flush_interval = flush_interval
@@ -167,17 +161,14 @@ class MyLogger:
167
161
  self.old_context = {}
168
162
 
169
163
  def __enter__(self):
170
- # 保存旧上下文并设置新上下文
171
164
  self.old_context = getattr(self.logger._context, 'data', {}).copy()
172
165
  self.logger._context.data.update(self.context_vars)
173
166
  return self.logger
174
167
 
175
168
  def __exit__(self, exc_type, exc_val, exc_tb):
176
- # 恢复旧上下文
177
169
  self.logger._context.data = self.old_context
178
170
  if exc_type is not None:
179
- self.logger.error(f"上下文内异常2: {exc_val}",
180
- extra={'类型': str(exc_type)})
171
+ self.logger.error(f"上下文内异常2: {exc_val}", extra={'类型': str(exc_type)})
181
172
  return False
182
173
 
183
174
  def _init_logging(self):
@@ -185,15 +176,10 @@ class MyLogger:
185
176
  valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
186
177
  if self.log_level not in valid_levels:
187
178
  self.log_level = 'INFO'
188
-
189
179
  self.logger.setLevel(self.log_level)
190
-
191
- # 防止重复添加handler
192
180
  if self.logger.handlers:
193
181
  for handler in self.logger.handlers[:]:
194
182
  self.logger.removeHandler(handler)
195
-
196
- # 定义日志格式
197
183
  if self.log_format.lower() == 'simple':
198
184
  class SimpleFormatter(logging.Formatter):
199
185
  def format(self, record):
@@ -255,14 +241,12 @@ class MyLogger:
255
241
  )
256
242
  file_handler.setFormatter(formatter)
257
243
  self._handlers.append(file_handler)
258
-
259
244
  if not self.enable_async:
260
245
  for handler in self._handlers:
261
246
  self.logger.addHandler(handler)
262
247
 
263
248
  def _setup_async_logging(self):
264
- # 标准库异步日志实现
265
- self._log_queue = queue.Queue(maxsize=self.buffer_size) # 无限长度,绝不阻塞
249
+ self._log_queue = queue.Queue(maxsize=self.buffer_size)
266
250
  queue_handler = logging.handlers.QueueHandler(self._log_queue)
267
251
  self.logger.addHandler(queue_handler)
268
252
  self._queue_listener = logging.handlers.QueueListener(
@@ -274,7 +258,6 @@ class MyLogger:
274
258
  """获取系统资源使用指标"""
275
259
  if not self.enable_metrics:
276
260
  return {}
277
-
278
261
  try:
279
262
  return {
280
263
  '内存': {
@@ -301,8 +284,7 @@ class MyLogger:
301
284
  }
302
285
  }
303
286
  except Exception as e:
304
- self.logger.warning(f"无法采集系统性能指标: {e}",
305
- extra={'extra_data': {'metrics_error': str(e)}})
287
+ self.logger.warning(f"无法采集系统性能指标: {e}", extra={'extra_data': {'metrics_error': str(e)}})
306
288
  return {}
307
289
 
308
290
  def _apply_filters(self, level: str, message: str, extra: Dict) -> bool:
@@ -310,11 +292,11 @@ class MyLogger:
310
292
  for filter_func in self.filters:
311
293
  try:
312
294
  if not filter_func(level, message, extra):
313
- return False # 如果过滤器返回 False,则丢弃该日志
295
+ return False
314
296
  except Exception as e:
315
297
  self.logger.warning(f"过滤失败: {e}",
316
298
  extra={'extra_data': {'filter_error': str(e)}})
317
- return True # 所有过滤器都返回 True,则记录该日志
299
+ return True
318
300
 
319
301
  def log_error_handler(retry_times=0, fallback_level='error'):
320
302
  """
@@ -334,11 +316,10 @@ class MyLogger:
334
316
  except Exception as e:
335
317
  last_exception = e
336
318
  if attempt < retry_times:
337
- time.sleep(0.1 * (attempt + 1)) # 简单的退避策略
319
+ time.sleep(0.1 * (attempt + 1))
338
320
  continue
339
321
 
340
322
  try:
341
- # 降级处理
342
323
  logging.basicConfig()
343
324
  fallback_logger = logging.getLogger(f"{getattr(self, 'name', 'mylogger')}_fallback")
344
325
  fallback_msg = f"[降级处理] {message}"[:1000]
@@ -359,23 +340,15 @@ class MyLogger:
359
340
  """同步日志记录(兼容异步,直接走logger)"""
360
341
  if not hasattr(self.logger, level.lower()):
361
342
  return
362
-
363
- # message 仅接收字符串类型
364
343
  if not isinstance(message, str):
365
344
  message = str(message)
366
-
367
- # 简化日志内容,避免过长
368
345
  if len(message) > self.message_limited:
369
346
  message = message[:self.message_limited] + '...'
370
-
371
- # 定期收集系统指标
372
347
  if self.enable_metrics:
373
348
  now = time.time()
374
349
  if now - self._last_metrics_time > self.metrics_interval:
375
350
  self._metrics_cache = self._get_system_metrics()
376
351
  self._last_metrics_time = now
377
-
378
- # 准备日志额外数据
379
352
  log_extra = {}
380
353
  if self.enable_metrics:
381
354
  log_extra['性能指标'] = self._metrics_cache
@@ -470,11 +443,8 @@ class MyLogger:
470
443
  """记录异常信息"""
471
444
  if not extra:
472
445
  extra = {}
473
-
474
- # 使用inspect获取调用栈
475
446
  frame = inspect.currentframe()
476
447
  try:
477
- # 向上追溯2层(1层是exception方法本身,2层是实际调用位置)
478
448
  caller_frame = frame.f_back.f_back
479
449
  extra.update({
480
450
  'module': caller_frame.f_globals.get('__name__', ''),
@@ -486,9 +456,7 @@ class MyLogger:
486
456
  '堆栈': self._format_traceback(exc_info)
487
457
  })
488
458
  finally:
489
- del frame # 避免循环引用
490
-
491
- # 直接使用logger的error方法记录,保留原始调用栈
459
+ del frame
492
460
  self.log('error', message, extra)
493
461
 
494
462
  def _format_traceback(self, exc_info):
@@ -556,7 +524,6 @@ class MyLogger:
556
524
  """关闭日志记录器,确保所有日志被刷新"""
557
525
  if self.enable_async and self._queue_listener:
558
526
  self._queue_listener.stop()
559
- # 关闭所有handler
560
527
  for handler in self.logger.handlers:
561
528
  try:
562
529
  handler.close()
@@ -569,7 +536,6 @@ class MyLogger:
569
536
  pass
570
537
 
571
538
  def main():
572
- # 创建日志记录器
573
539
  logger = MyLogger(
574
540
  name='my_app',
575
541
  logging_mode='both',
@@ -579,14 +545,11 @@ def main():
579
545
  max_log_size=50,
580
546
  backup_count=5,
581
547
  enable_async=False, # 是否启用异步日志
582
- sample_rate=1, # 采样50%的DEBUG/INFO日志
548
+ sample_rate=1, # 采样DEBUG/INFO日志
583
549
  sensitive_fields=[], # 敏感字段列表
584
550
  enable_metrics=False, # 是否启用性能指标
585
551
  )
586
-
587
552
  logger.info('123')
588
-
589
- # 确保所有日志被刷新
590
553
  logger.shutdown()
591
554
 
592
555
 
mdbq/mysql/s_query.py CHANGED
@@ -68,15 +68,12 @@ class QueryDatas:
68
68
  self.connect_timeout = connect_timeout
69
69
  self.read_timeout = read_timeout
70
70
  self.write_timeout = write_timeout
71
-
72
- # 连接池状态监控
73
71
  self._pool_stats = {
74
72
  'last_health_check': None,
75
73
  'health_check_interval': 300, # 5分钟检查一次
76
74
  'consecutive_failures': 0, # 连续失败次数
77
75
  'max_consecutive_failures': 3 # 最大连续失败次数
78
76
  }
79
-
80
77
  self.base_config = {
81
78
  'host': self.host,
82
79
  'port': int(self.port),
@@ -90,8 +87,6 @@ class QueryDatas:
90
87
  'write_timeout': write_timeout,
91
88
  'autocommit': True
92
89
  }
93
-
94
- # 创建连接池
95
90
  self.pool = self._create_connection_pool(maxconnections, mincached, maxcached)
96
91
 
97
92
  def _create_connection_pool(self, maxconnections: int, mincached: int, maxcached: int) -> PooledDB:
@@ -111,10 +106,7 @@ class QueryDatas:
111
106
  """
112
107
  if hasattr(self, 'pool') and self.pool is not None and self._check_pool_health():
113
108
  return self.pool
114
-
115
109
  self.pool = None
116
-
117
- # 连接参数 - 这些参数会传递给底层的连接创建函数
118
110
  connection_params = {
119
111
  'host': self.host,
120
112
  'port': int(self.port),
@@ -128,8 +120,6 @@ class QueryDatas:
128
120
  'write_timeout': self.write_timeout,
129
121
  'autocommit': True
130
122
  }
131
-
132
- # 连接池参数
133
123
  pool_params = {
134
124
  'creator': pymysql,
135
125
  'maxconnections': maxconnections,
@@ -140,9 +130,7 @@ class QueryDatas:
140
130
  'setsession': [],
141
131
  'ping': 7
142
132
  }
143
-
144
133
  try:
145
- # 创建连接池,将连接参数作为kwargs传递
146
134
  pool = PooledDB(**pool_params, **connection_params)
147
135
  logger.debug('连接池创建成功', {
148
136
  '连接池大小': maxconnections,
@@ -170,17 +158,11 @@ class QueryDatas:
170
158
  """
171
159
  if not self.pool:
172
160
  return False
173
-
174
161
  current_time = time.time()
175
- # 检查是否需要执行健康检查
176
162
  if (self._pool_stats['last_health_check'] is None or
177
163
  current_time - self._pool_stats['last_health_check'] > self._pool_stats['health_check_interval']):
178
-
179
164
  try:
180
- # 更新健康检查时间
181
165
  self._pool_stats['last_health_check'] = current_time
182
-
183
- # 检查连接是否可用
184
166
  with self.pool.connection() as conn:
185
167
  with conn.cursor() as cursor:
186
168
  cursor.execute('SELECT 1')
@@ -192,12 +174,9 @@ class QueryDatas:
192
174
  '连续失败次数': self._pool_stats['consecutive_failures']
193
175
  })
194
176
  return False
195
-
196
- # 重置连续失败计数
197
177
  self._pool_stats['consecutive_failures'] = 0
198
178
  logger.debug('连接池健康检查通过')
199
179
  return True
200
-
201
180
  except Exception as e:
202
181
  self._pool_stats['consecutive_failures'] += 1
203
182
  if self._pool_stats['consecutive_failures'] >= self._pool_stats['max_consecutive_failures']:
@@ -207,7 +186,6 @@ class QueryDatas:
207
186
  '连续失败次数': self._pool_stats['consecutive_failures']
208
187
  })
209
188
  return False
210
-
211
189
  return True
212
190
 
213
191
  @staticmethod
@@ -292,28 +270,23 @@ class QueryDatas:
292
270
  if self._pool_stats['consecutive_failures'] >= self._pool_stats['max_consecutive_failures']:
293
271
  if not self._check_pool_health():
294
272
  logger.warning('连接池不健康,尝试重新创建')
295
- # 使用默认值重新创建连接池
296
273
  self.pool = self._create_connection_pool(10, 2, 5)
297
- # 重置连续失败计数
298
274
  self._pool_stats['consecutive_failures'] = 0
299
275
 
300
276
  conn = self.pool.connection()
301
277
  if db_name:
302
- # 使用原生pymysql连接来选择数据库
303
278
  with conn.cursor() as cursor:
304
279
  cursor.execute(f"USE `{db_name}`")
305
280
  return conn
306
281
  except pymysql.OperationalError as e:
307
282
  error_code = e.args[0] if e.args else None
308
- if error_code in (2003, 2006, 2013): # 连接相关错误
283
+ if error_code in (2003, 2006, 2013):
309
284
  logger.error('数据库连接错误', {
310
285
  '错误代码': error_code,
311
286
  '错误信息': str(e),
312
287
  '数据库': db_name
313
288
  })
314
- # 使用默认值重新创建连接池
315
289
  self.pool = self._create_connection_pool(10, 2, 5)
316
- # 重置连续失败计数
317
290
  self._pool_stats['consecutive_failures'] = 0
318
291
  raise ConnectionError(f'数据库连接错误: {str(e)}')
319
292
  else:
@@ -389,18 +362,14 @@ class QueryDatas:
389
362
  """
390
363
  if not date_str:
391
364
  return default_date
392
-
393
- # 记录尝试的日期格式
394
365
  attempted_formats = []
395
366
  try:
396
- # 尝试多种日期格式
397
367
  for fmt in ['%Y-%m-%d', '%Y/%m/%d', '%Y%m%d', '%Y.%m.%d']:
398
368
  try:
399
369
  attempted_formats.append(fmt)
400
370
  return pd.to_datetime(date_str, format=fmt).strftime('%Y-%m-%d')
401
371
  except ValueError:
402
372
  continue
403
-
404
373
  # 如果所有格式都失败,使用pandas的自动解析
405
374
  attempted_formats.append('auto')
406
375
  return pd.to_datetime(date_str).strftime('%Y-%m-%d')
@@ -429,27 +398,18 @@ class QueryDatas:
429
398
  处理后的日期范围元组 (start_date, end_date),如果处理失败返回 (None, None)
430
399
  """
431
400
  try:
432
- # 如果两个日期都未提供,返回None表示不进行日期过滤
433
401
  if start_date is None and end_date is None:
434
402
  return None, None
435
-
436
- # 如果只提供了开始日期,结束日期设为今天
437
403
  if start_date is not None and end_date is None:
438
404
  end_date = datetime.datetime.today().strftime('%Y-%m-%d')
439
405
  logger.debug('未提供结束日期,使用当前日期', {'库': db_name, '表': table_name, '结束日期': end_date})
440
-
441
- # 如果只提供了结束日期,开始日期设为1970年
442
406
  if start_date is None and end_date is not None:
443
407
  start_date = '1970-01-01'
444
408
  logger.debug('未提供开始日期,使用默认日期', {'库': db_name, '表': table_name, '开始日期': start_date})
445
-
446
- # 格式化日期
447
409
  original_start = start_date
448
410
  original_end = end_date
449
411
  start_date = self.validate_and_format_date(start_date, '1970-01-01')
450
412
  end_date = self.validate_and_format_date(end_date, datetime.datetime.today().strftime('%Y-%m-%d'))
451
-
452
- # 如果日期格式被修改,记录日志
453
413
  if original_start != start_date:
454
414
  logger.debug('开始日期格式已调整', {
455
415
  '库': db_name,
@@ -543,12 +503,8 @@ class QueryDatas:
543
503
  if not cols_exist:
544
504
  logger.warning('表没有可用列')
545
505
  return []
546
-
547
- # 如果 projection 为 None、空字典或空列表,返回所有列
548
506
  if projection is None or projection == {} or projection == []:
549
507
  return list(cols_exist)
550
-
551
- # 验证列名是否包含特殊字符
552
508
  invalid_chars = set('`\'"\\')
553
509
  selected_columns = []
554
510
  for col in projection:
@@ -557,11 +513,9 @@ class QueryDatas:
557
513
  continue
558
514
  if col in cols_exist and projection[col]:
559
515
  selected_columns.append(col)
560
-
561
516
  if not selected_columns:
562
517
  logger.info('参数不匹配,返回所有列', {'参数': projection})
563
518
  return list(cols_exist)
564
-
565
519
  return selected_columns
566
520
 
567
521
  def _build_query_sql(self, db_name: str, table_name: str, selected_columns: List[str],
@@ -960,10 +914,7 @@ def main():
960
914
  username, password, host, port = my_cont['username'], my_cont['password'], my_cont['host'], int(my_cont['port'])
961
915
  host = 'localhost'
962
916
 
963
- # 创建QueryDatas实例
964
917
  qd = QueryDatas(username=username, password=password, host=host, port=port)
965
-
966
- # 执行查询
967
918
  df = qd.data_to_df('聚合数据', '店铺流量来源构成', limit=10)
968
919
  print(df)
969
920
 
mdbq/mysql/unique_.py CHANGED
@@ -321,14 +321,14 @@ def main():
321
321
  # "sku榜单": [['日期', '平台', '店铺名称', '条码']],
322
322
  # "spu榜单": [['日期', '平台', '店铺名称', '商品款号', '访客量']],
323
323
  # },
324
- "生意参谋3": {
325
- # "crm成交客户": [['客户id']],
326
- # "商品排行": [['日期', '店铺名称', '商品id']],
327
- "流量来源构成": [['日期', '店铺名称', '来源构成', '类别', '一级来源', '二级来源', '三级来源']],
328
- # "手淘搜索": [['日期', '店铺名称', '搜索词', '词类型', '访客数']],
329
- # "新品追踪": [['日期', '店铺名称', '商品id']],
330
- # "直播分场次效果": [['场次id']],
331
- },
324
+ # "生意参谋3": {
325
+ # "crm成交客户": [['客户id']],
326
+ # "商品排行": [['日期', '店铺名称', '商品id']],
327
+ # "流量来源构成": [['日期', '店铺名称', '来源构成', '类别', '一级来源', '二级来源', '三级来源']],
328
+ # "手淘搜索": [['日期', '店铺名称', '搜索词', '词类型', '访客数']],
329
+ # "新品追踪": [['日期', '店铺名称', '商品id']],
330
+ # "直播分场次效果": [['场次id']],
331
+ # },
332
332
  # "生意经3": {
333
333
  # "sku销量_按名称": [['日期', '店铺名称', '宝贝id', 'sku名称', '销售额']],
334
334
  # "sku销量_按商家编码": [['日期', '店铺名称', '宝贝id', 'sku编码', '销售额']],
mdbq/mysql/uploader.py CHANGED
@@ -19,7 +19,7 @@ import math
19
19
  warnings.filterwarnings('ignore')
20
20
  logger = mylogger.MyLogger(
21
21
  logging_mode='file',
22
- log_level='debug',
22
+ log_level='info',
23
23
  log_format='json',
24
24
  max_log_size=50,
25
25
  backup_count=5,
@@ -46,7 +46,7 @@ def count_decimal_places(num_str: str) -> Tuple[int, int]:
46
46
 
47
47
 
48
48
  class StatementCache(dict):
49
- """简单LRU缓存实现,用于SQL语句缓存"""
49
+ """LRU缓存实现,用于SQL语句缓存"""
50
50
  def __init__(self, maxsize=100):
51
51
  super().__init__()
52
52
  self._maxsize = maxsize
@@ -71,10 +71,10 @@ class StatementCache(dict):
71
71
 
72
72
  class MySQLUploader:
73
73
  """
74
- MySQL数据上传工具类
74
+ MySQL数据上传
75
75
 
76
- 提供了一系列方法用于将数据上传到MySQL数据库,支持自动建表、分表、数据验证等功能。
77
- 使用连接池管理数据库连接,提供错误重试机制。
76
+ 用于将数据上传到MySQL数据库,支持自动建表、分表、数据验证等功能。
77
+ 使用连接池管理数据库连接。
78
78
  """
79
79
  def __init__(
80
80
  self,
@@ -128,9 +128,7 @@ class MySQLUploader:
128
128
  self._max_cached_statements = 100 # 用于控制 StatementCache 类中缓存的 SQL 语句数量,最多缓存 100 条 SQL 语句
129
129
  self._table_metadata_cache = {}
130
130
  self.metadata_cache_ttl = 300 # 5分钟缓存时间
131
-
132
- # 创建连接池
133
- self.pool = self._create_connection_pool()
131
+ self.pool = self._create_connection_pool() # 创建连接池
134
132
 
135
133
  def _create_connection_pool(self) -> PooledDB:
136
134
  """
@@ -1204,6 +1202,9 @@ class MySQLUploader:
1204
1202
  '失败': total_failed
1205
1203
  })
1206
1204
 
1205
+ # 更新索引
1206
+ self._update_indexes(db_name, table_name, indexes)
1207
+
1207
1208
  @_execute_with_retry
1208
1209
  def _insert_data(
1209
1210
  self,
@@ -1496,25 +1497,6 @@ class MySQLUploader:
1496
1497
  logger.error('单行插入失败', {'库': db_name, '表': table_name, '错误': str(e)})
1497
1498
  return total_inserted, total_skipped, total_failed
1498
1499
 
1499
- def close(self) -> None:
1500
- """
1501
- 关闭连接池并清理资源
1502
- 这个方法会安全地关闭数据库连接池,并清理相关资源。
1503
- 建议结束时手动调用此方法。
1504
- :raises: 可能抛出关闭连接时的异常
1505
- """
1506
- try:
1507
- if hasattr(self, 'pool') and self.pool is not None:
1508
- try:
1509
- # self.pool.close() # PooledDB 没有 close 方法
1510
- self.pool = None
1511
- except Exception as e:
1512
- logger.warning('关闭连接池时出错', {'error': str(e)})
1513
- logger.debug('finished', {'uploader.py': '连接池关闭'})
1514
- except Exception as e:
1515
- logger.error('关闭连接池失败', {'uploader.py': str(e)})
1516
- raise
1517
-
1518
1500
  def _check_pool_health(self) -> bool:
1519
1501
  """
1520
1502
  检查连接池健康状态,防止连接泄露
@@ -1587,12 +1569,6 @@ class MySQLUploader:
1587
1569
  # pandas DataFrame
1588
1570
  return f"DataFrame shape={obj.shape}, head={obj.head(1).to_dict()}"
1589
1571
  return obj
1590
-
1591
- def __enter__(self):
1592
- return self
1593
-
1594
- def __exit__(self, exc_type, exc_val, exc_tb):
1595
- self.close()
1596
1572
 
1597
1573
  def _normalize_col(self, col: str) -> str:
1598
1574
  """
@@ -1601,6 +1577,112 @@ class MySQLUploader:
1601
1577
  safe = self._validate_identifier(col)
1602
1578
  return safe if self.case_sensitive else safe.lower()
1603
1579
 
1580
+ def _update_indexes(self, db_name: str, table_name: str, indexes: Optional[List[str]]):
1581
+ """
1582
+ 更新索引,避免重复添加或更新,同时注意大小写一致性。
1583
+
1584
+ :param db_name: 数据库名
1585
+ :param table_name: 表名
1586
+ :param indexes: 需要更新的索引列列表
1587
+ """
1588
+ if not indexes:
1589
+ return
1590
+
1591
+ # 规范化索引列名
1592
+ normalized_indexes = [self._normalize_col(idx) for idx in indexes]
1593
+
1594
+ # 获取现有索引
1595
+ try:
1596
+ existing_indexes = self._get_existing_indexes(db_name, table_name)
1597
+ except Exception as e:
1598
+ logger.error('获取现有索引时发生错误', {'库': db_name, '表': table_name, '错误': str(e)})
1599
+ raise
1600
+
1601
+ # 获取表中现有的列名
1602
+ try:
1603
+ existing_columns = self._get_existing_indexes(db_name, table_name)
1604
+ except Exception as e:
1605
+ logger.error('获取现有列时发生错误', {'库': db_name, '表': table_name, '错误': str(e)})
1606
+ raise
1607
+
1608
+ # 找出需要添加的索引
1609
+ indexes_to_add = [idx for idx in normalized_indexes if idx not in existing_indexes and idx in existing_columns]
1610
+
1611
+ # 添加新索引
1612
+ for idx in indexes_to_add:
1613
+ try:
1614
+ self._add_index(db_name, table_name, idx)
1615
+ except Exception as e:
1616
+ logger.error('添加索引时发生错误', {'库': db_name, '表': table_name, '列': idx, '错误': str(e)})
1617
+ raise
1618
+
1619
+ def _get_existing_indexes(self, db_name: str, table_name: str) -> Set[str]:
1620
+ """
1621
+ 获取表中现有的索引列名。
1622
+
1623
+ :param db_name: 数据库名
1624
+ :param table_name: 表名
1625
+ :return: 现有索引列名的集合
1626
+ """
1627
+ sql = """
1628
+ SELECT COLUMN_NAME
1629
+ FROM INFORMATION_SCHEMA.STATISTICS
1630
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
1631
+ """
1632
+ existing_indexes = set()
1633
+ try:
1634
+ with self._get_connection() as conn:
1635
+ with conn.cursor() as cursor:
1636
+ cursor.execute(sql, (db_name, table_name))
1637
+ existing_indexes = {row['COLUMN_NAME'] for row in cursor.fetchall()}
1638
+ except Exception as e:
1639
+ logger.error('获取现有索引失败', {'库': db_name, '表': table_name, '错误': str(e)})
1640
+ raise
1641
+ return existing_indexes
1642
+
1643
+ def _add_index(self, db_name: str, table_name: str, column: str):
1644
+ """
1645
+ 添加索引到指定列。
1646
+
1647
+ :param db_name: 数据库名
1648
+ :param table_name: 表名
1649
+ :param column: 需要添加索引的列名
1650
+ """
1651
+ sql = f'ALTER TABLE `{db_name}`.`{table_name}` ADD INDEX `idx_{column}` (`{column}`)'
1652
+ try:
1653
+ with self._get_connection() as conn:
1654
+ with conn.cursor() as cursor:
1655
+ cursor.execute(sql)
1656
+ conn.commit()
1657
+ logger.debug('已为列创建索引', {'库': db_name, '表': table_name, '列': column})
1658
+ except Exception as e:
1659
+ logger.error('创建索引失败', {'库': db_name, '表': table_name, '列': column, '错误': str(e)})
1660
+ raise
1661
+
1662
+ def __enter__(self):
1663
+ return self
1664
+
1665
+ def close(self) -> None:
1666
+ """
1667
+ 关闭连接池并清理资源
1668
+ 这个方法会安全地关闭数据库连接池,并清理相关资源。
1669
+ 建议结束时手动调用此方法。
1670
+ :raises: 可能抛出关闭连接时的异常
1671
+ """
1672
+ try:
1673
+ if hasattr(self, 'pool') and self.pool is not None:
1674
+ try:
1675
+ self.pool = None
1676
+ except Exception as e:
1677
+ logger.warning('关闭连接池时出错', {'error': str(e)})
1678
+ logger.debug('finished', {'uploader.py': '连接池关闭'})
1679
+ except Exception as e:
1680
+ logger.error('关闭连接池失败', {'uploader.py': str(e)})
1681
+ raise
1682
+
1683
+ def __exit__(self, exc_type, exc_val, exc_tb):
1684
+ self.close()
1685
+
1604
1686
 
1605
1687
  def main():
1606
1688
  dir_path = os.path.expanduser("~")
mdbq/redis/getredis.py CHANGED
@@ -9,7 +9,6 @@ from mdbq.log import mylogger
9
9
  from decimal import Decimal
10
10
  import orjson
11
11
 
12
- # 获取当前模块的日志记录器
13
12
  logger = mylogger.MyLogger(
14
13
  logging_mode='file',
15
14
  log_level='info',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mdbq
3
- Version: 4.0.6
3
+ Version: 4.0.8
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1,18 +1,18 @@
1
1
  mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
2
- mdbq/__version__.py,sha256=adjsrsRXzbHhnLKbu4FqmL5_f_7VZD0LhjX_Vvr0yOc,17
2
+ mdbq/__version__.py,sha256=ctSY0gLLZnr9k_avSjRnC3qI18wZwhq3iBz504pEU14,17
3
3
  mdbq/aggregation/__init__.py,sha256=EeDqX2Aml6SPx8363J-v1lz0EcZtgwIBYyCJV6CcEDU,40
4
- mdbq/aggregation/query_data.py,sha256=3GBdX0HWKvQ-B3NiZE_hzWbJ7sqClzCd8KTvXpVPnZ4,170452
4
+ mdbq/aggregation/query_data.py,sha256=86S_Iy3NNTkn-tNNLxlnwqDnXv3x3aHD1cCe1mL7jdg,165423
5
5
  mdbq/config/__init__.py,sha256=jso1oHcy6cJEfa7udS_9uO5X6kZLoPBF8l3wCYmr5dM,18
6
6
  mdbq/config/config.py,sha256=eaTfrfXQ65xLqjr5I8-HkZd_jEY1JkGinEgv3TSLeoQ,3170
7
7
  mdbq/log/__init__.py,sha256=Mpbrav0s0ifLL7lVDAuePEi1hJKiSHhxcv1byBKDl5E,15
8
- mdbq/log/mylogger.py,sha256=qBOHJK_h6R_SpfQ1yC5fAlJIEm6uro810i-47uA9C_U,22872
8
+ mdbq/log/mylogger.py,sha256=9w_o5mYB3FooIxobq_lSa6oCYTKIhPxDFox-jeLtUHI,21714
9
9
  mdbq/log/spider_logging.py,sha256=-ozWWEGm3HVv604ozs_OOvVwumjokmUPwbaodesUrPY,1664
10
10
  mdbq/mysql/__init__.py,sha256=A_DPJyAoEvTSFojiI2e94zP0FKtCkkwKP1kYUCSyQzo,11
11
11
  mdbq/mysql/deduplicator.py,sha256=8v3MC6TJ0YEiExWrTP9OXAxTYnL9XbpYL2vWaER1h2M,73099
12
12
  mdbq/mysql/mysql.py,sha256=pDg771xBugCMSTWeskIFTi3pFLgaqgyG3smzf-86Wn8,56772
13
- mdbq/mysql/s_query.py,sha256=RnVCwMQ_n9PcAimbMWbHe9k8eil8shtCfa3LwLBZi6c,41909
14
- mdbq/mysql/unique_.py,sha256=LZKa1LXQdy_aO78ekO1Ul2MNA-k3Sz4-4W5nF2gD8AM,21068
15
- mdbq/mysql/uploader.py,sha256=b3BZ4ElGekqpUuipEc-49TQzPO-q9WBRF17MHbzY0yQ,75044
13
+ mdbq/mysql/s_query.py,sha256=FSFrFZE5yzEbnpLrN2AmlRZ_VvTvfpIWaQUjZfLIi9g,40342
14
+ mdbq/mysql/unique_.py,sha256=Wgqq_PjAAD757JTa10wjYaJgssZ_C_ypU6DW56jbuyw,21074
15
+ mdbq/mysql/uploader.py,sha256=OtLWoGflmndc7z0A1aK1Qj-XVkWtGyN4Y5YnLsRovJc,78215
16
16
  mdbq/other/__init__.py,sha256=jso1oHcy6cJEfa7udS_9uO5X6kZLoPBF8l3wCYmr5dM,18
17
17
  mdbq/other/download_sku_picture.py,sha256=YU8DxKMXbdeE1OOKEA848WVp62jYHw5O4tXTjUdq9H0,44832
18
18
  mdbq/other/otk.py,sha256=iclBIFbQbhlqzUbcMMoePXBpcP1eZ06ZtjnhcA_EbmE,7241
@@ -22,10 +22,10 @@ mdbq/pbix/__init__.py,sha256=Trtfaynu9RjoTyLLYBN2xdRxTvm_zhCniUkVTAYwcjo,24
22
22
  mdbq/pbix/pbix_refresh.py,sha256=JUjKW3bNEyoMVfVfo77UhguvS5AWkixvVhDbw4_MHco,2396
23
23
  mdbq/pbix/refresh_all.py,sha256=OBT9EewSZ0aRS9vL_FflVn74d4l2G00wzHiikCC4TC0,5926
24
24
  mdbq/redis/__init__.py,sha256=YtgBlVSMDphtpwYX248wGge1x-Ex_mMufz4-8W0XRmA,12
25
- mdbq/redis/getredis.py,sha256=l3zBK7wrZl0oO42-_UGylyatnIp_SBw8wDDvof9fht4,23534
25
+ mdbq/redis/getredis.py,sha256=vpBuNc22uj9Vr-_Dh25_wpwWM1e-072EAAIBdB_IpL0,23494
26
26
  mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
27
27
  mdbq/spider/aikucun.py,sha256=hPRzLQvFIF4ibN8aP3Dg_ru5meac90faPyzOB22cj-o,20965
28
- mdbq-4.0.6.dist-info/METADATA,sha256=-KRATxP5UjeBL5WgMG2Knbwtw_816ptxsAau2S88pck,363
29
- mdbq-4.0.6.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
30
- mdbq-4.0.6.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
31
- mdbq-4.0.6.dist-info/RECORD,,
28
+ mdbq-4.0.8.dist-info/METADATA,sha256=zmc_fzk3uaGju9Nby1Xvta_2po8NNOG7CM0TvvDIiuU,363
29
+ mdbq-4.0.8.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
30
+ mdbq-4.0.8.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
31
+ mdbq-4.0.8.dist-info/RECORD,,
File without changes