#!/usr/bin/env bash # pdf2md.sh — 将 PDF 文件转换为结构化 Markdown # 依赖: poppler-utils (pdftotext) # 用法: pdf2md.sh [-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 [选项] 选项: -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