一个简单的羊毛claude-4.6最新版本的方法

如果翻墙后,https://arena.ai/,这个网站是个大模型竞技场的对比网站,里面N个网站,都可以免费使用,包括更新速度快,最新的比如claude 4.6都可以免费用的,但可惜

这个网站只能支持上传PDF,那么比如你有很多程序文件,假设放在某个文件夹下,则可以编写这一个PYTHO程序,运行后,把你的所有某个文件夹下的文件,每个内容都放到一个PDF中去,因为网站只支持每个PDF90页内,所以呢,你的文件多的话,则可以用这个程序,自动分卷为多个PDF,再上传到这个网站,就可以自动帮你分析你这些PDF里面的内容了,就等于你把你的很多个程序文件都上传到这个网站,可以羊毛claude-4.6了!下面给出程序:

python 复制代码
"""
代码文件夹导出为PDF工具 - 支持中文 + 自动分卷
将指定文件夹下所有代码文件的【完整代码内容】导出为PDF
超过指定页数时自动分卷,保证同一文件不会跨PDF
"""

import os
import sys
import io
from datetime import datetime
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib.colors import HexColor
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, PageBreak, Flowable
)
from reportlab.lib.enums import TA_CENTER
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont


# ============================================================
# 配置
# ============================================================
MAX_PAGES_PER_PDF = 90  # 每个PDF最大页数

CODE_EXTENSIONS = {
    '.py', '.html', '.htm', '.css', '.js', '.json', '.xml',
    '.yaml', '.yml', '.txt', '.md', '.sql', '.sh', '.bat',
    '.java', '.cpp', '.c', '.h', '.ts', '.jsx', '.tsx', '.vue',
    '.conf', '.ini', '.cfg'
}

IGNORE_DIRS = {
    '__pycache__', '.git', '.svn', 'node_modules',
    '.idea', '.vscode', 'venv', 'env', '.env',
    'dist', 'build', '.tox', '.pytest_cache'
}

IGNORE_FILES = {'.DS_Store', 'Thumbs.db', '.gitignore'}


# ============================================================
# 注册中文字体
# ============================================================
def register_fonts():
    """注册字体,返回 (正文字体名, 代码字体名)"""

    if sys.platform == 'win32':
        font_dir = r'C:\Windows\Fonts'
        text_candidates = [
            (os.path.join(font_dir, 'msyh.ttc'), 'MSYaHei'),
            (os.path.join(font_dir, 'simhei.ttf'), 'SimHei'),
            (os.path.join(font_dir, 'simsun.ttc'), 'SimSun'),
        ]
    elif sys.platform == 'darwin':
        text_candidates = [
            ('/System/Library/Fonts/PingFang.ttc', 'PingFang'),
            ('/System/Library/Fonts/STHeiti Light.ttc', 'STHeiti'),
        ]
    else:
        text_candidates = [
            ('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', 'WQYMicroHei'),
            ('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', 'NotoSansCJK'),
        ]

    text_font = 'Helvetica'
    for fpath, fname in text_candidates:
        if os.path.exists(fpath):
            try:
                pdfmetrics.registerFont(TTFont(fname, fpath))
                text_font = fname
                print(f"  [OK] 字体: {fname}")
                break
            except Exception as e:
                print(f"  [WARN] {fname} 失败: {e}")

    # 代码字体直接用中文字体,保证中文能显示
    code_font = text_font
    print(f"  [INFO] 代码字体: {code_font}")

    return text_font, code_font


# ============================================================
# 水平线
# ============================================================
class HLine(Flowable):
    def __init__(self, width, color=HexColor('#CCCCCC'), thickness=1):
        Flowable.__init__(self)
        self.line_width = width
        self.color = color
        self.thickness = thickness
        self.height = thickness + 4

    def draw(self):
        self.canv.setStrokeColor(self.color)
        self.canv.setLineWidth(self.thickness)
        self.canv.line(0, 2, self.line_width, 2)


