Files
nanocode/skills/baoyu-post-to-wechat/baoyu_optimized.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

793 lines
30 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
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
"""
baoyu-post-to-wechat 优化版
阶段1优化:图片优化 + Frontmatter支持
"""
import os
import json
import re
import requests
import yaml
import base64
import tempfile
import mimetypes
from pathlib import Path
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime
# 尝试导入Pillow(图片处理)
try:
from PIL import Image, ImageOps
HAS_PILLOW = True
except ImportError:
HAS_PILLOW = False
print("⚠️ Pillow未安装,图片优化功能受限")
# 配置文件路径
EXTEND_PATH = "/home/ubuntu/.nanobot/workspace/skills/baoyu-post-to-wechat/EXTEND.md"
WEWRITE_CONFIG = "/home/ubuntu/wewrite/config.yaml"
class ImageOptimizer:
"""图片优化器"""
@staticmethod
def optimize_image(image_path: str, max_size: int = 1024*1024) -> str:
"""
优化图片以适应微信要求
返回优化后的图片路径
"""
if not HAS_PILLOW:
print(f"⚠️ Pillow未安装,跳过图片优化: {os.path.basename(image_path)}")
return image_path
try:
with Image.open(image_path) as img:
original_format = img.format
original_size = os.path.getsize(image_path)
print(f"🖼️ 优化图片: {os.path.basename(image_path)} "
f"({original_format}, {original_size/1024:.1f}KB)")
# 转换模式
if img.mode not in ['RGB', 'L']:
img = img.convert('RGB')
# 检查是否需要调整尺寸(微信建议宽度≤2560)
max_width = 2560
if img.width > max_width:
ratio = max_width / img.width
new_height = int(img.height * ratio)
img = img.resize((max_width, new_height), Image.Resampling.LANCZOS)
print(f" ↳ 调整尺寸: {img.width}x{img.height}")
# 检查文件大小
if original_size > max_size:
# 创建临时文件
temp_file = tempfile.NamedTemporaryFile(
suffix='.jpg', delete=False, prefix='optimized_'
)
temp_path = temp_file.name
temp_file.close()
# 渐进式压缩
quality = 85
while quality >= 50:
img.save(temp_path, 'JPEG', quality=quality, optimize=True)
optimized_size = os.path.getsize(temp_path)
if optimized_size <= max_size or quality <= 50:
print(f" ↳ 压缩质量: {quality}% → {optimized_size/1024:.1f}KB")
return temp_path
quality -= 10
return temp_path
else:
# 如果图片已经符合要求,直接返回原路径
return image_path
except Exception as e:
print(f"❌ 图片优化失败 {os.path.basename(image_path)}: {e}")
return image_path
@staticmethod
def get_image_info(image_path: str) -> Dict:
"""获取图片信息"""
info = {
'path': image_path,
'filename': os.path.basename(image_path),
'exists': os.path.exists(image_path),
'size': os.path.getsize(image_path) if os.path.exists(image_path) else 0,
'optimized': False
}
if HAS_PILLOW and info['exists']:
try:
with Image.open(image_path) as img:
info.update({
'format': img.format,
'mode': img.mode,
'width': img.width,
'height': img.height,
'size_kb': info['size'] / 1024
})
except:
pass
return info
@staticmethod
def format_needs_conversion(image_path: str) -> bool:
"""检查图片格式是否需要转换"""
if not HAS_PILLOW:
return False
unsupported_formats = {'WEBP', 'GIF', 'BMP', 'TIFF', 'TIF', 'SVG', 'ICO'}
try:
with Image.open(image_path) as img:
return img.format in unsupported_formats
except:
return False
class MarkdownParser:
"""Markdown解析器(支持Frontmatter)"""
@staticmethod
def parse_frontmatter(content: str) -> Tuple[Dict, str]:
"""
解析Markdown中的YAML frontmatter
返回: (frontmatter_dict, markdown_body)
"""
# 匹配 frontmatter 模式: ---\n内容\n---\n
pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)$'
match = re.match(pattern, content, re.DOTALL | re.MULTILINE)
if match:
frontmatter_str = match.group(1)
body = match.group(2)
try:
frontmatter = yaml.safe_load(frontmatter_str) or {}
except yaml.YAMLError as e:
print(f"⚠️ Frontmatter解析错误: {e}")
frontmatter = {}
return frontmatter, body
else:
return {}, content
@staticmethod
def extract_title_from_body(body: str) -> str:
"""从正文中提取标题(第一个一级标题)"""
# 匹配 # 标题
match = re.search(r'^#\s+(.+)$', body, re.MULTILINE)
if match:
return match.group(1).strip()
return ""
@staticmethod
def extract_summary(body: str, max_length: int = 100) -> str:
"""提取摘要(纯文本,不超过指定长度)"""
# 移除Markdown标记
plain_text = re.sub(r'[#*`\-\[\]>]', '', body)
# 移除HTML标签
plain_text = re.sub(r'<[^>]+>', '', plain_text)
# 合并空白字符
plain_text = re.sub(r'\s+', ' ', plain_text).strip()
if len(plain_text) <= max_length:
return plain_text
else:
# 在完整词语处截断
truncated = plain_text[:max_length]
last_space = truncated.rfind(' ')
if last_space > max_length * 0.7: # 确保不是太短的截断
return truncated[:last_space] + "..."
else:
return truncated[:max_length-3] + "..."
class WeChatPublisher:
"""微信公众号发布器(优化版)"""
def __init__(self):
self.config = self._load_config()
self.access_token = None
self.image_optimizer = ImageOptimizer()
self.markdown_parser = MarkdownParser()
def _load_config(self) -> Dict:
"""加载配置"""
config = {}
# 加载 EXTEND.md
if os.path.exists(EXTEND_PATH):
with open(EXTEND_PATH, 'r', encoding='utf-8') as f:
for line in f:
if ':' in line and not line.strip().startswith('#'):
key, value = line.split(':', 1)
config[key.strip()] = value.strip()
# 回退到 WeWrite 配置
if 'app_id' not in config and os.path.exists(WEWRITE_CONFIG):
with open(WEWRITE_CONFIG, 'r', encoding='utf-8') as f:
wewrite_config = yaml.safe_load(f)
config['app_id'] = wewrite_config['wechat']['appid']
config['app_secret'] = wewrite_config['wechat']['secret']
config['default_author'] = wewrite_config['wechat']['author']
# 设置默认值
config.setdefault('default_theme', 'default')
config.setdefault('default_color', 'blue')
config.setdefault('need_open_comment', '1')
config.setdefault('only_fans_can_comment', '0')
return config
def get_access_token(self) -> str:
"""获取微信 access_token"""
if self.access_token:
return self.access_token
url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.config['app_id']}&secret={self.config['app_secret']}"
response = requests.get(url)
data = response.json()
if 'access_token' in data:
self.access_token = data['access_token']
return self.access_token
else:
raise Exception(f"获取 access_token 失败: {data}")
def upload_image(self, image_path: str, upload_type: str = "material") -> dict:
"""上传图片到微信
Args:
image_path: 图片路径
upload_type: "material" 用于封面(返回media_id), "body" 用于正文(返回url)
"""
access_token = self.get_access_token()
# 检查文件是否存在
if not os.path.exists(image_path):
raise FileNotFoundError(f"图片文件不存在: {image_path}")
# 优化图片(如果需要)
optimized_path = self.image_optimizer.optimize_image(image_path)
use_optimized = optimized_path != image_path
if use_optimized:
print(f" ↳ 使用优化后的图片: {os.path.basename(optimized_path)}")
upload_path = optimized_path
else:
upload_path = image_path
# 获取MIME类型
mime_type, _ = mimetypes.guess_type(upload_path)
if not mime_type:
mime_type = 'image/jpeg'
# 根据类型选择接口
if upload_type == "body":
# 正文图片使用 uploadimg 接口,返回URL
url = f"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token={access_token}"
else:
# 封面图片使用 add_material 接口,返回media_id
url = f"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={access_token}&type=image"
with open(upload_path, 'rb') as f:
files = {'media': (os.path.basename(upload_path), f, mime_type)}
response = requests.post(url, files=files)
data = response.json()
# 清理临时文件
if use_optimized and os.path.exists(optimized_path):
try:
os.unlink(optimized_path)
except:
pass
if upload_type == "body":
# 正文图片返回url (转换为https)
if 'url' in data:
url = data['url']
# 确保使用https
if url.startswith('http://'):
url = 'https://' + url[7:]
return {'url': url}
else:
raise Exception(f"上传正文图片失败: {data}")
else:
# 封面图片返回media_id
if 'media_id' in data:
return {'media_id': data['media_id']}
else:
raise Exception(f"上传封面图片失败: {data}")
def upload_images(self, image_paths: List[str]) -> Dict[str, str]:
"""批量上传正文图片(返回URL)"""
results = {}
print(f"📸 发现 {len(image_paths)} 张图片:")
for i, img_path in enumerate(image_paths, 1):
img_info = self.image_optimizer.get_image_info(img_path)
if not img_info['exists']:
print(f" {i}. ❌ 文件不存在: {img_info['filename']}")
results[img_path] = None
continue
# 显示图片信息
size_info = f"{img_info.get('size_kb', 0):.1f}KB"
if 'width' in img_info:
size_info = f"{img_info['width']}x{img_info['height']}, {size_info}"
format_info = img_info.get('format', '未知')
needs_convert = self.image_optimizer.format_needs_conversion(img_path)
status = "🔄 需要转换" if needs_convert else "✅ 格式正常"
print(f" {i}. {status} {img_info['filename']} ({format_info}, {size_info})")
print("⬆️ 上传图片到微信...")
for img_path in image_paths:
if not os.path.exists(img_path):
results[img_path] = None
continue
try:
# 正文图片使用body类型,返回URL
resp = self.upload_image(img_path, upload_type="body")
results[img_path] = resp['url']
print(f" ✅ 上传成功: {os.path.basename(img_path)}")
except Exception as e:
print(f" ❌ 上传失败 {os.path.basename(img_path)}: {e}")
results[img_path] = None
return results
def extract_images_from_markdown(self, markdown_content: str) -> List[str]:
"""从Markdown中提取图片路径"""
# 匹配Markdown图片语法: ![alt](path)
pattern = r'!\[.*?\]\((.*?)\)'
matches = re.findall(pattern, markdown_content)
# 过滤出本地文件路径(排除URL)
local_images = []
for match in matches:
if not match.startswith(('http://', 'https://')):
# 检查文件是否存在
if os.path.exists(match):
local_images.append(match)
else:
# 尝试相对路径
abs_path = os.path.abspath(match)
if os.path.exists(abs_path):
local_images.append(abs_path)
return local_images
def replace_images_in_markdown(self, markdown_content: str, image_map: Dict[str, str]) -> str:
"""将Markdown中的本地图片替换为微信图片URL"""
content = markdown_content
for img_path, img_url in image_map.items():
if img_url:
# 使用 uploadimg 接口返回的 URL
# 添加样式使其在微信中显示良好
wechat_img = f'<img src="{img_url}" style="display: block; width: 100%; margin: 1.5em auto;" alt="图片">'
# 替换Markdown图片语法
pattern = rf'!\[.*?\]\({re.escape(img_path)}\)'
content = re.sub(pattern, wechat_img, content)
return content
def markdown_to_html(self, markdown_content: str) -> str:
"""将Markdown转换为HTML(优化版)"""
# 处理标题
html = markdown_content
# 标题
html = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html, flags=re.MULTILINE)
html = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
html = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
html = re.sub(r'^#### (.*?)$', r'<h4>\1</h4>', html, flags=re.MULTILINE)
# 粗体和斜体
html = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html)
html = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html)
# 删除线
html = re.sub(r'~~(.*?)~~', r'<del>\1</del>', html)
# 内联代码
html = re.sub(r'`([^`]+)`', r'<code>\1</code>', html)
# 列表
lines = html.split('\n')
in_list = False
result_lines = []
for line in lines:
if re.match(r'^[*-] ', line):
if not in_list:
result_lines.append('<ul>')
in_list = True
content = line[2:].strip()
result_lines.append(f' <li>{content}</li>')
elif re.match(r'^\d+\. ', line):
if not in_list:
result_lines.append('<ol>')
in_list = True
content = re.sub(r'^\d+\. ', '', line).strip()
result_lines.append(f' <li>{content}</li>')
else:
if in_list:
result_lines.append('</ul>' if '<ul>' in result_lines else '</ol>')
in_list = False
result_lines.append(line)
if in_list:
result_lines.append('</ul>' if '<ul>' in result_lines else '</ol>')
html = '\n'.join(result_lines)
# 引用
html = re.sub(r'^> (.*?)$', r'<blockquote>\1</blockquote>', html, flags=re.MULTILINE)
# 代码块(简化处理)
html = re.sub(r'```.*?\n(.*?)\n```', r'<pre><code>\1</code></pre>', html, flags=re.DOTALL)
# 链接
html = re.sub(r'\[(.*?)\]\((.*?)\)', r'<a href="\2">\1</a>', html)
# 段落处理
lines = html.split('\n')
in_paragraph = False
final_lines = []
for line in lines:
if line.strip() == '':
if in_paragraph:
final_lines.append('</p>')
in_paragraph = False
final_lines.append('')
else:
if not in_paragraph and not line.startswith(('<h', '<li', '<img', '<blockquote', '<pre', '<ul', '<ol', '<a')):
final_lines.append('<p>')
in_paragraph = True
final_lines.append(line)
if in_paragraph:
final_lines.append('</p>')
html = '\n'.join(final_lines)
return html
def create_draft(self, title: str, content: str, author: str = None,
digest: str = None, cover_image_path: str = None) -> str:
"""创建公众号草稿(支持HTML内容)"""
access_token = self.get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/draft/add?access_token={access_token}"
# 上传封面图(使用material接口)- 封面图是必须的
thumb_media_id = None
if cover_image_path and os.path.exists(cover_image_path):
print(f"🖼️ 上传封面图: {os.path.basename(cover_image_path)}")
cover_resp = self.upload_image(cover_image_path, upload_type="material")
thumb_media_id = cover_resp.get('media_id')
# 如果没有提供封面图或上传失败,使用默认封面图
if not thumb_media_id:
default_cover = "/home/ubuntu/oh-my-writing-skill/example.png"
if os.path.exists(default_cover):
print(f"🖼️ 使用默认封面图: {os.path.basename(default_cover)}")
cover_resp = self.upload_image(default_cover, upload_type="material")
thumb_media_id = cover_resp.get('media_id')
# 如果没有提供作者,使用默认作者
if not author:
author = self.config.get('default_author', '')
# 如果没有提供摘要,从内容中提取
if not digest:
plain_text = re.sub(r'<[^>]+>', '', content) # 移除HTML标签
plain_text = re.sub(r'\s+', ' ', plain_text).strip()
digest = self.markdown_parser.extract_summary(plain_text)
# 确保摘要长度不超过100字符(微信限制)
if digest and len(digest) > 100:
digest = digest[:97] + "..."
# 构建文章数据
article = {
"title": title,
"author": author,
"digest": digest,
"content": content,
"show_cover_pic": 0,
"need_open_comment": int(self.config.get('need_open_comment', 1)),
"only_fans_can_comment": int(self.config.get('only_fans_can_comment', 0))
}
if thumb_media_id:
article["thumb_media_id"] = thumb_media_id
body = {"articles": [article]}
# 发送请求
headers = {"Content-Type": "application/json; charset=utf-8"}
response = requests.post(url, data=json.dumps(body, ensure_ascii=False).encode('utf-8'), headers=headers)
data = response.json()
if 'media_id' in data:
return data['media_id']
else:
raise Exception(f"创建草稿失败: {data}")
def submit_draft_for_publish(self, media_id: str) -> bool:
"""将草稿提交发布"""
access_token = self.get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token={access_token}"
body = {"media_id": media_id}
headers = {"Content-Type": "application/json; charset=utf-8"}
response = requests.post(url, data=json.dumps(body), headers=headers)
data = response.json()
if data.get('errcode') == 0:
print(f"✅ 发布提交成功,发布任务ID: {data.get('publish_id')}")
return True
else:
print(f"⚠️ 发布提交失败: {data}")
return False
def publish_markdown_with_images(self, markdown_path: str, title: str = None,
author: str = None, cover_image_path: str = None) -> Dict:
"""发布Markdown文章(支持图文混排 + Frontmatter)"""
print("=" * 60)
print("📝 baoyu-post-to-wechat 优化版 v2.0")
print("=" * 60)
# 检查文件
if not os.path.exists(markdown_path):
raise FileNotFoundError(f"Markdown文件不存在: {markdown_path}")
# ====== 博客同步(独立支线,不影响公众号发布)======
try:
from blog_sync import sync_markdown_to_blog
sync_markdown_to_blog(markdown_path, title, author)
# 博客同步成功/失败都不影响后续流程
except Exception as e:
print(f"⚠️ 博客同步异常(继续发布公众号): {e}")
# ==================================================
# 读取Markdown内容
with open(markdown_path, 'r', encoding='utf-8') as f:
markdown_content = f.read()
print(f"📄 读取文件: {os.path.basename(markdown_path)} ({len(markdown_content)} 字符)")
# 解析Frontmatter
print("🔍 解析Frontmatter...")
frontmatter, body = self.markdown_parser.parse_frontmatter(markdown_content)
if frontmatter:
print(" ✅ 发现Frontmatter:")
for key, value in frontmatter.items():
if key in ['title', 'author', 'date', 'tags']:
print(f" {key}: {value}")
# 确定标题(优先级:参数 > frontmatter > 正文提取 > 文件名)
if not title:
title = frontmatter.get('title', '')
if not title:
title = self.markdown_parser.extract_title_from_body(body)
if not title:
title = Path(markdown_path).stem
# 确定作者(优先级:参数 > frontmatter > 配置默认)
if not author:
author = frontmatter.get('author', '')
if not author:
author = self.config.get('default_author', '')
# 提取图片
print("🖼️ 提取图片...")
image_paths = self.extract_images_from_markdown(body)
if image_paths:
# 上传图片
image_map = self.upload_images(image_paths)
# 替换图片引用
print("🔄 替换图片引用...")
processed_body = self.replace_images_in_markdown(body, image_map)
else:
print("ℹ️ 未发现本地图片")
processed_body = body
# 转换为HTML
print("🔧 转换为HTML...")
html_content = self.markdown_to_html(processed_body)
# 提取摘要
print("📋 生成摘要...")
digest = self.markdown_parser.extract_summary(body)
print(f" 摘要: {digest[:50]}..." if len(digest) > 50 else f" 摘要: {digest}")
# 如果没有封面图,使用默认(如果不存在则不使用封面)
if not cover_image_path:
default_cover = "/home/ubuntu/default_cover.png"
if os.path.exists(default_cover):
cover_image_path = default_cover
print(f"🖼️ 使用默认封面图: {os.path.basename(default_cover)}")
else:
print("ℹ️ 未提供封面图,使用微信公众号默认封面")
# 创建草稿
print("📤 创建公众号草稿...")
media_id = self.create_draft(title, html_content, author, digest, cover_image_path)
# 提交发布(从草稿箱发布)
print("📤 提交公众号发布...")
try:
publish_result = self.submit_draft_for_publish(media_id)
publish_msg = "发布成功" if publish_result else "草稿创建成功(请手动发布)"
except Exception as e:
print(f"⚠️ 自动发布失败: {e}")
print("ℹ️ 请在微信公众号后台手动发布该草稿")
publish_msg = "草稿创建成功(请手动发布)"
print("=" * 60)
print(f"✅ {publish_msg}!")
print("=" * 60)
return {
"success": True,
"media_id": media_id,
"title": title,
"author": author,
"digest": digest[:100] + "..." if len(digest) > 100 else digest,
"images_uploaded": len([v for v in image_map.values() if v]) if image_paths else 0,
"has_frontmatter": bool(frontmatter),
"message": publish_msg
}
def publish_html(self, html_path: str, title: str = None, author: str = None,
cover_image_path: str = None) -> Dict:
"""发布HTML文章"""
print("📝 开始发布HTML文章...")
if not os.path.exists(html_path):
raise FileNotFoundError(f"HTML文件不存在: {html_path}")
# 读取HTML内容
with open(html_path, 'r', encoding='utf-8') as f:
html_content = f.read()
print(f"📄 读取文件: {os.path.basename(html_path)} ({len(html_content)} 字符)")
# 如果没有标题,使用文件名
if not title:
title = Path(html_path).stem
# 创建草稿
print("📤 创建公众号草稿...")
media_id = self.create_draft(title, html_content, author, None, cover_image_path)
print("✅ 发布成功!")
return {
"success": True,
"media_id": media_id,
"title": title,
"author": author or self.config.get('default_author', ''),
"message": "草稿创建成功"
}
def main():
"""命令行入口"""
import sys
import argparse
parser = argparse.ArgumentParser(
description='微信公众号发布工具(优化版)',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s markdown article.md
%(prog)s markdown article.md --title "自定义标题" --author "作者名"
%(prog)s markdown article.md --cover /path/to/cover.jpg
%(prog)s test
"""
)
subparsers = parser.add_subparsers(dest='command', help='命令')
# markdown 命令
md_parser = subparsers.add_parser('markdown', help='发布Markdown文章')
md_parser.add_argument('file', help='Markdown文件路径')
md_parser.add_argument('--title', help='文章标题(覆盖frontmatter)')
md_parser.add_argument('--author', help='作者(覆盖frontmatter)')
md_parser.add_argument('--cover', help='封面图路径')
# html 命令
html_parser = subparsers.add_parser('html', help='发布HTML文章')
html_parser.add_argument('file', help='HTML文件路径')
html_parser.add_argument('--title', help='文章标题')
html_parser.add_argument('--author', help='作者')
html_parser.add_argument('--cover', help='封面图路径')
# test 命令
test_parser = subparsers.add_parser('test', help='测试环境和配置')
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
publisher = WeChatPublisher()
try:
if args.command == 'markdown':
result = publisher.publish_markdown_with_images(
args.file, args.title, args.author, args.cover
)
print(json.dumps(result, ensure_ascii=False, indent=2))
elif args.command == 'html':
result = publisher.publish_html(
args.file, args.title, args.author, args.cover
)
print(json.dumps(result, ensure_ascii=False, indent=2))
elif args.command == 'test':
print("🔧 测试环境和配置...")
print(f"Python版本: {sys.version.split()[0]}")
print(f"Pillow可用: {'✅' if HAS_PILLOW else '❌'}")
print(f"PyYAML可用: ✅")
# 测试配置
print(f"\n📋 配置信息:")
print(f" AppID: {publisher.config.get('app_id')}")
print(f" 作者: {publisher.config.get('default_author')}")
# 测试access_token
try:
token = publisher.get_access_token()
print(f" Access Token: {token[:20]}...")
print("✅ 配置正常")
except Exception as e:
print(f"❌ 配置错误: {e}")
# 测试图片优化
if HAS_PILLOW:
print(f"\n🖼️ 图片优化测试:")
test_image = "/home/ubuntu/default_cover.png"
if os.path.exists(test_image):
info = publisher.image_optimizer.get_image_info(test_image)
print(f" 测试图片: {info.get('filename')}")
print(f" 格式: {info.get('format', '未知')}")
print(f" 尺寸: {info.get('width', 0)}x{info.get('height', 0)}")
print(f" 大小: {info.get('size_kb', 0):.1f}KB")
else:
print(" 测试图片不存在")
else:
parser.print_help()
except Exception as e:
print(json.dumps({
"success": False,
"error": str(e)
}, ensure_ascii=False, indent=2))
sys.exit(1)
if __name__ == "__main__":
main()