4774 字
24 分钟

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

引言#

作为站长,当你的网站拥有较庞大的文章及页面体量时,网站搬家总会遇到各类困惑。我们总希望原站点的数据“应搬尽搬”,但事实上很多文章本身可能并没有获得多少阅读与共鸣。

基于流量二八原则,支撑一个网站流量的几乎都来自那 20% 的页面。通过 Google Search Console 等平台拉取近一年的数据,可以佐证这个铁铮铮的事实:

“大部分人写的文章没价值,网站的大多数文章没流量。”

因此,当你的网站拥有较庞大的文章及页面体量时,可以通过 Google Search Console 的数据获取占比 80% 流量的页面列表(流量 TOP 20 的页面) + 近半年产生的新页面清单,合并得到的页面 URL 清单,单独创建一个包含这些页面 URL 列表的 links.txt 文件。这一部分才是整个网站的核心价值(当然,重要的笔记等内容另当别论)。

我想从 Typecho 导出有用的文章(包含站内图片和附件),同时希望导出的 md 文章页面能直接适配 Astro 静态博客框架并兼容 Firefly 主题,该怎么办呢?

这个工具正是面向这个需求而定制的。基于 links.txt 清单,批量导出文章 md 文件,并同时下载页面的图片和附件进行存储,在 md 文档中自动替换为相对地址重新引用。


功能介绍#

  1. 按需导出:从 links.txt 读取指定文章链接进行导出,只导出清单内的文章。
  2. 分类组织:按 Typecho 文章分类目录导出到对应的子目录,保持原有分类结构。
  3. Astro 5.0 标准 Frontmatter:生成符合 Astro 5.0 标准的 YAML Frontmatter,包含标题、日期、作者、标签、分类、摘要等字段。
  4. 智能图片处理
    • 自动处理引用式图片(![alt][id])和直接链接图片(![alt](url)),转换为直接链接格式并下载到本地。
    • 清理图片 URL 中的查询参数(保留片段标识,避免误匹配 Markdown 标题)。
  5. 文章摘要生成:自动从内容中提取前 100 字作为描述(description)。
  6. 自定义封面图片:支持通过配置为文章添加封面图片(image 属性),可使用 Firefly 主题内置的随机二次元图片 API 或自定义 URL。
  7. 媒体资源下载
    • 可选下载图片和附件到本地。
    • 媒体文件按 images/[年份]/[文章ID]/attachments/[年份]/[文章ID]/ 组织。
    • Markdown 中的媒体链接自动替换为相对路径。
  8. 忽略头图:可选跳过第一张图片(头图)的下载,并删除其在正文中的引用标记。
  9. 安全文件名:自动清理文件名中的非法字符,限制长度,避免隐藏文件或非法文件。

注意事项#

  1. 运行环境:需要 PHP 7.4+,并启用 MySQL PDO 扩展。建议开启 cURL 扩展以获得更好的下载性能。
  2. 网站备份:使用前请务必全量备份 Typecho 数据库和网站文件,防止误操作!!!
  3. 文件权限:确保 PHP 有权限在当前目录创建文件夹和写入文件。
  4. 网络连接:下载图片和附件需要能访问原站点的域名(可通过 siteDomain 配置)。
  5. 链接文件格式links.txt 每行一个完整的文章 URL,如 https://www.moewah.com/archives/123.html,程序会自动提取文章 ID(123)。
  6. 导出目录:程序会在运行目录下创建 astro_posts(可配置)文件夹,所有导出的文章和媒体文件将存放在其中。

源代码#

