每天上班第一件事就是处理各种群文件?手动上传下载搞得头晕眼花?用Python脚本解放双手其实比你想象的简单。我去年接手公司行政工作后发现,光是每天往20个部门群同步周报就要花掉半小时,直到用Python写了自动化脚本,现在每周五下午茶时间就能自动搞定所有文件分发。
先说说最核心的access_token获取问题。很多新手在这里容易踩坑,以为拿到一次token就能永久使用。实际上钉钉的access_token有效期是2小时,必须每次发送文件前重新获取。我最初没注意这个细节,脚本半夜跑批处理时总是失败。后来改成动态获取就稳了:
python复制def get_access_token(self):
url = f"https://oapi.dingtalk.com/gettoken?appkey={self.client_id}&appsecret={self.client_secret}"
try:
response = requests.get(url, timeout=10) # 增加超时设置
if response.json().get('errcode') != 0:
raise Exception(response.json().get('errmsg'))
return response.json().get("access_token")
except Exception as e:
print(f"获取token失败: {str(e)}")
# 这里可以加入邮件报警机制
return None
文件上传环节有个隐藏技巧:钉钉对media_id的生成有格式要求。有次上传200MB的视频文件总是失败,后来发现需要分块处理。对于大文件建议先压缩再上传:
python复制def get_media_id(self, file_path):
try:
# 自动压缩超过50MB的文件
if os.path.getsize(file_path) > 50*1024*1024:
file_path = self._compress_file(file_path)
with open(file_path, "rb") as f:
files = {'media': (os.path.basename(file_path), f)}
response = requests.post(
f"https://oapi.dingtalk.com/media/upload?access_token={self.access_token}&type=file",
files=files,
timeout=30
)
return response.json().get('media_id')
except Exception as e:
print(f"上传失败: {str(e)}")
return None
群机器人不只是发通知那么简单,结合Python能玩出很多花样。我们团队现在用机器人自动播报日报、提醒会议、甚至推送GitHub代码变更。最实用的当属智能@功能,可以根据消息内容自动@相关责任人。
消息签名是第一个门槛。钉钉要求每个请求都要带加密签名,这个步骤容易出错。建议把签名方法封装成独立函数:
python复制def _generate_signature(secret):
timestamp = str(int(time.time() * 1000))
secret_enc = secret.encode('utf-8')
string_to_sign = f"{timestamp}\n{secret}"
hmac_code = hmac.new(secret_enc, string_to_sign.encode('utf-8'),
digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
return timestamp, sign
发送带@的消息时有个坑:手机号必须带国际区号。我们行政小姐姐第一次用的时候,直接输本地手机号总是@失败。后来发现要补上+86:
python复制def send_text_with_at(content, mobiles):
webhook = "https://oapi.dingtalk.com/robot/send"
timestamp, sign = self._generate_signature(self.secret)
params = {
"access_token": self.token,
"timestamp": timestamp,
"sign": sign
}
data = {
"msgtype": "text",
"text": {"content": f"{content}"},
"at": {
"atMobiles": [f"+86{mobile}" for mobile in mobiles],
"isAtAll": False
}
}
response = requests.post(webhook, params=params, json=data)
return response.json()
当需要管理上百个群的自动化流程时,就不能再用单脚本方案了。我们研发部现在的架构包含任务队列、失败重试、结果回调等完整机制。分享几个关键设计点:
首先是凭证管理。不建议把access_token硬编码在脚本里,推荐用Redis做缓存。这里给出我们的Token管理类:
python复制class TokenManager:
def __init__(self, redis_conn):
self.redis = redis_conn
self.key_prefix = "dingtalk:token:"
def get_token(self, app_key):
cache_key = self.key_prefix + app_key
token = self.redis.get(cache_key)
if token:
return token.decode()
new_token = self._fetch_new_token(app_key)
if new_token:
self.redis.setex(cache_key, 7000, new_token) # 比官方有效期短10分钟
return new_token
return None
def _fetch_new_token(self, app_key):
# 调用钉钉接口获取新token
...
其次是消息队列设计。我们用的是Celery+RabbitMQ组合,确保消息不丢失。重要消息会持久化到MySQL:
python复制@app.task(bind=True, max_retries=3)
def async_send_dingtalk(self, chat_id, msg_type, content):
try:
result = DingTalkClient().send_message(
chat_id=chat_id,
msg_type=msg_type,
content=content
)
MessageLog.objects.create(
chat_id=chat_id,
content=content,
status=result.get('errcode') == 0
)
return result
except Exception as e:
self.retry(exc=e, countdown=60)
自动化脚本最怕半夜崩溃没人知道。我们吃过这个亏,现在建立了完整的监控体系。分享几个关键配置:
日志记录要包含完整上下文。建议用structlog这样的结构化日志工具:
python复制import structlog
logger = structlog.get_logger()
def send_file(file_path):
try:
log = logger.bind(file=file_path, chat_id=self.chat_id)
media_id = self.get_media_id(file_path)
if not media_id:
log.error("upload_failed")
return False
result = self._post_message(media_id)
log.info("send_complete", result=result)
return True
except Exception as e:
log.exception("send_error", error=str(e))
self._alert_admin(f"文件发送失败: {str(e)}")
return False
监控报警我们用的Prometheus+Grafana组合,这几个指标最有用:
python复制from prometheus_client import Counter, Histogram
SEND_COUNTER = Counter(
'dingtalk_messages_total',
'Total messages sent',
['msg_type', 'status']
)
LATENCY_HISTOGRAM = Histogram(
'dingtalk_request_duration_seconds',
'API latency distribution',
['api_method']
)
@LATENCY_HISTOGRAM.time()
def get_media_id(file_path):
start = time.time()
try:
result = _upload_file(file_path)
SEND_COUNTER.labels(msg_type='file', status='success').inc()
return result
except:
SEND_COUNTER.labels(msg_type='file', status='fail').inc()
raise