This commit is contained in:
zhangsan 2025-03-21 15:50:37 +08:00
parent eab6599075
commit 8bec7f4e65
3 changed files with 166 additions and 92 deletions

118
README.md
View File

@ -1,13 +1,26 @@
## 同步本地Markdown至Typecho站点
场景本人喜欢在本地用Typora写markdown文件但又想同时同步至Typecho发表成文章且由于md文件并不是一成不变的经常需要对各个文件缝缝补补要能实现本地更新/同步至博客更新。
本项目基于https://github.com/Sundae97/typecho-markdown-file-publisher
亲测适配Typecho1.2 php7.4.33
实现效果:
- [x] 将markdown发布到typecho
- [x] 发布前将markdown的图片资源上传到TencentCloud的COS中, 并替换markdown中的图片链接
- [x] 将md所在的文件夹名称作为post的category(mysql发布可以插入category, xmlrpc接口暂时不支持category操作)
- [x] 以title和category作为文章的唯一标识如果数据库中已有该数据将会更新现有文章否则新增文章。
环境Typecho1.2.1 php7.4.33
### 项目目录
![image-20250319173057792](D:\folder\study\md_files\output\image-20250319173057792.png)
`typecho_markdown_upload/main.py`是上传md文件到站点的核心脚本
`transfer_md/transfer.py`是对md文件进行预处理的脚本。
### **核心思路**
**一、预先准备**
@ -58,7 +71,7 @@ github地址[icret/EasyImages2.0: 简单图床 - 一款功能强大无数据
**文件结构统一**
```
```text
md_files
├── category1
│ ├── file1.md
@ -94,7 +107,7 @@ md_files
**初始化本地仓库**
```
```text
git init
```
@ -102,26 +115,26 @@ git init
将远程仓库地址添加为 `origin`(请将 `http://xxx` 替换为你的实际仓库地址):
```
```text
git remote add origin http://xxx
```
**添加文件并提交**
```
```text
git add .
git commit -m "Initial commit"
```
**推送到远程仓库:**
```
```text
git push -u origin master
```
**后续更新:**
**后续更新(可写个.bat批量执行**
```
```text
git add .
git commit -m "更新了xxx内容"
git push
@ -133,11 +146,11 @@ git push
**1. 确保脚本能够连接到 Typecho 使用的数据库**
本博客使用 docker-compose 部署 Typecho参考[【好玩儿的Docker项目】10分钟搭建一个Typecho博客太破口念念不忘必有回响-我不是咕咕鸽](https://blog.laoda.de/archives/docker-compose-install-typecho))。为了让脚本能访问 Typecho 的数据库,我将 Python 应用也通过 docker-compose 部署,这样所有服务均在同一网络中,互相之间可以直接通信。
本博客使用 docker-compose 部署 Typecho参考[【好玩儿的Docker项目】10分钟搭建一个Typecho博客太破口念念不忘必有回响-我不是咕咕鸽](https://blog.laoda.de/archives/docker-compose-install-typecho))。为了让脚本能访问 Typecho 的数据库,我将 Python 应用pyapp也通过 docker-compose 部署,这样所有服务均在同一网络中,互相之间可以直接通信。
参考docker-compose.yml如下
```
```text
services:
nginx:
image: nginx
@ -171,6 +184,8 @@ services:
pyapp:
build: ./markdown_operation # Dockerfile所在的目录
restart: "no"
volumes:
- /home/zy123/md_files:/markdown_operation/md_files
networks:
- web
env_file:
@ -201,9 +216,13 @@ networks:
**2. 将版本控制的 md_files 仓库克隆到 markdown_operation 目录中**
**2. 将 `md_files` 挂载到容器中,保持最新内容同步**
确保在容器内可以直接访问到 md_files 内容,因此我们将使用 Git 进行版本控制的 md_files 仓库克隆到 markdown_operation 内部。这样,无论是执行脚本还是其他操作,都能轻松访问和更新 Markdown 文件。
这样有几个优势:
- 不需要每次构建镜像或进入容器手动拉取;
- 本地更新 `md_files` 后,容器内自动同步,无需额外操作;
- 保持了宿主机上的 Git 版本控制和容器内的数据一致性。
@ -211,61 +230,100 @@ networks:
`pyapp`是本Python应用在容器内的名称。
构建镜像:
1.构建镜像:
```
```text
docker-compose build pyapp
```
启动容器并进入 Bash
2.启动容器并进入 Bash
```
```text
docker-compose run --rm -it pyapp /bin/bash
```
在容器内运行脚本:
3.在容器内运行脚本:
```
```text
python typecho_markdown_upload/main.py
```
2、3两步可合并为
```text
docker-compose run --rm pyapp python typecho_markdown_upload/main.py
```
![image-20250320103325650](https://pic.bitday.top/i/2025/03/20/h37pze-0.png)
此时可以打开博客验证一下是否成功发布文章了!
**如果失败可以验证mysql数据库:**
1⃣ 进入 MySQL 容器:
```
docker compose exec mysql mysql -uroot -p
```text
docker-compose exec mysql mysql -uroot -p
# 输入你的 root 密码
```
2⃣ 切换到 Typecho 数据库并列出表:
```
```text
USE typecho;
SHOW TABLES;
```
3⃣ 查看 `typecho_contents` 表结构(文章表):
```
```text
DESCRIBE typecho_contents;
SHOW CREATE TABLE typecho_contents\G
```
```text
mysql> DESCRIBE typecho_contents;
+--------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+----------------+
| cid | int(10) unsigned | NO | PRI | NULL | auto_increment |
| title | varchar(150) | YES | | NULL | |
| slug | varchar(150) | YES | UNI | NULL | |
| created | int(10) unsigned | YES | MUL | 0 | |
| modified | int(10) unsigned | YES | | 0 | |
| text | longtext | YES | | NULL | |
| order | int(10) unsigned | YES | | 0 | |
| authorId | int(10) unsigned | YES | | 0 | |
| template | varchar(32) | YES | | NULL | |
| type | varchar(16) | YES | | post | |
| status | varchar(16) | YES | | publish | |
| password | varchar(32) | YES | | NULL | |
| commentsNum | int(10) unsigned | YES | | 0 | |
| allowComment | char(1) | YES | | 0 | |
| allowPing | char(1) | YES | | 0 | |
| allowFeed | char(1) | YES | | 0 | |
| parent | int(10) unsigned | YES | | 0 | |
| views | int(11) | YES | | 0 | |
| agree | int(11) | YES | | 0 | |
+--------------+------------------+------+-----+---------+----------------+
```
4⃣ 查询当前文章数量(确认执行前后有无变化):
```
```text
SELECT COUNT(*) AS cnt FROM typecho_contents;
```
### **自动化**
1.windows下写脚本自动/手动提交每日更新
2.远程仓库监测到更新自动实现钩子脚本,更新md_files并执行脚本
### TODO
- [x] 将markdown发布到typecho
- [x] 发布前将markdown的图片资源上传到TencentCloud的COS中, 并替换markdown中的图片链接
- [x] 将md所在的文件夹名称作为post的category(mysql发布可以插入category, xmlrpc接口暂时不支持category操作)
- [ ] category的层级
- [ ] 发布前先获取所有post信息, 不发布已经发布过的post
- [ ] typecho_contents表中的slug字段代表链接中的日志缩略名如wordpress风格 `/archives/{slug}.html`目前是默认int自增有需要的话可以在插入文章时手动设置该字段。

View File

@ -194,6 +194,35 @@ def process_md_file_remote(md_file):
f.write(content)
print(f"已更新: {md_file}")
def add_language_to_codeblocks(filepath, language="text"):
'''
若代码块没有指定语言Typera中能正常显示但是博客中的markdown解析器通常会错误显示
'''
with open(filepath, 'r', encoding='utf-8') as f:
lines = f.readlines()
in_code_block = False
new_lines = []
for line in lines:
stripped = line.strip()
# 判断是否为代码块标记
if stripped.startswith("```"):
if not in_code_block:
# 当前为代码块的起始位置
# 如果这一行只有三个反引号,则添加语言参数
if stripped == "```":
# 这里使用replace仅替换第一次出现的```,保留原有的换行符和空白
line = line.replace("```", f"```{language}", 1)
in_code_block = True
else:
# 当前为代码块的结束位置,不添加语言
in_code_block = False
new_lines.append(line)
# 将修改后的内容写回文件(也可选择写入新文件)
with open(filepath, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
def scan_files(base_folder, exclude_folders):
"""
@ -218,6 +247,7 @@ def process_md_files(input_path, output_path, type, exclude_folders=None):
type == 1: process_md_file_local
type == 2: process_md_file_with_assets
type == 3: process_md_file_remote
type == 4:
"""
# 创建输出目录(如果不存在)
os.makedirs(output_path, exist_ok=True)
@ -235,10 +265,12 @@ def process_md_files(input_path, output_path, type, exclude_folders=None):
process_md_file_with_assets(md_file, output_path) # url改为本地assets方式图片和md文件都存output_path
elif type == 3:
process_md_file_remote(md_file) # 图片url改为公网链接
elif type == 4:
add_language_to_codeblocks(md_file)
else:
print(f"未知的处理类型: {type}")
print("处理完成!所有图片已保存至:", os.path.abspath(output_path))
print("该文件夹下的md_files已全部处理完成!", os.path.abspath(input_path))
if __name__ == "__main__":
@ -247,14 +279,12 @@ if __name__ == "__main__":
try:
type_value = int(sys.argv[1])
except ValueError:
print("第一个参数必须为整数表示处理类型1, 2 或 3")
print("第一个参数必须为整数表示处理类型1, 2 , 3, 4")
sys.exit(1)
else:
type_value = 3
type_value = 4
# 这里的输入输出路径根据实际情况修改
# input_path = os.getenv('BASE_FOLDER')
input_path=r'D:\folder\study\md_files'
# output_path = os.getenv('OUTPUT_FOLDER')
output_path=r'D:\folder\study\md_files\output'
input_path = os.getenv('BASE_FOLDER') #docker环境
output_path = os.getenv('OUTPUT_FOLDER')
process_md_files(input_path, output_path, type_value)

View File

@ -23,7 +23,7 @@ class TypechoDirectMysqlPublisher:
初始化分类列表到 self.__exist_categories
"""
cursor = self.__db.cursor()
sql = "SELECT mid, name FROM %s WHERE type='category'" % self.__categories_table_name
sql = f"SELECT mid, name FROM {self.__categories_table_name} WHERE type='category'"
cursor.execute(sql)
results = cursor.fetchall()
self.__exist_categories = []
@ -45,11 +45,10 @@ class TypechoDirectMysqlPublisher:
"""
cursor = self.__db.cursor()
sql = (
"INSERT INTO %s "
f"INSERT INTO {self.__categories_table_name} "
"(`name`, `slug`, `type`, `description`, `count`, `order`, `parent`) "
"VALUES "
"('%s', '%s', 'category', '', 0, 1, 0)"
) % (self.__categories_table_name, category_name, category_name)
f"VALUES ('{category_name}', '{category_name}', 'category', '', 0, 1, 0)"
)
cursor.execute(sql)
mid = cursor.lastrowid
self.__db.commit()
@ -63,11 +62,10 @@ class TypechoDirectMysqlPublisher:
typecho_relationships 中插入文章与分类的关联
"""
insert_relationship_sql = (
"INSERT INTO %s "
"(`cid`, `mid`) "
"VALUES "
"(%d, %d)"
) % (self.__relationships_table_name, cid, mid)
f"INSERT INTO {self.__relationships_table_name} "
f"(`cid`, `mid`) "
f"VALUES ({cid}, {mid})"
)
cursor.execute(insert_relationship_sql)
def __update_category_count(self, cursor, mid):
@ -75,14 +73,16 @@ class TypechoDirectMysqlPublisher:
分类下文章数 +1
"""
update_category_count_sql = (
"UPDATE %s SET `count`=`count`+1 WHERE mid=%d"
) % (self.__categories_table_name, mid)
f"UPDATE {self.__categories_table_name} SET `count`=`count`+1 WHERE mid={mid}"
)
cursor.execute(update_category_count_sql)
def publish_post(self, title, content, category):
"""
如果 (category, title) 已存在 更新旧文章
否则 插入新文章
如果 (category, title) 已存在则对比旧内容与新内容
- 若内容相同则跳过更新
- 若内容不同则更新
否则插入新文章
"""
cursor = self.__db.cursor()
@ -92,67 +92,53 @@ class TypechoDirectMysqlPublisher:
mid = self.__add_category(category)
# 2. 查找同一分类下,是否已存在相同 title 的文章
check_sql = """
SELECT c.cid
FROM %s c
JOIN %s r ON c.cid = r.cid
WHERE c.title = '%s'
AND r.mid = %d
check_sql = f"""
SELECT c.cid, c.text
FROM {self.__contents_table_name} c
JOIN {self.__relationships_table_name} r ON c.cid = r.cid
WHERE c.title = '{escape_string(title)}'
AND r.mid = {mid}
LIMIT 1
""" % (
self.__contents_table_name,
self.__relationships_table_name,
escape_string(title),
mid
)
"""
cursor.execute(check_sql)
exist_row = cursor.fetchone()
now_time_int = int(time.time())
content = '<!--markdown-->' + content
content_with_mark = '<!--markdown-->' + content # 新内容加上标记
escaped_content = escape_string(content_with_mark)
if exist_row:
# ========== 执行更新逻辑 ==========
cid = exist_row[0]
update_sql = """
UPDATE %s
SET modified=%d,
text='%s'
WHERE cid=%d
""" % (
self.__contents_table_name,
now_time_int,
escape_string(content),
cid
)
cursor.execute(update_sql)
old_content = exist_row[1] or ""
# 如果你需要修改 slug、authorId、status 等字段,可在这里加上
# 不需要改 relationships (分类关系) 和分类计数,因为 category 没变
print(f"[INFO] 更新文章成功: title={title}, cid={cid}, category={category}")
if old_content.strip() == content_with_mark.strip():
# 内容相同,不更新
print(f"[INFO] 文章已存在且内容未修改: title={title}, cid={cid}, category={category},跳过更新。")
return cid
else:
# 内容不同,执行更新
update_sql = f"""
UPDATE {self.__contents_table_name}
SET modified={now_time_int},
text='{escaped_content}'
WHERE cid={cid}
"""
cursor.execute(update_sql)
print(f"[INFO] 更新文章成功: title={title}, cid={cid}, category={category}")
else:
# ========== 执行插入逻辑 ==========
insert_sql = (
"INSERT INTO %s "
f"INSERT INTO {self.__contents_table_name} "
"(`title`, `slug`, `created`, `modified`, `text`, `order`, `authorId`, `template`, `type`, `status`, `password`, `commentsNum`, `allowComment`, `allowPing`, `allowFeed`, `parent`) "
"VALUES "
"('%s', NULL, %d, %d, '%s', 0, 1, NULL, 'post', 'publish', NULL, 0, '1', '1', '1', 0)"
) % (
self.__contents_table_name,
escape_string(title),
now_time_int,
now_time_int,
escape_string(content)
f"VALUES ('{escape_string(title)}', NULL, {now_time_int}, {now_time_int}, '{escaped_content}', 0, 1, NULL, 'post', 'publish', NULL, 0, '1', '1', '1', 0)"
)
cursor.execute(insert_sql)
cid = cursor.lastrowid
# slug = cid
update_slug_sql = (
"UPDATE %s SET slug=%d WHERE cid=%d"
) % (self.__contents_table_name, cid, cid)
update_slug_sql = f"UPDATE {self.__contents_table_name} SET slug={cid} WHERE cid={cid}"
cursor.execute(update_slug_sql)
# 建立文章与分类的关系