<?php
/**
* Typecho 文章导出工具 (适配 Astro 5.0 + Firefly 主题)
*
* 功能特性:
* 1. 从链接文件读取指定文章链接进行导出(默认 links.txt)
* 2. 按分类目录导出文章到对应的子目录
* 3. 构建符合 Astro 5.0 标准的 YAML Frontmatter
* 4. 自动处理引用式图片,转换为直接链接格式并下载到本地
* 5. 清理图片 URL 中的查询参数(保留片段标识,避免误匹配Markdown标题)
* 6. 生成文章摘要(自动截取前100字)
* 7. 支持自定义文章封面图片配置
* 8. 可选下载媒体资源(图片/附件)到本地
* 9. 媒体文件按年份/文章ID组织:images/[年份]/[文章ID]/ 和 attachments/[年份]/[文章ID]/
* 10. Markdown 中媒体链接替换为相对路径
* 11. 可选忽略头图(跳过下载且删除引用标记)
*
* 使用前请先配置数据库连接信息和导出选项
* 运行后会在指定目录生成按分类组织的 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 属性
// 媒体下载配置
'downloadMedia' => true, // 是否下载媒体资源到本地
'ignoreFirstImage' => true, // 是否忽略第一张图片(头图):跳过下载且删除引用标记
'maxFileSize' => 10 * 1024 * 1024, // 10MB限制
'timeout' => 30, // 下载超时(秒)
'retryCount' => 2, // 失败重试次数
'skipFailed' => true, // 跳过失败的下载(保留原URL)
'userAgent' => 'Mozilla/5.0 (Typecho Exporter)',
'linksFile' => 'links.txt', // 文章链接列表文件路径
'siteDomain' => 'https://www.moewah.com', // 网站域名,用于构建完整图片URL
];
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
]);
// 1.1 从链接文件提取文章ID
$articleIds = extractArticleIdsFromLinks($dbConfig['linksFile']);
if (empty($articleIds)) {
die("" . $dbConfig['linksFile'] . "中没有有效的文章ID\n");
}
// 2. 执行 SQL 查询
$placeholders = implode(',', array_fill(0, count($articleIds), '?'));
$sql = "
SELECT
c.cid, c.title, c.text, c.slug, c.created, c.modified,
u.name as author,
(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
LEFT JOIN typecho_users u ON c.authorId = u.uid
WHERE c.type = 'post' AND c.status = 'publish' AND c.cid IN ($placeholders)
ORDER BY c.created DESC
";
// 执行查询获取所有已发布的文章
$stmt = $pdo->prepare($sql);
$stmt->execute($articleIds);
$posts = $stmt->fetchAll();
if (!$posts) die("❌ 没有找到文章\n");
foreach ($posts as $index => $post) {
echo "\n[" . ($index + 1) . "/" . count($posts) . "] 处理文章: {$post['title']} (ID: {$post['cid']})\n";
// 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: " . json_encode($post['author'], JSON_UNESCAPED_UNICODE) . "\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. 处理正文内容并下载媒体资源
$createYear = date('Y', $post['created']);
// 初始化媒体下载器
$downloader = null;
if ($dbConfig['downloadMedia']) {
$downloader = new MediaDownloader($dbConfig['baseDir'], $dbConfig);
}
// 处理内容:下载媒体资源并替换为相对路径
if ($downloader) {
$content = processContentWithMediaDownload($content, $downloader, $post['cid'], $createYear, $dbConfig['ignoreFirstImage']);
}
// 9. 合并 Frontmatter 和正文内容
$fullOutput = $yaml . $content;
// 10. 写入文件到指定路径
if (file_put_contents($exportPath, $fullOutput)) {
echo "[" . ($index + 1) . "/" . count($posts) . "] {$post['title']}\n";
}
}
echo "✅ 完成!共导出 " . count($posts) . " 篇文章\n";
} catch (Exception $e) {
die("❌ 错误: " . $e->getMessage() . "\n");
}
/**
* 从链接文件提取文章ID
*
* @param string $linksFile 链接文件路径
* @return array 文章ID数组
*/
function extractArticleIdsFromLinks($linksFile) {
$ids = [];
if (!file_exists($linksFile)) {
die("❌ 找不到链接文件: {$linksFile}\n");
}
$lines = file($linksFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $url) {
if (preg_match('/\/(\d+)\.html$/', $url, $matches)) {
$ids[] = (int)$matches[1];
}
}
return array_unique($ids);
}
/**
* 媒体下载器类
*/
class MediaDownloader {
// 支持的图片后缀
const IMAGE_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'avif', 'ico'];
// 支持下载的附件后缀(白名单)
const ATTACHMENT_EXTS = [
// 文档类
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'odt',
// 压缩包
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
// 群晖相关
'spk',
// 音视频
'mp3', 'mp4', 'avi', 'mkv', 'mov', 'flv', 'wav', 'ogg',
// 其他
'apk', 'exe', 'dmg', 'iso', 'img'
];
private $baseDir;
private $downloaded = []; // URL缓存
private $config;
private $hasCurl;
private $downloadCount = [
'images' => 0,
'attachments' => 0,
'failed' => 0
];
public function __construct($baseDir, $config) {
$this->baseDir = rtrim($baseDir, '/\\');
$this->config = $config;
// 检查cURL扩展
$this->hasCurl = function_exists('curl_init');
}
/**
* 构建完整URL
*/
private function buildFullUrl($url) {
// 如果已经是完整URL,直接返回
if (preg_match('/^https?:\/\//i', $url)) {
return $url;
}
// 如果是相对路径,添加域名
$domain = $this->config['siteDomain'] ?? '';
if ($domain && strpos($url, '/') === 0) {
// 移除域名末尾的斜杠
$domain = rtrim($domain, '/');
return $domain . $url;
}
// 无法处理的URL
return $url;
}
/**
* 下载媒体资源并返回相对路径
*/
public function download($url, $articleId, $createYear) {
// 构建完整URL(处理相对路径)
$fullUrl = $this->buildFullUrl($url);
// 清理URL参数:移除查询字符串和Typecho特有的片段参数
// 处理格式: https://example.com/image.jpg?width=100#vwid=760&vhei=440
$cleanUrl = preg_replace('/\?[^#\s]+/', '', $fullUrl); // 移除查询参数
$cleanUrl = preg_replace('/#.*/', '', $cleanUrl); // 移除整个片段(Typecho使用片段存储尺寸参数)
// 检查缓存
$urlHash = md5($cleanUrl);
if (isset($this->downloaded[$urlHash])) {
return $this->downloaded[$urlHash]['relative_path'];
}
// 确定资源类型
$type = $this->getMediaType($cleanUrl);
if (!$type) {
return $url;
}
// 创建目标目录
$targetDir = "{$this->baseDir}/{$type}s/{$createYear}/{$articleId}";
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
// 生成文件名
$filename = $this->generateFilename($cleanUrl, $articleId, $type);
$fullPath = "{$targetDir}/{$filename}";
// 下载文件
$success = $this->downloadFile($cleanUrl, $fullPath);
if ($success) {
$relativePath = "../{$type}s/{$createYear}/{$articleId}/{$filename}";
$this->downloaded[$urlHash] = [
'url' => $cleanUrl,
'relative_path' => $relativePath,
'type' => $type,
'full_path' => $fullPath
];
$this->downloadCount[$type . 's']++;
return $relativePath;
} else {
$this->downloadCount['failed']++;
return $url; // 失败返回原URL
}
}
/**
* 判断是否为内部域名
*/
private function isInternalUrl($url) {
$domain = $this->config['siteDomain'] ?? '';
if (!$domain) return false;
$host = parse_url($url, PHP_URL_HOST);
if (!$host) return false;
// 移除协议和端口,只比较域名
$cleanConfigDomain = parse_url($domain, PHP_URL_HOST) ?: $domain;
$cleanUrlDomain = parse_url($host, PHP_URL_HOST) ?: $host;
return $cleanConfigDomain === $cleanUrlDomain;
}
/**
* 判断媒体类型
*/
private function getMediaType($url) {
// 1. 检查协议:只支持 http/https
$scheme = parse_url($url, PHP_URL_SCHEME);
if ($scheme && !in_array($scheme, ['http', 'https'])) {
return null;
}
// 2. 检查是否为内部域名:外部链接不下载
if (!$this->isInternalUrl($url)) {
return null;
}
$path = parse_url($url, PHP_URL_PATH);
if (!$path || $path === '/') {
return null;
}
// 3. 提取文件后缀
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
// 4. 无后缀URL跳过(内部链接但不带后缀,不处理)
if (!$ext) {
return null;
}
// 5. 根据后缀判断类型
if (in_array($ext, self::IMAGE_EXTS)) {
return 'image';
}
if (in_array($ext, self::ATTACHMENT_EXTS)) {
return 'attachment';
}
// 6. 其他后缀不下载
return null;
}
/**
* 生成安全文件名
*/
private function generateFilename($url, $articleId, $type) {
$path = parse_url($url, PHP_URL_PATH);
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$name = pathinfo($path, PATHINFO_FILENAME);
// 生成唯一文件名:文章ID_时间戳_哈希.扩展名
$timestamp = time();
$hash = substr(md5($url), 0, 6);
$safeName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $name);
if (empty($safeName)) {
$safeName = "{$type}_{$timestamp}";
}
return "{$articleId}_{$safeName}_{$hash}.{$ext}";
}
/**
* 文件下载实现
*/
private function downloadFile($url, $localPath) {
$timeout = $this->config['timeout'] ?? 30;
$maxSize = $this->config['maxFileSize'] ?? 10 * 1024 * 1024;
if ($this->hasCurl) {
// 使用 cURL 下载
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_MAXFILESIZE, $maxSize);
curl_setopt($ch, CURLOPT_USERAGENT, $this->config['userAgent'] ?? 'Mozilla/5.0 (Typecho Exporter)');
$data = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($httpCode === 200 && $data && strlen($data) > 0) {
return file_put_contents($localPath, $data) !== false;
}
} else {
// 使用 file_get_contents 作为备选
$context = stream_context_create([
'http' => [
'timeout' => $timeout,
'user_agent' => $this->config['userAgent'] ?? 'Mozilla/5.0 (Typecho Exporter)',
]
]);
$data = @file_get_contents($url, false, $context);
if ($data !== false && strlen($data) > 0 && strlen($data) <= $maxSize) {
return file_put_contents($localPath, $data) !== false;
}
}
return false;
}
/**
* 获取下载统计
*/
public function getStats() {
return $this->downloadCount;
}
}
/**
* 处理内容并下载媒体资源
*
* @param string $content 文章内容
* @param MediaDownloader $downloader 媒体下载器实例
* @param int $articleId 文章ID
* @param int $createYear 文章创建年份
* @param bool $ignoreFirstImage 是否忽略头图(跳过下载且删除引用标记)
* @return string 处理后的内容
*/
function processContentWithMediaDownload($content, $downloader, $articleId, $createYear, $ignoreFirstImage = true) {
echo " 🔍 开始处理媒体资源 (文章ID: {$articleId}, 年份: {$createYear})\n";
// 收集引用定义(需在URL清理之前执行)
$allRefs = [];
if (preg_match_all('/\s*\[([^\]]+)\]:\s*(\S+)/im', $content, $refMatches, PREG_SET_ORDER)) {
foreach ($refMatches as $match) {
$refId = trim($match[1]);
$url = $match[2];
$allRefs[$refId] = $url;
}
}
// 下载引用定义中的所有附件(即使未被引用)
foreach ($allRefs as $refId => $url) {
$downloader->download($url, $articleId, $createYear);
}
// 忽略头图:删除正文开头直接引用的图片(无任何文本段落的图片)
// 判断标准:内容最开头就是图片标记才算头图,段落后的图片保留
if ($ignoreFirstImage) {
$pattern = '/^!\[([^\]]*)\]\(([^)]*)\)\s*/';
$content = preg_replace($pattern, '', $content, 1);
$pattern = '/^!\[([^\]]*)\]\[([^\]]*)\]\s*/';
$content = preg_replace($pattern, '', $content, 1);
}
// 清理URL参数:仅清理直接链接中的查询参数(不清理#,避免误匹配Markdown标题)
$content = preg_replace_callback(
'/!\[([^\]]*)\]\(([^)]*)\)/',
function($matches) {
$altText = $matches[1];
$url = $matches[2];
$cleanUrl = preg_replace('/\?[^)]*/', '', $url);
return "![{$altText}]({$cleanUrl})";
},
$content
);
// 处理引用式链接:[text][id]
foreach ($allRefs as $refId => $url) {
$pattern = '/\[([^\]]+)\]\[\s*' . preg_quote($refId, '/') . '\s*\]/';
$content = preg_replace_callback($pattern, function($matches) use ($url, $downloader, $articleId, $createYear) {
$linkText = $matches[1];
$localPath = $downloader->download($url, $articleId, $createYear);
return "[{$linkText}]({$localPath})";
}, $content);
}
// 处理引用式图片:![alt][id]
foreach ($allRefs as $refId => $url) {
$pattern = '/!\[([^\]]*)\]\[\s*' . preg_quote($refId, '/') . '\s*\]/';
$content = preg_replace_callback($pattern, function($matches) use ($url, $downloader, $articleId, $createYear) {
$altText = $matches[1];
$localPath = $downloader->download($url, $articleId, $createYear);
return "![{$altText}]({$localPath})";
}, $content);
}
// 处理直接链接图片:![alt](url)
$content = preg_replace_callback(
'/!\[(.*?)\]\(([^\s\)]+)\)/i',
function($matches) use ($downloader, $articleId, $createYear) {
$altText = $matches[1];
$imageUrl = $matches[2];
$localPath = $downloader->download($imageUrl, $articleId, $createYear);
return "![{$altText}]({$localPath})";
},
$content
);
// 处理附件链接:[文字](url)
$content = preg_replace_callback(
'/\[([^\]]+)\]\(([^\s)]+)\)/',
function($matches) use ($downloader, $articleId, $createYear) {
$linkText = $matches[1];
$fileUrl = $matches[2];
$localPath = $downloader->download($fileUrl, $articleId, $createYear);
if ($localPath !== $fileUrl) {
return "[{$linkText}]({$localPath})";
}
return $matches[0];
},
$content
);
// 移除引用定义行
$content = preg_replace('/^\s*\[[^\]]+\]:\s*\S+.*$/m', '', $content);
// 清理多余空行
$content = preg_replace('/\n\s*\n\s*\n/', "\n\n", $content);
return trim($content);
}
/**
* 文件名清理函数
*
* @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, '.-');
}
?>

👆完整复制上方代码,保存命名为typecho-export-md.php 文件。


使用方法步骤#

第 1 步:准备链接文件#

从 Google Search Console 或其他渠道获取高流量页面 URL 列表,创建一个名为 links.txt 的文件,每行一个完整的文章 URL,例如:

https://www.moewah.com/archives/123.html
https://www.moewah.com/archives/456.html

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

用文本编辑器打开 typecho-export-md.php,找到 $dbConfig 数组,修改以下配置:

  • host:Typecho 数据库主机地址(通常是 localhost
  • name:Typecho 数据库名
  • user:数据库用户名
  • pass:数据库密码
  • siteDomain:你的网站域名(用于构建完整的图片/附件 URL)

第 3 步:调整导出选项(可选)#

根据需要调整其他配置项:

  • baseDir:导出文件存放的目录(默认 astro_posts
  • image:文章封面图片配置(默认 'api' 使用 Firefly 主题内置随机二次元图片 API)
  • downloadMedia:是否下载媒体资源(图片/附件)到本地
  • ignoreFirstImage:是否忽略第一张图片(头图)

第 4 步:运行导出脚本#

typecho-export-md.phplinks.txt 上传到网站根目录(两文件放在同一目录),通过命令行或浏览器访问该 PHP 文件(确保 PHP 环境支持)。例如在命令行中:

Terminal window
php typecho-export-md.php

第 5 步:获取导出结果#

程序运行后,会在当前目录下创建 astro_posts 文件夹(或你指定的目录),其中按分类存放了所有导出的 Markdown 文件,同时图片和附件会按年份/文章ID组织在 images/attachments/ 子目录中。

第 6 步:导入 Astro 项目#

将导出的 astro_posts 整个文件夹复制到你的 Astro 项目的 src/content/blog/(或你配置的 content 目录)下,即可直接使用。Frontmatter 已适配 Astro 5.0 标准,并兼容 Firefly 主题。


核心配置说明#

配置项默认值说明
host'localhost'Typecho 数据库主机地址
name'数据库名'Typecho 数据库名称
user'数据库用户名'数据库用户名
pass'数据库密码'数据库密码
baseDir'astro_posts'导出文件存放的根目录
image'api'文章封面图片配置。'api' 表示使用 Firefly 主题内置的随机二次元图片 API;可改为其他图片 URL 或留空 '' 不生成 image 属性
downloadMediatrue是否下载图片和附件到本地
ignoreFirstImagetrue是否忽略第一张图片(头图),跳过下载且删除引用标记
maxFileSize10 * 1024 * 1024下载文件大小限制(10MB)
timeout30下载超时时间(秒)
retryCount2下载失败重试次数
skipFailedtrue跳过失败的下载(保留原 URL)
userAgent'Mozilla/5.0 (Typecho Exporter)'下载时使用的 User-Agent
linksFile'links.txt'文章链接列表文件路径(相对于脚本所在目录)
siteDomain'https://www.moewah.com'网站域名,用于构建完整的图片/附件 URL(重要!)

结语#

这个工具帮助你从 Typecho 中精准导出 有价值的文章,并自动处理媒体资源,生成完全适配 Astro 5.0 + Firefly 主题 的 Markdown 文件。通过结合 Google Search Console 的数据,你可以在搬家时专注于保留真正带来流量和价值的页面,避免无谓的数据迁移。

如果你在使用过程中遇到问题,请检查以下几点:

  1. 数据库连接信息是否正确
  2. PHP 是否已安装 MySQL PDO 扩展
  3. links.txt 文件是否存在且格式正确
  4. 网站域名配置是否与图片/附件的原始域名一致

祝你的博客迁移顺利,在新平台上焕发新生!

支持与分享

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

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

目录