pyxllib 0.3.96__py3-none-any.whl → 0.3.197__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.
- pyxllib/algo/geo.py +12 -0
- pyxllib/algo/intervals.py +1 -1
- pyxllib/algo/matcher.py +78 -0
- pyxllib/algo/pupil.py +187 -19
- pyxllib/algo/specialist.py +2 -1
- pyxllib/algo/stat.py +38 -2
- {pyxlpr → pyxllib/autogui}/__init__.py +1 -1
- pyxllib/autogui/activewin.py +246 -0
- pyxllib/autogui/all.py +9 -0
- pyxllib/{ext/autogui → autogui}/autogui.py +40 -11
- pyxllib/autogui/uiautolib.py +362 -0
- pyxllib/autogui/wechat.py +827 -0
- pyxllib/autogui/wechat_msg.py +421 -0
- pyxllib/autogui/wxautolib.py +84 -0
- pyxllib/cv/slidercaptcha.py +137 -0
- pyxllib/data/echarts.py +123 -12
- pyxllib/data/jsonlib.py +89 -0
- pyxllib/data/pglib.py +514 -30
- pyxllib/data/sqlite.py +231 -4
- pyxllib/ext/JLineViewer.py +14 -1
- pyxllib/ext/drissionlib.py +277 -0
- pyxllib/ext/kq5034lib.py +0 -1594
- pyxllib/ext/robustprocfile.py +497 -0
- pyxllib/ext/unixlib.py +6 -5
- pyxllib/ext/utools.py +108 -95
- pyxllib/ext/webhook.py +32 -14
- pyxllib/ext/wjxlib.py +88 -0
- pyxllib/ext/wpsapi.py +124 -0
- pyxllib/ext/xlwork.py +9 -0
- pyxllib/ext/yuquelib.py +1003 -71
- pyxllib/file/docxlib.py +1 -1
- pyxllib/file/libreoffice.py +165 -0
- pyxllib/file/movielib.py +9 -0
- pyxllib/file/packlib/__init__.py +112 -75
- pyxllib/file/pdflib.py +1 -1
- pyxllib/file/pupil.py +1 -1
- pyxllib/file/specialist/dirlib.py +1 -1
- pyxllib/file/specialist/download.py +10 -3
- pyxllib/file/specialist/filelib.py +266 -55
- pyxllib/file/xlsxlib.py +205 -50
- pyxllib/file/xlsyncfile.py +341 -0
- pyxllib/prog/cachetools.py +64 -0
- pyxllib/prog/filelock.py +42 -0
- pyxllib/prog/multiprogs.py +940 -0
- pyxllib/prog/newbie.py +9 -2
- pyxllib/prog/pupil.py +129 -60
- pyxllib/prog/specialist/__init__.py +176 -2
- pyxllib/prog/specialist/bc.py +5 -2
- pyxllib/prog/specialist/browser.py +11 -2
- pyxllib/prog/specialist/datetime.py +68 -0
- pyxllib/prog/specialist/tictoc.py +12 -13
- pyxllib/prog/specialist/xllog.py +5 -5
- pyxllib/prog/xlosenv.py +7 -0
- pyxllib/text/airscript.js +744 -0
- pyxllib/text/charclasslib.py +17 -5
- pyxllib/text/jiebalib.py +6 -3
- pyxllib/text/jinjalib.py +32 -0
- pyxllib/text/jsa_ai_prompt.md +271 -0
- pyxllib/text/jscode.py +159 -4
- pyxllib/text/nestenv.py +1 -1
- pyxllib/text/newbie.py +12 -0
- pyxllib/text/pupil/common.py +26 -0
- pyxllib/text/specialist/ptag.py +2 -2
- pyxllib/text/templates/echart_base.html +11 -0
- pyxllib/text/templates/highlight_code.html +17 -0
- pyxllib/text/templates/latex_editor.html +103 -0
- pyxllib/text/xmllib.py +76 -14
- pyxllib/xl.py +2 -1
- pyxllib-0.3.197.dist-info/METADATA +48 -0
- pyxllib-0.3.197.dist-info/RECORD +126 -0
- {pyxllib-0.3.96.dist-info → pyxllib-0.3.197.dist-info}/WHEEL +1 -2
- pyxllib/ext/autogui/__init__.py +0 -8
- pyxllib-0.3.96.dist-info/METADATA +0 -51
- pyxllib-0.3.96.dist-info/RECORD +0 -333
- pyxllib-0.3.96.dist-info/top_level.txt +0 -2
- pyxlpr/ai/__init__.py +0 -5
- pyxlpr/ai/clientlib.py +0 -1281
- pyxlpr/ai/specialist.py +0 -286
- pyxlpr/ai/torch_app.py +0 -172
- pyxlpr/ai/xlpaddle.py +0 -655
- pyxlpr/ai/xltorch.py +0 -705
- pyxlpr/data/__init__.py +0 -11
- pyxlpr/data/coco.py +0 -1325
- pyxlpr/data/datacls.py +0 -365
- pyxlpr/data/datasets.py +0 -200
- pyxlpr/data/gptlib.py +0 -1291
- pyxlpr/data/icdar/__init__.py +0 -96
- pyxlpr/data/icdar/deteval.py +0 -377
- pyxlpr/data/icdar/icdar2013.py +0 -341
- pyxlpr/data/icdar/iou.py +0 -340
- pyxlpr/data/icdar/rrc_evaluation_funcs_1_1.py +0 -463
- pyxlpr/data/imtextline.py +0 -473
- pyxlpr/data/labelme.py +0 -866
- pyxlpr/data/removeline.py +0 -179
- pyxlpr/data/specialist.py +0 -57
- pyxlpr/eval/__init__.py +0 -85
- pyxlpr/paddleocr.py +0 -776
- pyxlpr/ppocr/__init__.py +0 -15
- pyxlpr/ppocr/configs/rec/multi_language/generate_multi_language_configs.py +0 -226
- pyxlpr/ppocr/data/__init__.py +0 -135
- pyxlpr/ppocr/data/imaug/ColorJitter.py +0 -26
- pyxlpr/ppocr/data/imaug/__init__.py +0 -67
- pyxlpr/ppocr/data/imaug/copy_paste.py +0 -170
- pyxlpr/ppocr/data/imaug/east_process.py +0 -437
- pyxlpr/ppocr/data/imaug/gen_table_mask.py +0 -244
- pyxlpr/ppocr/data/imaug/iaa_augment.py +0 -114
- pyxlpr/ppocr/data/imaug/label_ops.py +0 -789
- pyxlpr/ppocr/data/imaug/make_border_map.py +0 -184
- pyxlpr/ppocr/data/imaug/make_pse_gt.py +0 -106
- pyxlpr/ppocr/data/imaug/make_shrink_map.py +0 -126
- pyxlpr/ppocr/data/imaug/operators.py +0 -433
- pyxlpr/ppocr/data/imaug/pg_process.py +0 -906
- pyxlpr/ppocr/data/imaug/randaugment.py +0 -143
- pyxlpr/ppocr/data/imaug/random_crop_data.py +0 -239
- pyxlpr/ppocr/data/imaug/rec_img_aug.py +0 -533
- pyxlpr/ppocr/data/imaug/sast_process.py +0 -777
- pyxlpr/ppocr/data/imaug/text_image_aug/__init__.py +0 -17
- pyxlpr/ppocr/data/imaug/text_image_aug/augment.py +0 -120
- pyxlpr/ppocr/data/imaug/text_image_aug/warp_mls.py +0 -168
- pyxlpr/ppocr/data/lmdb_dataset.py +0 -115
- pyxlpr/ppocr/data/pgnet_dataset.py +0 -104
- pyxlpr/ppocr/data/pubtab_dataset.py +0 -107
- pyxlpr/ppocr/data/simple_dataset.py +0 -372
- pyxlpr/ppocr/losses/__init__.py +0 -61
- pyxlpr/ppocr/losses/ace_loss.py +0 -52
- pyxlpr/ppocr/losses/basic_loss.py +0 -135
- pyxlpr/ppocr/losses/center_loss.py +0 -88
- pyxlpr/ppocr/losses/cls_loss.py +0 -30
- pyxlpr/ppocr/losses/combined_loss.py +0 -67
- pyxlpr/ppocr/losses/det_basic_loss.py +0 -208
- pyxlpr/ppocr/losses/det_db_loss.py +0 -80
- pyxlpr/ppocr/losses/det_east_loss.py +0 -63
- pyxlpr/ppocr/losses/det_pse_loss.py +0 -149
- pyxlpr/ppocr/losses/det_sast_loss.py +0 -121
- pyxlpr/ppocr/losses/distillation_loss.py +0 -272
- pyxlpr/ppocr/losses/e2e_pg_loss.py +0 -140
- pyxlpr/ppocr/losses/kie_sdmgr_loss.py +0 -113
- pyxlpr/ppocr/losses/rec_aster_loss.py +0 -99
- pyxlpr/ppocr/losses/rec_att_loss.py +0 -39
- pyxlpr/ppocr/losses/rec_ctc_loss.py +0 -44
- pyxlpr/ppocr/losses/rec_enhanced_ctc_loss.py +0 -70
- pyxlpr/ppocr/losses/rec_nrtr_loss.py +0 -30
- pyxlpr/ppocr/losses/rec_sar_loss.py +0 -28
- pyxlpr/ppocr/losses/rec_srn_loss.py +0 -47
- pyxlpr/ppocr/losses/table_att_loss.py +0 -109
- pyxlpr/ppocr/metrics/__init__.py +0 -44
- pyxlpr/ppocr/metrics/cls_metric.py +0 -45
- pyxlpr/ppocr/metrics/det_metric.py +0 -82
- pyxlpr/ppocr/metrics/distillation_metric.py +0 -73
- pyxlpr/ppocr/metrics/e2e_metric.py +0 -86
- pyxlpr/ppocr/metrics/eval_det_iou.py +0 -274
- pyxlpr/ppocr/metrics/kie_metric.py +0 -70
- pyxlpr/ppocr/metrics/rec_metric.py +0 -75
- pyxlpr/ppocr/metrics/table_metric.py +0 -50
- pyxlpr/ppocr/modeling/architectures/__init__.py +0 -32
- pyxlpr/ppocr/modeling/architectures/base_model.py +0 -88
- pyxlpr/ppocr/modeling/architectures/distillation_model.py +0 -60
- pyxlpr/ppocr/modeling/backbones/__init__.py +0 -54
- pyxlpr/ppocr/modeling/backbones/det_mobilenet_v3.py +0 -268
- pyxlpr/ppocr/modeling/backbones/det_resnet_vd.py +0 -246
- pyxlpr/ppocr/modeling/backbones/det_resnet_vd_sast.py +0 -285
- pyxlpr/ppocr/modeling/backbones/e2e_resnet_vd_pg.py +0 -265
- pyxlpr/ppocr/modeling/backbones/kie_unet_sdmgr.py +0 -186
- pyxlpr/ppocr/modeling/backbones/rec_mobilenet_v3.py +0 -138
- pyxlpr/ppocr/modeling/backbones/rec_mv1_enhance.py +0 -258
- pyxlpr/ppocr/modeling/backbones/rec_nrtr_mtb.py +0 -48
- pyxlpr/ppocr/modeling/backbones/rec_resnet_31.py +0 -210
- pyxlpr/ppocr/modeling/backbones/rec_resnet_aster.py +0 -143
- pyxlpr/ppocr/modeling/backbones/rec_resnet_fpn.py +0 -307
- pyxlpr/ppocr/modeling/backbones/rec_resnet_vd.py +0 -286
- pyxlpr/ppocr/modeling/heads/__init__.py +0 -54
- pyxlpr/ppocr/modeling/heads/cls_head.py +0 -52
- pyxlpr/ppocr/modeling/heads/det_db_head.py +0 -118
- pyxlpr/ppocr/modeling/heads/det_east_head.py +0 -121
- pyxlpr/ppocr/modeling/heads/det_pse_head.py +0 -37
- pyxlpr/ppocr/modeling/heads/det_sast_head.py +0 -128
- pyxlpr/ppocr/modeling/heads/e2e_pg_head.py +0 -253
- pyxlpr/ppocr/modeling/heads/kie_sdmgr_head.py +0 -206
- pyxlpr/ppocr/modeling/heads/multiheadAttention.py +0 -163
- pyxlpr/ppocr/modeling/heads/rec_aster_head.py +0 -393
- pyxlpr/ppocr/modeling/heads/rec_att_head.py +0 -202
- pyxlpr/ppocr/modeling/heads/rec_ctc_head.py +0 -88
- pyxlpr/ppocr/modeling/heads/rec_nrtr_head.py +0 -826
- pyxlpr/ppocr/modeling/heads/rec_sar_head.py +0 -402
- pyxlpr/ppocr/modeling/heads/rec_srn_head.py +0 -280
- pyxlpr/ppocr/modeling/heads/self_attention.py +0 -406
- pyxlpr/ppocr/modeling/heads/table_att_head.py +0 -246
- pyxlpr/ppocr/modeling/necks/__init__.py +0 -32
- pyxlpr/ppocr/modeling/necks/db_fpn.py +0 -111
- pyxlpr/ppocr/modeling/necks/east_fpn.py +0 -188
- pyxlpr/ppocr/modeling/necks/fpn.py +0 -138
- pyxlpr/ppocr/modeling/necks/pg_fpn.py +0 -314
- pyxlpr/ppocr/modeling/necks/rnn.py +0 -92
- pyxlpr/ppocr/modeling/necks/sast_fpn.py +0 -284
- pyxlpr/ppocr/modeling/necks/table_fpn.py +0 -110
- pyxlpr/ppocr/modeling/transforms/__init__.py +0 -28
- pyxlpr/ppocr/modeling/transforms/stn.py +0 -135
- pyxlpr/ppocr/modeling/transforms/tps.py +0 -308
- pyxlpr/ppocr/modeling/transforms/tps_spatial_transformer.py +0 -156
- pyxlpr/ppocr/optimizer/__init__.py +0 -61
- pyxlpr/ppocr/optimizer/learning_rate.py +0 -228
- pyxlpr/ppocr/optimizer/lr_scheduler.py +0 -49
- pyxlpr/ppocr/optimizer/optimizer.py +0 -160
- pyxlpr/ppocr/optimizer/regularizer.py +0 -52
- pyxlpr/ppocr/postprocess/__init__.py +0 -55
- pyxlpr/ppocr/postprocess/cls_postprocess.py +0 -33
- pyxlpr/ppocr/postprocess/db_postprocess.py +0 -234
- pyxlpr/ppocr/postprocess/east_postprocess.py +0 -143
- pyxlpr/ppocr/postprocess/locality_aware_nms.py +0 -200
- pyxlpr/ppocr/postprocess/pg_postprocess.py +0 -52
- pyxlpr/ppocr/postprocess/pse_postprocess/__init__.py +0 -15
- pyxlpr/ppocr/postprocess/pse_postprocess/pse/__init__.py +0 -29
- pyxlpr/ppocr/postprocess/pse_postprocess/pse/setup.py +0 -14
- pyxlpr/ppocr/postprocess/pse_postprocess/pse_postprocess.py +0 -118
- pyxlpr/ppocr/postprocess/rec_postprocess.py +0 -654
- pyxlpr/ppocr/postprocess/sast_postprocess.py +0 -355
- pyxlpr/ppocr/tools/__init__.py +0 -14
- pyxlpr/ppocr/tools/eval.py +0 -83
- pyxlpr/ppocr/tools/export_center.py +0 -77
- pyxlpr/ppocr/tools/export_model.py +0 -129
- pyxlpr/ppocr/tools/infer/predict_cls.py +0 -151
- pyxlpr/ppocr/tools/infer/predict_det.py +0 -300
- pyxlpr/ppocr/tools/infer/predict_e2e.py +0 -169
- pyxlpr/ppocr/tools/infer/predict_rec.py +0 -414
- pyxlpr/ppocr/tools/infer/predict_system.py +0 -204
- pyxlpr/ppocr/tools/infer/utility.py +0 -629
- pyxlpr/ppocr/tools/infer_cls.py +0 -83
- pyxlpr/ppocr/tools/infer_det.py +0 -134
- pyxlpr/ppocr/tools/infer_e2e.py +0 -122
- pyxlpr/ppocr/tools/infer_kie.py +0 -153
- pyxlpr/ppocr/tools/infer_rec.py +0 -146
- pyxlpr/ppocr/tools/infer_table.py +0 -107
- pyxlpr/ppocr/tools/program.py +0 -596
- pyxlpr/ppocr/tools/test_hubserving.py +0 -117
- pyxlpr/ppocr/tools/train.py +0 -163
- pyxlpr/ppocr/tools/xlprog.py +0 -748
- pyxlpr/ppocr/utils/EN_symbol_dict.txt +0 -94
- pyxlpr/ppocr/utils/__init__.py +0 -24
- pyxlpr/ppocr/utils/dict/ar_dict.txt +0 -117
- pyxlpr/ppocr/utils/dict/arabic_dict.txt +0 -162
- pyxlpr/ppocr/utils/dict/be_dict.txt +0 -145
- pyxlpr/ppocr/utils/dict/bg_dict.txt +0 -140
- pyxlpr/ppocr/utils/dict/chinese_cht_dict.txt +0 -8421
- pyxlpr/ppocr/utils/dict/cyrillic_dict.txt +0 -163
- pyxlpr/ppocr/utils/dict/devanagari_dict.txt +0 -167
- pyxlpr/ppocr/utils/dict/en_dict.txt +0 -63
- pyxlpr/ppocr/utils/dict/fa_dict.txt +0 -136
- pyxlpr/ppocr/utils/dict/french_dict.txt +0 -136
- pyxlpr/ppocr/utils/dict/german_dict.txt +0 -143
- pyxlpr/ppocr/utils/dict/hi_dict.txt +0 -162
- pyxlpr/ppocr/utils/dict/it_dict.txt +0 -118
- pyxlpr/ppocr/utils/dict/japan_dict.txt +0 -4399
- pyxlpr/ppocr/utils/dict/ka_dict.txt +0 -153
- pyxlpr/ppocr/utils/dict/korean_dict.txt +0 -3688
- pyxlpr/ppocr/utils/dict/latin_dict.txt +0 -185
- pyxlpr/ppocr/utils/dict/mr_dict.txt +0 -153
- pyxlpr/ppocr/utils/dict/ne_dict.txt +0 -153
- pyxlpr/ppocr/utils/dict/oc_dict.txt +0 -96
- pyxlpr/ppocr/utils/dict/pu_dict.txt +0 -130
- pyxlpr/ppocr/utils/dict/rs_dict.txt +0 -91
- pyxlpr/ppocr/utils/dict/rsc_dict.txt +0 -134
- pyxlpr/ppocr/utils/dict/ru_dict.txt +0 -125
- pyxlpr/ppocr/utils/dict/ta_dict.txt +0 -128
- pyxlpr/ppocr/utils/dict/table_dict.txt +0 -277
- pyxlpr/ppocr/utils/dict/table_structure_dict.txt +0 -2759
- pyxlpr/ppocr/utils/dict/te_dict.txt +0 -151
- pyxlpr/ppocr/utils/dict/ug_dict.txt +0 -114
- pyxlpr/ppocr/utils/dict/uk_dict.txt +0 -142
- pyxlpr/ppocr/utils/dict/ur_dict.txt +0 -137
- pyxlpr/ppocr/utils/dict/xi_dict.txt +0 -110
- pyxlpr/ppocr/utils/dict90.txt +0 -90
- pyxlpr/ppocr/utils/e2e_metric/Deteval.py +0 -574
- pyxlpr/ppocr/utils/e2e_metric/polygon_fast.py +0 -83
- pyxlpr/ppocr/utils/e2e_utils/extract_batchsize.py +0 -87
- pyxlpr/ppocr/utils/e2e_utils/extract_textpoint_fast.py +0 -457
- pyxlpr/ppocr/utils/e2e_utils/extract_textpoint_slow.py +0 -592
- pyxlpr/ppocr/utils/e2e_utils/pgnet_pp_utils.py +0 -162
- pyxlpr/ppocr/utils/e2e_utils/visual.py +0 -162
- pyxlpr/ppocr/utils/en_dict.txt +0 -95
- pyxlpr/ppocr/utils/gen_label.py +0 -81
- pyxlpr/ppocr/utils/ic15_dict.txt +0 -36
- pyxlpr/ppocr/utils/iou.py +0 -54
- pyxlpr/ppocr/utils/logging.py +0 -69
- pyxlpr/ppocr/utils/network.py +0 -84
- pyxlpr/ppocr/utils/ppocr_keys_v1.txt +0 -6623
- pyxlpr/ppocr/utils/profiler.py +0 -110
- pyxlpr/ppocr/utils/save_load.py +0 -150
- pyxlpr/ppocr/utils/stats.py +0 -72
- pyxlpr/ppocr/utils/utility.py +0 -80
- pyxlpr/ppstructure/__init__.py +0 -13
- pyxlpr/ppstructure/predict_system.py +0 -187
- pyxlpr/ppstructure/table/__init__.py +0 -13
- pyxlpr/ppstructure/table/eval_table.py +0 -72
- pyxlpr/ppstructure/table/matcher.py +0 -192
- pyxlpr/ppstructure/table/predict_structure.py +0 -136
- pyxlpr/ppstructure/table/predict_table.py +0 -221
- pyxlpr/ppstructure/table/table_metric/__init__.py +0 -16
- pyxlpr/ppstructure/table/table_metric/parallel.py +0 -51
- pyxlpr/ppstructure/table/table_metric/table_metric.py +0 -247
- pyxlpr/ppstructure/table/tablepyxl/__init__.py +0 -13
- pyxlpr/ppstructure/table/tablepyxl/style.py +0 -283
- pyxlpr/ppstructure/table/tablepyxl/tablepyxl.py +0 -118
- pyxlpr/ppstructure/utility.py +0 -71
- pyxlpr/xlai.py +0 -10
- /pyxllib/{ext/autogui → autogui}/virtualkey.py +0 -0
- {pyxllib-0.3.96.dist-info → pyxllib-0.3.197.dist-info/licenses}/LICENSE +0 -0
pyxllib/ext/kq5034lib.py
CHANGED
@@ -10,1597 +10,3 @@
|
|
10
10
|
1、使用中文命名
|
11
11
|
2、牺牲了一定的工程、扩展灵活性,用了尽可能多的自动推导,而不是参数配置
|
12
12
|
"""
|
13
|
-
from pyxllib.prog.pupil import check_install_package
|
14
|
-
|
15
|
-
check_install_package('fire') # 自动安装依赖包
|
16
|
-
|
17
|
-
from collections import Counter, defaultdict
|
18
|
-
from datetime import date
|
19
|
-
import datetime
|
20
|
-
import math
|
21
|
-
import os
|
22
|
-
import re
|
23
|
-
import time
|
24
|
-
from io import StringIO
|
25
|
-
|
26
|
-
import fire
|
27
|
-
import pandas as pd
|
28
|
-
from tqdm import tqdm
|
29
|
-
import requests
|
30
|
-
import requests_cache
|
31
|
-
|
32
|
-
from pyxllib.text.pupil import chinese2digits, grp_chinese_char
|
33
|
-
from pyxllib.file.xlsxlib import openpyxl
|
34
|
-
from pyxllib.file.specialist import XlPath, get_encoding
|
35
|
-
from pyxllib.prog.pupil import run_once
|
36
|
-
from pyxllib.prog.specialist import parse_datetime, browser, TicToc, dprint
|
37
|
-
from pyxllib.cv.rgbfmt import RgbFormatter
|
38
|
-
from pyxllib.data.sqlite import Connection
|
39
|
-
from pyxllib.ext.seleniumlib import XlChrome
|
40
|
-
|
41
|
-
|
42
|
-
class Xiaoetong:
|
43
|
-
""" 写一个觉观的api
|
44
|
-
"""
|
45
|
-
|
46
|
-
def __init__(self):
|
47
|
-
self.app_id = ''
|
48
|
-
self.client_id = ''
|
49
|
-
self.secret_key = ''
|
50
|
-
self.token = ''
|
51
|
-
|
52
|
-
def login(self, app_id, client_id, secret_key):
|
53
|
-
""" 登录,获取token
|
54
|
-
"""
|
55
|
-
self.app_id = app_id
|
56
|
-
self.client_id = client_id
|
57
|
-
self.secret_key = secret_key
|
58
|
-
|
59
|
-
# 启用缓存
|
60
|
-
requests_cache.install_cache('access_token_cache', expire_after=600) # 设置缓存过期时间xx(单位:秒)
|
61
|
-
# 接口地址
|
62
|
-
url = "https://api.xiaoe-tech.com/token"
|
63
|
-
params = {
|
64
|
-
"app_id": self.app_id,
|
65
|
-
"client_id": self.client_id,
|
66
|
-
"secret_key": self.secret_key,
|
67
|
-
"grant_type": "client_credential"
|
68
|
-
}
|
69
|
-
response = requests.get(url, params=params)
|
70
|
-
if response.status_code == 200:
|
71
|
-
result = response.json()
|
72
|
-
if result['code'] == 0:
|
73
|
-
# access_token 是小鹅通开放api的全局唯一接口调用凭据,店铺调用各接口时都需使用 access_token ,开发者需要进行妥善保管;
|
74
|
-
self.token = result['data']['access_token']
|
75
|
-
else:
|
76
|
-
raise Exception("Error getting access token: {}".format(result['msg']))
|
77
|
-
else:
|
78
|
-
raise Exception("HTTP request failed with status code {}".format(response.status_code))
|
79
|
-
|
80
|
-
def get_alive_user_list(self, resource_id, page_size=100):
|
81
|
-
""" 获取直播间用户
|
82
|
-
"""
|
83
|
-
# 1 获取总页数
|
84
|
-
url = "https://api.xiaoe-tech.com/xe.alive.user.list/1.0.0" # 接口地址【路径:API列表 -> 直播管理 -> 获取直播间用户列表】
|
85
|
-
data_1 = {
|
86
|
-
"access_token": self.token,
|
87
|
-
"resource_id": resource_id,
|
88
|
-
"page": 1,
|
89
|
-
"page_size": page_size
|
90
|
-
}
|
91
|
-
response_1 = requests.post(url, data=data_1)
|
92
|
-
result_1 = response_1.json()
|
93
|
-
page = math.ceil(result_1['data']['total'] / page_size) # 页数
|
94
|
-
|
95
|
-
# 2 获取直播间用户数据
|
96
|
-
lst = result_1['data']['list']
|
97
|
-
for i in range(1, page): # 为什么从1开始,因为第一页的数据上面已经获取到了,这里没必要从新获取一次
|
98
|
-
data = {
|
99
|
-
"access_token": self.token,
|
100
|
-
"resource_id": resource_id,
|
101
|
-
"page": i + 1,
|
102
|
-
"page_size": page_size
|
103
|
-
}
|
104
|
-
response = requests.post(url, data=data)
|
105
|
-
result = response.json()
|
106
|
-
data_1 = result['data']['list']
|
107
|
-
lst += data_1
|
108
|
-
# lst.extend(data_1)
|
109
|
-
return lst
|
110
|
-
|
111
|
-
def get_elock_actor(self, activity_id, page_size=100):
|
112
|
-
""" 获取打卡参与用户
|
113
|
-
"""
|
114
|
-
# 获取总页数
|
115
|
-
url = "https://api.xiaoe-tech.com/xe.elock.actor/1.0.0" # 接口地址【路径:API列表 -> 打卡管理 -> 获取打卡参与用户】
|
116
|
-
data_1 = {
|
117
|
-
"access_token": self.token,
|
118
|
-
"activity_id": activity_id,
|
119
|
-
"page_index": 1,
|
120
|
-
"page_size": page_size
|
121
|
-
}
|
122
|
-
response_1 = requests.post(url, data=data_1)
|
123
|
-
result_1 = response_1.json()
|
124
|
-
page = math.ceil(result_1['data']['count'] / page_size) # 页数
|
125
|
-
# 获取打卡用户数据
|
126
|
-
lst = result_1['data']['list']
|
127
|
-
for i in range(1, page): # 为什么从1开始,因为第一页的数据上面已经获取到了,这里没必要从新获取一次
|
128
|
-
data = {
|
129
|
-
"access_token": self.token,
|
130
|
-
"activity_id": activity_id,
|
131
|
-
"page_index": i + 1,
|
132
|
-
"page_size": page_size
|
133
|
-
}
|
134
|
-
response = requests.post(url, data=data)
|
135
|
-
result = response.json()
|
136
|
-
data_1 = result['data']['list']
|
137
|
-
lst += data_1
|
138
|
-
# lst.extend(data_1)
|
139
|
-
return lst
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
class 网课考勤:
|
144
|
-
def __init__(self, today=None):
|
145
|
-
self.返款标题 = ''
|
146
|
-
self.表格路径 = r'考勤.xlsx'
|
147
|
-
self.在线表格 = 'https://docs.qq.com/sheet/DUlF1UnRackJ2Vm5U' # 生成日报用
|
148
|
-
self.开课日期 = '2022-01-08'
|
149
|
-
self.视频返款 = [20, 15, 10, 5, 0, 0] # 直播(当堂)/第1天(当天)/第2天/第3天/第4天/第5天,完成观看的依次返款额。
|
150
|
-
self.打卡返款 = {5: 100, 10: 150, 15: 200} # 打卡满5/10/15次的返款额
|
151
|
-
self._init(today)
|
152
|
-
|
153
|
-
self.driver = None # 浏览器脚本
|
154
|
-
|
155
|
-
def __1_基础数据处理(self):
|
156
|
-
pass
|
157
|
-
|
158
|
-
def _init(self, today=None):
|
159
|
-
""" 可以手动指定today,适合某些场景下的调试 """
|
160
|
-
self.课次数 = len(self.课程链接) - 1 # 课次一般都是固定21课,不用改
|
161
|
-
self.达标时长 = 30 # 单位:分钟。在线听课的时长达到此标准以上,才计算为完成一节课的学习
|
162
|
-
# 一般是3天,因为回放第1、2、3天都会数额,第4天就没数额了,所以第4天返款
|
163
|
-
self.回放返款天数 = sum((x > 0) for x in self.视频返款[1:])
|
164
|
-
|
165
|
-
self.root = XlPath(self.表格路径).parent
|
166
|
-
self.wb = openpyxl.load_workbook(self.表格路径)
|
167
|
-
self.ws = self.wb['考勤表']
|
168
|
-
if today:
|
169
|
-
dt = parse_datetime(today)
|
170
|
-
self.today = date(dt.year, dt.month, dt.day)
|
171
|
-
else:
|
172
|
-
self.today = date.today()
|
173
|
-
self.开课日期 = date.fromisoformat(self.开课日期)
|
174
|
-
# self.觉观禅课 = (self.开课日期.day == 1)
|
175
|
-
self.觉观禅课 = '觉观' in self.返款标题
|
176
|
-
self.当天课次 = (self.today - self.开课日期).days + 1 # 这是用于逻辑运算的,可能超过实际课次数
|
177
|
-
self.当天课次2 = min(self.当天课次, self.课次数)
|
178
|
-
self.结束课次 = self.当天课次 - len(self.视频返款) + 1 # 这是用于逻辑运算的,可能有负值
|
179
|
-
self.结束课次2 = max(0, self.结束课次)
|
180
|
-
try:
|
181
|
-
self.用户列表 = pd.read_csv(self.get_file('数据表/用户列表导出*.csv'), dtype={16: str})
|
182
|
-
except:
|
183
|
-
self.用户列表 = None
|
184
|
-
# 用户列表 = 用户列表[用户列表['账号状态'] == '正常']
|
185
|
-
|
186
|
-
self.考勤表出现次数 = Counter()
|
187
|
-
for f in self.root.glob('数据表/**/*直播观看详情*.csv'):
|
188
|
-
df = pd.read_csv(f, skiprows=1)
|
189
|
-
self.考勤表出现次数 += Counter([x.strip() for x in df['用户ID']])
|
190
|
-
for f in self.root.glob('数据表/**/*直播用户列表*.csv'):
|
191
|
-
df = pd.read_csv(f)
|
192
|
-
self.考勤表出现次数 += Counter([x.strip() for x in df['用户ID']])
|
193
|
-
|
194
|
-
self.异常data = {}
|
195
|
-
|
196
|
-
if hasattr(self, '异常处理'):
|
197
|
-
self.异常处理()
|
198
|
-
|
199
|
-
def get_file(self, p):
|
200
|
-
# 返回最后一个匹配的文件
|
201
|
-
return list(self.root.glob(p))[-1]
|
202
|
-
|
203
|
-
def 查找用户(self, 昵称='', 手机号='', *, debug=False):
|
204
|
-
"""
|
205
|
-
:param 昵称: 昵称可以输入一个列表,会在目标昵称、真实姓名同时做检索
|
206
|
-
:param 手机号: 手机也可以输入一个列表,因为有些人可能报名时手机号填错,可以增加一些匹配规则
|
207
|
-
:return:
|
208
|
-
"""
|
209
|
-
|
210
|
-
# 1 统一输入参数格式
|
211
|
-
def tolist(x):
|
212
|
-
if x and not isinstance(x, list):
|
213
|
-
x = [x]
|
214
|
-
x = [str(a) for a in x if a]
|
215
|
-
return x
|
216
|
-
|
217
|
-
def check_telphone(v, refs):
|
218
|
-
if not isinstance(v, str):
|
219
|
-
try:
|
220
|
-
v = int(v)
|
221
|
-
except:
|
222
|
-
pass
|
223
|
-
v = str(v)
|
224
|
-
for a in refs:
|
225
|
-
if a in v:
|
226
|
-
return True
|
227
|
-
|
228
|
-
昵称 = tolist(昵称)
|
229
|
-
手机号 = tolist(手机号)
|
230
|
-
手机号 = [f'{x}' for x in 手机号 if (x and x != 'None')]
|
231
|
-
|
232
|
-
# 2 查找所有可能匹配的项目
|
233
|
-
ls = []
|
234
|
-
for idx, x in self.用户列表.iterrows():
|
235
|
-
logo = 0
|
236
|
-
if '真实姓名' in x:
|
237
|
-
x['姓名'] = x['真实姓名']
|
238
|
-
|
239
|
-
if 昵称:
|
240
|
-
if x['昵称'] in 昵称 or x['姓名'] in 昵称:
|
241
|
-
logo += 1
|
242
|
-
if 手机号:
|
243
|
-
if check_telphone(x.get('账户绑定手机号'), 手机号) or check_telphone(x.get('最近采集的手机号'), 手机号):
|
244
|
-
logo += 2
|
245
|
-
if logo:
|
246
|
-
ls.append([x, logo])
|
247
|
-
|
248
|
-
ls.sort(key=lambda x: x[1], reverse=True)
|
249
|
-
ls = [x[0] for x in ls]
|
250
|
-
|
251
|
-
if debug:
|
252
|
-
print('\n'.join(map(self.用户信息摘要, ls)))
|
253
|
-
return
|
254
|
-
|
255
|
-
return ls
|
256
|
-
|
257
|
-
def 用户信息摘要(self, x):
|
258
|
-
""" 输入series类型的一个条目,输出其摘要信息 """
|
259
|
-
ls = [f'{k}={v}' for k, v in x.items() if (not isinstance(v, float) or not math.isnan(v))]
|
260
|
-
if x['用户ID'] in self.考勤表出现次数:
|
261
|
-
ls.append('考勤' + str(self.考勤表出现次数[x['用户ID']]) + '次')
|
262
|
-
return ', '.join(ls)
|
263
|
-
|
264
|
-
def 匹配用户ID(self, sheet_name='报名表'):
|
265
|
-
|
266
|
-
def try2int(x):
|
267
|
-
try:
|
268
|
-
return int(x)
|
269
|
-
except:
|
270
|
-
if isinstance(x, float) and math.isnan(x):
|
271
|
-
return ''
|
272
|
-
return x
|
273
|
-
|
274
|
-
def todict(row):
|
275
|
-
""" 将表格第i行的数据提取为字典格式
|
276
|
-
"""
|
277
|
-
msg = {}
|
278
|
-
for k, x in enumerate(['真实姓名', '微信昵称', '手机号', '错误手机号']):
|
279
|
-
msg[x] = ws.cell2(row, x).value
|
280
|
-
return msg
|
281
|
-
|
282
|
-
ws = self.wb[sheet_name]
|
283
|
-
for row in tqdm(list(ws.iterrows('真实姓名')), desc='匹配进度'):
|
284
|
-
# todo 这里要考虑相似度整体权重的,而不是几个属性的优先级
|
285
|
-
x = todict(row)
|
286
|
-
x['手机号'] = str(x['手机号']).lstrip('`')
|
287
|
-
x['错误手机号'] = str(x['错误手机号']).lstrip('`')
|
288
|
-
|
289
|
-
待查手机号 = [x['手机号']]
|
290
|
-
t = try2int(x['错误手机号'])
|
291
|
-
if t:
|
292
|
-
待查手机号.append(t)
|
293
|
-
ls = self.查找用户([x['真实姓名'], x['微信昵称']], 待查手机号)
|
294
|
-
摘要ls = list(map(self.用户信息摘要, ls))
|
295
|
-
|
296
|
-
用户ID = ''
|
297
|
-
if len(ls) == 1:
|
298
|
-
# 只有一个关联的直接匹配上,否则填空
|
299
|
-
用户ID = ls[0]['用户ID']
|
300
|
-
else:
|
301
|
-
# 只有一条有考勤记录时
|
302
|
-
flags = [('考勤' in text) for text in 摘要ls]
|
303
|
-
if sum(flags) == 1:
|
304
|
-
idx = flags.index(1)
|
305
|
-
用户ID = ls[idx]['用户ID']
|
306
|
-
if 用户ID == '':
|
307
|
-
flags = [('账号状态=正常' in text) for text in 摘要ls]
|
308
|
-
if sum(flags) == 1:
|
309
|
-
idx = flags.index(1)
|
310
|
-
用户ID = ls[idx]['用户ID']
|
311
|
-
# 如果用户ID还是空的,则用手机号能匹配上的那条
|
312
|
-
if 用户ID == '':
|
313
|
-
for i, text in enumerate(摘要ls):
|
314
|
-
if str(x['手机号']) in text:
|
315
|
-
用户ID = ls[i]['用户ID']
|
316
|
-
break
|
317
|
-
ws.cell2(row, '用户ID', 用户ID)
|
318
|
-
ws.cell2(row, '参考信息', '\n'.join(摘要ls))
|
319
|
-
row += 1
|
320
|
-
self.wb.save(self.表格路径)
|
321
|
-
|
322
|
-
def 获取每日统计表(self):
|
323
|
-
ls = []
|
324
|
-
columns = ['课次', '用户ID', '观看日期', '在线时长(分钟)']
|
325
|
-
for f in self.root.glob('数据表/*.csv'):
|
326
|
-
if m := re.match(r'(\d{4}\-\d{2}\-\d{2}).+?课.*?(\d+).+?直播观看详情', f.stem):
|
327
|
-
stat_day, 课次 = date.fromisoformat(m.group(1)), int(m.group(2))
|
328
|
-
skiprows = 1
|
329
|
-
elif m := re.search(r'届(?:念住|觉观).+?(\d+).+?直播用户列表.+?(\d{4}\-\d{2}\-\d{2})', f.stem):
|
330
|
-
stat_day, 课次 = date.fromisoformat(m.group(2)), int(m.group(1))
|
331
|
-
skiprows = 0
|
332
|
-
elif m := re.search(r'第(\d+)堂.+?届(?:念住|觉观).+?直播用户列表.+?(\d{4}\-\d{2}\-\d{2})', f.stem):
|
333
|
-
stat_day, 课次 = date.fromisoformat(m.group(2)), int(m.group(1))
|
334
|
-
skiprows = 0
|
335
|
-
elif m := re.search(r'本体音艺网课-(\d+).+?直播用户列表.+?(\d{4}\-\d{2}\-\d{2})', f.stem):
|
336
|
-
stat_day, 课次 = date.fromisoformat(m.group(2)), int(m.group(1))
|
337
|
-
skiprows = 0
|
338
|
-
else:
|
339
|
-
continue
|
340
|
-
|
341
|
-
if stat_day > self.today: # 超过self.today的数据不记录
|
342
|
-
continue
|
343
|
-
观看日期 = (stat_day - self.开课日期).days - 课次 + 1
|
344
|
-
df = pd.read_csv(f, skiprows=skiprows)
|
345
|
-
for idx, r in df.iterrows():
|
346
|
-
k = '累计观看时长(秒)' if 观看日期 else '直播观看时长(秒)'
|
347
|
-
ls.append([课次, r['用户ID'].strip(), 观看日期, int(int(r[k]) / 60)])
|
348
|
-
|
349
|
-
# ext: 异常数据修正,trick
|
350
|
-
for k, v in self.异常data.items():
|
351
|
-
课次, 用户ID = k
|
352
|
-
# 观看日期和在线时长可以乱填,主要是把这个用户课次记录下,后面遍历才会处理到
|
353
|
-
ls.append([int(re.search(r'\d+', 课次).group()), 用户ID, 0, 0])
|
354
|
-
|
355
|
-
df = pd.DataFrame.from_records(ls, columns=columns)
|
356
|
-
return df
|
357
|
-
|
358
|
-
def 获取考勤表中出现的所有用户名(self):
|
359
|
-
df = self.获取每日统计表()
|
360
|
-
print(*set(df['用户ID']), sep='\n')
|
361
|
-
|
362
|
-
def 修订(self, 学号, 课次, 完成标记, *args):
|
363
|
-
try:
|
364
|
-
user_id = self.ws.cell2({'学号': 学号}, '用户ID').value
|
365
|
-
except ValueError:
|
366
|
-
return
|
367
|
-
if isinstance(课次, int):
|
368
|
-
课次 = f'第{课次:02}课'
|
369
|
-
self.异常data[课次, user_id] = 完成标记
|
370
|
-
|
371
|
-
def 更新统计表(self, df):
|
372
|
-
# 1 辅助函数
|
373
|
-
ws = self.ws
|
374
|
-
id2row = {}
|
375
|
-
cel = ws.findcel('用户ID').down()
|
376
|
-
while cel.value:
|
377
|
-
id2row[cel.value] = cel.row
|
378
|
-
cel = cel.down()
|
379
|
-
|
380
|
-
col = ws.findcol('第01课') - 1
|
381
|
-
|
382
|
-
def 完成标记(items):
|
383
|
-
""" 输入某个用户某个课次每天的累计观看时长 """
|
384
|
-
x = {t['观看日期']: t['在线时长(分钟)'] for idx, t in items.iterrows()}
|
385
|
-
for k in sorted(x.keys()):
|
386
|
-
if x[k] >= self.达标时长:
|
387
|
-
return f'第{k}天回放' if k else '完成当堂学习'
|
388
|
-
return f'不足{self.达标时长}分钟'
|
389
|
-
|
390
|
-
def write(课次, 用户ID, value):
|
391
|
-
# 优先使用修订的标记
|
392
|
-
value = self.异常data.get((f'第{课次:02}课', 用户ID), value)
|
393
|
-
cel = ws.cell2(id2row[用户ID], f'第{课次:02}课')
|
394
|
-
if cel is None:
|
395
|
-
return
|
396
|
-
cel.value = value
|
397
|
-
color = None
|
398
|
-
if '完成当堂学习' in value:
|
399
|
-
color = RgbFormatter.from_name('鲜绿色')
|
400
|
-
elif '回放' in value:
|
401
|
-
color = RgbFormatter.from_name('黄色')
|
402
|
-
v1 = self.视频返款[0]
|
403
|
-
idx = min(int(re.search(r'第(\d+)天', value).group(1)),
|
404
|
-
len(self.视频返款) - 1)
|
405
|
-
v2 = self.视频返款[idx]
|
406
|
-
|
407
|
-
if v2:
|
408
|
-
color = color.light((v1 - v2) / v2) # 根据返款额度自动变浅
|
409
|
-
else: # 如果无返款额度
|
410
|
-
color = RgbFormatter.from_name('灰色')
|
411
|
-
elif 课次 <= self.当天课次 - len(self.视频返款) + 1:
|
412
|
-
cel.value = '未完成学习'
|
413
|
-
color = RgbFormatter.from_name('红色')
|
414
|
-
|
415
|
-
if color:
|
416
|
-
cel.fill_color(color)
|
417
|
-
|
418
|
-
# 2 遍历更新考勤情况数据
|
419
|
-
for [课次, 用户ID], items in df.groupby(['课次', '用户ID']):
|
420
|
-
if 用户ID not in id2row:
|
421
|
-
continue
|
422
|
-
write(课次, 用户ID, 完成标记(items))
|
423
|
-
|
424
|
-
# 3 处理剩余用户
|
425
|
-
for i in range(1, min(self.课次数 + 1, self.结束课次 + 1)):
|
426
|
-
for k, v in id2row.items():
|
427
|
-
t = ws.cell(v, col + i).value
|
428
|
-
if not t or t in ('未开始学习', f'不足{self.达标时长}分钟'):
|
429
|
-
write(i, k, '未完成学习')
|
430
|
-
for i in range(max(self.结束课次 + 1, 1), min(self.课次数 + 1, self.当天课次 + 1)):
|
431
|
-
for k, v in id2row.items():
|
432
|
-
if not ws.cell(v, col + i).value:
|
433
|
-
write(i, k, '未开始学习')
|
434
|
-
|
435
|
-
def 生成今日返款文件(self, msg):
|
436
|
-
""" 生成今日返款文件,及缺勤报告
|
437
|
-
|
438
|
-
生成的csv文件共4列:订单号,反馈额,备注说明,重复校验码
|
439
|
-
第4列可以设置一个64长度以内的标识来判重,防止重复退款
|
440
|
-
因为每个学员每个课次理论上应该只有一次退款操作
|
441
|
-
所以我使用"{订单号}_class{课次号}"来生成校验码
|
442
|
-
|
443
|
-
注意,涉及给人看的数据,大部分课次编号都不会加前导0,
|
444
|
-
涉及程序、文件名对齐的,则一般都会设前导0
|
445
|
-
"""
|
446
|
-
ls = [] # 返款汇总
|
447
|
-
name = [] # 生成的csv文件名
|
448
|
-
|
449
|
-
ws = self.ws
|
450
|
-
_col = ws.findcol('第01课') - 1
|
451
|
-
|
452
|
-
# 1 初期要详细提示
|
453
|
-
if self.当天课次 < 4:
|
454
|
-
msg.append('第1次建议大家都查看下完整考勤表,记一下自己的学号,方便以后核对考勤数据。'
|
455
|
-
'以后群里统一以学号的方式汇报异常的考勤数据,未列出则都是"正常完成当堂学习"的学员。')
|
456
|
-
msg.append('如果发现自己考勤数据不对,可以群里、私聊反馈给我修正。')
|
457
|
-
|
458
|
-
# 2 要过期的课程
|
459
|
-
if 0 < self.结束课次 + 1 <= self.课次数:
|
460
|
-
msg.append(f'第{self.结束课次 + 1}课回放、打卡第{len(self.视频返款) - 1}天,还未完成学习的同学们请抓紧时间。')
|
461
|
-
|
462
|
-
# 3 回放
|
463
|
-
回放课次 = self.当天课次 - self.回放返款天数
|
464
|
-
if 0 < 回放课次 <= self.课次数:
|
465
|
-
title = f'第{回放课次}课回放'
|
466
|
-
msg.append('【' + title + '名单】')
|
467
|
-
if 回放课次 == 1:
|
468
|
-
msg[-1] += f'(第1课仍有{len(self.视频返款) - self.回放返款天数 - 1}天可以回放)'
|
469
|
-
|
470
|
-
d = {}
|
471
|
-
for i in range(1, self.回放返款天数 + 1):
|
472
|
-
d[f'第{i}天回放'] = []
|
473
|
-
d['未完成学习'] = []
|
474
|
-
|
475
|
-
col = _col + 回放课次
|
476
|
-
for r in ws.iterrows('用户ID'):
|
477
|
-
v = ws.cell(r, col).value
|
478
|
-
if v and '回放' in v:
|
479
|
-
t = int(re.search(r'第(\d+)天', v).group(1))
|
480
|
-
if self.视频返款[t]:
|
481
|
-
订单号 = ws.cell2(r, '交易订单号').value
|
482
|
-
if 订单号:
|
483
|
-
cols = [订单号, self.视频返款[t],
|
484
|
-
f'{self.返款标题}第{回放课次}课第{t}天完成回放',
|
485
|
-
f'{订单号}_class{回放课次:02}'] # 防止重复返款的校验码
|
486
|
-
ls.append(','.join(map(str, cols)))
|
487
|
-
else:
|
488
|
-
v = '未完成学习'
|
489
|
-
elif v in (f'不足{self.达标时长}分钟', '未开始学习'):
|
490
|
-
v = '未完成学习'
|
491
|
-
if v and v != '完成当堂学习':
|
492
|
-
d[v].append(ws.cell2(r, '学号').value)
|
493
|
-
|
494
|
-
is_empty = True
|
495
|
-
for k, v in d.items():
|
496
|
-
if v:
|
497
|
-
m = re.search(r'\d+', k)
|
498
|
-
t = int(m.group()) if m else 5
|
499
|
-
msg[-1] += f'\n{k}(返{self.视频返款[t]}元): ' + ','.join(map(str, v))
|
500
|
-
is_empty = False
|
501
|
-
if is_empty:
|
502
|
-
msg[-1] = f'第{回放课次}课因当堂满勤,无回放名单'
|
503
|
-
else:
|
504
|
-
name.append(f'第{回放课次}课回放')
|
505
|
-
|
506
|
-
# 4 当堂
|
507
|
-
if self.当天课次 <= self.课次数:
|
508
|
-
title = f'第{self.当天课次}课当堂'
|
509
|
-
msg.append('【' + title + '缺勤名单】')
|
510
|
-
if self.当天课次 == 1:
|
511
|
-
msg[-1] += '(注意只统计早晨直播情况,今日的回放数据明天会更新)'
|
512
|
-
# d = {f'不足{self.达标时长}分钟': [], '未开始学习': []}
|
513
|
-
d = []
|
514
|
-
|
515
|
-
col = _col + self.当天课次
|
516
|
-
for r in ws.iterrows('用户ID'):
|
517
|
-
v = ws.cell(r, col).value
|
518
|
-
if v == '完成当堂学习':
|
519
|
-
订单号 = ws.cell2(r, '交易订单号').value
|
520
|
-
if 订单号:
|
521
|
-
cols = [订单号, self.视频返款[0],
|
522
|
-
f'{self.返款标题}第{self.当天课次}课完成当堂学习',
|
523
|
-
f'{订单号}_class{self.当天课次:02}'] # 防止重复返款的校验码
|
524
|
-
ls.append(','.join(map(str, cols)))
|
525
|
-
else:
|
526
|
-
d.append(ws.cell(r, 1).value)
|
527
|
-
|
528
|
-
if d:
|
529
|
-
msg[-1] += '\n' + ','.join(map(str, d))
|
530
|
-
else:
|
531
|
-
msg[-1] = title + '满勤!!!'
|
532
|
-
name.append(f'第{self.当天课次}课当堂')
|
533
|
-
|
534
|
-
# 5 第22课的处理
|
535
|
-
if self.觉观禅课 and self.当天课次 == 23:
|
536
|
-
name.append('第22课')
|
537
|
-
for r in ws.iterrows('用户ID'):
|
538
|
-
v = ws.cell2(r, '交易订单号').value
|
539
|
-
if v:
|
540
|
-
cols = [v, self.视频返款[0],
|
541
|
-
f'{self.返款标题}第{self.当天课次 - 1}课',
|
542
|
-
f'{v}_class22'] # 防止重复返款的校验码
|
543
|
-
ls.append(','.join(map(str, cols)))
|
544
|
-
|
545
|
-
# 6 返款文件
|
546
|
-
if ls:
|
547
|
-
ls = [x for x in ls if ('订单号' not in x and 'None' not in x)]
|
548
|
-
(self.root / (f'第{self.当天课次:02}天 ' + '+'.join(name) + '返款.csv')).write_text('\n'.join(ls))
|
549
|
-
|
550
|
-
# 7 提示信息
|
551
|
-
if name:
|
552
|
-
msg.append('+'.join(name) + '促学金已返款,同学们请查收。')
|
553
|
-
if self.觉观禅课 and self.当天课次 == 22:
|
554
|
-
msg.append('今晚第22课答疑不考勤,明天统一返款。')
|
555
|
-
return msg
|
556
|
-
|
557
|
-
def 计算打卡返款(self, 打卡次数):
|
558
|
-
from bisect import bisect_right
|
559
|
-
|
560
|
-
ks = [0] + list(self.打卡返款.keys())
|
561
|
-
ks.sort()
|
562
|
-
|
563
|
-
def find_le(a, x):
|
564
|
-
'Find rightmost value less than or equal to x'
|
565
|
-
i = bisect_right(a, x)
|
566
|
-
if i:
|
567
|
-
return a[i - 1]
|
568
|
-
raise ValueError
|
569
|
-
|
570
|
-
k = find_le(ks, 打卡次数)
|
571
|
-
return self.打卡返款.get(k, 0)
|
572
|
-
|
573
|
-
def 打卡统计(self, msg):
|
574
|
-
""" 不知道平台有每个课程的打卡次数,自己用另外一个途径暴力搞的,220226周六15:59 现在应该没用了"""
|
575
|
-
from openpyxl.styles import PatternFill
|
576
|
-
|
577
|
-
# 1 至少两个目录才统计
|
578
|
-
dirs = list((self.root / '数据表').glob_dirs('用户学习统计*'))
|
579
|
-
if len(dirs) < 2:
|
580
|
-
return
|
581
|
-
|
582
|
-
# 2 读取数据
|
583
|
-
def merge_data(d):
|
584
|
-
""" 输入目录,汇总目录下所有xlsx文件的打卡数据
|
585
|
-
|
586
|
-
导出的打卡数据文件尽量小些,不然这个操作速度有点慢。。。
|
587
|
-
"""
|
588
|
-
data = {}
|
589
|
-
for f in d.glob('*.xlsx'):
|
590
|
-
if f.stem.startswith('~$'):
|
591
|
-
continue
|
592
|
-
df = pd.read_excel(f, header=1)
|
593
|
-
for idx, x in df.iterrows():
|
594
|
-
user_id = x['用户ID\t']
|
595
|
-
if user_id == user_id: # nan数值的特性,这个条件不成立
|
596
|
-
value = x['提交打卡\t']
|
597
|
-
data[user_id.strip()] = int(value) if value == value else 0
|
598
|
-
else:
|
599
|
-
continue
|
600
|
-
|
601
|
-
return data
|
602
|
-
|
603
|
-
d1, d2 = dirs[0], dirs[-1]
|
604
|
-
data1 = merge_data(d1)
|
605
|
-
data2 = merge_data(d2)
|
606
|
-
|
607
|
-
# 3 d1数据标准化
|
608
|
-
# 最理想的是d1恰好是开课前一天的统计数据,那么d2和d1直接相减即可
|
609
|
-
# 如果d1提前了,无法判断具体打卡日期,所以还是直接相减,只能都算上
|
610
|
-
# 如果d1延后了,那么要对d1的数据进行预处理,要减掉对应天数,但最低为0,不能为负数
|
611
|
-
def parse_date(name):
|
612
|
-
""" 输入文件名,获得其日期 """
|
613
|
-
m = re.search(r'(\d{2})(\d{2})(\d{2})', name)
|
614
|
-
year, month, day = m.groups()
|
615
|
-
year = '20' + year
|
616
|
-
return date(int(year), int(month), int(day))
|
617
|
-
|
618
|
-
delta = (parse_date(d1.stem) - self.开课日期).days + 1
|
619
|
-
if delta > 0:
|
620
|
-
data1 = {k: max(0, v - delta) for k, v in data1.items()}
|
621
|
-
|
622
|
-
# 4 计算新的打卡次数
|
623
|
-
# 打卡数是有可能超过21次的
|
624
|
-
data3 = {k: v - data1.get(k, 0) for k, v in data2.items()}
|
625
|
-
|
626
|
-
# 5 将打卡次数写入表格
|
627
|
-
ls = [] # 返款汇总
|
628
|
-
ws = self.ws
|
629
|
-
color0 = [(255, 0, 0), (255, 255, 128), (255, 255, 0), (0, 255, 0)]
|
630
|
-
for i in ws.iterrows('用户ID'):
|
631
|
-
c1 = ws.cell2(i, ['打卡返款', '打卡数'])
|
632
|
-
v = c1.value = data3.get(ws.cell2(i, '用户ID').value, 0)
|
633
|
-
v //= 5
|
634
|
-
if v:
|
635
|
-
money = self.打卡返款[min(v - 1, 2)]
|
636
|
-
订单号 = ws.cell2(i, '交易订单号').value
|
637
|
-
cols = [订单号, money, f'{self.返款标题}返学修日志促学金', f'{订单号}_journal']
|
638
|
-
if cols[0]:
|
639
|
-
ls.append(','.join(map(str, cols)))
|
640
|
-
else:
|
641
|
-
money = 0
|
642
|
-
|
643
|
-
c2 = ws.cell2(i, ['打卡返款', '返款'])
|
644
|
-
c2.value = money
|
645
|
-
|
646
|
-
color = RgbFormatter(*color0[min(3, v)])
|
647
|
-
c1.fill = PatternFill(fgColor=color.hex[-6:], fill_type="solid")
|
648
|
-
c2.fill = PatternFill(fgColor=color.hex[-6:], fill_type="solid")
|
649
|
-
|
650
|
-
# 并计算当前的返款额
|
651
|
-
desc = '/'.join(map(str, self.打卡返款))
|
652
|
-
if self.当天课次 == 25:
|
653
|
-
msg.append('已生成截止目前的打卡数据,同学们可以预先核对下,明天最后更新打卡数据后返款。'
|
654
|
-
f'打卡达到"5/10/15"次,依次返回"{desc}"元。'
|
655
|
-
'注:因技术原因,打卡数据无法精确计算,统计遵循宁可多算但无漏算的原则,所以部分同学打卡数会超过21次。')
|
656
|
-
elif self.当天课次 == self.课次数 + len(self.视频返款) - 1:
|
657
|
-
# 生成打卡返款
|
658
|
-
msg.append('已完成学修日志(打卡)促学金的返款。'
|
659
|
-
f'打卡达到"5/10/15"次,依次返回"{desc}"元。')
|
660
|
-
(self.root / '学修日志返款.csv').write_text('\n'.join(ls))
|
661
|
-
|
662
|
-
def 打卡统计2(self, msg):
|
663
|
-
from openpyxl.styles import PatternFill
|
664
|
-
|
665
|
-
# 1 有打卡统计表才计算
|
666
|
-
files = list((self.root / '数据表').glob_files('*打卡*.csv'))
|
667
|
-
if len(files) < 1:
|
668
|
-
return
|
669
|
-
|
670
|
-
# 2 读取数据
|
671
|
-
# df = pd.read_csv(files[-1])
|
672
|
-
# data = {}
|
673
|
-
# for idx, row in df.iterrows():
|
674
|
-
# data[row['用户id']] = row['打卡次数']
|
675
|
-
|
676
|
-
df = None
|
677
|
-
try:
|
678
|
-
df = pd.read_excel(files[-1]) # 220804周四08:28,小鹅通更新了模板
|
679
|
-
except ValueError:
|
680
|
-
pass
|
681
|
-
|
682
|
-
if df is None:
|
683
|
-
try:
|
684
|
-
df = pd.read_csv(files[-1]) # 221005周三09:19,小鹅通又双叒更新了
|
685
|
-
except UnicodeDecodeError:
|
686
|
-
pass
|
687
|
-
|
688
|
-
if df is None:
|
689
|
-
try:
|
690
|
-
df = pd.read_csv(files[-1], encoding="ANSI") # 240226周一11:21,
|
691
|
-
except UnicodeDecodeError:
|
692
|
-
pass
|
693
|
-
|
694
|
-
if df is None:
|
695
|
-
raise ValueError
|
696
|
-
|
697
|
-
df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
|
698
|
-
df.columns = df.columns.map(lambda x: x.strip() if isinstance(x, str) else x)
|
699
|
-
|
700
|
-
# data = Counter([x for x in df['用户id']])
|
701
|
-
try:
|
702
|
-
data = {row['用户id']: row['打卡次数'] for _, row in df.iterrows()}
|
703
|
-
except KeyError: # 230202周四19:49,另一种实际是xlsx格式,然后再转出csv的情况
|
704
|
-
data = Counter([row['user_id'] for _, row in df.iterrows()])
|
705
|
-
|
706
|
-
# 3 将打卡次数写入表格
|
707
|
-
ls = [] # 返款汇总
|
708
|
-
ws = self.ws
|
709
|
-
最高返款额 = max(self.打卡返款.values())
|
710
|
-
for i in ws.iterrows('用户ID'):
|
711
|
-
c1 = ws.cell2(i, ['打卡返款', '打卡数'])
|
712
|
-
打卡次数 = c1.value = data.get(ws.cell2(i, '用户ID').value, 0)
|
713
|
-
返款额 = self.计算打卡返款(打卡次数)
|
714
|
-
|
715
|
-
if 返款额 > 0:
|
716
|
-
订单号 = ws.cell2(i, '交易订单号').value
|
717
|
-
cols = [订单号, 返款额, f'{self.返款标题}返学修日志促学金', f'{订单号}_journal']
|
718
|
-
ls.append(','.join(map(str, cols)))
|
719
|
-
|
720
|
-
c2 = ws.cell2(i, ['打卡返款', '返款'])
|
721
|
-
c2.value = 返款额
|
722
|
-
|
723
|
-
if 返款额 <= 0:
|
724
|
-
color = RgbFormatter(255, 0, 0)
|
725
|
-
elif 返款额 < 最高返款额:
|
726
|
-
color = RgbFormatter(255, 255, 0).light(1 - 返款额 / 最高返款额)
|
727
|
-
elif 返款额 == 最高返款额: # 最高返款额
|
728
|
-
color = RgbFormatter(0, 255, 0)
|
729
|
-
|
730
|
-
c1.fill = PatternFill(fgColor=color.hex[-6:], fill_type="solid")
|
731
|
-
c2.fill = PatternFill(fgColor=color.hex[-6:], fill_type="solid")
|
732
|
-
|
733
|
-
if ls:
|
734
|
-
ls = [x for x in ls if ('无订单号' not in x and 'None' not in x)]
|
735
|
-
|
736
|
-
# 4 生成通知,及返款文件
|
737
|
-
desc1 = '/'.join(map(str, self.打卡返款.keys()))
|
738
|
-
desc2 = '/'.join(map(str, self.打卡返款.values()))
|
739
|
-
if self.结束课次 == self.课次数 - 1:
|
740
|
-
msg.append('已生成截止目前的打卡数据,同学们可以预先核对下,明天最后更新打卡数据后返款。'
|
741
|
-
f'打卡达到"{desc1}"次,依次返回"{desc2}"元。')
|
742
|
-
elif self.结束课次 == self.课次数:
|
743
|
-
# 生成打卡返款
|
744
|
-
msg.append('已完成学修日志(打卡)促学金的返款。'
|
745
|
-
f'打卡达到"{desc1}"次,依次返回"{desc2}"元。')
|
746
|
-
(self.root / '学修日志返款.csv').write_text('\n'.join(ls))
|
747
|
-
|
748
|
-
def write_返款总计(self):
|
749
|
-
ws = self.ws
|
750
|
-
name2idx = {k: i for i, k in enumerate(['完成当堂学习', '第1天回放', '第2天回放', '第3天回放', '第4天回放'])}
|
751
|
-
for i in ws.iterrows('用户ID'):
|
752
|
-
total = 0
|
753
|
-
for k in range(1, 22):
|
754
|
-
cel = ws.cell2(i, f'第{k:02}课')
|
755
|
-
if not cel:
|
756
|
-
continue
|
757
|
-
v = cel.value
|
758
|
-
if v in name2idx:
|
759
|
-
idx = name2idx[v]
|
760
|
-
if idx and k > self.当天课次 - self.回放返款天数: # 还没返款的回放金额
|
761
|
-
continue
|
762
|
-
total += self.视频返款[idx]
|
763
|
-
v2 = ws.cell2(i, ['打卡返款', '返款']).value
|
764
|
-
if isinstance(v2, int):
|
765
|
-
total += v2
|
766
|
-
if self.觉观禅课 and self.当天课次 > 22:
|
767
|
-
total += self.视频返款[0]
|
768
|
-
ws.cell2(i, ['已返款', '总计'], total)
|
769
|
-
|
770
|
-
def 考勤日报(self, debug=False, journal=False):
|
771
|
-
msg = [] # 报告内容
|
772
|
-
|
773
|
-
# 1 日常返款
|
774
|
-
df = self.获取每日统计表()
|
775
|
-
self.更新统计表(df)
|
776
|
-
self.生成今日返款文件(msg)
|
777
|
-
|
778
|
-
# 2 打卡返款
|
779
|
-
if journal or self.结束课次 == self.课次数:
|
780
|
-
self.打卡统计2(msg)
|
781
|
-
self.write_返款总计()
|
782
|
-
|
783
|
-
# 3 结课
|
784
|
-
if self.当天课次 == (self.课次数 + len(self.视频返款) - 1):
|
785
|
-
msg.append('考勤返款工作已全部完成,若对自己促学金还有疑问的近日可以再跟我反馈。')
|
786
|
-
|
787
|
-
# 4 输出日报
|
788
|
-
if len(msg) > 1:
|
789
|
-
msg = '\n'.join([f'{i}、{x}' for i, x in enumerate(msg, start=1)]) # 编号
|
790
|
-
else:
|
791
|
-
msg = msg[0]
|
792
|
-
if self.在线表格:
|
793
|
-
msg = f'【完整考勤表】{self.在线表格}\n' + msg
|
794
|
-
print(msg)
|
795
|
-
|
796
|
-
# 5 展示或保存表格内容
|
797
|
-
if debug:
|
798
|
-
browser.html(self.ws.to_html())
|
799
|
-
else:
|
800
|
-
# self.wb.save(self.表格路径)
|
801
|
-
self.wb.save(self.root / (XlPath(self.表格路径).stem + '.xlsx'))
|
802
|
-
|
803
|
-
def cmd(self):
|
804
|
-
""" 初始化类后不关闭,继续轮询执行功能 """
|
805
|
-
cmd = input('>')
|
806
|
-
while cmd != 'exit':
|
807
|
-
fire.Fire(self, cmd)
|
808
|
-
cmd = input('>')
|
809
|
-
|
810
|
-
def __call__(self, *args, **kwargs):
|
811
|
-
print('程序测试通过,可正常使用')
|
812
|
-
|
813
|
-
def 表格内容对齐(self, ws_name1, ws_name2, col_name):
|
814
|
-
wb = self.wb
|
815
|
-
wb.merge_sheets_by_keycol([wb[ws_name1], wb[ws_name2]], col_name)
|
816
|
-
|
817
|
-
# 保存。一般不用重新再加载self.wb、self.ws,因为这里需求本来基本都是要分段重新执行程序的。
|
818
|
-
wb.save(self.表格路径)
|
819
|
-
|
820
|
-
def 匹配交易单号(self, value):
|
821
|
-
""" 更加智能的一条龙匹配操作
|
822
|
-
|
823
|
-
:param value: 需要输入待检索的金额值
|
824
|
-
"""
|
825
|
-
# 1 读取账单,账单文件可能不唯一,可能有重复可以自动去重
|
826
|
-
files = XlPath('数据表').rglob_files('*基本账户*.csv')
|
827
|
-
df_list = []
|
828
|
-
for f in files:
|
829
|
-
df = pd.read_csv(f)
|
830
|
-
df_list.append(df)
|
831
|
-
df = pd.concat(df_list, ignore_index=True)
|
832
|
-
# 按照"资金流水单号"去重
|
833
|
-
df = df.drop_duplicates(subset=['资金流水单号'], keep='first')
|
834
|
-
df = df[df['收支类型'] == '`收入']
|
835
|
-
df.reset_index(drop=True, inplace=True)
|
836
|
-
|
837
|
-
# 2 因为情况比较特殊,这里不调用通用的对齐功能,而是定制化写过
|
838
|
-
ws = self.wb['报名表']
|
839
|
-
|
840
|
-
data = ws.iterrows('真实姓名', to_dict=['交易单号'])
|
841
|
-
last_row = -1
|
842
|
-
for i, row in data:
|
843
|
-
last_row = i
|
844
|
-
# 在 df['微信支付业务单号'] 找是否有 row['交易单号']
|
845
|
-
items = df[df['微信支付业务单号'] == row['交易单号']]
|
846
|
-
if items.empty:
|
847
|
-
continue
|
848
|
-
|
849
|
-
# 如果匹配,理论上只有一条
|
850
|
-
item = items.iloc[0]
|
851
|
-
ws.cell2(i, '交易订单号').value = item['业务凭证号'][1:]
|
852
|
-
ws.cell2(i, '订单金额').value = item['收支金额(元)']
|
853
|
-
|
854
|
-
# 在df中去掉所有items
|
855
|
-
df = df.drop(items.index)
|
856
|
-
|
857
|
-
# 3 匹配完后,还有目标金额的数据要列出来
|
858
|
-
items = df[df['收支金额(元)'] == f'`{value:.2f}']
|
859
|
-
for idx, row in items.iterrows():
|
860
|
-
last_row += 1
|
861
|
-
ws.cell2(last_row, '交易单号').value = row['微信支付业务单号']
|
862
|
-
ws.cell2(last_row, '交易订单号').value = row['业务凭证号'][1:]
|
863
|
-
ws.cell2(last_row, '订单金额').value = row['收支金额(元)']
|
864
|
-
|
865
|
-
# 4 保存
|
866
|
-
self.wb.save(self.表格路径)
|
867
|
-
|
868
|
-
def __2_自动浏览网页(self):
|
869
|
-
pass
|
870
|
-
|
871
|
-
def ensure_driver(self):
|
872
|
-
if not hasattr(self, 'driver'):
|
873
|
-
self.driver = None
|
874
|
-
if not self.driver:
|
875
|
-
self.driver = XlChrome()
|
876
|
-
return self.driver
|
877
|
-
|
878
|
-
def 登录小鹅通(self, name, passwd):
|
879
|
-
# 登录小鹅通
|
880
|
-
driver = self.ensure_driver()
|
881
|
-
driver.get('https://admin.xiaoe-tech.com/t/login#/acount')
|
882
|
-
driver.locate('//*[@id="common_template_mounted_el_container"]'
|
883
|
-
'/div/div[1]/div[3]/div/div[4]/div/div[1]/div[1]/div/div[2]/input').send_keys(name)
|
884
|
-
driver.locate('//*[@id="common_template_mounted_el_container"]'
|
885
|
-
'/div/div[1]/div[3]/div/div[4]/div/div[1]/div[2]/div/div/input').send_keys(passwd)
|
886
|
-
driver.click('//*[@id="common_template_mounted_el_container"]/div/div[1]/div[3]/div/div[4]/div/div[2]')
|
887
|
-
|
888
|
-
# 然后自己手动操作验证码
|
889
|
-
# 以及选择"店铺"
|
890
|
-
|
891
|
-
def 登录微信支付(self):
|
892
|
-
driver = self.ensure_driver()
|
893
|
-
driver.get('https://pay.weixin.qq.com/index.php/core/home/login')
|
894
|
-
|
895
|
-
def 下载课次考勤数据(self, 起始课=None, 终止课=None, 文件名前缀=''):
|
896
|
-
if 起始课 is None:
|
897
|
-
起始课 = max(1, self.结束课次)
|
898
|
-
if 终止课 is None:
|
899
|
-
终止课 = min(self.当天课次, self.课次数)
|
900
|
-
|
901
|
-
# 1 遍历课程下载表格
|
902
|
-
driver = self.driver
|
903
|
-
for i in range(起始课, 终止课 + 1):
|
904
|
-
print(f'第{i}课', self.课程链接[i])
|
905
|
-
driver.get('https://admin.xiaoe-tech.com/t/data_center/index') # 必须要找个过渡页,不然不会更新课程链接
|
906
|
-
driver.get(self.课程链接[i])
|
907
|
-
# 不能写'第{i}课',会有叫'第{i}堂'等其他情况
|
908
|
-
driver.locate_text('//*[@id="app"]/div/div/div[1]/div[2]/div[1]/div[2]', f'{i}') # 检查页面
|
909
|
-
|
910
|
-
driver.click('//*[@id="tab-studentTab"]/span') # 直播间用户
|
911
|
-
driver.click('//*[@id="pane-studentTab"]/div/div[2]/div[2]/form/div[2]/button[2]/span/span') # 导出列表
|
912
|
-
driver.click('//*[@id="data-export-container"]/div/div[2]/div/div[2]/div[2]/button[2]/span/span') # 导出
|
913
|
-
time.sleep(1)
|
914
|
-
|
915
|
-
# 2 下载表格
|
916
|
-
# 2.1 等待下载文件生成完毕
|
917
|
-
while True:
|
918
|
-
driver.get('https://admin.xiaoe-tech.com/t/basic-platform/downloadCenter')
|
919
|
-
if '任务撤回' in driver.locate('//*[@id="downloadsCenter"]/div[4]/div/div[4]/div[2]/table/tbody/tr[1]/'
|
920
|
-
'td[9]/div/div/span').text:
|
921
|
-
time.sleep(1)
|
922
|
-
else:
|
923
|
-
time.sleep(2) # 不能下载太快,还是要稍微等一会
|
924
|
-
break
|
925
|
-
|
926
|
-
# 2.2 下载表格
|
927
|
-
files = []
|
928
|
-
for i in range(1, 终止课 - 起始课 + 2):
|
929
|
-
driver.click(f'//*[@id="downloadsCenter"]/div[4]/div/div[4]/div[2]/table/tbody/tr[{i}]/'
|
930
|
-
f'td[9]/div/div/span[1]')
|
931
|
-
file = driver.locate(f'//*[@id="downloadsCenter"]/div[4]/div/div[3]/table/tbody/tr[{i}]/'
|
932
|
-
f'td[1]/div/div/div').text + '.csv' # 下载后还会再多一个csv后缀
|
933
|
-
files.append(file)
|
934
|
-
time.sleep(1)
|
935
|
-
|
936
|
-
# 2.3 移动表格
|
937
|
-
time.sleep(1)
|
938
|
-
# 这样应该也不是很泛用,有的人浏览器下载可能会放到各种自定义的其他地方
|
939
|
-
download_path = XlPath(os.path.join(os.environ['USERPROFILE'], 'Downloads'))
|
940
|
-
for file in files:
|
941
|
-
src_file = (download_path / file)
|
942
|
-
while True:
|
943
|
-
if src_file.is_file():
|
944
|
-
src_file.move(self.root / '数据表' / (文件名前缀 + file), if_exists='replace')
|
945
|
-
break
|
946
|
-
else:
|
947
|
-
time.sleep(1)
|
948
|
-
|
949
|
-
def 批量退款(self):
|
950
|
-
self.driver.get('https://pay.weixin.qq.com/index.php/xphp/cbatchrefund/batch_refund#/pages/index/index')
|
951
|
-
|
952
|
-
def 申请单条退款(self, 凭证号, 退款金额=0, 退款原因=''):
|
953
|
-
driver = self.ensure_driver()
|
954
|
-
driver.get('https://pay.weixin.qq.com/index.php/core/refundapply')
|
955
|
-
driver.locate('//*[@id="app"]/div/div[2]/div[2]/div[2]/div/span/input').send_keys(凭证号)
|
956
|
-
driver.click('//*[@id="applyRefundBtn"]') # 申请退款
|
957
|
-
driver.locate('//*[@id="app"]/div/div[2]/div[2]/div[3]/div[2]/div/div[1]/div/span[1]/input').send_keys(
|
958
|
-
str(退款金额))
|
959
|
-
driver.locate('//*[@id="textInput"]').send_keys(退款原因)
|
960
|
-
# driver.click('//*[@id="commitRefundApplyBtn"]') # 建议手动点"提交申请"
|
961
|
-
|
962
|
-
|
963
|
-
class KqDb(Connection):
|
964
|
-
""" 五一身心行修考勤工具 """
|
965
|
-
|
966
|
-
def __init__(self, dbfile='5034.db', wbpath='考勤.xlsx', *args, **kwargs):
|
967
|
-
super().__init__(dbfile, *args, **kwargs)
|
968
|
-
p = XlPath(wbpath)
|
969
|
-
self.wb = openpyxl.load_workbook(p)
|
970
|
-
self.outwb = p.with_name(p.stem + '+' + p.suffix)
|
971
|
-
# 开营第几天
|
972
|
-
self.days = (datetime.datetime.today() - datetime.datetime(2022, 4, 29)).days
|
973
|
-
|
974
|
-
def update_小鹅通数据(self):
|
975
|
-
# 1 观看记录
|
976
|
-
def add_one_csv(path, 课次名=None):
|
977
|
-
""" 添加一个csv文件的数据 """
|
978
|
-
# 1 确定表格存在
|
979
|
-
if not self.has_table('观看记录'):
|
980
|
-
cols = ['课次名 text', '用户ID text', '用户昵称 text',
|
981
|
-
'直播间停留秒数 integet', '累计观看秒数 integer', '直播观看秒数 integer',
|
982
|
-
'回放观看秒数 integer',
|
983
|
-
'首次进入时间 text', '最近进入时间 text', '记录时间点 text']
|
984
|
-
cols = ', '.join(cols)
|
985
|
-
self.execute(f'CREATE TABLE 观看记录 ({cols}, PRIMARY KEY (课次名, 用户ID, 直播间停留秒数))')
|
986
|
-
|
987
|
-
# 2 解析csv的内容到sql中
|
988
|
-
if not 课次名:
|
989
|
-
课次名 = re.search(r'《(.+?)》', str(path)).group(1)
|
990
|
-
if 课次名 == '测试链接2':
|
991
|
-
课次名 = '2022五一线上觉观营-0429开营'
|
992
|
-
|
993
|
-
time_tag = datetime.datetime.fromtimestamp(os.stat(path).st_ctime).strftime('%Y-%m-%d %H:%M:%S')
|
994
|
-
df = pd.read_csv(path, skiprows=1)
|
995
|
-
for idx, row in df.iterrows():
|
996
|
-
d = {'课次名': 课次名,
|
997
|
-
'用户ID': row['用户ID'].strip(),
|
998
|
-
'用户昵称': row['用户昵称'],
|
999
|
-
'直播间停留秒数': row['直播间停留时长(秒)'],
|
1000
|
-
'累计观看秒数': row['累计观看时长(秒)'],
|
1001
|
-
'直播观看秒数': row['直播观看时长(秒)'],
|
1002
|
-
'回放观看秒数': row['回放观看时长(秒)'],
|
1003
|
-
'首次进入时间': row['首次进入时间'].strip(),
|
1004
|
-
'最近进入时间': row['最近进入时间'].strip(),
|
1005
|
-
'记录时间点': time_tag,
|
1006
|
-
}
|
1007
|
-
self.insert_row('观看记录', d)
|
1008
|
-
self.commit()
|
1009
|
-
|
1010
|
-
for f in XlPath('数据表').glob('*直播观看详情*.csv'):
|
1011
|
-
add_one_csv(f)
|
1012
|
-
|
1013
|
-
# 2 打卡记录
|
1014
|
-
self.execute('DROP TABLE IF EXISTS 打卡记录')
|
1015
|
-
# 时间点最新的数据
|
1016
|
-
f = list(XlPath('数据表').glob('*打卡日记*.csv'))[-1]
|
1017
|
-
df = pd.read_excel(f)
|
1018
|
-
df.columns = df.columns.str.replace('\t', '') # 删除列名里的\t
|
1019
|
-
df = df.replace('\t', '', regex=True) # 删除值里的\t
|
1020
|
-
df.to_sql('打卡记录', con=self)
|
1021
|
-
|
1022
|
-
def update_用户列表(self, path):
|
1023
|
-
""" 更新用户列表数据 """
|
1024
|
-
# 1 删除旧表,创建新表
|
1025
|
-
self.execute('DROP TABLE IF EXISTS 用户列表')
|
1026
|
-
# 需要预取一下数据,知道所有字段
|
1027
|
-
df = pd.read_csv(path)
|
1028
|
-
cols = {k: 'text' for k in df.columns}
|
1029
|
-
cols['年龄'] = 'integer'
|
1030
|
-
desc = ','.join([f'{k} {v}' for k, v in cols.items()])
|
1031
|
-
self.execute(f'CREATE TABLE 用户列表 ({desc}, PRIMARY KEY (用户ID))')
|
1032
|
-
self.commit()
|
1033
|
-
|
1034
|
-
# 2 插入每一行数据
|
1035
|
-
for idx, row in df.iterrows():
|
1036
|
-
for k in ['账户绑定手机号', '最近采集手机号']:
|
1037
|
-
# 有的是写成公式的'=手机号'
|
1038
|
-
if isinstance(row[k], str) and '=' in row[k]:
|
1039
|
-
row[k] = re.search(r'\d+', row[k]).group()
|
1040
|
-
self.insert_row('用户列表', row.to_dict())
|
1041
|
-
self.commit()
|
1042
|
-
|
1043
|
-
def 用户信息摘要(self, x):
|
1044
|
-
""" 输入series类型的一个条目,输出其摘要信息 """
|
1045
|
-
ls = [f'{k}={v}' for k, v in x.items() if (not isinstance(v, float) or not math.isnan(v))]
|
1046
|
-
v = x["用户ID"].strip()
|
1047
|
-
# if v == 'u_615026b5b5db9_0uLTQndbsC':
|
1048
|
-
# print(v)
|
1049
|
-
n = self.execute(f'SELECT COUNT(*) FROM 观看记录 WHERE 用户ID="{v}"').fetchone()[0]
|
1050
|
-
if n:
|
1051
|
-
ls.append(f'考勤{n}次')
|
1052
|
-
return ', '.join(ls)
|
1053
|
-
|
1054
|
-
def 查找用户(self, 昵称='', 手机号='', *, debug=False):
|
1055
|
-
"""
|
1056
|
-
:param 昵称: 昵称可以输入一个列表,会在目标昵称、真实姓名同时做检索
|
1057
|
-
:param 手机号: 手机也可以输入一个列表,因为有些人可能报名时手机号填错,可以增加一些匹配规则
|
1058
|
-
:return:
|
1059
|
-
"""
|
1060
|
-
|
1061
|
-
# 1 统一输入参数格式
|
1062
|
-
def tolist(x):
|
1063
|
-
if x and not isinstance(x, list):
|
1064
|
-
x = [x]
|
1065
|
-
x = [str(a) for a in x if a]
|
1066
|
-
return x
|
1067
|
-
|
1068
|
-
def check_telphone(v, refs):
|
1069
|
-
if not isinstance(v, str):
|
1070
|
-
try:
|
1071
|
-
v = int(v)
|
1072
|
-
except:
|
1073
|
-
pass
|
1074
|
-
v = str(v)
|
1075
|
-
for a in refs:
|
1076
|
-
if a in v:
|
1077
|
-
return True
|
1078
|
-
|
1079
|
-
昵称 = tolist(昵称)
|
1080
|
-
手机号 = tolist(手机号)
|
1081
|
-
手机号 = [f'{x}' for x in 手机号]
|
1082
|
-
|
1083
|
-
# 2 查找所有可能匹配的项目
|
1084
|
-
ls = []
|
1085
|
-
for x in self.exec_dict('SELECT * FROM 用户列表'):
|
1086
|
-
logo = False
|
1087
|
-
if 昵称:
|
1088
|
-
if x['昵称'] in 昵称 or x['真实姓名'] in 昵称:
|
1089
|
-
logo = True
|
1090
|
-
if 手机号:
|
1091
|
-
if check_telphone(x['账户绑定手机号'], 手机号) or check_telphone(x['最近采集手机号'], 手机号):
|
1092
|
-
logo = True
|
1093
|
-
if logo:
|
1094
|
-
ls.append(x)
|
1095
|
-
|
1096
|
-
if debug:
|
1097
|
-
print('\n'.join(map(self.用户信息摘要, ls)))
|
1098
|
-
return
|
1099
|
-
|
1100
|
-
return ls
|
1101
|
-
|
1102
|
-
def 匹配用户ID(self, wbpath, sheet='报名表'):
|
1103
|
-
p = XlPath(wbpath)
|
1104
|
-
wb = openpyxl.load_workbook(wbpath)
|
1105
|
-
ws = wb[sheet]
|
1106
|
-
|
1107
|
-
def try2int(x):
|
1108
|
-
try:
|
1109
|
-
return int(x)
|
1110
|
-
except:
|
1111
|
-
if isinstance(x, float) and math.isnan(x):
|
1112
|
-
return ''
|
1113
|
-
return x
|
1114
|
-
|
1115
|
-
def todict(row):
|
1116
|
-
""" 将表格第i行的数据提取为字典格式
|
1117
|
-
"""
|
1118
|
-
msg = {}
|
1119
|
-
for k, x in enumerate(['姓名', '微信昵称', '手机', '微信']):
|
1120
|
-
msg[x] = ws.cell2(row, x).value
|
1121
|
-
return msg
|
1122
|
-
|
1123
|
-
for row in tqdm(list(ws.iterrows('姓名')), desc='匹配进度'):
|
1124
|
-
x = todict(row)
|
1125
|
-
待查手机号 = [x['手机']]
|
1126
|
-
t = try2int(x['微信'])
|
1127
|
-
if t:
|
1128
|
-
待查手机号.append(t)
|
1129
|
-
ls = self.查找用户([x['姓名'], x['微信昵称']], 待查手机号)
|
1130
|
-
# 只有一个关联的直接匹配上,否则填空
|
1131
|
-
ws.cell2(row, '用户ID', ls[0]['用户ID'] if len(ls) == 1 else '')
|
1132
|
-
ws.cell2(row, '参考信息', '\n'.join(map(self.用户信息摘要, ls)))
|
1133
|
-
row += 1
|
1134
|
-
|
1135
|
-
# if len(ls) > 1 and ls[1]['用户ID'] == 'u_615026b5b5db9_0uLTQndbsC':
|
1136
|
-
# break
|
1137
|
-
|
1138
|
-
wb.save(str(p.with_name(p.stem + '+' + p.suffix)))
|
1139
|
-
|
1140
|
-
def get_user_观看打卡记录(self, user_id):
|
1141
|
-
""" 获得一个用户的所有观看记录情况 """
|
1142
|
-
|
1143
|
-
# 1 观看记录
|
1144
|
-
classes = [
|
1145
|
-
['2022五一线上觉观营-0429开营', '2022-04-29 19:00', '2022-04-29 20:58', 0],
|
1146
|
-
['4.30第一堂', '2022-04-30 05:20', '2022-04-30 06:27', 420],
|
1147
|
-
['4.30第二堂', '2022-04-30 08:00', '2022-04-30 10:40', 2400],
|
1148
|
-
# 张国莉师兄说以时间表为准,所以我适当放宽。以15:30结束为准,结束前3分钟退出不算早退。
|
1149
|
-
['4.30第三堂', '2022-04-30 13:30', '2022-04-30 15:33', 180],
|
1150
|
-
['4.30第四堂', '2022-04-30 19:30', '2022-04-30 21:03', 180],
|
1151
|
-
['5.1第一堂', '2022-05-01 05:20', '2022-05-01 06:17', 0],
|
1152
|
-
['5.1第二堂', '2022-05-01 08:00', '2022-05-01 10:13', 13 * 60],
|
1153
|
-
['5.1第三堂', '2022-05-01 13:30', '2022-05-01 15:11', 0],
|
1154
|
-
['5.1第四堂', '2022-05-01 19:30', '2022-05-01 21:00', 0],
|
1155
|
-
['5.2第一堂', '2022-05-02 05:20', '2022-05-02 06:03', 0],
|
1156
|
-
['5.2第二堂', '2022-05-02 08:00', '2022-05-02 10:07', 7 * 60],
|
1157
|
-
['5.2第三堂', '2022-05-02 13:30', '2022-05-02 15:28', 0],
|
1158
|
-
['5.2第四堂', '2022-05-02 19:30', '2022-05-02 21:02', 2 * 60],
|
1159
|
-
['5.3第一堂', '2022-05-03 05:20', '2022-05-03 06:11', 0],
|
1160
|
-
['5.3第二堂', '2022-05-03 08:00', '2022-05-03 10:12', 12 * 60],
|
1161
|
-
['5.3第三堂', '2022-05-03 13:30', '2022-05-03 15:43', 13 * 60],
|
1162
|
-
['5.3第四堂', '2022-05-03 19:30', '2022-05-03 20:57', 0],
|
1163
|
-
['5.4第一堂', '2022-05-04 05:20', '2022-05-04 06:22', 2 * 60],
|
1164
|
-
['5.4第二堂', '2022-05-04 08:00', '2022-05-04 09:39', 0],
|
1165
|
-
['5.4第三堂', '2022-05-04 13:30', '2022-05-04 15:43', 13 * 60],
|
1166
|
-
['5.4第四堂', '2022-05-04 19:30', '2022-05-04 21:02', 2 * 60],
|
1167
|
-
['5.5第一堂', '2022-05-05 05:20', '2022-05-05 06:14', 0],
|
1168
|
-
['5.5第二堂', '2022-05-05 08:00', '2022-05-05 10:05', 5 * 60],
|
1169
|
-
['5.5第三堂', '2022-05-05 13:30', '2022-05-05 15:21', 0],
|
1170
|
-
['5.5第四堂', '2022-05-05 19:30', '2022-05-05 21:02', 2 * 60],
|
1171
|
-
['5.6第一堂', '2022-05-06 05:20', '2022-05-06 06:23', 3 * 60],
|
1172
|
-
['5.6第二堂', '2022-05-06 08:00', '2022-05-06 10:15', 15 * 60],
|
1173
|
-
['5.6第三堂', '2022-05-06 13:30', '2022-05-06 15:26', 0],
|
1174
|
-
]
|
1175
|
-
|
1176
|
-
def 课次message(name):
|
1177
|
-
for k, item in enumerate(classes):
|
1178
|
-
if item[0] == name:
|
1179
|
-
da = datetime.datetime.fromisoformat(item[1])
|
1180
|
-
db = datetime.datetime.fromisoformat(item[2])
|
1181
|
-
return k, da, db, item[3]
|
1182
|
-
|
1183
|
-
items = self.exec_dict(f'SELECT * FROM 观看记录 WHERE 用户ID="{user_id}"')
|
1184
|
-
ls = ['缺勤'] * len(classes) + [''] * (36 - len(classes))
|
1185
|
-
for x in items:
|
1186
|
-
k, da, db, bias = 课次message(x['课次名'])
|
1187
|
-
|
1188
|
-
# d1: 提早进入直播间的,顺推至标准开课时间
|
1189
|
-
d1 = datetime.datetime.fromisoformat(x['首次进入时间'])
|
1190
|
-
if d1 < da:
|
1191
|
-
if d1 <= datetime.datetime(2022, 5, 4) and user_id != 'u_612d861be8fde_BfDyBtACTv':
|
1192
|
-
# 5月4日之前的,缺勤做了大放水~~ 提前登录的,可以换好多正课时长~~ 但是李娟的单独调整
|
1193
|
-
x['直播观看秒数'] += (da - d1).seconds
|
1194
|
-
d1 = da
|
1195
|
-
|
1196
|
-
# d2: 进入时间,加上直播间观察时间作为退出时间
|
1197
|
-
d2 = d1 + datetime.timedelta(seconds=x['直播观看秒数'])
|
1198
|
-
# 结束时间不超过db
|
1199
|
-
if d2 > db:
|
1200
|
-
d2 = db
|
1201
|
-
|
1202
|
-
desc = ''
|
1203
|
-
if max((d1 - da).seconds, 0) + max((db - d2).seconds, 0) >= (1800 + bias):
|
1204
|
-
desc = ',缺勤'
|
1205
|
-
else:
|
1206
|
-
if (d1 - da).seconds > 59:
|
1207
|
-
desc = ',迟到'
|
1208
|
-
|
1209
|
-
# 直播结束才进入的
|
1210
|
-
if d1 >= db:
|
1211
|
-
ls[k] = '缺勤'
|
1212
|
-
else:
|
1213
|
-
ls[k] = d1.strftime('%H:%M') + '/' + d2.strftime('%H:%M') + desc
|
1214
|
-
观看记录 = ls
|
1215
|
-
|
1216
|
-
if user_id == 'u_61ad24c13ec15_qBIqwSvejE': # 1组占萍
|
1217
|
-
观看记录[0] = '19:09/20:58,迟到'
|
1218
|
-
|
1219
|
-
# 2 打卡记录
|
1220
|
-
def brief_time(x, ref):
|
1221
|
-
""" x相对ref的简化时间显示 """
|
1222
|
-
t = int((x - ref).seconds / 60)
|
1223
|
-
tag = f'{t // 60:02}:{t % 60:02}'
|
1224
|
-
if t < 0:
|
1225
|
-
tag = '-' + tag
|
1226
|
-
return tag
|
1227
|
-
|
1228
|
-
打卡记录 = [''] * 9
|
1229
|
-
start_day = datetime.datetime(2022, 4, 29)
|
1230
|
-
for k, tag in enumerate(['立下学修目标', '第一天', '第二天', '第三天', '第四天', '第五天', '第六天', '第七天']):
|
1231
|
-
x = self.execute(f'SELECT 打卡时间 FROM 打卡记录 WHERE user_id="{user_id}" AND 所属主题="{tag}"').fetchall()
|
1232
|
-
x = [a[0] for a in x] # 只有一列数据
|
1233
|
-
if not x:
|
1234
|
-
打卡记录[k] = '未打卡'
|
1235
|
-
else:
|
1236
|
-
x = sorted(x) # 排序后分别处理
|
1237
|
-
d1 = start_day + datetime.timedelta(days=k)
|
1238
|
-
d2 = start_day + datetime.timedelta(days=k, hours=19)
|
1239
|
-
打卡记录[k] = ','.join([brief_time(datetime.datetime.fromisoformat(a), d1) for a in x])
|
1240
|
-
if datetime.datetime.fromisoformat(x[0]) > d2:
|
1241
|
-
打卡记录[k] += ',迟打'
|
1242
|
-
|
1243
|
-
# 3 观看和打卡数据合并
|
1244
|
-
用户记录 = [观看记录[0], 打卡记录[0]]
|
1245
|
-
for i in range(1, 9):
|
1246
|
-
b = i * 4 + 1
|
1247
|
-
用户记录 += 观看记录[b - 4:b] + [打卡记录[i]]
|
1248
|
-
|
1249
|
-
return 用户记录
|
1250
|
-
|
1251
|
-
def update_wb(self):
|
1252
|
-
""" 更新工作薄考勤内容 """
|
1253
|
-
ws = self.wb['考勤表']
|
1254
|
-
# 1 考勤数据
|
1255
|
-
for i in ws.iterrows('用户ID'):
|
1256
|
-
user_id = ws.cell2(i, '用户ID').value
|
1257
|
-
msg = self.get_user_观看打卡记录(user_id)
|
1258
|
-
for j, v in enumerate(msg, start=10):
|
1259
|
-
if v:
|
1260
|
-
c = ws.cell(i, j)
|
1261
|
-
v0 = c.value
|
1262
|
-
if v0: # 如果有特殊标记,需要修正处理
|
1263
|
-
m = re.search(grp_chinese_char(), v) # 判断原有v中是否有中文
|
1264
|
-
if m: # 如果v有中文,需要替换为v0的标记
|
1265
|
-
v = re.sub(grp_chinese_char() + '+', v0, v)
|
1266
|
-
elif v: # 如果v没有中文,直接在加上特殊标记后缀
|
1267
|
-
v += f',{v0}'
|
1268
|
-
else: # 如果v也没有标记,保留v0原始标记内容
|
1269
|
-
v = v0
|
1270
|
-
c.value = v
|
1271
|
-
if '上班' in v or '工作' in v:
|
1272
|
-
color = '鲜红'
|
1273
|
-
elif '请假' in v:
|
1274
|
-
color = '灰色'
|
1275
|
-
elif '迟到' in v or '迟打' in v:
|
1276
|
-
color = '鲜黄'
|
1277
|
-
elif '缺勤' in v or '未打卡' in v:
|
1278
|
-
color = '鲜红'
|
1279
|
-
else:
|
1280
|
-
color = '鲜绿色'
|
1281
|
-
c.fill_color(color)
|
1282
|
-
|
1283
|
-
# 2 剩余促学金
|
1284
|
-
for i in ws.iterrows('用户ID'):
|
1285
|
-
money = 1000
|
1286
|
-
累计缺勤天数 = 0
|
1287
|
-
for t in range(1, min(8, (datetime.datetime.today() - datetime.datetime(2022, 4, 28)).days)): # 第几天
|
1288
|
-
logo = False
|
1289
|
-
# 每天上课情况
|
1290
|
-
迟到_num, 缺勤_num, 请假_num = 0, 0, 0
|
1291
|
-
for jj in range(1, 5):
|
1292
|
-
j = t * 5 + 6 + jj
|
1293
|
-
v = ws.cell(i, j).value
|
1294
|
-
if not v:
|
1295
|
-
v = ''
|
1296
|
-
if '迟到' in v:
|
1297
|
-
迟到_num += 1
|
1298
|
-
elif '缺勤' in v or '上班' in v or '工作' in v:
|
1299
|
-
缺勤_num += 1
|
1300
|
-
elif '请假' in v:
|
1301
|
-
请假_num += 1
|
1302
|
-
if 迟到_num + 缺勤_num >= 3:
|
1303
|
-
money -= 240
|
1304
|
-
logo = True
|
1305
|
-
else:
|
1306
|
-
money -= 迟到_num * 25
|
1307
|
-
money -= 缺勤_num * 60
|
1308
|
-
# money -= 请假_num * 20
|
1309
|
-
|
1310
|
-
if 缺勤_num:
|
1311
|
-
累计缺勤天数 += 1
|
1312
|
-
if 累计缺勤天数 >= 3:
|
1313
|
-
money -= 1000
|
1314
|
-
else:
|
1315
|
-
累计缺勤天数 = 0
|
1316
|
-
|
1317
|
-
# 每天打卡情况
|
1318
|
-
v = ws.cell(i, t * 5 + 11).value
|
1319
|
-
if v and not logo:
|
1320
|
-
if '迟打' in v:
|
1321
|
-
money -= 30
|
1322
|
-
elif '未打卡' in v:
|
1323
|
-
money -= 60
|
1324
|
-
ws.cell2(i, ['促学金', '剩余']).value = max(0, money)
|
1325
|
-
|
1326
|
-
# 3 给整行加上分割线
|
1327
|
-
from openpyxl.styles import Border, Side, Alignment
|
1328
|
-
for i in ws.iterrows('用户ID'):
|
1329
|
-
v = ws.cell(i, 1).value
|
1330
|
-
if isinstance(v, str) and '组' in v:
|
1331
|
-
side = Side(border_style='medium', color='000000')
|
1332
|
-
border = Border(top=side)
|
1333
|
-
for j in range(1, 50):
|
1334
|
-
ws.cell(i, j).border = border
|
1335
|
-
if j > 9:
|
1336
|
-
ws.cell(i, j).alignment = Alignment(horizontal='left', vertical='center')
|
1337
|
-
|
1338
|
-
# 4 各组综合情况
|
1339
|
-
# 4.1 各组打卡情况
|
1340
|
-
打卡天数 = 6
|
1341
|
-
ls = []
|
1342
|
-
title = '' # 组名
|
1343
|
-
columns = ['分组', '姓名', '日期', '打卡状态']
|
1344
|
-
for i in ws.iterrows('用户ID'):
|
1345
|
-
v = ws.cell(i, 1).value
|
1346
|
-
if isinstance(v, str) and '组' in v:
|
1347
|
-
title = f'第{int(chinese2digits(v[1:-1])):02}组'
|
1348
|
-
for j in range(打卡天数):
|
1349
|
-
打卡状态 = ws.cell(i, 16 + j * 5).value
|
1350
|
-
ls.append([title, ws.cell2(i, '姓名').value, j, 打卡状态])
|
1351
|
-
df = pd.DataFrame.from_records(ls, columns=columns)
|
1352
|
-
|
1353
|
-
ls = []
|
1354
|
-
columns = ['分组', '总人数', '每天平均补打人数及比例', '每天平均未打人数及未打比例',
|
1355
|
-
'4月30日未打', '5月1日未打', '5月2日未打', '5月3日未打', '5月4日未打', '5月5日未打']
|
1356
|
-
for title, items in df.groupby('分组'):
|
1357
|
-
n = len(items)
|
1358
|
-
a = sum(items['打卡状态'].str.contains('迟打'))
|
1359
|
-
b = sum(items['打卡状态'].str.contains('未打卡'))
|
1360
|
-
record = [title, n // 打卡天数, f'{a / 打卡天数:.1f},{a / n:.0%}', f'{b / 打卡天数:.1f},{b / n:.0%}']
|
1361
|
-
for j in range(打卡天数):
|
1362
|
-
msg = []
|
1363
|
-
补打, 未打 = 0, 0
|
1364
|
-
# 迟打
|
1365
|
-
# xs = items[(items['日期'] == j) & (items['打卡状态'].str.contains('迟打'))]['姓名']
|
1366
|
-
# if len(xs):
|
1367
|
-
# 补打 += len(xs)
|
1368
|
-
# msg.append(f'补打{len(xs)}人')
|
1369
|
-
# 未打
|
1370
|
-
xs = items[(items['日期'] == j) & (items['打卡状态'].str.contains('未打卡'))]['姓名']
|
1371
|
-
if len(xs):
|
1372
|
-
未打 += len(xs)
|
1373
|
-
msg.append(f'{len(xs)}人:{",".join(xs)}')
|
1374
|
-
record.append(','.join(msg))
|
1375
|
-
ls.append(record)
|
1376
|
-
|
1377
|
-
df = pd.DataFrame.from_records(ls, columns=columns)
|
1378
|
-
# browser(df)
|
1379
|
-
|
1380
|
-
# 4.2 每天打卡情况
|
1381
|
-
self.wb.save(self.outwb)
|
1382
|
-
|
1383
|
-
|
1384
|
-
class 课次数据:
|
1385
|
-
""" 读取课次数据 """
|
1386
|
-
|
1387
|
-
def __init__(self):
|
1388
|
-
# 将各种不同来源的数据,按用户ID分组存储起来
|
1389
|
-
self.用户观看数据 = defaultdict(list) # 每个用户的观看数据情况
|
1390
|
-
self.start_day = None # 记录所有加载文件中,最早的时间戳,大概率就是这个课次的开课时间
|
1391
|
-
|
1392
|
-
def add_files(self, path, pattern):
|
1393
|
-
files = XlPath(path).select_file(pattern)
|
1394
|
-
for f in files:
|
1395
|
-
self.add_考勤数据(f)
|
1396
|
-
|
1397
|
-
def add_考勤数据(self, file):
|
1398
|
-
""" 推荐使用这个综合接口,代替专用接口,这个接口能实现一些综合性的操作
|
1399
|
-
保存的用户数据,存储三元数值(观看时长,进度)
|
1400
|
-
150,表示观看时间满30分钟
|
1401
|
-
0~100,表示有进度数据,0%~100%
|
1402
|
-
"""
|
1403
|
-
# 0 有普通考勤表,和从圈子下载的表格两种
|
1404
|
-
enc = get_encoding(file.read_bytes()) or 'gbk'
|
1405
|
-
df = pd.read_csv(file, encoding=enc, encoding_errors='ignore')
|
1406
|
-
|
1407
|
-
if '参与状态' in df.columns:
|
1408
|
-
self.add_圈子进度表(file)
|
1409
|
-
else:
|
1410
|
-
self.add_小鹅通考勤表(file)
|
1411
|
-
|
1412
|
-
dates = re.findall(r'(\d{4}-\d{2}-\d{2})_\d{2}-\d{2}-\d{2}', file.stem)
|
1413
|
-
if dates:
|
1414
|
-
file_day = datetime.datetime.strptime(dates[-1], "%Y-%m-%d").date()
|
1415
|
-
if self.start_day is None or self.start_day > file_day:
|
1416
|
-
self.start_day = file_day
|
1417
|
-
|
1418
|
-
def add_小鹅通考勤表(self, file):
|
1419
|
-
file = XlPath(file)
|
1420
|
-
|
1421
|
-
df = pd.read_csv(file, encoding='utf8')
|
1422
|
-
assert '直播观看时长(秒)' in df.columns, f'好像下载到空数据表 {file}'
|
1423
|
-
|
1424
|
-
for idx, x in df.iterrows():
|
1425
|
-
data = {'文件名': XlPath(file).stem}
|
1426
|
-
data['直播分钟'] = int(x['直播观看时长(秒)']) // 60
|
1427
|
-
data['回放分钟'] = int(x['回放观看时长(秒)']) // 60
|
1428
|
-
self.用户观看数据[x['用户ID']].append(data)
|
1429
|
-
|
1430
|
-
def add_圈子进度表(self, file):
|
1431
|
-
""" 圈子的考勤数据比较特别,需要独立的一个体系来存储
|
1432
|
-
大部分会有播放进度,
|
1433
|
-
"""
|
1434
|
-
file = XlPath(file)
|
1435
|
-
# 圈子数据目前是gbk编码,这样会有些问题。但可能哪天平台就改成utf8编码了,所以这里提前做好兼容。
|
1436
|
-
enc = get_encoding(file.read_bytes()) or 'gbk'
|
1437
|
-
df = pd.read_csv(file, encoding=enc, encoding_errors='ignore')
|
1438
|
-
|
1439
|
-
for idx, x in df.iterrows():
|
1440
|
-
data = {'文件名': file.stem}
|
1441
|
-
|
1442
|
-
if '播放进度' in x: # 一般会有详细的播放进度
|
1443
|
-
data['百分比进度'] = int(re.search(r'\d+', x['播放进度']).group())
|
1444
|
-
else: # 如果没有,也可以通过参与状态知道是否完成
|
1445
|
-
data['百分比进度'] = 100 if x['参与状态'] == '已完成' else 0
|
1446
|
-
# 虽然"已完成",但显示进度可能不是'100%',要补一个修正
|
1447
|
-
if x['参与状态'] == '已完成':
|
1448
|
-
data['百分比进度'] = 100
|
1449
|
-
|
1450
|
-
self.用户观看数据[x['用户ID']].append(data)
|
1451
|
-
|
1452
|
-
def 禅宗考勤结果(self, user_id):
|
1453
|
-
""" 禅宗考勤是比较简单的,不用考虑回放情况,只要在所有数据中,判断出有完成就行
|
1454
|
-
当然,细节考虑,也还是可以详细区分不同情况。
|
1455
|
-
|
1456
|
-
:return:
|
1457
|
-
str,一般是返回字符串,表示结论
|
1458
|
-
dict,但也可以返回一个字典,表示特殊的格式设置,这种是用于单元格颜色设置
|
1459
|
-
"""
|
1460
|
-
直播分钟, 回放分钟, 百分比进度 = 0, 0, 0
|
1461
|
-
|
1462
|
-
if user_id not in self.用户观看数据:
|
1463
|
-
return {'value': f'未开始', 'color': '白色'}
|
1464
|
-
|
1465
|
-
# 1 遍历所有文件数据,获得所有最大值
|
1466
|
-
data = self.用户观看数据[user_id] # 这个类的功能框架,已经按照user_id进行了数据分组统计
|
1467
|
-
for x in data:
|
1468
|
-
# 前两条是兼容以前的通用的直播课程数据
|
1469
|
-
直播分钟 = max(直播分钟, x.get('直播分钟', 0))
|
1470
|
-
回放分钟 = max(回放分钟, x.get('回放分钟', 0))
|
1471
|
-
# 这一条是兼容禅宗特有的百分比进度
|
1472
|
-
百分比进度 = max(百分比进度, x.get('百分比进度', 0))
|
1473
|
-
|
1474
|
-
# 2 归纳出本课次考勤结论
|
1475
|
-
yellow = RgbFormatter.from_name('鲜黄')
|
1476
|
-
if 百分比进度 == 100 or 回放分钟 >= 30:
|
1477
|
-
return {'value': '已完成', 'color': '鲜绿色'}
|
1478
|
-
elif 百分比进度:
|
1479
|
-
return {'value': f'进度{百分比进度}%', 'color': yellow.light((100 - 百分比进度) / 100)}
|
1480
|
-
elif 回放分钟:
|
1481
|
-
return {'value': f'观看{回放分钟}分钟', 'color': yellow.light((100 - 回放分钟) / 30)}
|
1482
|
-
else:
|
1483
|
-
return {'value': f'未开始', 'color': '白色'}
|
1484
|
-
|
1485
|
-
def 小鹅通考勤结果(self, user_id, 需求分钟=30):
|
1486
|
-
""" 一般是把一个课次的多天数据添加到一起用,用这个接口获得该课次的观看状态
|
1487
|
-
比如是当堂,还是第1天回放等
|
1488
|
-
|
1489
|
-
:param user_id: 要获得的用户信息
|
1490
|
-
:param 需求分钟: 判断完成的依据分钟数
|
1491
|
-
"""
|
1492
|
-
# 1 检查文件日期
|
1493
|
-
if user_id not in self.用户观看数据:
|
1494
|
-
return '未开始学习'
|
1495
|
-
|
1496
|
-
data = self.用户观看数据[user_id]
|
1497
|
-
|
1498
|
-
# 理论上这里文件名就应该是排好序的
|
1499
|
-
filenames = [x['文件名'] for x in data]
|
1500
|
-
|
1501
|
-
for i, filename in enumerate(filenames):
|
1502
|
-
# 后面一串是时分秒,并不提取,但是用来限定格式匹配,避免匹配错
|
1503
|
-
dates = re.findall(r'(\d{4}-\d{2}-\d{2})_\d{2}-\d{2}-\d{2}', filename)
|
1504
|
-
if not dates: # 找不到时间戳的不处理
|
1505
|
-
continue
|
1506
|
-
|
1507
|
-
file_date_obj = datetime.datetime.strptime(dates[0], "%Y-%m-%d").date()
|
1508
|
-
delta_day = (file_date_obj - self.start_day).days
|
1509
|
-
|
1510
|
-
# 2 按顺序提取时间
|
1511
|
-
if delta_day == 0 and data[i]['直播分钟'] >= 需求分钟:
|
1512
|
-
return '完成当堂学习'
|
1513
|
-
elif delta_day > 0 and data[i]['直播分钟'] + data[i]['回放分钟'] >= 需求分钟:
|
1514
|
-
return f'第{delta_day}天回放'
|
1515
|
-
|
1516
|
-
if data[-1]['直播分钟'] + data[-1]['回放分钟']:
|
1517
|
-
return f'不足{需求分钟}分钟'
|
1518
|
-
else:
|
1519
|
-
return '未开始学习'
|
1520
|
-
|
1521
|
-
def 小鹅通考勤结果2(self, user_id, 返款梯度, 要求在线分钟=30):
|
1522
|
-
""" 输入视频返款的梯度,这个函数相比`小鹅通考勤结果`,还会返回单元格颜色格式,对应的返款额 """
|
1523
|
-
# 1 判断课程回放是不是结束了
|
1524
|
-
text = self.小鹅通考勤结果(user_id, 要求在线分钟)
|
1525
|
-
if text == '未开始学习' or '不足' in text: # 判断该课次是否已经结束了
|
1526
|
-
delta_day = datetime.timedelta(days=len(返款梯度))
|
1527
|
-
if (delta_day + self.start_day) <= datetime.date.today():
|
1528
|
-
text = '未完成学习'
|
1529
|
-
|
1530
|
-
if text == '完成当堂学习':
|
1531
|
-
return text, '鲜绿色', 返款梯度[0]
|
1532
|
-
elif m := re.match(r'第(\d+)天回放', text):
|
1533
|
-
t = int(m.group(1))
|
1534
|
-
money = 返款梯度[t]
|
1535
|
-
if money:
|
1536
|
-
color = RgbFormatter.from_name('黄色')
|
1537
|
-
color = color.light((返款梯度[0] - money) / money) # 根据返款额度自动变浅
|
1538
|
-
else:
|
1539
|
-
color = '灰色'
|
1540
|
-
return text, color, money
|
1541
|
-
elif text == '未开始学习' or '不足' in text:
|
1542
|
-
return text, '白色', 0
|
1543
|
-
elif text == '未完成学习':
|
1544
|
-
return text, '红色', 0
|
1545
|
-
else:
|
1546
|
-
raise ValueError
|
1547
|
-
|
1548
|
-
|
1549
|
-
def get_driver(_driver_store=[None]): # trick
|
1550
|
-
""" 考勤这边固定一个driver来使用 """
|
1551
|
-
if _driver_store[0] is None:
|
1552
|
-
_driver_store[0] = XlChrome()
|
1553
|
-
if not _driver_store[0]: # 如果驱动没了,重新启动
|
1554
|
-
_driver_store[0] = XlChrome()
|
1555
|
-
return _driver_store[0]
|
1556
|
-
|
1557
|
-
|
1558
|
-
def 登录小鹅通(name, passwd):
|
1559
|
-
# 登录小鹅通
|
1560
|
-
driver = get_driver()
|
1561
|
-
driver.get('https://admin.xiaoe-tech.com/t/login#/acount')
|
1562
|
-
driver.locate('//*[@id="common_template_mounted_el_container"]'
|
1563
|
-
'/div/div[1]/div[3]/div/div[4]/div/div[1]/div[1]/div/div[2]/input').send_keys(name)
|
1564
|
-
driver.locate('//*[@id="common_template_mounted_el_container"]'
|
1565
|
-
'/div/div[1]/div[3]/div/div[4]/div/div[1]/div[2]/div/div/input').send_keys(passwd)
|
1566
|
-
driver.click('//*[@id="common_template_mounted_el_container"]/div/div[1]/div[3]/div/div[4]/div/div[2]')
|
1567
|
-
|
1568
|
-
# 然后自己手动操作验证码
|
1569
|
-
# 以及选择"店铺"
|
1570
|
-
|
1571
|
-
|
1572
|
-
def 下载课次考勤数据(课程链接, 检查文本=''):
|
1573
|
-
# 1 遍历课程下载表格
|
1574
|
-
driver = get_driver()
|
1575
|
-
driver.get('https://admin.xiaoe-tech.com/t/data_center/index') # 必须要找个过渡页,不然不会更新课程链接
|
1576
|
-
driver.get(课程链接)
|
1577
|
-
# 不能写'第{i}课',会有叫'第{i}堂'等其他情况
|
1578
|
-
if 检查文本: # 出现指定的文本才操作下一步
|
1579
|
-
driver.locate_text('//*[@id="app"]/div/div/div[1]/div[2]/div[1]/div[2]', 检查文本)
|
1580
|
-
else: # 默认等待3秒
|
1581
|
-
time.sleep(3)
|
1582
|
-
|
1583
|
-
driver.click('//*[@id="tab-studentTab"]/span') # 直播间用户
|
1584
|
-
driver.click('//*[@id="pane-studentTab"]/div/div[2]/div[2]/form/div[2]/button[2]/span/span') # 导出列表
|
1585
|
-
driver.click('//*[@id="data-export-container"]/div/div[2]/div/div[2]/div[2]/button[2]/span/span') # 导出
|
1586
|
-
|
1587
|
-
|
1588
|
-
def 登录微信支付():
|
1589
|
-
driver = get_driver()
|
1590
|
-
driver.get('https://pay.weixin.qq.com/index.php/core/home/login')
|
1591
|
-
|
1592
|
-
|
1593
|
-
def 聚合读取考勤数据(data_dir, name, judge_minute=30):
|
1594
|
-
"""
|
1595
|
-
1、在目录"data_folder"下,找前缀包含name的所有文件
|
1596
|
-
2、并将其按日期排序,整理考勤数据,判断当堂、第一天完成等
|
1597
|
-
3、课次完成的标记,是judge_minute要满足特定分钟数
|
1598
|
-
"""
|
1599
|
-
# 理论上获得的第一份就是当堂数据,第二份则是回放数据
|
1600
|
-
files = list(XlPath(data_dir).rglob_files(f'*{name}*'))
|
1601
|
-
data = 课次数据()
|
1602
|
-
for f in files:
|
1603
|
-
data.add_考勤数据(f)
|
1604
|
-
|
1605
|
-
for user_id in data.用户观看数据:
|
1606
|
-
print(data.小鹅通考勤结果2(user_id, [100, 90, 80, 70, 0, 0]))
|