2721 字
14 分钟

Typecho文章批量导出至Astro Markdown工具

引言#

Typecho 作为一款轻量级的博客系统,曾经是许多博主的选择。然而,随着技术的不断发展,Typecho 已经很久没有更新了。今年我发现了 Astro 这个现代化静态站点生成框架,同时被 Firefly 主题 这个非常符合我审美的主题所吸引,于是毅然决定将博客迁移到 Astro。

为了方便 Typecho 用户迁移导出 Markdown 文档,并适配 Astro 的 Firefly 主题而制作的定制化的导出工具。它不仅能将 Typecho 的文章导出为标准的 Markdown 格式,还能自动生成符合 Firefly 主题要求的 Frontmatter 配置。

功能介绍#

本工具提供了以下核心功能:

  1. 按分类目录导出:文章将按照分类自动组织到对应的子目录中
  2. 符合 Astro 5.0 标准:生成标准的 YAML Frontmatter,包含所有必要字段
  3. 智能图片处理
    • 自动将引用式图片转换为直接链接格式
    • 清理 Typecho 图片 URL 中的冗余参数(如 #vwid
    • 移除所有图片引用定义行
  4. 自动摘要生成:从文章内容中智能提取前100字作为描述
  5. 灵活的封面图片配置:支持使用 Firefly 主题内置的随机二次元图片 API,或自定义图片 URL
  6. 优化日期格式:Frontmatter 中的日期格式简化为 YYYY-MM-DD,更易阅读
  7. 安全文件名处理:自动清理文件名中的非法字符,确保文件系统兼容性

使用方法#

⚠️ ⚠️⚠️特别提醒

使用本工具请务必预先全量备份服务器的网站数据(数据库+网站文件),本工具不处理博客服务器中的静态图片资源,仅导出文章和适配生成 Frontmatter 属性。

也因此,图片资源存储在网站服务器中的使用者请务必斟酌再三,这部分使用者后期需要打包下载服务器的图片文件导入到 Astro,然后批处理 md 文档中的图片引用地址改成相对地址才能正常显示图片哦。

第一步:准备导出脚本#

在你的 Typecho 网站根目录创建一个 converter.php 文件,将以下完整代码复制到文件中:

<?php
/**
* Typecho 文章导出工具 (适配 Astro 5.0 + Firefly 主题)
*
* 功能特性:
* 1. 按分类目录导出文章到对应的子目录
* 2. 构建符合 Astro 5.0 标准的 YAML Frontmatter
* 3. 自动处理引用式图片,转换为直接链接格式
* 4. 清理图片 URL 中的冗余参数
* 5. 生成文章摘要(自动截取前100字)
* 6. 支持自定义文章封面图片配置
*
* 使用前请先配置数据库连接信息和导出选项
* 运行后会在指定目录生成按分类组织的 Markdown 文件
*/
// 1. 基础配置
error_reporting(E_ALL);
ini_set('display_errors', 1);
$dbConfig = [
'host' => 'localhost', // 数据库主机地址
'name' => '数据库名', // 数据库名称
'user' => '数据库用户名', // 数据库用户名
'pass' => '数据库密码', // 数据库密码
'baseDir' => 'astro_posts', // 导出文件存放的目录
'image' => 'api' // 文章封面图片配置:默认值为 'api',将使用 Firefly 主题内置的随机二次元图片 API
// 可自定义为其他图片 URL,例如:'https://example.com/image.jpg'
// 或者保留为空字符串 '' 则不生成 image 属性
];
try {
// 创建数据库连接
$dsn = "mysql:host={$dbConfig['host']};dbname={$dbConfig['name']};charset=utf8mb4";
$pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
echo "✅ 数据库连接成功\n";
// 2. 执行 SQL 查询
$sql = "
SELECT
c.cid, c.title, c.text, c.slug, c.created, c.modified,
(SELECT GROUP_CONCAT(m.name) FROM typecho_relationships r
JOIN typecho_metas m ON r.mid = m.mid
WHERE r.cid = c.cid AND m.type = 'tag') as tags,
(SELECT m.name FROM typecho_relationships r
JOIN typecho_metas m ON r.mid = m.mid
WHERE r.cid = c.cid AND m.type = 'category' LIMIT 1) as category
FROM typecho_contents c
WHERE c.type = 'post' AND c.status = 'publish'
ORDER BY c.created DESC
";
// 执行查询获取所有已发布的文章
$stmt = $pdo->query($sql);
$posts = $stmt->fetchAll();
if (!$posts) die("❌ 没有找到文章\n");
echo "📂 开始导出 " . count($posts) . " 篇文章...\n";
foreach ($posts as $index => $post) {
// 3. 处理分类目录:根据文章分类创建对应的导出目录
$categoryName = !empty($post['category']) ? $post['category'] : 'Uncategorized';
$targetFolder = $dbConfig['baseDir'] . DIRECTORY_SEPARATOR . sanitize_filename($categoryName);
if (!is_dir($targetFolder)) {
mkdir($targetFolder, 0755, true);
}
// 4. 处理文件名:移除非法字符,确保文件名安全
$fileName = sanitize_filename($post['title']);
$exportPath = $targetFolder . DIRECTORY_SEPARATOR . $fileName . ".md";
// Windows 系统需要转换编码
if (strpos(PHP_OS, "WIN") !== false) {
$exportPath = iconv("UTF-8", "GBK//IGNORE", $exportPath);
}
// 5. 处理文章内容:移除 Typecho 的 Markdown 注释标记
$rawContent = $post['text'];
$content = str_replace('<!--markdown-->', '', $rawContent);
// 6. 生成文章摘要:从内容中提取前100字作为描述
// 6.1 清理文本:移除所有 HTML 和 Markdown 标记
$cleanText = strip_tags($content); // 去除 HTML 标签
$cleanText = preg_replace('/!\[.*?\]\(.*?\)/s', '', $cleanText); // 去除图片标记
$cleanText = preg_replace('/!\[.*?\]\[.*?\]/s', '', $cleanText); // 去除引用式图片标记
$cleanText = preg_replace('/\[(.*?)\]\(.*?\)/s', '$1', $cleanText); // 去除链接标记但保留文字
$cleanText = preg_replace('/#+\s*/', '', $cleanText); // 去除标题标记
$cleanText = preg_replace('/\s+/', ' ', $cleanText); // 合并多余空格
$cleanText = trim($cleanText);
// 6.2 截取前100字作为摘要
$description = mb_substr($cleanText, 0, 100, 'UTF-8');
if (mb_strlen($cleanText, 'UTF-8') > 100) {
$description .= '...';
}
// 7. 准备 Frontmatter(符合 Astro 5.0 标准)
$pubDate = date('Y-m-d', $post['created']); // 发布日期,格式:YYYY-MM-DD
$upDate = date('Y-m-d', $post['modified']); // 更新日期,格式:YYYY-MM-DD
$tags = $post['tags'] ? explode(',', $post['tags']) : []; // 标签字符串转数组
$tags = array_filter(array_map('trim', $tags)); // 清理标签中的空格
$yaml = "---\n";
$yaml .= "title: " . json_encode($post['title'], JSON_UNESCAPED_UNICODE) . "\n";
$yaml .= "published: $pubDate\n";
$yaml .= "updated: $upDate\n";
$yaml .= "author: \"MoeWah\"\n";
// 生成 image 属性,如果配置不为空则添加
if (!empty($dbConfig['image'])) {
$yaml .= "image: " . json_encode($dbConfig['image'], JSON_UNESCAPED_UNICODE) . "\n";
}
$yaml .= "description: " . json_encode($description, JSON_UNESCAPED_UNICODE) . "\n";
$yaml .= "category: " . json_encode($categoryName, JSON_UNESCAPED_UNICODE) . "\n";
// 7.2 处理标签:生成正确的 YAML 数组格式
if (count($tags) > 0) {
$yaml .= "tags:\n - " . implode("\n - ", array_map(fn($t) => json_encode($t, JSON_UNESCAPED_UNICODE), $tags)) . "\n";
} else {
$yaml .= "tags: []\n"; // 空数组,避免生成 `- []` 的错误格式
}
$yaml .= "slug: " . json_encode($post['slug'], JSON_UNESCAPED_UNICODE) . "\n";
$yaml .= "draft: false\n";
$yaml .= "toc: true\n";
$yaml .= "---\n\n";
// 8. 处理正文内容:将所有引用式图片转换为直接链接格式
// 8.1 清理直接链接图片中的 #vwid 后缀参数(Typecho 特有的图片参数)
$content = preg_replace('/(https?:\/\/[^\s\)]+?)(#vwid=[^?\s\)]+)/i', '$1', $content);
// 8.2 收集所有图片引用定义(Markdown 引用格式:[id]: URL)
$imageRefs = [];
if (preg_match_all('/^\[([^\]]+)\]:\s*(https?:\/\/[^\s]+)/im', $content, $refMatches, PREG_SET_ORDER)) {
foreach ($refMatches as $match) {
$refId = $match[1];
$imageUrl = $match[2];
// 清理图片URL后缀参数
$cleanUrl = preg_replace('/(#vwid=[^?\s\)]+)/i', '', $imageUrl);
$imageRefs[$refId] = $cleanUrl;
}
}
// 8.3 将引用式图片标记转换为直接链接格式
// 处理格式:![描述][id]、![][id]、![id]
foreach ($imageRefs as $refId => $imageUrl) {
// 匹配格式 ![描述][id] 或 ![][id]
$pattern = '/!\[(.*?)\]\[\s*' . preg_quote($refId, '/') . '\s*\]/';
$content = preg_replace_callback($pattern, function($matches) use ($imageUrl) {
$altText = $matches[1];
return "![$altText]($imageUrl)";
}, $content);
// 匹配格式 ![id] 这种简写形式
$pattern2 = '/!\[\s*' . preg_quote($refId, '/') . '\s*\]/';
$content = preg_replace($pattern2, "![]($imageUrl)", $content);
}
// 8.4 移除所有图片引用定义行(已经转换为直接链接,不再需要引用定义)
$content = preg_replace('/\[[^\]]+\]:\s*https?:\/\/.*$/m', '', $content);
// 8.5 清理多余的空行(三个以上连续空行合并为两个空行)
$content = preg_replace('/\n\s*\n\s*\n/', "\n\n", $content);
$content = trim($content);
// 9. 合并 Frontmatter 和正文内容
$fullOutput = $yaml . $content;
// 10. 写入文件到指定路径
if (file_put_contents($exportPath, $fullOutput)) {
echo "[" . ($index + 1) . "] 已导出: {$post['title']}\n";
}
}
echo "\n🎉 导出完成!已将引用式图片转换为直接链接格式。\n";
echo "📁 文件保存在目录: " . $dbConfig['baseDir'] . "\n";
} catch (Exception $e) {
die("❌ 错误: " . $e->getMessage() . "\n");
}
/**
* 文件名清理函数
*
* @param string $filename 原始文件名
* @return string 清理后的安全文件名
*
* 功能:
* 1. 替换非法字符为连字符
* 2. 限制文件名长度不超过80个字符
* 3. 去除文件名两端的点和连字符
*/
function sanitize_filename($filename) {
// 定义需要替换的非法字符
$invalid = array(" ", "?", "\\", "/", ":", "|", "*", "\"", "<", ">");
$filename = str_replace($invalid, '-', $filename);
// 限制文件名长度,避免过长的文件名
$filename = mb_substr($filename, 0, 80, 'utf-8');
// 去除文件名两端的点和连字符(避免隐藏文件或非法文件)
return trim($filename, '.-');
}
?>

第二步:配置数据库连接#

用文本编辑器打开 converter.php,找到以下配置部分,将数据库信息更改为你自己的 Typecho 数据库信息:

$dbConfig = [
'host' => 'localhost', // 数据库主机地址(通常为 localhost)
'name' => '数据库名', // Typecho 数据库名称
'user' => '数据库用户名', // 数据库用户名
'pass' => '数据库密码', // 数据库密码
'baseDir' => 'astro_posts', // 导出文件存放的目录(可修改)
'image' => 'api' // 封面图片配置(默认使用 Firefly 主题 API)
];

封面图片配置说明:#

  • 'api':默认值,使用 Firefly 主题内置的随机二次元图片 API
  • 'https://example.com/image.jpg':自定义静态图片 URL
  • '':留空则不生成 image 属性

第三步:运行导出脚本#

打开你的服务器终端,进入到 Typecho 网站根目录,执行以下命令:

Terminal window
php converter.php

脚本将自动连接数据库,导出所有已发布的文章,并显示导出进度:

✅ 数据库连接成功
📂 开始导出 23 篇文章...
[1] 已导出: 我的第一篇文章
[2] 已导出: 技术分享:PHP 开发技巧
...
🎉 导出完成!已将引用式图片转换为直接链接格式。
📁 文件保存在目录: astro_posts

第四步:获取导出的文件#

导出的 Markdown 文件将按照分类组织在 astro_posts/ 目录下:

astro_posts/
├── 技术文章/
│ ├── 我的第一篇文章.md
│ └── 技术分享-PHP-开发技巧.md
├── 生活随笔/
│ └── 周末游记.md
└── Uncategorized/
└── 未分类文章.md

每个 Markdown 文件都包含完整的 Frontmatter 和文章内容,可以直接用于 Astro + Firefly 主题。

导出的 Frontmatter 示例#

以下是导出的 Markdown 文件中的 Frontmatter 示例:

---
title: "我的第一篇文章"
published: 2019-06-19
updated: 2021-04-30
author: "MoeWah"
image: "api"
description: "这篇文章是关于 Typecho 导出工具的详细介绍..."
category: Astro教程
tags:
- "PHP"
- "Typecho"
- "博客迁移"
slug: "my-first-post"
draft: false
toc: true
---

结语#

这个导出工具是我从 Typecho 迁移到 Astro + Firefly 过程中的产物,希望它也能帮助到其他有类似需求的博主。工具已经优化了 Frontmatter 格式,处理了图片引用转换,并提供了灵活的配置选项。

最后,祝大家在博客迁移过程中一切顺利,享受 “Astro 框架 + Firefly 主题”带来的现代化博客体验!


注意事项:

  1. 请确保在运行脚本前备份你的 Typecho 数据库
  2. 脚本只会导出已发布的文章(status = 'publish'
  3. 如果文章数量很多,导出过程可能需要一些时间
  4. 导出的 Markdown 文件可以直接复制到 Astro 项目的 src/content/posts/ 目录中使用

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Typecho文章批量导出至Astro Markdown工具
https://blog.moewah.com/posts/typecho-to-markdown-batch-export-tool/
作者
GoWah
发布于
2026-01-10
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
GoWah
Hello, I'm GoWah.
分类
标签
站点统计
文章
160
分类
9
标签
350
总字数
301,106
运行时长
0
最后活动
0 天前

目录