# -*- coding:utf-8 -*- """ @Author : xuxingchen @Contact : xuxingchen@sinochem.com @Desc : 杂项 """ import base64 import hashlib import os import time import warnings from io import BytesIO import re import random import string import threading from datetime import datetime, timedelta import socket import psutil from typing import Optional import numpy as np import pandas as pd import pytz import requests from PIL import Image import paho.mqtt.client as mqtt from openpyxl.styles import PatternFill, Font, Border, Side from openpyxl.utils.dataframe import dataframe_to_rows from openpyxl.workbook import Workbook from pydantic import BaseModel from utils import logger def get_ip_address(interface_name: str) -> tuple[str, str]: interfaces = psutil.net_if_addrs() interface = interfaces.get(interface_name, None) ipv4_address, ipv6_address = "", "" if interface: for address in interface: if address.family == socket.AF_INET: ipv4_address = address.address elif address.family == socket.AF_INET6 and not address.address.startswith("fe80"): ipv6_address = address.address return ipv4_address, ipv6_address def extract_fixed_length_number(number_str: str, fixed_length: int = 2) -> str: """提取一串存在数值的字符串中的第一个数值,对其从右往左取值定值,不足补0""" pattern = re.compile(r'[0-9]+') number = pattern.search(number_str) if number is None: return "0" * fixed_length else: if len(number[0]) < fixed_length: return "0" * (fixed_length - len(number[0])) + number[0] else: return number[0][-fixed_length:] def snake2camel(key: str) -> str: """snake命名风格转成camel命名风格""" parts = key.split('_') return parts[0] + ''.join(word.capitalize() for word in parts[1:]) def snake2camel_list_dict(snake_list: list[dict]) -> list[dict]: """将list[dict]中所有的snake格式的key转换成camel格式的key命名风格""" camel_list = [] for snake_dict in snake_list: camel_dict = {} for key, value in snake_dict.items(): camel_dict[snake2camel(key)] = value camel_list.append(camel_dict) return camel_list def clear_log_file(log_path: str, day: int = 7): """每7天清除一次日志文件""" creation_time = os.path.getctime(log_path) days_since_creation = (time.time() - creation_time) / (60 * 60 * 24) if os.path.exists(log_path) and days_since_creation >= day: try: f0 = open(log_path, "r", encoding="utf8") f1 = open(f"{log_path}.old", "w", encoding="utf8") f1.write(f0.read()) f1.close() f0.close() os.remove(log_path) print(f"日志文件 {logger.LOGGER_PATH} 完成重置") except Exception as e: print(f"日志文件 {logger.LOGGER_PATH} 重置失败: {e}") def generate_captcha_text(characters: int = 6) -> str: """生成指定长度的随机文本""" letters_and_digits = string.ascii_letters + string.digits return ''.join(random.choice(letters_and_digits) for _ in range(characters)) def encrypt_number(phone_number: str, key: bytes = b'7A') -> str: """将电话号码加密为字符串""" phone_bytes = phone_number.encode('utf-8') # 将字符串转换为字节 encrypted_bytes = bytes( [byte ^ key[i % len(key)] for i, byte in enumerate(phone_bytes)] ) # 使用Base64编码加密后的字节 encrypted_number = base64.b64encode(encrypted_bytes).decode('utf-8') return encrypted_number def decrypt_number(encrypted_number: str, key: bytes = b'7A') -> str: """将字符串解密为电话号码""" # 使用Base64解码加密后的字符串 encrypted_bytes = base64.b64decode(encrypted_number) decrypted_bytes = bytes( [byte ^ key[i % len(key)] for i, byte in enumerate(encrypted_bytes)] ) decrypted_number = decrypted_bytes.decode('utf-8') return decrypted_number def now_tz_datetime(days: int = 0) -> str: future_time = datetime.now() + timedelta(days=days) return future_time.strftime("%Y-%m-%dT%H:%M:%S.") + f"{future_time.microsecond // 1000:03d}Z" def now_datetime_nanosecond(days: int = 0) -> str: future_time = datetime.now() + timedelta(days=days) return future_time.strftime("%Y-%m-%d %H:%M:%S.%f") def now_datetime_second(days: int = 0) -> str: future_time = datetime.now() + timedelta(days=days) return future_time.strftime("%Y-%m-%d %H:%M:%S") def millisecond_timestamp2tz(timestamp_13: str): timestamp = int(timestamp_13) / 1000 dt_utc = datetime.fromtimestamp(timestamp, tz=pytz.UTC) # 转换为所需的时区,这里以北京时间(China Standard Time)为例 china_tz = pytz.timezone('Asia/Shanghai') return dt_utc.astimezone(china_tz).strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt_utc.microsecond // 1000:03d}Z" def is_image_url_valid(url: str) -> bool: try: # 发送请求获取URL内容 response = requests.get(url) response.raise_for_status() # 如果状态码不是200,会抛出异常 # 将内容加载为图片 image = Image.open(BytesIO(response.content)) image.verify() # 验证图像文件是否可读 # 如果上面的代码没有抛出异常,说明图片存在且格式可读 return True except (requests.RequestException, IOError): # 如果有任何异常,说明图片不可用或格式不可读 return False def is_image_valid(path: str) -> bool: try: # 将内容加载为图片 image = Image.open(open(path, 'rb')) image.verify() # 验证图像文件是否可读 # 如果上面的代码没有抛出异常,说明图片存在且格式可读 return True except (requests.RequestException, IOError): # 如果有任何异常,说明图片不可用或格式不可读 return False def get_file_md5(file_path): content = open(file_path, 'rb') md5hash = hashlib.md5(content.read()) return md5hash.hexdigest() def sql_export_xls(query, db_connection, save_file_path, sheet_title, sheet_header: Optional[list] = None, header_background_color: str = "808080", header_font_color: str = "ffffff"): df = pd.read_sql_query(query, db_connection) wb = Workbook() ws = wb.active ws.title = sheet_title for i, r in enumerate(dataframe_to_rows(df, index=False, header=True)): if sheet_header is not None and i == 0: ws.append(sheet_header) continue ws.append(r) # 表头样式 header_fill = PatternFill(start_color=header_background_color, fill_type="solid") header_font = Font(color=header_font_color, bold=True) for cell in ws[1]: cell.fill = header_fill cell.font = header_font # 边框样式 表头无边框-数据无顶实线框 thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), bottom=Side(style='thin') ) for i, row in enumerate(ws.iter_rows()): if i == 0: continue for cell in row: cell.border = thin_border # 单元格宽度自适应调整 for column in ws.columns: max_length = 0 column = list(column) for cell in column: if cell.value is not None: cell_length = len(str(cell.value)) if re.search(r'[\u4e00-\u9fff]', str(cell.value)): cell_length += len(re.findall(r'[\u4e00-\u9fff]', str(cell.value))) if cell_length > max_length: max_length = cell_length adjusted_width = (max_length + 2) ws.column_dimensions[column[0].column_letter].width = adjusted_width wb.save(save_file_path) def valid_xls(file: BytesIO, required_columns: Optional[list]) -> tuple[bool, str, Optional[list]]: """如果校验通过返回list结构的数据,如果检验不通过返回None""" with warnings.catch_warnings(): warnings.simplefilter("ignore") try: df = pd.read_excel(file) df = df.replace(np.nan, None) if all(col in df.columns for col in required_columns): return True, "", df[required_columns].values.tolist() else: missing_cols = [col for col in required_columns if col not in df.columns] return False, f"缺少必要的列: {', '.join(missing_cols)}", None except Exception as e: return False, f"文件解析失败 {type(e).__name__}, {e}", None class BasicCallback(BaseModel): status: bool message: Optional[str] class InvalidException(Exception): def __init__(self, message: str): self.message = message class UserData: def __init__(self): self.table_handle = None self.topic: Optional[str] = None self.topics: list = [] self.table_handler = None self.message = None self.token = None self.status: dict = {} self.clients: dict = {} self.lock = threading.Lock() # 添加一个锁用于线程同步 def set_table_handle(self, value): with self.lock: self.table_handle = value def set_topic(self, value: str): with self.lock: self.topic = value def set_topics(self, value: list): with self.lock: self.topics = value def set_table_handler(self, value): with self.lock: self.table_handler = value def set_message(self, value): with self.lock: self.message = value def set_token(self, value): with self.lock: self.token = value def set_status(self, value: dict): with self.lock: self.status = value def set_status_add(self, key, value): with self.lock: self.status[key] = value def set_status_remove(self, key): with self.lock: if self.status and key in self.status.keys(): self.status.pop(key) def get_status(self, key): if self.status and key in self.status.keys(): return self.status[key] def set_clients(self, value: dict): with self.lock: self.clients = value def set_client_add(self, key, value): with self.lock: self.clients[key] = value def create_mqtt_client(broker_host, broker_port, userdata: UserData, on_message=None, on_publish=None, on_connect=None, on_disconnect=None, client_id: str = "", username: str = "", password: str = ""): if client_id != "": client = mqtt.Client(client_id=client_id) else: client = mqtt.Client() client.user_data_set(userdata) if on_connect: client.on_connect = on_connect if on_disconnect: client.on_disconnect = on_disconnect if on_message: client.on_message = on_message if on_publish: client.on_publish = on_publish client.username_pw_set(username, password) client.connect(broker_host, broker_port) return client def on_connect(client, userdata, flags, rc): logger.Logger.init(logger.new_dc(f"🔗 Mqtt connection! {{rc: {rc}}} 🔗", '[1;32m')) if userdata.topics: _topics = [(topic, 0) for topic in userdata.topics] client.subscribe(_topics) logger.Logger.debug(f"subscribe topics: {userdata.topics}") def on_disconnect(client, userdata, rc): logger.Logger.info(logger.new_dc(f"🔌 Break mqtt connection! {{rc: {rc}}} 🔌", "[1m")) def on_publish(client, userdata, rc): logger.Logger.debug(f"{userdata.topic} <- {userdata.message}")