Files
nanocode/it_automation_enhanced/scripts/xiaohongshu_multi_image.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

719 lines
28 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
小红书多图发布器
支持文字分页、多图片轮播、不同背景风格
支持Markdown格式渲染(标题、加粗等)
"""
import os
import sys
import logging
import tempfile
import subprocess
import re
from PIL import Image, ImageDraw, ImageFont
from typing import List, Tuple, Optional, Dict, Any
logger = logging.getLogger(__name__)
class XiaohongshuMultiImagePublisher:
"""小红书多图发布器"""
def __init__(self):
"""初始化发布器"""
self.temp_dir = "/tmp/xiaohongshu_multi_images"
os.makedirs(self.temp_dir, exist_ok=True)
# 字体设置
self.font_path = "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
self.font_path_bold = "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc" # 粗体字体
self.title_font_size = 42 # 主标题(文章标题)
self.heading1_font_size = 38 # ## 标题
self.heading2_font_size = 34 # ### 标题
self.content_font_size = 32 # 正文
self.line_spacing = 1.5 # 1.5倍行距
# 图片尺寸(小红书推荐)
self.image_width = 1080
self.image_height = 1440
# 边距
self.margin = 80
self.top_margin = 120
self.bottom_margin = 100
def split_text_to_pages(self, text: str, max_chars_per_page: int = 800) -> List[str]:
"""
将文本分割成多页,保持段落完整性
Args:
text: 原始文本
max_chars_per_page: 每页最大字符数
Returns:
分页后的文本列表
"""
pages = []
# 按段落分割
paragraphs = text.split('\n\n')
current_page = ""
current_chars = 0
for para in paragraphs:
para = para.strip()
if not para:
continue
para_with_newline = para + '\n\n'
para_length = len(para_with_newline)
# 如果段落本身超过一页,需要进一步分割
if para_length > max_chars_per_page:
# 先保存当前页
if current_page:
pages.append(current_page.strip())
current_page = ""
current_chars = 0
# 分割长段落
lines = para.split('\n')
current_line = ""
for line in lines:
line = line.strip()
if not line:
continue
line_with_newline = line + '\n'
line_length = len(line_with_newline)
if current_chars + line_length > max_chars_per_page:
if current_line:
pages.append(current_line.strip())
current_line = line
current_chars = len(line_with_newline)
else:
pages.append(line)
current_chars = 0
else:
if current_line:
current_line += '\n' + line
current_chars += line_length
else:
current_line = line
current_chars = line_length
# 保存最后一部分
if current_line:
pages.append(current_line.strip())
current_page = ""
current_chars = 0
# 正常段落
elif current_chars + para_length > max_chars_per_page:
# 当前页已满,开始新页
pages.append(current_page.strip())
current_page = para_with_newline
current_chars = para_length
else:
# 添加到当前页
current_page += para_with_newline
current_chars += para_length
# 保存最后一页
if current_page.strip():
pages.append(current_page.strip())
logger.info(f"文本分割完成: {len(pages)} 页")
for i, page in enumerate(pages):
logger.debug(f"第 {i+1} 页: {len(page)} 字符")
return pages
def parse_markdown_line(self, line: str) -> Dict[str, Any]:
"""
解析单行Markdown,提取格式信息
Args:
line: 原始文本行
Returns:
包含格式信息的字典:
{
'text': 纯文本内容(不含Markdown标记),
'type': 'heading1'/'heading2'/'bold'/'normal',
'level': 标题级别(1, 2等,仅标题类型)
}
"""
line = line.strip()
if not line:
return {'text': '', 'type': 'normal'}
# 匹配 ## 标题 (H2)
h2_match = re.match(r'^##\s+(.+)$', line)
if h2_match:
return {
'text': h2_match.group(1).strip(),
'type': 'heading1',
'level': 2
}
# 匹配 ### 标题 (H3)
h3_match = re.match(r'^###\s+(.+)$', line)
if h3_match:
return {
'text': h3_match.group(1).strip(),
'type': 'heading2',
'level': 3
}
# 匹配 **加粗** (支持多段)
if '**' in line:
# 移除 ** 标记,标记为加粗
text = re.sub(r'\*\*(.+?)\*\*', r'\1', line)
return {
'text': text,
'type': 'bold'
}
# 普通文本
return {
'text': line,
'type': 'normal'
}
def get_font_for_style(self, style_type: str, base_style: str = "tech") -> Tuple[ImageFont.FreeTypeFont, Tuple[int, int, int]]:
"""
根据文本样式获取对应的字体和颜色
Args:
style_type: 文本类型 ('heading1', 'heading2', 'bold', 'normal')
base_style: 基础风格 ('tech', 'warm', 'modern')
Returns:
(字体, 颜色) 元组
"""
# 根据类型选择字体大小
if style_type == 'heading1': # ## 标题
font_size = self.heading1_font_size
elif style_type == 'heading2': # ### 标题
font_size = self.heading2_font_size
else: # bold, normal
font_size = self.content_font_size
# 加载字体
try:
font = ImageFont.truetype(self.font_path, font_size)
except:
font = ImageFont.load_default()
# 根据风格选择颜色
if base_style == "tech":
if style_type in ['heading1', 'heading2']:
color = (255, 220, 100) # 金黄色标题(科技风格强调)
elif style_type == 'bold':
color = (150, 200, 255) # 亮蓝色加粗
else:
color = (220, 220, 255) # 浅蓝白色正文
else: # warm, modern
if style_type in ['heading1', 'heading2']:
color = (180, 80, 40) # 深棕色标题
elif style_type == 'bold':
color = (40, 80, 150) # 深蓝色加粗
else:
color = (80, 80, 80) # 深灰色正文
return font, color
def create_background(self, style: str = "tech") -> Image.Image:
"""
创建背景图片
Args:
style: 背景风格
- tech: 科技风格(蓝色/黑色)
- warm: 温暖风格(米黄色)
- modern: 现代风格
Returns:
PIL Image对象
"""
if style == "tech":
# 科技风格:蓝色渐变
from PIL import ImageFilter
# 创建渐变背景
bg = Image.new('RGB', (self.image_width, self.image_height), color=(10, 20, 40))
# 添加科技感元素
draw = ImageDraw.Draw(bg)
# 添加网格线
for i in range(0, self.image_width, 50):
draw.line([(i, 0), (i, self.image_height)], fill=(30, 60, 120, 30), width=1)
for i in range(0, self.image_height, 50):
draw.line([(0, i), (self.image_width, i)], fill=(30, 60, 120, 30), width=1)
# 添加光点
import random
for _ in range(50):
x = random.randint(0, self.image_width)
y = random.randint(0, self.image_height)
radius = random.randint(1, 3)
draw.ellipse([x-radius, y-radius, x+radius, y+radius],
fill=(100, 150, 255, 150))
# 模糊效果
bg = bg.filter(ImageFilter.GaussianBlur(radius=1))
elif style == "warm":
# 温暖风格:米黄色背景
bg = Image.new('RGB', (self.image_width, self.image_height), color=(245, 245, 220))
# 添加纹理
draw = ImageDraw.Draw(bg)
# 添加纸张纹理效果
import random
for _ in range(2000):
x = random.randint(0, self.image_width)
y = random.randint(0, self.image_height)
brightness = random.randint(230, 245)
draw.point((x, y), fill=(brightness, brightness, 210))
else: # modern 或其他
# 现代风格:简洁背景
bg = Image.new('RGB', (self.image_width, self.image_height), color=(240, 240, 245))
# 添加简单装饰
draw = ImageDraw.Draw(bg)
# 顶部和底部的装饰线
draw.line([(0, 50), (self.image_width, 50)], fill=(200, 200, 220), width=2)
draw.line([(0, self.image_height-50), (self.image_width, self.image_height-50)],
fill=(200, 200, 220), width=2)
return bg
def create_text_image(self, title: str, content: str, page_num: int = 1,
total_pages: int = 1, style: str = "tech") -> str:
"""
创建文字图片
Args:
title: 标题
content: 内容文本
page_num: 当前页码
total_pages: 总页数
style: 背景风格
Returns:
图片文件路径
"""
try:
# 创建背景
bg = self.create_background(style)
draw = ImageDraw.Draw(bg)
# 加载字体
try:
title_font = ImageFont.truetype(self.font_path, self.title_font_size)
content_font = ImageFont.truetype(self.font_path, self.content_font_size)
except:
# 回退到默认字体
title_font = ImageFont.load_default()
content_font = ImageFont.load_default()
logger.warning("使用默认字体,中文字体可能无法正常显示")
# 计算可用区域
usable_width = self.image_width - 2 * self.margin
usable_height = self.image_height - self.top_margin - self.bottom_margin
# 绘制标题
title_color = (255, 255, 255) if style == "tech" else (50, 50, 50)
title_y = self.top_margin
# 标题可能需要换行
title_lines = []
current_line = ""
for char in title:
test_line = current_line + char
bbox = draw.textbbox((0, 0), test_line, font=title_font)
text_width = bbox[2] - bbox[0]
if text_width > usable_width:
title_lines.append(current_line)
current_line = char
else:
current_line = test_line
if current_line:
title_lines.append(current_line)
# 绘制标题行
for i, line in enumerate(title_lines):
bbox = draw.textbbox((0, 0), line, font=title_font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = self.margin + (usable_width - text_width) // 2
y = title_y + i * (text_height + 10)
# 文字阴影(科技风格)
if style == "tech":
draw.text((x+2, y+2), line, fill=(0, 0, 0, 150), font=title_font)
draw.text((x, y), line, fill=title_color, font=title_font)
# 更新Y坐标
title_height = len(title_lines) * (text_height + 10)
content_y = title_y + title_height + 40
# 绘制内容(支持Markdown格式)
current_y = content_y
# 按行分割内容
lines = content.split('\n')
for line in lines:
if not line.strip():
current_y += int(self.content_font_size * self.line_spacing * 0.5)
continue
# 解析Markdown格式
parsed = self.parse_markdown_line(line)
text = parsed['text']
line_type = parsed['type']
if not text:
continue
# 根据类型获取字体和颜色
font, text_color = self.get_font_for_style(line_type, style)
# 标题类型添加额外间距
if line_type in ['heading1', 'heading2']:
current_y += int(self.content_font_size * 0.5) # 标题前加间距
# 检查是否需要换行
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
if text_width > usable_width:
# 需要自动换行
words = []
current_word = ""
for char in text:
if char == ' ' or char == ' ' or char == ',' or char == '。':
if current_word:
words.append(current_word)
current_word = ""
words.append(char)
else:
current_word += char
if current_word:
words.append(current_word)
# 构建行
current_line = ""
for word in words:
test_line = current_line + word
bbox = draw.textbbox((0, 0), test_line, font=font)
test_width = bbox[2] - bbox[0]
if test_width > usable_width:
# 绘制当前行
bbox = draw.textbbox((0, 0), current_line, font=font)
text_height = bbox[3] - bbox[1]
if current_y + text_height > self.image_height - self.bottom_margin:
break # 超出页面范围
# 绘制文字(加粗类型添加下划线效果增强)
if line_type == 'bold' and style == "tech":
draw.text((self.margin, current_y + 2), current_line,
fill=(0, 0, 0, 100), font=font) # 轻微阴影
draw.text((self.margin, current_y), current_line,
fill=text_color, font=font)
current_y += int(text_height * self.line_spacing)
current_line = word
else:
current_line = test_line
# 绘制最后一行
if current_line and current_y <= self.image_height - self.bottom_margin:
bbox = draw.textbbox((0, 0), current_line, font=font)
text_height = bbox[3] - bbox[1]
if line_type == 'bold' and style == "tech":
draw.text((self.margin, current_y + 2), current_line,
fill=(0, 0, 0, 100), font=font)
draw.text((self.margin, current_y), current_line,
fill=text_color, font=font)
current_y += int(text_height * self.line_spacing)
else:
# 单行文本
bbox = draw.textbbox((0, 0), text, font=font)
text_height = bbox[3] - bbox[1]
if current_y + text_height > self.image_height - self.bottom_margin:
break # 超出页面范围
# 标题居中,其他左对齐
if line_type in ['heading1', 'heading2']:
x = self.margin + (usable_width - text_width) // 2
else:
x = self.margin
# 绘制文字(加粗类型添加轻微阴影增强效果)
if line_type == 'bold' and style == "tech":
draw.text((x + 1, current_y + 1), text,
fill=(0, 0, 0, 100), font=font)
draw.text((x, current_y), text, fill=text_color, font=font)
current_y += int(text_height * self.line_spacing)
# 标题类型添加额外间距
if line_type in ['heading1', 'heading2']:
current_y += int(self.content_font_size * 0.3) # 标题后加间距
# 添加页码
page_text = f"{page_num}/{total_pages}"
bbox = draw.textbbox((0, 0), page_text, font=content_font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
page_x = self.image_width - self.margin - text_width
page_y = self.image_height - self.bottom_margin + 20
draw.text((page_x, page_y), page_text, fill=(150, 150, 150), font=content_font)
# 保存图片
timestamp = os.times().elapsed
image_path = os.path.join(self.temp_dir, f"xhs_page_{page_num}_{int(timestamp)}.png")
bg.save(image_path, 'PNG', quality=95)
logger.info(f"创建图片第 {page_num}/{total_pages} 页: {image_path}")
return image_path
except Exception as e:
logger.error(f"创建图片失败: {e}")
return None
def publish_to_xiaohongshu(self, title: str, content: str, tags: List[str] = None,
style: str = "tech") -> bool:
"""
发布到小红书(多图版本)
Args:
title: 标题
content: 内容(将自动分页)
tags: 标签列表
style: 背景风格
Returns:
是否成功
"""
try:
# 1. 文本分页
logger.info(f"开始处理文章: {title}")
pages = self.split_text_to_pages(content)
if not pages:
logger.error("文本分页失败")
return False
# 2. 创建多张图片
image_paths = []
for i, page_content in enumerate(pages):
page_num = i + 1
image_path = self.create_text_image(
title=title if i == 0 else f"{title} (续{page_num})",
content=page_content,
page_num=page_num,
total_pages=len(pages),
style=style
)
if image_path and os.path.exists(image_path):
image_paths.append(image_path)
logger.info(f"图片创建成功: {image_path}")
else:
logger.warning(f"第 {page_num} 页图片创建失败")
if not image_paths:
logger.error("没有成功创建任何图片")
return False
# 3. 构建小红书内容(去掉原文链接)
xhs_content = f"【{title}】\n\n"
# 添加原文摘要(前200字符),过滤掉原文链接
content_preview = content[:200] + "..." if len(content) > 200 else content
# 过滤掉"原文链接:"相关内容
import re
# 移除"原文链接:"及其后面的URL
content_preview = re.sub(r'原文链接:.*?(?=\n|$)', '', content_preview)
# 移除URL链接
content_preview = re.sub(r'https?://\S+', '', content_preview)
# 清理多余的空行
content_preview = re.sub(r'\n\s*\n', '\n\n', content_preview).strip()
xhs_content += f"{content_preview}\n\n"
# 添加标签
if tags:
xhs_content += " ".join([f"#{tag}" for tag in tags])
# 4. 发布到小红书
logger.info(f"准备发布到小红书: {len(image_paths)} 张图片")
# 构建xhs命令(使用完整路径避免PATH问题)
xhs_cmd = os.environ.get('XHS_CLI_PATH', '/home/ubuntu/.local/bin/xhs')
cmd = [xhs_cmd, 'post', '--title', title, '--body', xhs_content]
# 添加多张图片参数
for img_path in image_paths:
cmd.extend(['--images', img_path])
# 添加标签(如果有)
if tags:
tags_str = ','.join(tags[:5])
cmd.extend(['--topic', tags_str])
logger.info(f"执行命令: {' '.join(cmd[:10])}...")
# 执行发布命令
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode == 0:
logger.info("✅ 小红书发布成功!")
# 尝试提取笔记ID
import re
output = result.stdout or result.stderr
note_id_match = re.search(r'id:\s*([a-f0-9]+)', output, re.IGNORECASE)
if note_id_match:
note_id = note_id_match.group(1)
logger.info(f"📝 笔记ID: {note_id}")
else:
logger.info(f"📄 输出: {output[:200]}...")
# 清理临时图片
for img_path in image_paths:
try:
os.remove(img_path)
logger.debug(f"清理临时图片: {img_path}")
except:
pass
return True
else:
logger.error(f"❌ 小红书发布失败: {result.stderr}")
return False
except Exception as e:
logger.error(f"发布过程中出现错误: {e}")
return False
def test_multi_image(self):
"""测试多图功能"""
test_title = "测试多图发布功能"
test_content = """这是一篇测试文章,用于验证小红书多图发布功能。
随着人工智能技术的快速发展,越来越多的企业开始将AI应用于实际业务中。从自然语言处理到计算机视觉,从机器学习到深度学习,AI正在改变我们的工作和生活方式。
在技术领域,最近有几个重要的发展趋势值得关注:
1. **大语言模型的普及**:像GPT-4、Claude这样的模型正在被广泛应用于内容创作、代码编写、客户服务等领域。
2. **边缘AI的兴起**:随着设备计算能力的提升,越来越多的AI应用可以在本地设备上运行,减少了对云端的依赖。
3. **多模态AI的发展**:能够同时处理文本、图像、音频等多种输入形式的AI系统正在成为新的研究热点。
4. **AI伦理和安全性**:随着AI应用的普及,如何确保AI系统的安全性、公平性和透明度成为了重要议题。
对于开发者来说,掌握AI技术已经成为了必备技能。无论是前端开发、后端开发还是数据分析,AI都能提供强大的辅助工具。
未来,我们可以期待更多创新的AI应用出现,它们将进一步提升生产效率,改善用户体验,推动社会进步。
当然,技术的发展也带来了一些挑战,比如数据隐私、算法偏见、就业影响等问题都需要我们认真思考和解决。
总的来说,AI技术正在以前所未有的速度发展,它既带来了机遇也带来了挑战。我们需要以开放的心态拥抱变化,同时也要保持警惕,确保技术的发展能够造福全人类。"""
test_tags = ["技术测试", "多图发布", "AI技术", "小红书", "自动化"]
print("测试小红书多图发布功能...")
print(f"标题: {test_title}")
print(f"内容长度: {len(test_content)} 字符")
print(f"标签: {', '.join(test_tags)}")
print("")
# 测试文本分页
print("1. 测试文本分页...")
pages = self.split_text_to_pages(test_content, max_chars_per_page=500)
print(f" 分页结果: {len(pages)} 页")
# 测试图片生成
print("\n2. 测试图片生成...")
for i, page in enumerate(pages[:2]): # 只测试前2页
print(f" 生成第 {i+1} 页图片...")
img_path = self.create_text_image(
title=test_title if i == 0 else f"{test_title} (续{i+1})",
content=page,
page_num=i+1,
total_pages=len(pages),
style="tech"
)
if img_path:
print(f" ✅ 图片生成成功: {img_path}")
# 显示图片信息
try:
with Image.open(img_path) as img:
print(f" 尺寸: {img.size}, 格式: {img.format}")
except:
pass
else:
print(f" ❌ 图片生成失败")
print("\n3. 测试发布功能(模拟)...")
print(" ⚠️ 实际发布需要xhs-cli和有效的Cookie")
print(" 可以调用 publish_to_xiaohongshu() 方法进行实际发布")
return True
def main():
"""主函数"""
import sys
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
publisher = XiaohongshuMultiImagePublisher()
if len(sys.argv) > 1 and sys.argv[1] == "test":
publisher.test_multi_image()
else:
print("使用方法:")
print(" python3 xiaohongshu_multi_image.py test # 测试功能")
print("")
print("编程接口:")
print(" publisher = XiaohongshuMultiImagePublisher()")
print(" success = publisher.publish_to_xiaohongshu(title, content, tags, style)")
if __name__ == "__main__":
main()