Skill: lava-chen/ielts-extractor (version 1.1.0)

# IELTS 试题数据提取 Skill ## 概述 从剑桥雅思 PDF 提取阅读文章和题目。 ## 触发条件 用户要求"提取雅思试题"时使用。 ## 流程(4步) ### 1. 定位 PDF 和页码 - 查找 "Test X" + "READING PASSAGE Y" - 记录起始页码 ### 2. 提取文章 - **必须连续提取多页** - 处理两栏布局 - 检查字数: 1500-2500词/篇 详见: `references/pdf-extraction.md` ### 3. 提取题目 - 按大题分组 - 选择正确题型 - 完整选项(单选每题A-D) 详见: `references/question-types.md` ### 4. 保存 JSON - 使用 content 字段 - 选项格式正确 详见: `references/json-format.md` ## 数据文件 ``` ielts-tracker/data/tests/cambridge-{4,5,6}/test-{1-4}/test.json ielts-tracker/ielts-app/public/images/ ``` ## 题型速查 | 题型 | type | |------|------| | 标题配对 | matching-headings | | 判断题 | yes-no-not-given | | 单选 | multiple-choice-single | | 多选 | multiple-answer | | 表格 | table-completion | | 填空 | fill-blank-summary |

path: extract_article.py
content: #!/usr/bin/env python3
"""
IELTS Reading Passage Extractor
从剑桥雅思 PDF 提取阅读文章
"""

import pdfplumber
import json
import sys

def find_reading_passage_pages(pdf, test_num, passage_num):
    """查找指定 Test 和 Passage 的起始页码"""
    for i, page in enumerate(pdf.pages):
        text = page.extract_text()
        if text and f"Test {test_num}" in text and f"READING PASSAGE {passage_num}" in text:
            return i
    return None

def extract_article(pdf, start_page, num_pages):
    """从指定页码范围提取文章"""
    article = ""
    for i in range(start_page, min(start_page + num_pages, len(pdf.pages))):
        article += pdf.pages[i].extract_text()
    return article

def extract_twocolumn_page(page):
    """从两栏页面提取文本（按阅读顺序）"""
    chars = page.chars
    mid = page.width / 2
    
    left = sorted([c for c in chars if c['x0'] < mid], key=lambda x: (x['top'], x['x0']))
    right = sorted([c for c in chars if c['x0'] >= mid], key=lambda x: (x['top'], x['x0']))
    
    left_text = ''.join([c['text'] for c in left])
    right_text = ''.join([c['text'] for c in right])
    
    return left_text + right_text

def main(pdf_path, test_num, passage_num, num_pages=3):
    """主函数"""
    with pdfplumber.open(pdf_path) as pdf:
        # 查找起始页
        start_page = find_reading_passage_pages(pdf, test_num, passage_num)
        
        if start_page is None:
            print(f"找不到 Test {test_num} Reading Passage {passage_num}")
            return
        
        print(f"找到: Test {test_num} Passage {passage_num}, 起始页 {start_page + 1}")
        
        # 提取文章
        article = extract_article(pdf, start_page, num_pages)
        
        print("\n=== 提取的文章 ===")
        print(article[:2000])
        print("..." if len(article) > 2000 else "")
        
        return article

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("用法: python extract_article.py <pdf_path> <test_num> <passage_num> [pages]")
        print("示例: python extract_article.py '剑桥雅思4.pdf' 2 1 3")
        sys.exit(1)
    
    pdf_path = sys.argv[1]
    test_num = int(sys.argv[2])
    passage_num = int(sys.argv[3])
    num_pages = int(sys.argv[4]) if len(sys.argv) > 4 else 3
    
    main(pdf_path, test_num, passage_num, num_pages)


path: references/json-format.md
content: # JSON 格式规范

## 基本结构

```json
{
  "id": "reading-p1",
  "title": "文章标题",
  "content": "文章正文...",
  "question_groups": [
    {
      "id": "g1",
      "title": "Questions 1-4",
      "type": "multiple-choice-single",
      "instruction": "Choose correct letter...",
      "questions": [...]
    }
  ]
}
```

## 选项格式

### 共享选项(题组级)
```json
{
  "type": "matching-headings",
  "options": [{"id": "A", "text": "..."}],
  "questions": [{"id": "q1", "answer": "A"}]
}
```

### 独立选项(每题)
```json
{
  "type": "multiple-choice-single",
  "questions": [
    {
      "id": "q1",
      "options": [{"id": "A", "text": "..."}],
      "answer": "A"
    }
  ]
}
```

### 表格题
```json
{
  "type": "table-completion",
  "image": "/images/xxx.png",
  "questions": [{"id": "q1", "answer": "xxx"}]
}
```

### 填空题
```json
{
  "type": "fill-blank-summary",
  "passage_fragment": "原文挖空 [1] [2]",
  "questions": [{"id": "q1", "content": "[1]答案", "answer": "xxx"}]
}
```


path: references/pdf-extraction.md
content: # PDF 提取指南

## 定位文章和题目

### 页码查找
```python
with pdfplumber.open(pdf_path) as pdf:
    for i, page in enumerate(pdf.pages):
        text = page.extract_text()
        if "Test 2" in text and "READING PASSAGE 1" in text:
            print(f"Page {i+1}")
```

### 常见页码(Cambridge 4)
- Test 2: P43(P1), P47(P2), P50(P3)
- Test 3: P66(P1), P72(P2), P75(P3)

## 提取文章

### 关键规则:多页连续提取
```python
# 错误 ❌
text = pdf.pages[65].extract_text()

# 正确 ✅
text = ""
for i in range(65, 69):  # 连续多页
    text += pdf.pages[i].extract_text()
```

### 两栏布局
```python
def extract_twocolumn(page):
    chars = page.chars
    mid = page.width / 2
    left = sorted([c for c in chars if c['x0'] < mid], key=lambda x: (x['top'], x['x0']))
    right = sorted([c for c in chars if c['x0'] >= mid], key=lambda x: (x['top'], x['x0']))
    return ''.join([c['text'] for c in left]) + ''.join([c['text'] for c in right])
```

## 表格题截图

```python
import fitz  # PyMuPDF
doc = fitz.open(pdf_path)
page = doc.load_page(48)  # 0-indexed
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))
pix.save("public/images/xxx.png")
```

## 数据文件位置

```
ielts-tracker/
├── data/
│   └── tests/
│       └── cambridge-{4,5,6}/
│           └── test-{1,2,3,4}/
│               └── test.json
└── ielts-app/
    └── public/
        └── images/
```


path: references/question-types.md
content: # 题型标准

## 8种核心题型

| type | 中文名 | 选项位置 |
|------|--------|---------|
| `matching-headings` | 段落标题配对 | 题组级 |
| `matching-sentence` | 句子首尾配对 | 题组级 |
| `matching-label` | 标签配对 | 题组级 |
| `multiple-choice-single` | 多项选择(单) | 每题独立 |
| `multiple-answer` | 多项选择(多) | 题组级 |
| `yes-no-not-given` | 判断题 | 无 |
| `sentence-completion` | 完成句子 | 无/题组级 |
| `fill-blank-summary` | 摘要填空 | 无/原文片段 |
| `table-completion` | 表格完成 | 图片+题目 |

## 题型检查清单

- [ ] 文章字数: 1500-2500词/篇
- [ ] 总题数: 40道
- [ ] 选项完整: 单选每题4个选项(A-D)
- [ ] 答案对应: 答案与题目匹配


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)