Files
2026-04-19 21:47:08 +08:00

233 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# pdf2md.sh — 将 PDF 文件转换为结构化 Markdown
# 依赖: poppler-utils (pdftotext)
# 用法: pdf2md.sh <input.pdf> [-o output.md] [-f N] [-l N] [--raw] [--no-toc]
set -euo pipefail
# === 默认值 ===
INPUT=""
OUTPUT=""
FIRST_PAGE=""
LAST_PAGE=""
RAW_MODE=false
NO_TOC=false
# === 参数解析 ===
usage() {
cat <<'EOF'
用法: pdf2md.sh <input.pdf> [选项]
选项:
-o FILE 输出文件路径 (默认: 同名 .md, "-" 表示 stdout)
-f N 起始页码
-l N 结束页码
--raw 跳过后处理,输出原始文本
--no-toc 不生成目录
-h, --help 显示帮助信息
示例:
pdf2md.sh doc.pdf
pdf2md.sh doc.pdf -o output.md
pdf2md.sh doc.pdf -f 1 -l 10
pdf2md.sh doc.pdf --raw -o -
EOF
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-o) OUTPUT="$2"; shift 2 ;;
-f) FIRST_PAGE="$2"; shift 2 ;;
-l) LAST_PAGE="$2"; shift 2 ;;
--raw) RAW_MODE=true; shift ;;
--no-toc) NO_TOC=true; shift ;;
-h|--help) usage ;;
-*) echo "未知选项: $1" >&2; exit 1 ;;
*)
if [[ -z "$INPUT" ]]; then
INPUT="$1"
else
echo "错误: 多个输入文件" >&2; exit 1
fi
shift
;;
esac
done
# === 校验输入 ===
if [[ -z "$INPUT" ]]; then
echo "错误: 未指定输入 PDF 文件" >&2
usage
fi
if [[ ! -f "$INPUT" ]]; then
echo "错误: 文件不存在: $INPUT" >&2
exit 1
fi
if ! command -v pdftotext &>/dev/null; then
echo "错误: pdftotext 未安装。请运行: sudo apt-get install poppler-utils" >&2
exit 1
fi
# === 确定输出路径 ===
if [[ -z "$OUTPUT" ]]; then
OUTPUT="${INPUT%.pdf}.md"
fi
# === 构建 pdftotext 命令 ===
PDFTOTEXT_ARGS=(-layout -enc UTF-8 -eol unix -nopgbrk)
[[ -n "$FIRST_PAGE" ]] && PDFTOTEXT_ARGS+=(-f "$FIRST_PAGE")
[[ -n "$LAST_PAGE" ]] && PDFTOTEXT_ARGS+=(-l "$LAST_PAGE")
# === 提取文本 ===
RAW_TEXT=$(pdftotext "${PDFTOTEXT_ARGS[@]}" "$INPUT" - 2>/dev/null) || {
echo "错误: pdftotext 执行失败" >&2
exit 1
}
# === 原始模式直接输出 ===
if [[ "$RAW_MODE" == true ]]; then
if [[ "$OUTPUT" == "-" ]]; then
echo "$RAW_TEXT"
else
echo "$RAW_TEXT" > "$OUTPUT"
echo "已输出原始文本: $OUTPUT"
fi
exit 0
fi
# === 后处理: 转换为结构化 Markdown ===
# 使用 Python 做后处理(更可靠的正则和文本分析)
RESULT=$(python3 -c "
import sys
import re
def is_list_item(line):
'''检测是否为列表项'''
return bool(re.match(r'^[-\u2022*]\s', line) or re.match(r'^\d+[.)]\s', line))
def is_separator(line):
'''检测是否为分隔线'''
return bool(re.match(r'^[-=_*]{3,}$', line))
def is_all_upper(line):
'''检测是否全大写(允许数字、标点、空格)'''
stripped = re.sub(r'[A-Z0-9\s:._\-/\\()&,;!?\x27\x22]', '', line)
return len(stripped) == 0 and len(line) > 3
def process(text):
lines = text.split('\n')
output = []
toc = []
prev_blank = True
in_code = False
for line in lines:
stripped = line.strip()
length = len(stripped)
leading = len(line) - len(line.lstrip())
# 空行
if length == 0:
if in_code:
output.append('\`\`\`')
in_code = False
prev_blank = True
continue
# --- 代码块检测: 连续缩进 >= 4 空格 ---
if leading >= 4 and not in_code:
# 检查前一行是否也是缩进的(或当前已连续)
output.append('\`\`\`')
output.append(line[4:])
in_code = True
prev_blank = False
continue
elif in_code and leading >= 4:
output.append(line[4:])
prev_blank = False
continue
elif in_code and leading < 4:
output.append('\`\`\`')
in_code = False
# --- 列表项 (优先级高于标题检测) ---
if is_list_item(stripped):
# 将 bullet 统一为 -
fixed = re.sub(r'^[\u2022*]\s', '- ', stripped)
output.append(fixed)
prev_blank = False
continue
# --- 分隔线 ---
if is_separator(stripped):
output.append('')
output.append('---')
prev_blank = True
continue
# --- 表格检测: 含 | 或连续制表符 ---
if '|' in stripped or (leading > 0 and '\t' in stripped):
output.append(stripped)
prev_blank = False
continue
# --- 标题检测 ---
# 条件: 前有空行 + 短行(<=60字符) + 非列表项 + 非分隔线
if prev_blank and 3 < length <= 80:
if is_all_upper(stripped):
# 全大写 → ## 级标题
title = stripped
output.append('')
output.append(f'## {title}')
toc.append(f'- {title}')
prev_blank = False
continue
elif length <= 60:
# 短行 + 前空行 → ### 级标题
output.append('')
output.append(f'### {stripped}')
prev_blank = False
continue
# --- 普通文本 ---
output.append(stripped)
prev_blank = False
if in_code:
output.append('\`\`\`')
return output, toc
raw = sys.stdin.read()
out_lines, toc = process(raw)
# 文档标题
filename = '${INPUT##*/}'
filename = filename.rsplit('.', 1)[0]
print(f'# {filename}')
print()
# 目录
if toc and len(toc) > 0:
print('## 目录')
print()
for item in toc:
print(item)
print()
# 正文
for line in out_lines:
print(line)
" <<< "$RAW_TEXT")
# === 写入输出 ===
if [[ "$OUTPUT" == "-" ]]; then
echo "$RESULT"
else
echo "$RESULT" > "$OUTPUT"
echo "已生成: $OUTPUT ($(wc -l < "$OUTPUT") 行)"
fi