Skill: ziyi-z-z/wos-paper-downloader (version 1.0.0)

--- name: wos-paper-downloader description: "Web of Science 文献搜索与下载助手。自动从 WOS 搜索文献、提取元数据，并协助批量下载 PDF。使用场景：需要批量获取学术文献时，特别是环境心理学、恢复性环境等研究方向。需要有效的 WOS 机构订阅。" --- # wos-paper-downloader 自动从 Web of Science 搜索文献、提取元数据，并协助批量下载 PDF。 **⚠️ 重要说明：** - 需要有效的 Web of Science 机构订阅 - PDF 下载需要个人账号登录（脚本会指导你完成） - 请遵守机构的使用政策和版权规定 ## 功能特点 1. **智能检索** - 根据关键词自动生成优化的 WOS 检索式 2. **元数据提取** - 导出标题、作者、期刊、DOI、摘要等信息 3. **批量下载** - 自动下载开放获取(OA)文献，协助获取订阅文献 4. **自动整理** - 按日期/主题自动分类存储 ## 使用方法 ### 快速开始 ```bash # 1. 运行主脚本 python scripts/wos_downloader.py # 2. 按提示输入检索关键词 # 3. 脚本会生成检索结果和下载列表 ``` ### 命令行参数 ```bash # 基础检索 python scripts/wos_downloader.py --query "restorative environment health" --limit 50 # 高级检索（使用WOS检索式） python scripts/wos_downloader.py --advanced --query 'TS=("attention restoration theory") AND TS=(health)' --limit 100 # 指定输出目录 python scripts/wos_downloader.py --query "green space mental health" --output ./papers/2024 # 只下载开放获取文献 python scripts/wos_downloader.py --query "nature exposure" --oa-only ``` ## 工作流程 ### 第一步：配置环境 1. 确保你有 Web of Science 访问权限 2. 安装依赖： ```bash pip install -r scripts/requirements.txt ``` ### 第二步：执行检索 脚本会： - 打开浏览器并导航到 Web of Science - 根据你的关键词执行检索 - 导出检索结果（标题、作者、DOI等） ### 第三步：下载文献 - **开放获取文献**：自动通过 Unpaywall/API 下载 - **订阅文献**：生成下载清单，指导你手动批量下载 ### 第四步：整理归档 自动按以下结构整理： ``` papers/ ├── 2024-01-15_restorative-environment/ │ ├── metadata.csv # 文献元数据 │ ├── download_list.txt # 待下载清单 │ ├── oa_papers/ # 开放获取文献 │ │ ├── paper1.pdf │ │ └── paper2.pdf │ └── subscribed/ # 订阅文献（手动下载后放入） ``` ## 检索式构建指南 ### 环境心理学常用检索式 ``` # 恢复性环境 TS=("attention restoration theory" OR "stress recovery theory" OR "restorative environment*") AND TS=(health OR well-being) # 亲环境行为 TS=("pro-environmental behavior" OR "sustainable behavior") AND TS=(attitude* OR motivation) # 绿色空间与健康 TS=("green space" OR "greenspace" OR "urban nature") AND TS=(health OR "mental health" OR well-being) ``` ### 检索技巧 - `TS=` - 主题检索（标题+摘要+关键词） - `AU=` - 作者检索 - `SO=` - 期刊检索 - `PY=` - 出版年份 - `*` - 通配符（如 `restor*` 匹配 restore, restorative, restoration） ## 注意事项 1. **下载限制**：Web of Science 有每日下载限额，脚本会自动控制频率 2. **版权问题**：只下载你有权限访问的文献 3. **稳定性**：网络波动可能导致中断，脚本支持断点续传 ## 故障排除 **问题：无法登录 Web of Science** - 确认你通过机构网络或 VPN 访问 - 检查账号是否有效 **问题：下载速度慢** - 使用 `--oa-only` 只下载开放获取文献 - 调整 `--delay` 参数控制请求间隔 **问题：PDF 下载失败** - 部分文献可能没有 OA 版本 - 检查 `download_list.txt` 手动获取 ## 参考资料 - [Web of Science 高级检索指南](references/wos_search_guide.md) - [DOI 解析与开放获取检测](references/doi_resolver.md) ## 依赖 - Python 3.8+ - Selenium / Playwright（浏览器自动化） - requests（HTTP 请求） - pandas（数据处理）