# ============================================================
# 页面计数器
# ============================================================
class PageCountDocTemplate(SimpleDocTemplate):
    """继承SimpleDocTemplate,记录总页数"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.page_count = 0

    def afterPage(self):
        self.page_count += 1


# ============================================================
# 工具函数
# ============================================================
def scan_folder(root_path):
    result = {}
    for dirpath, dirnames, filenames in os.walk(root_path):
        dirnames[:] = sorted([d for d in dirnames if d not in IGNORE_DIRS])
        rel_dir = os.path.relpath(dirpath, root_path)
        if rel_dir == '.':
            rel_dir = ''

        code_files = []
        for fname in sorted(filenames):
            if fname in IGNORE_FILES:
                continue
            ext = os.path.splitext(fname)[1].lower()
            if ext in CODE_EXTENSIONS:
                code_files.append((fname, os.path.join(dirpath, fname)))

        if code_files:
            result[rel_dir] = code_files

    sorted_result = []
    if '' in result:
        sorted_result.append(('', result['']))
    for key in sorted(result.keys()):
        if key != '':
            sorted_result.append((key, result[key]))
    return sorted_result


def read_file(file_path):
    try:
        size = os.path.getsize(file_path)
        if size > 5 * 1024 * 1024:
            return f"[文件过大跳过: {size / 1024 / 1024:.1f} MB]"
        for enc in ['utf-8', 'gbk', 'gb2312', 'latin-1']:
            try:
                with open(file_path, 'r', encoding=enc) as f:
                    return f.read()
            except (UnicodeDecodeError, UnicodeError):
                continue
        return "[无法解码文件内容]"
    except Exception as e:
        return f"[读取出错: {e}]"


def get_icon(ext):
    icons = {
        '.py': '[PY]', '.html': '[HTML]', '.htm': '[HTML]', '.css': '[CSS]',
        '.js': '[JS]', '.json': '[JSON]', '.xml': '[XML]', '.md': '[MD]',
        '.txt': '[TXT]', '.sql': '[SQL]', '.yaml': '[YAML]', '.yml': '[YAML]',
        '.sh': '[SH]', '.bat': '[BAT]', '.java': '[JAVA]', '.vue': '[VUE]',
    }
    return icons.get(ext, '[FILE]')


def format_size(size):
    if size < 1024:
        return f"{size} B"
    elif size < 1024 * 1024:
        return f"{size / 1024:.1f} KB"
    else:
        return f"{size / 1024 / 1024:.1f} MB"


def escape_xml(text):
    return (text
            .replace('&', '&amp;')
            .replace('<', '&lt;')
            .replace('>', '&gt;')
            .replace('"', '&quot;')
            .replace("'", '&#39;'))


# ============================================================
# 创建样式
# ============================================================
def create_styles(text_font, code_font):
    """创建所有需要的样式"""
    styles = {}

    styles['cover_title'] = ParagraphStyle(
        'CoverTitle', fontName=text_font, fontSize=26,
        alignment=TA_CENTER, textColor=HexColor('#2C3E50'), spaceAfter=20)

    styles['cover_info'] = ParagraphStyle(
        'CoverInfo', fontName=text_font, fontSize=12,
        alignment=TA_CENTER, textColor=HexColor('#7F8C8D'), spaceAfter=8)

    styles['toc_title'] = ParagraphStyle(
        'TOCTitle', fontName=text_font, fontSize=20,
        textColor=HexColor('#2C3E50'), spaceBefore=5, spaceAfter=12)

    styles['toc_dir'] = ParagraphStyle(
        'TOCDir', fontName=text_font, fontSize=12,
        textColor=HexColor('#E67E22'), spaceBefore=10, spaceAfter=3, leftIndent=10)

    styles['toc_file'] = ParagraphStyle(
        'TOCFile', fontName=text_font, fontSize=10,
        textColor=HexColor('#34495E'), spaceBefore=1, spaceAfter=1, leftIndent=30)

    styles['section'] = ParagraphStyle(
        'Section', fontName=text_font, fontSize=18,
        textColor=HexColor('#2980B9'), spaceBefore=10, spaceAfter=8)

    styles['fname'] = ParagraphStyle(
        'FName', fontName=text_font, fontSize=13,
        textColor=HexColor('#FFFFFF'), backColor=HexColor('#34495E'),
        spaceBefore=12, spaceAfter=4, borderPadding=(6, 10, 6, 10))

    styles['finfo'] = ParagraphStyle(
        'FInfo', fontName=text_font, fontSize=8,
        textColor=HexColor('#95A5A6'), spaceAfter=6, leftIndent=5)

    styles['code'] = ParagraphStyle(
        'Code', fontName=code_font, fontSize=7.5, leading=10,
        leftIndent=5, rightIndent=5, spaceBefore=3, spaceAfter=3,
        backColor=HexColor('#F7F8FA'), borderWidth=0.5,
        borderColor=HexColor('#DEE2E6'), borderPadding=6,
        wordWrap='CJK')

    styles['empty'] = ParagraphStyle(
        'Empty', fontName=text_font, fontSize=9,
        textColor=HexColor('#999999'), leftIndent=10)

    styles['vol_info'] = ParagraphStyle(
        'VolInfo', fontName=text_font, fontSize=14,
        alignment=TA_CENTER, textColor=HexColor('#E74C3C'), spaceAfter=10)

    return styles


# ============================================================
# 为单个文件构建 story 元素
# ============================================================
def build_file_story(fname, fpath, root_path, styles, usable_width):
    """为一个文件生成 story 元素列表"""
    elements = []
    ext = os.path.splitext(fname)[1].lower()
    rel_path = os.path.relpath(fpath, root_path)

    # 文件标题
    elements.append(Paragraph(f"{get_icon(ext)}  {fname}", styles['fname']))

    # 文件信息
    size = format_size(os.path.getsize(fpath))
    mtime = datetime.fromtimestamp(os.path.getmtime(fpath))
    elements.append(Paragraph(
        f"路径: {rel_path}  |  大小: {size}  |  "
        f"修改: {mtime.strftime('%Y-%m-%d %H:%M:%S')}", styles['finfo']))

    # 读取完整代码
    content = read_file(fpath)

    if content.strip():
        lines = content.split('\n')
        num_w = len(str(len(lines)))

        coded_lines = []
        for line_no, line in enumerate(lines, 1):
            safe = escape_xml(line)
            safe = safe.replace(' ', '&nbsp;')
            safe = safe.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;')
            coded_lines.append(
                f"<font color='#AAAAAA'>{str(line_no).rjust(num_w)}</font>"
                f"<font color='#DDDDDD'>&nbsp;|&nbsp;</font>"
                f"<font color='#333333'>{safe}</font>"
            )

        code_html = '<br/>'.join(coded_lines)

        try:
            elements.append(Paragraph(code_html, styles['code']))
        except Exception:
            try:
                simple = escape_xml(content)
                simple = simple.replace(' ', '&nbsp;')
                simple = simple.replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;')
                simple = simple.replace('\n', '<br/>')
                elements.append(Paragraph(
                    f"<font color='#333333'>{simple}</font>", styles['code']))
            except Exception:
                elements.append(Paragraph(
                    f"[{fname} 内容渲染失败]", styles['empty']))
    else:
        elements.append(Paragraph("[空文件]", styles['empty']))

    elements.append(Spacer(1, 8))
    elements.append(HLine(usable_width, HexColor('#EEEEEE'), 0.5))

    return elements


# ============================================================
# 试算单个文件占多少页
# ============================================================
def estimate_pages(file_elements, usable_width):
    """
    将文件元素写入一个临时PDF(内存中),获取实际页数
    """
    buf = io.BytesIO()
    tmp_doc = PageCountDocTemplate(
        buf, pagesize=A4,
        leftMargin=1.5 * cm, rightMargin=1.5 * cm,
        topMargin=1.5 * cm, bottomMargin=1.5 * cm,
    )
    try:
        tmp_doc.build(list(file_elements))  # 用副本
        pages = tmp_doc.page_count
    except Exception:
        pages = 1
    buf.close()
    return pages


# ============================================================
# 构建封面和目录
# ============================================================
def build_cover(folder_name, root_path, file_list, styles, usable_width,
                vol_num=None, total_vols=None):
    """构建封面 + 目录页"""
    story = []

    # 封面
    story.append(Spacer(1, 80))
    story.append(Paragraph("代码文件导出", styles['cover_title']))
    story.append(Spacer(1, 20))

    if vol_num is not None:
        story.append(Paragraph(
            f"第 {vol_num} 卷 / 共 {total_vols} 卷", styles['vol_info']))
        story.append(Spacer(1, 10))

    story.append(Paragraph(f"项目: {folder_name}", styles['cover_info']))
    story.append(Paragraph(f"路径: {root_path}", styles['cover_info']))
    story.append(Paragraph(
        f"本卷文件数: {len(file_list)}", styles['cover_info']))
    story.append(Paragraph(
        f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
        styles['cover_info']))
    story.append(PageBreak())

    # 目录
    story.append(Paragraph("目 录", styles['toc_title']))
    story.append(HLine(usable_width, HexColor('#2C3E50'), 2))
    story.append(Spacer(1, 8))

    # file_list: [(rel_dir, fname, fpath), ...]
    current_dir = None
    idx = 0
    for rel_dir, fname, fpath in file_list:
        if rel_dir != current_dir:
            current_dir = rel_dir
            dir_label = "根目录" if rel_dir == '' else rel_dir
            story.append(Paragraph(f"[DIR] {dir_label}/", styles['toc_dir']))

        idx += 1
        ext = os.path.splitext(fname)[1].lower()
        size = format_size(os.path.getsize(fpath))
        story.append(Paragraph(
            f"{idx}. {get_icon(ext)} {fname}  ({size})", styles['toc_file']))

    story.append(PageBreak())
    return story


# ============================================================
# 主函数
# ============================================================
def generate_pdf(root_path, output_pdf=None, max_pages=MAX_PAGES_PER_PDF):
    root_path = os.path.abspath(root_path)
    folder_name = os.path.basename(root_path)

    if not os.path.isdir(root_path):
        print(f"  [ERROR] 不是有效文件夹: {root_path}")
        sys.exit(1)

    if output_pdf is None:
        output_pdf = f"{folder_name}_code_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"

    # 去掉扩展名,后面拼接卷号
    base_name, base_ext = os.path.splitext(output_pdf)
    if not base_ext:
        base_ext = '.pdf'

    print(f"\n{'=' * 60}")
    print(f"  源文件夹:    {root_path}")
    print(f"  输出文件名:  {base_name}{base_ext}")
    print(f"  每卷最大页数: {max_pages}")
    print(f"{'=' * 60}")

    # 1. 注册字体
    text_font, code_font = register_fonts()

    # 2. 扫描文件
    file_structure = scan_folder(root_path)
    total_files = sum(len(f) for _, f in file_structure)

    if total_files == 0:
        print("\n  [ERROR] 未找到代码文件!")
        return

    # 展开为扁平列表: [(rel_dir, fname, fpath), ...]
    flat_files = []
    for rel_dir, files in file_structure:
        for fname, fpath in files:
            flat_files.append((rel_dir, fname, fpath))

    print(f"\n  共 {total_files} 个文件:")
    for rel_dir, fname, fpath in flat_files:
        prefix = f"  /{folder_name}/" if rel_dir == '' else f"  /{folder_name}/{rel_dir}/"
        print(f"    {prefix}{fname}")

    # 3. 创建样式
    styles = create_styles(text_font, code_font)

    # 临时doc参数(用于估算页数)
    doc_kwargs = dict(
        pagesize=A4,
        leftMargin=1.5 * cm, rightMargin=1.5 * cm,
        topMargin=1.5 * cm, bottomMargin=1.5 * cm,
    )

    # 计算可用宽度
    page_w, page_h = A4
    usable_width = page_w - 1.5 * cm * 2

    # ============================================================
    # 4. 预计算每个文件的 story 和页数
    # ============================================================
    print(f"\n  正在预估每个文件页数...")

    file_data = []  # [(rel_dir, fname, fpath, story_elements, page_count), ...]

    for i, (rel_dir, fname, fpath) in enumerate(flat_files):
        rel_path = os.path.relpath(fpath, root_path)
        elements = build_file_story(fname, fpath, root_path, styles, usable_width)
        pages = estimate_pages(elements, usable_width)
        file_data.append((rel_dir, fname, fpath, elements, pages))

        line_count = len(read_file(fpath).split('\n'))
        print(f"    [{i + 1}/{total_files}] {rel_path}  "
              f"({line_count} 行, ~{pages} 页)")

    total_content_pages = sum(d[4] for d in file_data)
    print(f"\n  代码内容预估总页数: ~{total_content_pages} 页")

    # ============================================================
    # 5. 分卷:按页数限制分组,同一文件绝不拆分
    # ============================================================
    # 封面+目录估算为 2~3 页,预留 3 页
    COVER_TOC_RESERVE = 3

    volumes = []       # [[(rel_dir, fname, fpath, elements, pages), ...], ...]
    current_volume = []
    current_pages = COVER_TOC_RESERVE  # 当前卷已用页数(预留封面目录)

    for item in file_data:
        rel_dir, fname, fpath, elements, pages = item

        # 如果当前卷加上这个文件会超过限制
        if current_volume and (current_pages + pages > max_pages):
            # 保存当前卷,开始新卷
            volumes.append(current_volume)
            current_volume = []
            current_pages = COVER_TOC_RESERVE

        current_volume.append(item)
        current_pages += pages

    # 最后一卷
    if current_volume:
        volumes.append(current_volume)

    total_vols = len(volumes)

    if total_vols == 1:
        print(f"\n  总页数未超过 {max_pages},生成单个PDF")
    else:
        print(f"\n  需要分为 {total_vols} 卷:")
        for vi, vol in enumerate(volumes, 1):
            vol_pages = sum(item[4] for item in vol) + COVER_TOC_RESERVE
            vol_files = len(vol)
            print(f"    第 {vi} 卷: {vol_files} 个文件, ~{vol_pages} 页")

    # ============================================================
    # 6. 逐卷生成PDF
    # ============================================================
    output_files = []

    for vol_idx, volume_items in enumerate(volumes, 1):

        # 确定输出文件名
        if total_vols == 1:
            pdf_filename = f"{base_name}{base_ext}"
        else:
            pdf_filename = f"{base_name}_vol{vol_idx}of{total_vols}{base_ext}"

        print(f"\n{'- ' * 30}")
        print(f"  生成: {pdf_filename}")
        print(f"  包含 {len(volume_items)} 个文件")

        # 创建 doc
        doc = SimpleDocTemplate(pdf_filename, **doc_kwargs)
        story = []

        # 当前卷的文件列表(用于封面目录)
        vol_file_list = [(item[0], item[1], item[2]) for item in volume_items]

        # 封面 + 目录
        if total_vols == 1:
            cover = build_cover(
                folder_name, root_path, vol_file_list,
                styles, usable_width)
        else:
            cover = build_cover(
                folder_name, root_path, vol_file_list,
                styles, usable_width,
                vol_num=vol_idx, total_vols=total_vols)
        story.extend(cover)

        # 代码内容
        current_dir = None
        for item_idx, (rel_dir, fname, fpath, elements, pages) in enumerate(volume_items):

            # 如果进入新目录,打印目录标题
            if rel_dir != current_dir:
                current_dir = rel_dir
                dir_label = (f"根目录 ({folder_name}/)"
                             if rel_dir == '' else f"{rel_dir}/")
                story.append(Paragraph(f"[DIR] {dir_label}", styles['section']))
                story.append(HLine(usable_width, HexColor('#2980B9'), 1))
                story.append(Spacer(1, 6))

            # 加入文件内容
            story.extend(elements)

            rel_path = os.path.relpath(fpath, root_path)
            print(f"    + {rel_path}")

        # 写入PDF
        doc.build(story)
        pdf_size = format_size(os.path.getsize(pdf_filename))
        pdf_path = os.path.abspath(pdf_filename)
        output_files.append(pdf_path)
        print(f"  [OK] {pdf_path} ({pdf_size})")

    # ============================================================
    # 7. 汇总
    # ============================================================
    print(f"\n{'=' * 60}")
    print(f"  全部完成!")
    print(f"  项目: {folder_name}")
    print(f"  文件总数: {total_files}")
    print(f"  生成PDF: {total_vols} 个")

    for i, f in enumerate(output_files, 1):
        size = format_size(os.path.getsize(f))
        print(f"    [{i}] {f} ({size})")

    print(f"{'=' * 60}\n")
    return output_files


# ============================================================
# 入口
# ============================================================
if __name__ == '__main__':
    print("\n" + "=" * 60)
    print("  代码文件夹 -> PDF 导出工具")
    print("  - 支持中文注释/内容")
    print(f"  - 超过 {MAX_PAGES_PER_PDF} 页自动分卷")
    print("  - 同一文件不会跨卷拆分")
    print("=" * 60)

    if len(sys.argv) >= 2:
        folder = sys.argv[1]
        out = sys.argv[2] if len(sys.argv) >= 3 else None
    else:
        folder = input("\n请输入文件夹路径: ").strip().strip('"').strip("'")
        out_input = input("输出PDF文件名 (回车=自动命名): ").strip()
        out = out_input if out_input else None

    if not folder:
        print("  未输入路径!")
        sys.exit(1)

    generate_pdf(folder, out)
相关推荐
蚕豆哥5 小时前
【2026马年重启】我的 Primavera P6/Unifier 技术笔记,继续更新!
ai·oracle·项目管理·unifier·p6·进度管理·甲骨文
CoderJia程序员甲5 小时前
GitHub 热榜项目 - 日榜(2026-02-06)
人工智能·ai·大模型·github·ai教程
带刺的坐椅6 小时前
Claude Code Skills,Google A2A Skills,Solon AI Skills 有什么区别?
java·ai·solon·a2a·claudecode·skills
南宫乘风6 小时前
Claude Code 从 0 到 1 实战全攻略:掌握下一代编程 Agent 的核心能力
ai·claude·mcp
CBeann7 小时前
企业级规则引擎落地实战:动态脚本引擎 QLExpress ,真香!
java·ai·大模型·规则引擎·qlexpress·大厂实战项目
Thexhy7 小时前
Ollama 指南
ai·大模型
水中加点糖8 小时前
小白都能看懂的——车牌检测与识别(最新版YOLO26快速入门)
人工智能·yolo·目标检测·计算机视觉·ai·车牌识别·lprnet
AGI-四顾8 小时前
文生图模型选型速览
人工智能·ai
YongCheng_Liang9 小时前
零基础学 AI:AI 基础能力夯实 —— 编程语言与工具篇
ai