feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。
**目前能用的(真环境实测,非推断)**:
profile/other : 200, 7075 字节 —— 博主主页指标(粉丝/获赞/作品数/昵称)
aweme/detail : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)
**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
挑中的恰好是「批量拉作品列表」这个最敏感的动作。
2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
代价,过几小时要重测)。
所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。
**排查中控住变量后得到的两条事实**(都写进注释了):
· `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
(我先前猜的 sec-ch-ua 不是关键。)
· 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
从 CDP 取齐 cookie + UA + hints,而不是各自写死。
「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。
测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
🎯 AI 评论生成器 — 基于本地 oMLX 模型
|
||||
|
||||
通过本地 oMLX API (localhost:8000) 生成小红书风格 AI 评论。
|
||||
作为静态语料库的增强替代,支持上下文感知(基于笔记标题/内容)。
|
||||
|
||||
用法:
|
||||
from matrix_modules.comment.ai_generator import AICommentGenerator
|
||||
|
||||
gen = AICommentGenerator()
|
||||
comment = gen.generate(topic="美食", style="种草")
|
||||
comment = gen.generate_with_context(note_title="周末去苏州旅游攻略")
|
||||
comment = gen.generate_from_note(page=page) # 从当前页面提取内容
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
import urllib.request
|
||||
import re
|
||||
from typing import Optional, List
|
||||
|
||||
# 默认 oMLX API 地址
|
||||
OLLAMA_BASE = "http://localhost:8000/v1"
|
||||
API_KEY = "5omlx"
|
||||
|
||||
# 系统提示词 — 小红书风格评论生成
|
||||
SYSTEM_PROMPT = """你是一个小红书重度用户,正在给笔记写评论。
|
||||
|
||||
你的评论风格要求:
|
||||
1. 亲切自然,像真人用户的真实感受(不是营销号)
|
||||
2. 长度10-25字,简洁但有信息量
|
||||
3. 可以带少量 emoji(但要自然,不要堆砌)
|
||||
4. 语气因人而异:种草类热情、经验类实用、旅行美景类温暖
|
||||
5. 避免重复、套路化的表达
|
||||
|
||||
你生成的每条评论必须:
|
||||
- 与笔记主题相关(基于给定的主题/标题/内容)
|
||||
- 语气自然真实
|
||||
- 不营销、不推销、不带链接
|
||||
- 不涉及政治、色情、暴力
|
||||
|
||||
只输出评论文本本身,不要加引号、前缀或其他格式。"""
|
||||
|
||||
|
||||
def _call_omlx(messages: list, max_tokens: int = 64,
|
||||
temperature: float = 0.9, timeout: int = 10) -> Optional[str]:
|
||||
"""
|
||||
调用 oMLX 本地 API 生成文本。
|
||||
|
||||
Args:
|
||||
messages: OpenAI 格式的消息列表
|
||||
max_tokens: 最大生成 token 数
|
||||
temperature: 生成温度 (0-1)
|
||||
timeout: 超时秒数
|
||||
|
||||
Returns:
|
||||
生成的文本,失败返回 None
|
||||
"""
|
||||
payload = json.dumps({
|
||||
"model": "Qwen3.5-4B-MLX-4bit", # 可用模型
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"stream": False,
|
||||
"max_completion_tokens": 100,
|
||||
}).encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{OLLAMA_BASE}/chat/completions",
|
||||
data=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
with opener.open(req, timeout=timeout) as resp:
|
||||
result = json.loads(resp.read().decode("utf-8"))
|
||||
text = result["choices"][0]["message"]["content"].strip()
|
||||
# 清理可能的引号
|
||||
text = text.strip("\"'「」『』")
|
||||
return text
|
||||
except Exception as e:
|
||||
print(f" ⚠️ oMLX 调用失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 主类
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
class AICommentGenerator:
|
||||
"""AI 评论生成器 — 基于 oMLX 本地模型"""
|
||||
|
||||
def __init__(self, base_url: str = OLLAMA_BASE, api_key: str = API_KEY,
|
||||
model: str = "Qwen3.5-4B-MLX-4bit"):
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self._used_comments: set = set() # 会话级去重
|
||||
|
||||
def set_url(self, url: str, key: str):
|
||||
"""设置 oMLX 地址和密钥"""
|
||||
self.base_url = url
|
||||
self.api_key = key
|
||||
|
||||
def reset_session(self):
|
||||
"""重置去重记录"""
|
||||
self._used_comments.clear()
|
||||
|
||||
def _is_duplicate(self, text: str) -> bool:
|
||||
"""检查是否与已用评论重复"""
|
||||
text_clean = text.strip().lower()
|
||||
if text_clean in self._used_comments:
|
||||
return True
|
||||
# 相似度检查:如果已有的评论包含当前文本或反之
|
||||
for used in self._used_comments:
|
||||
if text_clean in used or used in text_clean:
|
||||
return True
|
||||
return False
|
||||
|
||||
def generate(self, topic: str = "",
|
||||
style: str = "general",
|
||||
max_tokens: int = 64,
|
||||
temperature: float = 0.9,
|
||||
retries: int = 3) -> str:
|
||||
"""
|
||||
生成一条小红书风格评论。
|
||||
|
||||
Args:
|
||||
topic: 评论主题(空字符串则随机)
|
||||
style: 风格 (general, zhongcao/种草, tips/技巧, travel/旅行, food/美食)
|
||||
max_tokens: 生成长度
|
||||
temperature: 生成温度
|
||||
retries: 失败重试次数
|
||||
|
||||
Returns:
|
||||
评论文本(失败时 fallback 到随机静态语料)
|
||||
"""
|
||||
style_desc = {
|
||||
"general": "日常评论,语气自然亲切",
|
||||
"zhongcao": "种草风,热情推荐,简短有力",
|
||||
"tips": "实用技术型,分享经验感受",
|
||||
"travel": "旅行/风景类,温暖治愈",
|
||||
"food": "美食类,表达馋了/想尝试",
|
||||
}.get(style, "日常评论,语气自然亲切")
|
||||
|
||||
for attempt in range(retries):
|
||||
content = f"生成一条小红书评论。\n"
|
||||
if topic:
|
||||
content += f"主题: {topic}\n"
|
||||
content += f"风格: {style_desc}\n"
|
||||
content += "只输出评论文本,不要加任何前缀或格式。"
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": content},
|
||||
]
|
||||
|
||||
text = _call_omlx(messages, max_tokens=max_tokens,
|
||||
temperature=temperature)
|
||||
if text and not self._is_duplicate(text):
|
||||
self._used_comments.add(text.strip().lower())
|
||||
return text
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
# 所有重试失败,fallback 到静态语料
|
||||
return self._fallback_comment()
|
||||
|
||||
def generate_with_context(self, note_title: str = "",
|
||||
note_content: str = "",
|
||||
style: str = "general") -> str:
|
||||
"""
|
||||
基于笔记上下文生成相关评论。
|
||||
|
||||
Args:
|
||||
note_title: 笔记标题
|
||||
note_content: 笔记内容摘要
|
||||
style: 评论风格
|
||||
|
||||
Returns:
|
||||
评论文本
|
||||
"""
|
||||
context = "根据以下笔记内容生成一条评论:\n"
|
||||
if note_title:
|
||||
context += f"标题: {note_title}\n"
|
||||
if note_content:
|
||||
context += f"内容: {note_content[:200]}\n"
|
||||
context += "评论要基于笔记内容,像是真实用户的自然感受。"
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": context},
|
||||
]
|
||||
|
||||
text = _call_omlx(messages, max_tokens=80, temperature=0.85)
|
||||
if text and not self._is_duplicate(text):
|
||||
self._used_comments.add(text.strip().lower())
|
||||
return text
|
||||
|
||||
return self._fallback_comment(topic=note_title[:20]
|
||||
if note_title else "")
|
||||
|
||||
async def generate_from_note(self, page) -> str:
|
||||
"""
|
||||
从当前页面提取笔记内容并生成评论。
|
||||
|
||||
Args:
|
||||
page: Playwright Page 对象
|
||||
|
||||
Returns:
|
||||
评论文本
|
||||
"""
|
||||
try:
|
||||
note_info = await page.evaluate("""
|
||||
() => {
|
||||
const title = document.querySelector('.title, h1, [class*=note-title]');
|
||||
const content = document.querySelector('.content, .desc, [class*=content]');
|
||||
return {
|
||||
title: title ? title.textContent.trim().substring(0, 100) : '',
|
||||
content: content ? content.textContent.trim().substring(0, 200) : '',
|
||||
};
|
||||
}
|
||||
""")
|
||||
except Exception:
|
||||
note_info = {"title": "", "content": ""}
|
||||
|
||||
return self.generate_with_context(
|
||||
note_title=note_info.get("title", ""),
|
||||
note_content=note_info.get("content", ""),
|
||||
)
|
||||
|
||||
def _fallback_comment(self, topic: str = "") -> str:
|
||||
"""静态语料 fallback"""
|
||||
from matrix_modules.comment.xhs.corpus import get_comment
|
||||
return get_comment(length="medium")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 便捷函数(与 comment_corpus.py 接口一致)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
_global_generator: AICommentGenerator = None
|
||||
|
||||
|
||||
def _get_generator() -> AICommentGenerator:
|
||||
"""获取全局生成器实例"""
|
||||
global _global_generator
|
||||
if _global_generator is None:
|
||||
_global_generator = AICommentGenerator()
|
||||
return _global_generator
|
||||
|
||||
|
||||
def ai_comment(topic: str = "", style: str = "general") -> str:
|
||||
"""获取一条 AI 生成的小红书评论"""
|
||||
return _get_generator().generate(topic=topic, style=style)
|
||||
|
||||
|
||||
def reset_ai_session():
|
||||
"""重置 AI 评论去重记录"""
|
||||
_get_generator().reset_session()
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 测试入口
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🎯 AI 评论生成器测试\n")
|
||||
|
||||
gen = AICommentGenerator()
|
||||
|
||||
# 测试:不同主题
|
||||
for topic in ["美食探店心得", "周末旅行攻略", "护肤好物分享", "穿搭技巧总结"]:
|
||||
comment = gen.generate(topic=topic)
|
||||
print(f" 📝 [{topic}] → {comment}")
|
||||
|
||||
# 测试:上下文感知
|
||||
print()
|
||||
comment = gen.generate_with_context(
|
||||
note_title="在苏州发现一家宝藏咖啡馆",
|
||||
note_content="周末和朋友去了平江路一家新开的咖啡馆,环境超好,咖啡也很棒!",
|
||||
)
|
||||
print(f" 📝 [上下文] → {comment}")
|
||||
|
||||
# 测试:oMLX 不可用时 fallback
|
||||
print()
|
||||
gen.set_url("http://localhost:9999", "invalid_key")
|
||||
fallback = gen.generate(topic="测试")
|
||||
print(f" 📝 [fallback] → {fallback}")
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
小红书评论语料模块
|
||||
"""
|
||||
from .corpus import get_comment, get_comments, random_comment, short_comment, long_comment
|
||||
|
||||
__all__ = ["get_comment", "get_comments", "random_comment", "short_comment", "long_comment"]
|
||||
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
小红书评论语料库
|
||||
风格: 种草/经验分享/生活方式/美食/旅行/美妆/穿搭
|
||||
特点: 语气亲切、emoji 适度、实用性强、避免营销感
|
||||
|
||||
分类标签:
|
||||
- lifestyle: 生活方式/日常
|
||||
- food: 美食/探店
|
||||
- travel: 旅行/出游
|
||||
- beauty: 美妆/护肤
|
||||
- fashion: 穿搭/时尚
|
||||
- tips: 经验/技巧
|
||||
- emotion: 情感共鸣
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 会话级去重 — 同一运行中不重复发送相同评论
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
_used_comments: set = set()
|
||||
|
||||
|
||||
def reset_session():
|
||||
"""清空已用评论记录(新账号/新运行开始时调用)"""
|
||||
_used_comments.clear()
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 按分类组织的语料
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
CORPUS = {
|
||||
"lifestyle": [
|
||||
"这就是我理想的生活状态",
|
||||
"看完立刻去试了,真的有用",
|
||||
"收藏了,周末就安排",
|
||||
"这个思路太棒了,学到了",
|
||||
"已经开始期待了",
|
||||
"这就是我想要的感觉",
|
||||
"看完心情都变好了",
|
||||
"太真实了,完全说到心坎里",
|
||||
"这个分享太及时了",
|
||||
"已经转发给闺蜜了",
|
||||
"这就是生活该有的样子",
|
||||
"看完立刻动手试了一下",
|
||||
"这个角度从来没想过",
|
||||
"太治愈了,反复看了好几遍",
|
||||
"这就是我最近在找的东西",
|
||||
],
|
||||
"food": [
|
||||
"看着就很有食欲",
|
||||
"下次去一定要试试",
|
||||
"这个做法收藏了",
|
||||
"看起来好好吃",
|
||||
"已经流口水了",
|
||||
"这个搭配绝了",
|
||||
"周末就去做",
|
||||
"求具体做法",
|
||||
"这个店在哪里",
|
||||
"看起来就很正宗",
|
||||
"已经加入收藏夹",
|
||||
"这个摆盘太美了",
|
||||
"食材都很新鲜的样子",
|
||||
"这个口味应该很适合我",
|
||||
"已经截图保存了",
|
||||
],
|
||||
"travel": [
|
||||
"这个地方太美了",
|
||||
"已经加入旅行清单",
|
||||
"下次假期就去",
|
||||
"这个机位太绝了",
|
||||
"攻略做得很详细",
|
||||
"风景太治愈了",
|
||||
"已经分享给朋友了",
|
||||
"这个路线规划得很好",
|
||||
"看完立刻想去",
|
||||
"这个地方人不多吧",
|
||||
"住宿看起来不错",
|
||||
"这个角度拍出来太好看了",
|
||||
"已经收藏了,下次参考",
|
||||
"这个景点值得去吗",
|
||||
"交通方便吗",
|
||||
],
|
||||
"beauty": [
|
||||
"这个效果太明显了",
|
||||
"已经种草了",
|
||||
"求链接",
|
||||
"这个颜色很适合我",
|
||||
"肤质看起来变好了",
|
||||
"这个步骤学到了",
|
||||
"已经加入购物车",
|
||||
"这个手法很关键",
|
||||
"效果能维持多久",
|
||||
"敏感肌可以用吗",
|
||||
"这个搭配思路很好",
|
||||
"已经截图保存教程",
|
||||
"这个产品的确不错",
|
||||
"步骤很详细,收藏了",
|
||||
"这个技巧太实用了",
|
||||
],
|
||||
"fashion": [
|
||||
"这套搭配太绝了",
|
||||
"颜色搭配得很好",
|
||||
"这个风格很适合你",
|
||||
"已经截图保存",
|
||||
"求衣服链接",
|
||||
"这个配饰点睛了",
|
||||
"整体比例很好",
|
||||
"这个单品很百搭",
|
||||
"已经加入灵感库",
|
||||
"这个配色可以学",
|
||||
"风格很独特",
|
||||
"这套日常也能穿",
|
||||
"细节处理得很好",
|
||||
"这个搭配思路学到了",
|
||||
"已经转发给姐妹了",
|
||||
],
|
||||
"tips": [
|
||||
"这个技巧太实用了",
|
||||
"已经收藏,下次用",
|
||||
"步骤写得很清楚",
|
||||
"这个坑我也踩过",
|
||||
"感谢分享,省了很多时间",
|
||||
"这个方法效率很高",
|
||||
"已经试过了,确实好用",
|
||||
"这个思路可以举一反三",
|
||||
"细节讲得很到位",
|
||||
"这个工具我也在用",
|
||||
"已经加入知识库",
|
||||
"这个经验太宝贵了",
|
||||
"看完立刻去实践了",
|
||||
"这个总结很到位",
|
||||
"已经分享给同事了",
|
||||
],
|
||||
"emotion": [
|
||||
"太真实了",
|
||||
"完全共鸣",
|
||||
"这就是我想说的",
|
||||
"看完很有感触",
|
||||
"太懂这种感觉了",
|
||||
"说得太对了",
|
||||
"这就是生活",
|
||||
"看完沉默了",
|
||||
"太戳心了",
|
||||
"这个视角很独特",
|
||||
"完全同意",
|
||||
"这就是成长吧",
|
||||
"看完想了很多",
|
||||
"这个分享很有力量",
|
||||
"感谢分享",
|
||||
],
|
||||
}
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 通用短评(不分类,随机使用)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
GENERAL_SHORT = [
|
||||
"学到了",
|
||||
"收藏了",
|
||||
"确实如此",
|
||||
"很有道理",
|
||||
"太棒了",
|
||||
"感谢分享",
|
||||
"已三连",
|
||||
"好文",
|
||||
"mark",
|
||||
"码住",
|
||||
"实用",
|
||||
"干货",
|
||||
"受教了",
|
||||
"涨知识了",
|
||||
"这个好",
|
||||
"不错",
|
||||
"可以",
|
||||
"666",
|
||||
"绝了",
|
||||
"牛",
|
||||
]
|
||||
|
||||
# ── 三级接力语料(评论→回复→再回复的自然互动链)──
|
||||
# 每个分类含 first/reply/second 三层
|
||||
CHAIN_CORPUS = {
|
||||
"food": [
|
||||
{"first": "看着就很有食欲,周末去试试", "reply": "同意!上次去吃过一次确实不错", "second": "我也去过,他家那个招牌菜绝了"},
|
||||
{"first": "这个做法收藏了,回家就做", "reply": "做了好几次了,家人都说好吃", "second": "是的,跟着做了一次很成功"},
|
||||
],
|
||||
"travel": [
|
||||
{"first": "这个地方太美了,已加入旅行清单", "reply": "去年去过,风景确实好,值得二刷", "second": "求攻略!准备下个月去"},
|
||||
{"first": "这个机位太绝了,拍得真好", "reply": "早上五点去占的位置,值得", "second": "太卷了哈哈哈,不过效果确实好"},
|
||||
],
|
||||
"tech": [
|
||||
{"first": "分析得很透彻,学到了", "reply": "对,第三点我之前完全没想到", "second": "是的,按这个方法试了效果很好"},
|
||||
],
|
||||
"lifestyle": [
|
||||
{"first": "这就是我理想的生活状态", "reply": "同款生活!每天都很充实", "second": "羡慕了,我也要这样过"},
|
||||
],
|
||||
"emotion": [
|
||||
{"first": "太真实了,完全说到心坎里", "reply": "是啊,经历过的人才懂", "second": "抱抱,都会好起来的"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_chain(group: str = "food", position: str = "first") -> str:
|
||||
"""获取三级接力语料。group=分类, position=first/reply/second"""
|
||||
import random
|
||||
pool = CHAIN_CORPUS.get(group, CHAIN_CORPUS["food"])
|
||||
item = random.choice(pool)
|
||||
return item.get(position, item["first"])
|
||||
|
||||
|
||||
def get_chain_group(group: str = "food") -> dict:
|
||||
"""获取一组完整的三级接力语料"""
|
||||
import random
|
||||
pool = CHAIN_CORPUS.get(group, CHAIN_CORPUS["food"])
|
||||
return random.choice(pool)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 评论生成器
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def get_comment(category: str = None, length: str = "medium") -> str:
|
||||
"""
|
||||
获取一条小红书风格评论(自动去重:同一运行中不会重复返回相同评论)
|
||||
|
||||
Args:
|
||||
category: 分类标签 (lifestyle/food/travel/beauty/fashion/tips/emotion)
|
||||
None 时随机选择分类
|
||||
length: 长度控制
|
||||
- short: 2~8 字短评
|
||||
- medium: 10~25 字常规评论 (默认)
|
||||
- long: 25~50 字长评论
|
||||
|
||||
Returns:
|
||||
评论文本
|
||||
"""
|
||||
global _used_comments
|
||||
|
||||
if length == "short":
|
||||
available = [c for c in GENERAL_SHORT if c not in _used_comments]
|
||||
if not available:
|
||||
available = GENERAL_SHORT # 用完了就复用
|
||||
comment = random.choice(available)
|
||||
_used_comments.add(comment)
|
||||
return comment
|
||||
|
||||
if category and category in CORPUS:
|
||||
pool = [c for c in CORPUS[category] if c not in _used_comments]
|
||||
if not pool:
|
||||
pool = CORPUS[category] # 用完了就复用
|
||||
else:
|
||||
all_items = []
|
||||
for cat_items in CORPUS.values():
|
||||
all_items.extend(cat_items)
|
||||
pool = [c for c in all_items if c not in _used_comments]
|
||||
if not pool:
|
||||
pool = all_items # 用完了就复用
|
||||
|
||||
comment = random.choice(pool)
|
||||
_used_comments.add(comment)
|
||||
|
||||
# 长度控制
|
||||
if length == "long" and len(comment) < 20:
|
||||
# 追加一条短评组合
|
||||
comment += "。" + random.choice(GENERAL_SHORT)
|
||||
elif length == "medium" and len(comment) > 30:
|
||||
# 截断到合适长度
|
||||
comment = comment[:28] + "..."
|
||||
|
||||
return comment
|
||||
|
||||
|
||||
def get_comments(count: int = 3, category: str = None, length: str = "medium") -> list:
|
||||
"""获取多条不重复评论"""
|
||||
results = []
|
||||
pool = []
|
||||
|
||||
if category and category in CORPUS:
|
||||
pool = CORPUS[category].copy()
|
||||
else:
|
||||
for cat_items in CORPUS.values():
|
||||
pool.extend(cat_items)
|
||||
|
||||
# 去重并随机选择
|
||||
unique_pool = list(set(pool))
|
||||
if count > len(unique_pool):
|
||||
count = len(unique_pool)
|
||||
|
||||
selected = random.sample(unique_pool, count)
|
||||
|
||||
for comment in selected:
|
||||
if length == "short":
|
||||
comment = random.choice(GENERAL_SHORT)
|
||||
elif length == "long" and len(comment) < 20:
|
||||
comment += "。" + random.choice(GENERAL_SHORT)
|
||||
results.append(comment)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 快捷函数
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def random_comment() -> str:
|
||||
"""随机获取一条中等长度评论(养号默认使用)"""
|
||||
return get_comment(length="medium")
|
||||
|
||||
|
||||
def short_comment() -> str:
|
||||
"""获取短评"""
|
||||
return get_comment(length="short")
|
||||
|
||||
|
||||
def long_comment() -> str:
|
||||
"""获取长评"""
|
||||
return get_comment(length="long")
|
||||
Reference in New Issue
Block a user