path: scripts/example.py
content: #!/usr/bin/env python3
"""
Example helper script for wos-paper-downloader

This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.

Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""

def main():
    print("This is an example script for wos-paper-downloader")
    # TODO: Add actual script logic here
    # This could be data processing, file conversion, API calls, etc.

if __name__ == "__main__":
    main()


path: scripts/requirements.txt
content: requests>=2.28.0
pandas>=1.5.0


path: scripts/wos_downloader.py
content: #!/usr/bin/env python3
"""
Web of Science 文献下载器
支持检索、元数据提取和批量下载
"""

import argparse
import csv
import json
import os
import re
import sys
import time
from datetime import datetime
from pathlib import Path
from urllib.parse import quote, urlencode

import requests


class WOSDownloader:
    """Web of Science 文献下载管理器"""
    
    def __init__(self, output_dir=None, delay=2):
        self.delay = delay  # 请求间隔（秒）
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
        
        # 设置输出目录
        if output_dir:
            self.output_dir = Path(output_dir)
        else:
            timestamp = datetime.now().strftime('%Y-%m-%d')
            self.output_dir = Path(f'papers/{timestamp}_wos_search')
        
        self.output_dir.mkdir(parents=True, exist_ok=True)
        
        # 子目录
        self.oa_dir = self.output_dir / 'oa_papers'
        self.subscribed_dir = self.output_dir / 'subscribed'
        self.oa_dir.mkdir(exist_ok=True)
        self.subscribed_dir.mkdir(exist_ok=True)
        
        # 数据存储
        self.papers = []
        
    def build_search_url(self, query, advanced=False):
        """构建 Web of Science 检索 URL"""
        base_url = 'https://www.webofscience.com/wos/woscc/advanced-search'
        
        if advanced:
            # 高级检索模式
            search_query = query
        else:
            # 基础检索模式 - 自动添加 TS= 前缀
            search_query = f'TS=({query})'
        
        # 注意：实际执行需要浏览器自动化
        print(f'检索式: {search_query}')
        return search_query
    
    def search_with_browser(self, query, limit=50):
        """
        使用浏览器执行检索（需要手动登录）
        这是半自动化方案，指导用户完成检索
        """
        search_url = self.build_search_url(query)
        
        print('\n' + '='*60)
        print('Web of Science 文献检索助手')
        print('='*60)
        print(f'\n检索关键词: {query}')
        print(f'限制数量: {limit}')
        print('\n--- 手动操作步骤 ---')
        print('1. 打开浏览器访问: https://www.webofscience.com')
        print('2. 使用你的机构账号登录')
        print('3. 进入高级检索页面')
        print(f'4. 输入检索式: {search_url}')
        print('5. 执行检索并按相关性排序')
        print('6. 选择前 {} 条记录，导出为「其他文件格式」'.format(limit))
        print('7. 选择「制表符分隔的 UTF-8 格式」')
        print('8. 选择「全记录与引用的参考文献」')
        print('9. 导出并保存到: {}'.format(self.output_dir))
        print('\n导出完成后，将文件命名为 "wos_export.txt" 放在上述目录')
        print('='*60)
        
        return True
    
    def parse_wos_export(self, filepath):
        """解析 Web of Science 导出的文本文件"""
        papers = []
        
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # WOS 导出文件以 PT（出版物类型）分隔记录
            records = content.split('PT ')
            
            for record in records[1:]:  # 跳过第一个空项
                paper = self._parse_record(record)
                if paper:
                    papers.append(paper)
            
            print(f'成功解析 {len(papers)} 条记录')
            return papers
            
        except Exception as e:
            print(f'解析文件出错: {e}')
            return []
    
    def _parse_record(self, record):
        """解析单条记录"""
        paper = {}
        
        # 提取各字段
        patterns = {
            'title': r'TI (.+?)(?=\n[A-Z]{2} |\Z)',
            'authors': r'AU (.+?)(?=\n[A-Z]{2} |\Z)',
            'source': r'SO (.+?)(?=\n[A-Z]{2} |\Z)',
            'doi': r'DI (.+?)(?=\n[A-Z]{2} |\Z)',
            'abstract': r'AB (.+?)(?=\n[A-Z]{2} |\Z)',
            'year': r'PY (\d{4})',
            'volume': r'VL (.+?)(?=\n[A-Z]{2} |\Z)',
            'issue': r'IS (.+?)(?=\n[A-Z]{2} |\Z)',
            'pages': r'PG (.+?)(?=\n[A-Z]{2} |\Z)',
            'wos_id': r'UT (.+?)(?=\n[A-Z]{2} |\Z)',
        }
        
        for field, pattern in patterns.items():
            match = re.search(pattern, record, re.DOTALL)
            if match:
                value = match.group(1).strip()
                # 清理多行内容
                value = ' '.join(value.split())
                paper[field] = value
        
        return paper if paper else None
    
    def check_open_access(self, doi):
        """
        检查文献是否为开放获取
        使用 Unpaywall API
        """
        if not doi:
            return None
        
        # 清理 DOI
        doi = doi.replace('doi:', '').replace('DOI:', '').strip()
        
        url = f'https://api.unpaywall.org/v2/{doi}?email=[REDACTED_SECRET]'
        
        try:
            time.sleep(self.delay)  # 控制请求频率
            response = requests.get(url, timeout=10)
            
            if response.status_code == 200:
                data = response.json()
                return {
                    'is_oa': data.get('is_oa', False),
                    'oa_url': data.get('best_oa_location', {}).get('url_for_pdf') if data.get('best_oa_location') else None,
                    'journal': data.get('journal_name', ''),
                    'publisher': data.get('publisher', '')
                }
        except Exception as e:
            print(f'检查 OA 状态出错 ({doi}): {e}')
        
        return None
    
    def download_pdf(self, url, filename):
        """下载 PDF 文件"""
        try:
            time.sleep(self.delay)
            response = self.session.get(url, timeout=30, stream=True)
            
            if response.status_code == 200:
                filepath = self.oa_dir / filename
                with open(filepath, 'wb') as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)
                print(f'  ✓ 下载成功: {filename}')
                return True
            else:
                print(f'  ✗ 下载失败 (HTTP {response.status_code}): {filename}')
                return False
                
        except Exception as e:
            print(f'  ✗ 下载出错: {filename} - {e}')
            return False
    
    def process_papers(self, papers, oa_only=False):
        """处理文献列表，检查OA状态并下载"""
        print(f'\n开始处理 {len(papers)} 篇文献...')
        print('='*60)
        
        results = {
            'oa_found': 0,
            'oa_downloaded': 0,
            'subscription_required': 0,
            'no_doi': 0
        }
        
        subscription_list = []
        
        for i, paper in enumerate(papers, 1):
            print(f'\n[{i}/{len(papers)}] {paper.get("title", "Unknown")[:60]}...')
            
            doi = paper.get('doi', '')
            if not doi:
                print('  - 无 DOI，跳过')
                results['no_doi'] += 1
                continue
            
            # 检查开放获取状态
            oa_info = self.check_open_access(doi)
            
            if oa_info and oa_info['is_oa'] and oa_info['oa_url']:
                results['oa_found'] += 1
                print(f'  ✓ 发现开放获取版本')
                
                # 生成文件名
                safe_title = re.sub(r'[^\w\s-]', '', paper.get('title', 'unknown')[:50])
                filename = f"{i:03d}_{safe_title.replace(' ', '_')}.pdf"
                
                # 下载
                if self.download_pdf(oa_info['oa_url'], filename):
                    results['oa_downloaded'] += 1
                    paper['local_pdf'] = str(self.oa_dir / filename)
                    paper['oa_status'] = 'downloaded'
                else:
                    paper['oa_status'] = 'download_failed'
            else:
                results['subscription_required'] += 1
                print('  - 需要订阅权限')
                paper['oa_status'] = 'subscription_required'
                subscription_list.append(paper)
        
        # 保存结果
        self.save_results(papers, subscription_list)
        
        # 打印统计
        print('\n' + '='*60)
        print('处理完成！')
        print(f'  开放获取文献: {results["oa_found"]} (成功下载 {results["oa_downloaded"]})')
        print(f'  需要订阅: {results["subscription_required"]}')
        print(f'  无 DOI: {results["no_doi"]}')
        print(f'\n文件保存位置: {self.output_dir}')
        print('='*60)
        
        return results
    
    def save_results(self, all_papers, subscription_list):
        """保存结果到文件"""
        # 保存所有元数据为 CSV
        csv_path = self.output_dir / 'metadata.csv'
        if all_papers:
            keys = all_papers[0].keys()
            with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f:
                writer = csv.DictWriter(f, fieldnames=keys)
                writer.writeheader()
                writer.writerows(all_papers)
            print(f'\n元数据已保存: {csv_path}')
        
        # 保存订阅文献清单
        if subscription_list:
            list_path = self.output_dir / 'download_list.txt'
            with open(list_path, 'w', encoding='utf-8') as f:
                f.write('需要手动下载的文献清单\n')
                f.write('='*60 + '\n\n')
                for paper in subscription_list:
                    f.write(f"标题: {paper.get('title', 'N/A')}\n")
                    f.write(f"作者: {paper.get('authors', 'N/A')[:100]}...\n")
                    f.write(f"期刊: {paper.get('source', 'N/A')}\n")
                    f.write(f"年份: {paper.get('year', 'N/A')}\n")
                    f.write(f"DOI: {paper.get('doi', 'N/A')}\n")
                    f.write(f"WOS ID: {paper.get('wos_id', 'N/A')}\n")
                    f.write('-'*60 + '\n\n')
            print(f'订阅文献清单: {list_path}')
        
        # 保存为 JSON（便于程序处理）
        json_path = self.output_dir / 'papers.json'
        with open(json_path, 'w', encoding='utf-8') as f:
            json.dump(all_papers, f, ensure_ascii=False, indent=2)


def main():
    parser = argparse.ArgumentParser(description='Web of Science 文献下载助手')
    parser.add_argument('--query', '-q', help='检索关键词')
    parser.add_argument('--advanced', '-a', action='store_true', help='使用高级检索模式')
    parser.add_argument('--limit', '-l', type=int, default=50, help='限制数量（默认50）')
    parser.add_argument('--output', '-o', help='输出目录')
    parser.add_argument('--oa-only', action='store_true', help='只处理开放获取文献')
    parser.add_argument('--delay', '-d', type=float, default=2, help='请求间隔（秒）')
    parser.add_argument('--parse', help='解析已导出的 WOS 文件')
    
    args = parser.parse_args()
    
    # 初始化下载器
    downloader = WOSDownloader(output_dir=args.output, delay=args.delay)
    
    if args.parse:
        # 解析已有文件
        print(f'解析文件: {args.parse}')
        papers = downloader.parse_wos_export(args.parse)
        if papers:
            downloader.process_papers(papers, oa_only=args.oa_only)
    
    elif args.query:
        # 指导用户进行检索
        downloader.search_with_browser(args.query, args.limit)
        
        print('\n等待你完成导出...')
        export_path = downloader.output_dir / 'wos_export.txt'
        
        if export_path.exists():
            print(f'\n发现导出文件，开始处理...')
            papers = downloader.parse_wos_export(export_path)
            if papers:
                downloader.process_papers(papers, oa_only=args.oa_only)
        else:
            print(f'\n请将导出的文件保存到: {export_path}')
            print('然后运行: python wos_downloader.py --parse {}'.format(export_path))
    
    else:
        parser.print_help()
        print('\n示例:')
        print('  python wos_downloader.py -q "restorative environment health" -l 50')
        print('  python wos_downloader.py --parse ./papers/2024-01-15/wos_export.txt')


if __name__ == '__main__':
    main()


path: references/api_reference.md
content: # Reference Documentation for Wos Paper Downloader

This is a placeholder for detailed reference documentation.
Replace with actual reference content or delete if not needed.

Example real reference docs from other skills:
- product-management/references/communication.md - Comprehensive guide for status updates
- product-management/references/context_building.md - Deep-dive on gathering context
- bigquery/references/ - API references and query examples

## When Reference Docs Are Useful

Reference docs are ideal for:
- Comprehensive API documentation
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for main SKILL.md
- Content that's only needed for specific use cases

## Structure Suggestions

### API Reference Example
- Overview
- Authentication
- Endpoints with examples
- Error codes
- Rate limits

### Workflow Guide Example
- Prerequisites
- Step-by-step instructions
- Common patterns
- Troubleshooting
- Best practices


path: references/doi_resolver.md
content: # DOI 解析与开放获取检测

## Unpaywall API

本项目使用 Unpaywall API 检测文献的开放获取状态。

### API 端点
```
https://api.unpaywall.org/v2/{doi}?email={your_email}
```

### 响应字段

| 字段 | 说明 |
|------|------|
| `is_oa` | 是否为开放获取 |
| `oa_status` | OA 类型（gold, green, bronze, hybrid, closed） |
| `best_oa_location` | 最佳 OA 版本位置 |
| `best_oa_location.url_for_pdf` | PDF 直接下载链接 |

### OA 类型说明

- **Gold** - 完全开放获取期刊
- **Green** - 机构库或预印本服务器中的版本
- **Bronze** - 出版商网站免费阅读（无开放许可）
- **Hybrid** - 订阅期刊中的 OA 文章（APC 付费）
- **Closed** - 需要订阅

## 其他开放获取资源

### 1. PubMed Central (PMC)
生物医学文献的全文库：
```
https://www.ncbi.nlm.nih.gov/pmc/articles/PMC{pmcid}/
```

### 2. arXiv
预印本服务器（物理、数学、计算机等）：
```
https://arxiv.org/pdf/{arxiv_id}.pdf
```

### 3. bioRxiv / medRxiv
生命科学预印本：
```
https://www.biorxiv.org/content/{doi}.full.pdf
```

### 4. ResearchGate / Academia.edu
学者社交平台，可能有作者自存档版本

### 5. 机构库
大学机构库（如 MIT DSpace、arXiv 等）

## DOI 解析服务

### Crossref API
获取文献元数据：
```
https://api.crossref.org/works/{doi}
```

### DOI.org
解析 DOI 到出版商页面：
```
https://doi.org/{doi}
```

## 注意事项

1. **尊重版权** - 只下载你有权限访问的文献
2. **API 限制** - Unpaywall 有速率限制，请控制请求频率
3. **准确性** - OA 状态可能随时间变化，建议定期更新


path: references/wos_search_guide.md
content: # Web of Science 高级检索指南

## 检索字段代码

| 代码 | 含义 | 示例 |
|------|------|------|
| TS | 主题（标题+摘要+关键词） | TS=(environmental psychology) |
| TI | 标题 | TI=(restorative environment) |
| AU | 作者 | AU=(Kaplan S) |
| SO | 出版物/期刊 | SO=(Journal of Environmental Psychology) |
| PY | 出版年份 | PY=(2020-2024) |
| AB | 摘要 | AB=(nature exposure) |
| KP | 关键词 | KP=(green space) |
| DO | DOI | DO=(10.1016/j.jenvp.2020.101456) |
| WC | Web of Science 类别 | WC=(Psychology Multidisciplinary) |
| AD | 地址/机构 | AD=(University of Michigan) |

## 布尔运算符

- **AND** - 同时包含所有词
- **OR** - 包含任一词语
- **NOT** - 排除特定词
- **NEAR/n** - 两词相距n个词以内
- **SAME** - 同一字段中

## 检索式示例

### 恢复性环境
```
TS=("attention restoration theory" OR "stress recovery theory" OR "restorative environment*") AND TS=(health OR well-being OR wellbeing)
```

### 绿色空间与心理健康（限定年份）
```
TS=("green space" OR "greenspace" OR "urban nature" OR "park access") AND TS=("mental health" OR depression OR anxiety OR stress) AND PY=(2015-2024)
```

### 特定作者的研究
```
AU=(Kaplan R OR Kaplan S) AND TS=(restorative OR nature)
```

### 特定期刊
```
SO=("Journal of Environmental Psychology") AND TS=(restorative)
```

## 通配符

- **\*** - 零个或多个字符（如 `restor*` 匹配 restore, restorative, restoration）
- **?** - 单个字符（如 `wom?n` 匹配 woman, women）
- **$** - 零个或一个字符（如 `colo$r` 匹配 color, colour）

## 短语检索

使用引号进行精确短语匹配：
```
"attention restoration theory"
```

## 导出设置建议

1. **记录内容**: 选择「全记录与引用的参考文献」获取完整信息
2. **文件格式**: 选择「制表符分隔的 UTF-8 格式」便于程序解析
3. **排序**: 按「被引频次（降序）」获取高影响力文献

## 检索结果精炼

在左侧精炼面板中可按以下维度筛选：
- 研究领域
- 文献类型（Article, Review 等）
- 出版年份
- 期刊
- 作者
- 机构
- 开放获取


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