mirror of
https://github.com/comeonzhj/Auto-Redbook-Skills.git
synced 2026-03-27 12:49:27 +08:00
Add files via upload
This commit is contained in:
202
scripts/publish_xhs.py
Normal file
202
scripts/publish_xhs.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
小红书笔记发布脚本
|
||||
将生成的图片卡片发布到小红书
|
||||
|
||||
使用方法:
|
||||
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png card_2.png
|
||||
|
||||
环境变量:
|
||||
在同目录下创建 .env 文件,配置 XHS_COOKIE:
|
||||
XHS_COOKIE=your_cookie_string_here
|
||||
|
||||
依赖安装:
|
||||
pip install xhs python-dotenv
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
from xhs import XhsClient
|
||||
except ImportError as e:
|
||||
print(f"缺少依赖: {e}")
|
||||
print("请运行: pip install xhs python-dotenv")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_cookie():
|
||||
"""从 .env 文件加载 Cookie"""
|
||||
# 尝试从当前目录加载 .env
|
||||
env_path = Path.cwd() / '.env'
|
||||
if env_path.exists():
|
||||
load_dotenv(env_path)
|
||||
|
||||
# 也尝试从脚本目录加载
|
||||
script_env = Path(__file__).parent.parent / '.env'
|
||||
if script_env.exists():
|
||||
load_dotenv(script_env)
|
||||
|
||||
cookie = os.getenv('XHS_COOKIE')
|
||||
if not cookie:
|
||||
print("❌ 错误: 未找到 XHS_COOKIE 环境变量")
|
||||
print("请在当前目录创建 .env 文件,添加以下内容:")
|
||||
print("XHS_COOKIE=your_cookie_string_here")
|
||||
sys.exit(1)
|
||||
|
||||
return cookie
|
||||
|
||||
|
||||
def create_client(cookie: str) -> XhsClient:
|
||||
"""创建小红书客户端"""
|
||||
try:
|
||||
# 使用本地签名
|
||||
from xhs.help import sign as local_sign
|
||||
|
||||
def sign_func(uri, data=None, a1="", web_session=""):
|
||||
return local_sign(uri, data, a1=a1)
|
||||
|
||||
client = XhsClient(cookie=cookie, sign=sign_func)
|
||||
return client
|
||||
except Exception as e:
|
||||
print(f"❌ 创建客户端失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def validate_images(image_paths: list) -> list:
|
||||
"""验证图片文件是否存在"""
|
||||
valid_images = []
|
||||
for path in image_paths:
|
||||
if os.path.exists(path):
|
||||
valid_images.append(os.path.abspath(path))
|
||||
else:
|
||||
print(f"⚠️ 警告: 图片不存在 - {path}")
|
||||
|
||||
if not valid_images:
|
||||
print("❌ 错误: 没有有效的图片文件")
|
||||
sys.exit(1)
|
||||
|
||||
return valid_images
|
||||
|
||||
|
||||
def publish_note(client: XhsClient, title: str, desc: str, images: list,
|
||||
is_private: bool = False, post_time: str = None):
|
||||
"""发布图文笔记"""
|
||||
try:
|
||||
print(f"\n🚀 准备发布笔记...")
|
||||
print(f" 📌 标题: {title}")
|
||||
print(f" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}")
|
||||
print(f" 🖼️ 图片数量: {len(images)}")
|
||||
|
||||
result = client.create_image_note(
|
||||
title=title,
|
||||
desc=desc,
|
||||
files=images,
|
||||
is_private=is_private,
|
||||
post_time=post_time
|
||||
)
|
||||
|
||||
print("\n✨ 笔记发布成功!")
|
||||
if isinstance(result, dict):
|
||||
note_id = result.get('note_id') or result.get('id')
|
||||
if note_id:
|
||||
print(f" 📎 笔记ID: {note_id}")
|
||||
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发布失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_user_info(client: XhsClient):
|
||||
"""获取当前登录用户信息"""
|
||||
try:
|
||||
info = client.get_self_info()
|
||||
print(f"\n👤 当前用户: {info.get('nickname', '未知')}")
|
||||
return info
|
||||
except Exception as e:
|
||||
print(f"⚠️ 无法获取用户信息: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='将图片发布为小红书笔记'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--title', '-t',
|
||||
required=True,
|
||||
help='笔记标题(不超过20字)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--desc', '-d',
|
||||
default='',
|
||||
help='笔记描述/正文内容'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--images', '-i',
|
||||
nargs='+',
|
||||
required=True,
|
||||
help='图片文件路径(可以多个)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--private',
|
||||
action='store_true',
|
||||
help='是否设为私密笔记'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--post-time',
|
||||
default=None,
|
||||
help='定时发布时间(格式:2024-01-01 12:00:00)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='仅验证,不实际发布'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 验证标题长度
|
||||
if len(args.title) > 20:
|
||||
print(f"⚠️ 警告: 标题超过20字,将被截断")
|
||||
args.title = args.title[:20]
|
||||
|
||||
# 加载 Cookie
|
||||
cookie = load_cookie()
|
||||
|
||||
# 验证图片
|
||||
valid_images = validate_images(args.images)
|
||||
|
||||
# 创建客户端
|
||||
client = create_client(cookie)
|
||||
|
||||
# 获取用户信息(验证 Cookie 有效性)
|
||||
get_user_info(client)
|
||||
|
||||
if args.dry_run:
|
||||
print("\n🔍 验证模式 - 不会实际发布")
|
||||
print(f" 📌 标题: {args.title}")
|
||||
print(f" 📝 描述: {args.desc}")
|
||||
print(f" 🖼️ 图片: {valid_images}")
|
||||
print("\n✅ 验证通过,可以发布")
|
||||
return
|
||||
|
||||
# 发布笔记
|
||||
publish_note(
|
||||
client=client,
|
||||
title=args.title,
|
||||
desc=args.desc,
|
||||
images=valid_images,
|
||||
is_private=args.private,
|
||||
post_time=args.post_time
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
268
scripts/render_xhs.js
Normal file
268
scripts/render_xhs.js
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 小红书卡片渲染脚本 - Node.js 版本
|
||||
* 将 Markdown 文件渲染为小红书风格的图片卡片
|
||||
*
|
||||
* 使用方法:
|
||||
* node render_xhs.js <markdown_file> [--output-dir <output_directory>]
|
||||
*
|
||||
* 依赖安装:
|
||||
* npm install marked js-yaml playwright
|
||||
* npx playwright install chromium
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { chromium } = require('playwright');
|
||||
const { marked } = require('marked');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
// 获取脚本所在目录
|
||||
const SCRIPT_DIR = path.dirname(__dirname);
|
||||
const ASSETS_DIR = path.join(SCRIPT_DIR, 'assets');
|
||||
|
||||
// 卡片尺寸配置 (3:4 比例)
|
||||
const CARD_WIDTH = 1080;
|
||||
const CARD_HEIGHT = 1440;
|
||||
|
||||
/**
|
||||
* 解析 Markdown 文件,提取 YAML 头部和正文内容
|
||||
*/
|
||||
function parseMarkdownFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// 解析 YAML 头部
|
||||
const yamlPattern = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
||||
const yamlMatch = content.match(yamlPattern);
|
||||
|
||||
let metadata = {};
|
||||
let body = content;
|
||||
|
||||
if (yamlMatch) {
|
||||
try {
|
||||
metadata = yaml.load(yamlMatch[1]) || {};
|
||||
} catch (e) {
|
||||
metadata = {};
|
||||
}
|
||||
body = content.slice(yamlMatch[0].length);
|
||||
}
|
||||
|
||||
return {
|
||||
metadata,
|
||||
body: body.trim()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照 --- 分隔符拆分正文为多张卡片内容
|
||||
*/
|
||||
function splitContentBySeparator(body) {
|
||||
const parts = body.split(/\n---+\n/);
|
||||
return parts.filter(part => part.trim()).map(part => part.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Markdown 转换为 HTML
|
||||
*/
|
||||
function convertMarkdownToHtml(mdContent) {
|
||||
// 处理 tags(以 # 开头的标签)
|
||||
const tagsPattern = /((?:#[\w\u4e00-\u9fa5]+\s*)+)$/m;
|
||||
const tagsMatch = mdContent.match(tagsPattern);
|
||||
let tagsHtml = "";
|
||||
|
||||
if (tagsMatch) {
|
||||
const tagsStr = tagsMatch[1];
|
||||
mdContent = mdContent.slice(0, tagsMatch.index).trim();
|
||||
const tags = tagsStr.match(/#([\w\u4e00-\u9fa5]+)/g);
|
||||
if (tags) {
|
||||
tagsHtml = '<div class="tags-container">';
|
||||
for (const tag of tags) {
|
||||
tagsHtml += `<span class="tag">${tag}</span>`;
|
||||
}
|
||||
tagsHtml += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 转换 Markdown 为 HTML
|
||||
const html = marked.parse(mdContent, {
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
return html + tagsHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 HTML 模板
|
||||
*/
|
||||
function loadTemplate(templateName) {
|
||||
const templatePath = path.join(ASSETS_DIR, templateName);
|
||||
return fs.readFileSync(templatePath, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成封面 HTML
|
||||
*/
|
||||
function generateCoverHtml(metadata) {
|
||||
let template = loadTemplate('cover.html');
|
||||
|
||||
let emoji = metadata.emoji || '📝';
|
||||
let title = metadata.title || '标题';
|
||||
let subtitle = metadata.subtitle || '';
|
||||
|
||||
// 限制标题和副标题长度
|
||||
if (title.length > 15) {
|
||||
title = title.slice(0, 15);
|
||||
}
|
||||
if (subtitle.length > 15) {
|
||||
subtitle = subtitle.slice(0, 15);
|
||||
}
|
||||
|
||||
template = template.replace('{{EMOJI}}', emoji);
|
||||
template = template.replace('{{TITLE}}', title);
|
||||
template = template.replace('{{SUBTITLE}}', subtitle);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成正文卡片 HTML
|
||||
*/
|
||||
function generateCardHtml(content, pageNumber = 1, totalPages = 1) {
|
||||
let template = loadTemplate('card.html');
|
||||
|
||||
const htmlContent = convertMarkdownToHtml(content);
|
||||
const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : '';
|
||||
|
||||
template = template.replace('{{CONTENT}}', htmlContent);
|
||||
template = template.replace('{{PAGE_NUMBER}}', pageText);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Playwright 将 HTML 渲染为图片
|
||||
*/
|
||||
async function renderHtmlToImage(htmlContent, outputPath, width = CARD_WIDTH, height = CARD_HEIGHT) {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({
|
||||
viewport: { width, height }
|
||||
});
|
||||
|
||||
// 设置 HTML 内容
|
||||
await page.setContent(htmlContent, {
|
||||
waitUntil: 'networkidle'
|
||||
});
|
||||
|
||||
// 等待字体加载
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 获取实际内容高度
|
||||
const contentHeight = await page.evaluate(() => {
|
||||
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
});
|
||||
|
||||
// 确保高度至少为 1440px(3:4 比例)
|
||||
const actualHeight = Math.max(height, contentHeight);
|
||||
|
||||
// 截图
|
||||
await page.screenshot({
|
||||
path: outputPath,
|
||||
clip: { x: 0, y: 0, width, height: actualHeight },
|
||||
type: 'png'
|
||||
});
|
||||
|
||||
console.log(` ✅ 已生成: ${outputPath}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主渲染函数:将 Markdown 文件渲染为多张卡片图片
|
||||
*/
|
||||
async function renderMarkdownToCards(mdFile, outputDir) {
|
||||
console.log(`\n🎨 开始渲染: ${mdFile}`);
|
||||
|
||||
// 确保输出目录存在
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 解析 Markdown 文件
|
||||
const data = parseMarkdownFile(mdFile);
|
||||
const { metadata, body } = data;
|
||||
|
||||
// 分割正文内容
|
||||
const cardContents = splitContentBySeparator(body);
|
||||
const totalCards = cardContents.length;
|
||||
|
||||
console.log(` 📄 检测到 ${totalCards} 张正文卡片`);
|
||||
|
||||
// 生成封面
|
||||
if (metadata.emoji || metadata.title) {
|
||||
console.log(' 📷 生成封面...');
|
||||
const coverHtml = generateCoverHtml(metadata);
|
||||
const coverPath = path.join(outputDir, 'cover.png');
|
||||
await renderHtmlToImage(coverHtml, coverPath);
|
||||
}
|
||||
|
||||
// 生成正文卡片
|
||||
for (let i = 0; i < cardContents.length; i++) {
|
||||
const pageNum = i + 1;
|
||||
console.log(` 📷 生成卡片 ${pageNum}/${totalCards}...`);
|
||||
const cardHtml = generateCardHtml(cardContents[i], pageNum, totalCards);
|
||||
const cardPath = path.join(outputDir, `card_${pageNum}.png`);
|
||||
await renderHtmlToImage(cardHtml, cardPath);
|
||||
}
|
||||
|
||||
console.log(`\n✨ 渲染完成!图片已保存到: ${outputDir}`);
|
||||
return totalCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析命令行参数
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log('用法: node render_xhs.js <markdown_file> [--output-dir <output_directory>]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let markdownFile = null;
|
||||
let outputDir = process.cwd();
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--output-dir' || args[i] === '-o') {
|
||||
outputDir = args[i + 1];
|
||||
i++;
|
||||
} else if (!args[i].startsWith('-')) {
|
||||
markdownFile = args[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!markdownFile) {
|
||||
console.error('❌ 错误: 请指定 Markdown 文件');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(markdownFile)) {
|
||||
console.error(`❌ 错误: 文件不存在 - ${markdownFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { markdownFile, outputDir };
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
const { markdownFile, outputDir } = parseArgs();
|
||||
await renderMarkdownToCards(markdownFile, outputDir);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('❌ 渲染失败:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
242
scripts/render_xhs.py
Normal file
242
scripts/render_xhs.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
小红书卡片渲染脚本 - Python 版本
|
||||
将 Markdown 文件渲染为小红书风格的图片卡片
|
||||
|
||||
使用方法:
|
||||
python render_xhs.py <markdown_file> [--output-dir <output_directory>]
|
||||
|
||||
依赖安装:
|
||||
pip install markdown pyyaml pillow playwright
|
||||
playwright install chromium
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import markdown
|
||||
import yaml
|
||||
from playwright.async_api import async_playwright
|
||||
except ImportError as e:
|
||||
print(f"缺少依赖: {e}")
|
||||
print("请运行: pip install markdown pyyaml playwright && playwright install chromium")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# 获取脚本所在目录
|
||||
SCRIPT_DIR = Path(__file__).parent.parent
|
||||
ASSETS_DIR = SCRIPT_DIR / "assets"
|
||||
|
||||
# 卡片尺寸配置 (3:4 比例)
|
||||
CARD_WIDTH = 1080
|
||||
CARD_HEIGHT = 1440
|
||||
|
||||
|
||||
def parse_markdown_file(file_path: str) -> dict:
|
||||
"""解析 Markdown 文件,提取 YAML 头部和正文内容"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 解析 YAML 头部
|
||||
yaml_pattern = r'^---\s*\n(.*?)\n---\s*\n'
|
||||
yaml_match = re.match(yaml_pattern, content, re.DOTALL)
|
||||
|
||||
metadata = {}
|
||||
body = content
|
||||
|
||||
if yaml_match:
|
||||
try:
|
||||
metadata = yaml.safe_load(yaml_match.group(1)) or {}
|
||||
except yaml.YAMLError:
|
||||
metadata = {}
|
||||
body = content[yaml_match.end():]
|
||||
|
||||
return {
|
||||
'metadata': metadata,
|
||||
'body': body.strip()
|
||||
}
|
||||
|
||||
|
||||
def split_content_by_separator(body: str) -> list:
|
||||
"""按照 --- 分隔符拆分正文为多张卡片内容"""
|
||||
# 使用 --- 作为分隔符,但要排除 YAML 头部的 ---
|
||||
parts = re.split(r'\n---+\n', body)
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
|
||||
|
||||
def convert_markdown_to_html(md_content: str) -> str:
|
||||
"""将 Markdown 转换为 HTML"""
|
||||
# 处理 tags(以 # 开头的标签)
|
||||
tags_pattern = r'((?:#[\w\u4e00-\u9fa5]+\s*)+)$'
|
||||
tags_match = re.search(tags_pattern, md_content, re.MULTILINE)
|
||||
tags_html = ""
|
||||
|
||||
if tags_match:
|
||||
tags_str = tags_match.group(1)
|
||||
md_content = md_content[:tags_match.start()].strip()
|
||||
tags = re.findall(r'#([\w\u4e00-\u9fa5]+)', tags_str)
|
||||
if tags:
|
||||
tags_html = '<div class="tags-container">'
|
||||
for tag in tags:
|
||||
tags_html += f'<span class="tag">#{tag}</span>'
|
||||
tags_html += '</div>'
|
||||
|
||||
# 转换 Markdown 为 HTML
|
||||
html = markdown.markdown(
|
||||
md_content,
|
||||
extensions=['extra', 'codehilite', 'tables', 'nl2br']
|
||||
)
|
||||
|
||||
return html + tags_html
|
||||
|
||||
|
||||
def load_template(template_name: str) -> str:
|
||||
"""加载 HTML 模板"""
|
||||
template_path = ASSETS_DIR / template_name
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def generate_cover_html(metadata: dict) -> str:
|
||||
"""生成封面 HTML"""
|
||||
template = load_template('cover.html')
|
||||
|
||||
emoji = metadata.get('emoji', '📝')
|
||||
title = metadata.get('title', '标题')
|
||||
subtitle = metadata.get('subtitle', '')
|
||||
|
||||
# 限制标题和副标题长度
|
||||
if len(title) > 15:
|
||||
title = title[:15]
|
||||
if len(subtitle) > 15:
|
||||
subtitle = subtitle[:15]
|
||||
|
||||
html = template.replace('{{EMOJI}}', emoji)
|
||||
html = html.replace('{{TITLE}}', title)
|
||||
html = html.replace('{{SUBTITLE}}', subtitle)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def generate_card_html(content: str, page_number: int = 1, total_pages: int = 1) -> str:
|
||||
"""生成正文卡片 HTML"""
|
||||
template = load_template('card.html')
|
||||
|
||||
html_content = convert_markdown_to_html(content)
|
||||
|
||||
page_text = f"{page_number}/{total_pages}" if total_pages > 1 else ""
|
||||
|
||||
html = template.replace('{{CONTENT}}', html_content)
|
||||
html = html.replace('{{PAGE_NUMBER}}', page_text)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
async def render_html_to_image(html_content: str, output_path: str, width: int = CARD_WIDTH, height: int = CARD_HEIGHT):
|
||||
"""使用 Playwright 将 HTML 渲染为图片"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
page = await browser.new_page(viewport={'width': width, 'height': height})
|
||||
|
||||
# 创建临时 HTML 文件
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
temp_html_path = f.name
|
||||
|
||||
try:
|
||||
await page.goto(f'file://{temp_html_path}')
|
||||
await page.wait_for_load_state('networkidle')
|
||||
|
||||
# 等待字体加载
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
# 获取实际内容高度
|
||||
content_height = await page.evaluate('''() => {
|
||||
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
}''')
|
||||
|
||||
# 确保高度至少为 1440px(3:4 比例)
|
||||
actual_height = max(height, content_height)
|
||||
|
||||
# 截图
|
||||
await page.screenshot(
|
||||
path=output_path,
|
||||
clip={'x': 0, 'y': 0, 'width': width, 'height': actual_height},
|
||||
type='png'
|
||||
)
|
||||
|
||||
print(f" ✅ 已生成: {output_path}")
|
||||
|
||||
finally:
|
||||
os.unlink(temp_html_path)
|
||||
await browser.close()
|
||||
|
||||
|
||||
async def render_markdown_to_cards(md_file: str, output_dir: str):
|
||||
"""主渲染函数:将 Markdown 文件渲染为多张卡片图片"""
|
||||
print(f"\n🎨 开始渲染: {md_file}")
|
||||
|
||||
# 确保输出目录存在
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 解析 Markdown 文件
|
||||
data = parse_markdown_file(md_file)
|
||||
metadata = data['metadata']
|
||||
body = data['body']
|
||||
|
||||
# 分割正文内容
|
||||
card_contents = split_content_by_separator(body)
|
||||
total_cards = len(card_contents)
|
||||
|
||||
print(f" 📄 检测到 {total_cards} 张正文卡片")
|
||||
|
||||
# 生成封面
|
||||
if metadata.get('emoji') or metadata.get('title'):
|
||||
print(" 📷 生成封面...")
|
||||
cover_html = generate_cover_html(metadata)
|
||||
cover_path = os.path.join(output_dir, 'cover.png')
|
||||
await render_html_to_image(cover_html, cover_path)
|
||||
|
||||
# 生成正文卡片
|
||||
for i, content in enumerate(card_contents, 1):
|
||||
print(f" 📷 生成卡片 {i}/{total_cards}...")
|
||||
card_html = generate_card_html(content, i, total_cards)
|
||||
card_path = os.path.join(output_dir, f'card_{i}.png')
|
||||
await render_html_to_image(card_html, card_path)
|
||||
|
||||
print(f"\n✨ 渲染完成!图片已保存到: {output_dir}")
|
||||
return total_cards
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='将 Markdown 文件渲染为小红书风格的图片卡片'
|
||||
)
|
||||
parser.add_argument(
|
||||
'markdown_file',
|
||||
help='Markdown 文件路径'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output-dir', '-o',
|
||||
default=os.getcwd(),
|
||||
help='输出目录(默认为当前工作目录)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.markdown_file):
|
||||
print(f"❌ 错误: 文件不存在 - {args.markdown_file}")
|
||||
sys.exit(1)
|
||||
|
||||
asyncio.run(render_markdown_to_cards(args.markdown_file, args.output_dir))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user