- 文章资讯
- Cursor + MCP 两小时速爬 huggingface
Cursor + MCP 两小时速爬 huggingface
Davidon 2 days ago
搭建开发环境
- 下载和安装 Cursor
- 搭建 MCP 环境(需要 mcp:1. Playwright ,2. Mysql-server)
- 进入 MCP 社区 https://smithery.ai/,找到需要的 playwright MCP(类似 selenium 的自动化工具),进入详情页 Playwright,在页面右侧选择 cursor 安装,根据提示和操作步骤,安装 MCP
- 继续安装 MCP:Mysql-server(可以和 mysql 交互的 MCP)
- 进入 cursor -> settings -> MCP -> 查看安装的 MCP 状态,确保 MCP 启用并状态可用
- 需要将 cursor 的对话模式改为
agent
模式
描述需求,撰写提示词
我需要使用 playwright mcp 分析爬取目标网页
https://huggingface.co/models
从所有的 main > div > div > section.pt-8.border-gray-100.col-span-full.lg:col-span-6.xl:col-span-7.pb-12 > div.relative > div > article.overview-card-wrapper group/repo 中爬取页面上列表中所有的模型数据,并收集如下字段
模型名称
字段名称:api_name
位置:article > a > div > header > h4 ::text
更新时间
字段名称:version_time
位置:time 元素的 datetime 属性,
要求:需要保留时间格式 时分秒,就在列表页中,不需要进入详情页爬取
下载数
字段名称:downloads
要求:需要格式化为整数
模型类别
字段名称:category
位置:在模型详情页中的 body > div.flex.min-h-dvh.flex-col > main > div.SVELTE_HYDRATER.contents > header > div > div.mb-3.flex.flex-wrap.md\:mb-4 > a:nth-child(1) > div > span 的 text 中
模型文档
字段名称:readme_en
位置:模型详情页中的 div.model-card-content.prose
要求:爬取其原始 html 字符串
url:
字段名称:url
记录模型详情页的 url
尝试分析爬取前两页的内容,不需要入库,直接输出就好
测试提示词
发送对话,启动 playwright 自动化工具,拉起浏览器,访问目标网页并进行爬虫分析,得到响应反馈结果
我来帮你使用 Playwright 爬取 Hugging Face 的模型数据。我们需要先打开浏览器,访问页面,然后获取页面快照来定位元素。
首先,让我们打开浏览器并访问目标网页:
让我们获取页面快照来分析数据。我看到每个模型的信息都在 article 标签中,包含了我们需要的所有信息。我将编写一个简单的程序来提取这些信息并格式化输出。
让我们先看一下第一个模型的数据作为示例:
模型名称: microsoft/bitnet-b1.58-2B-4T
模型类别: Text Generation
更新时间: 2 days ago
下载数: 13k
我们可以看到所有需要的信息都在页面中。现在让我们来提取所有模型的信息:
根据页面快照,我将为您提取所有模型的信息。以下是当前页面上所有模型的数据:
以下是整理后的 Hugging Face 模型数据(Markdown 格式):
Hugging Face 模型数据列表
模型名称 | 类别 | 更新时间 | 下载数 |
---|---|---|---|
microsoft/bitnet-b1.58-2B-4T | Text Generation | 2 days ago | 13,000 |
HiDream-ai/HiDream-I1-Full | Text-to-Image | 6 days ago | 24,300 |
microsoft/MAI-DS-R1 | 未指定 | 5 days ago | 284 |
Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro-2.0 | Text-to-Image | 2 days ago | 10,100 |
THUDM/GLM-4-32B-0414 | Text Generation | 7 days ago | 4,190 |
Wan-AI/Wan2.1-FLF2V-14B-720P | 未指定 | 5 days ago | 12,200 |
deepseek-ai/DeepSeek-V3-0324 | Text Generation | 26 days ago | 245,000 |
agentica-org/DeepCoder-14B-Preview | Text Generation | 12 days ago | 33,900 |
black-forest-labs/FLUX.1-dev | Text-to-Image | Aug 16, 2024 | 2,060,000 |
nari-labs/Dia-1.6B | 未指定 | about 10 hours ago | 100 |
microsoft/bitnet-b1.58-2B-4T-gguf | Text Generation | 5 days ago | 11,200 |
moonshotai/Kimi-VL-A3B-Thinking | Image-Text-to-Text | 1 day ago | 31,700 |
Qwen/Qwen2.5-Omni-7B | Any-to-Any | 7 days ago | 171,000 |
THUDM/GLM-Z1-32B-0414 | Text Generation | 7 days ago | 1,870 |
ByteDance-Seed/UI-TARS-1.5-7B | Image-Text-to-Text | 4 days ago | 3,030 |
sand-ai/MAGI-1 | Image-to-Video | about 14 hours ago | 80 |
Junfeng5/Liquid_V1_7B | Any-to-Any | Mar 20 | 10,800 |
Comfy-Org/HiDream-I1_ComfyUI | 未指定 | 5 days ago | 72 |
bytedance-research/ChatTS-14B | Text Generation | 4 days ago | 177 |
nvidia/Llama-3.1-Nemotron-8B-UltraLong-4M-Instruct | Text Generation | 5 days ago | 1,150 |
lllyasviel/FramePackI2V_HY | 未指定 | 8 days ago | 99,100 |
deepseek-ai/DeepSeek-R1 | Text Generation | 26 days ago | 1,710,000 |
hexgrad/Kokoro-82M | Text-to-Speech | 11 days ago | 2,020,000 |
google/gemma-3-27b-it | Image-Text-to-Text | Mar 22 | 656,000 |
OnomaAIResearch/Illustrious-XL-v2.0 | 未指定 | 3 days ago | 57 |
ibm-granite/granite-3.3-8b-instruct | Text Generation | 5 days ago | 7,490 |
OpenGVLab/InternVL3-78B | Image-Text-to-Text | 5 days ago | 12,500 |
Menlo/ReZero-v0.1-llama-3.2-3b-it-grpo-250404 | Text Generation | 5 days ago | 1,780 |
THUDM/GLM-Z1-Rumination-32B-0414 | Text Generation | 7 days ago | 800 |
google/gemma-3-27b-it-qat-q4_0-gguf | Image-Text-to-Text | 11 days ago | 65,300 |
根据反馈结果,继续修改提示词
更新时间格式不正确
<time datetime="2025-04-20T06:27:03" title="Sun, 20 Apr 2025 06:27:03 GMT">2 days ago</time>
我需要的目标数据的 datetime 属性 并格式化成 YYYY-MM-DD HH:mm:ss 的格式
重新爬取数据,得到正确的的数据,并继续优化提示词,细化爬虫需求。 注:这里有个问题,初始爬虫是从列表中直接爬取的 category,这个数据源不是很稳定,很多模型的 category 并不是在这里显示,后改为从详情页中爬取
1. 你可能已经注意到了,这列表具有分页功能,共计 100 页,我需要将所有分页中的数据都爬取下来
2. 我初始的爬虫需求中,还有 `category` 和 `readme_en` 字段,位于模型的详情页中,需要你继续进入模型的详情页中,爬取并组装这两个字段
字段名称:category
位置:在模型详情页中的 body > div.flex.min-h-dvh.flex-col > main > div.SVELTE_HYDRATER.contents > header > div > div.mb-3.flex.flex-wrap.md\:mb-4 > a:nth-child(1) > div > span 的 text 中
字段名称:readme_en
位置:模型详情页中的 div.model-card-content.prose
要求:爬取其原始 html 字符串
尝试帮我先爬取前两页的内容,并输出,验证爬取结果
发送提示词,验证前两页的爬取结果,如果有问题,继续调整提示词进行纠错,如果没问题则继续细化爬虫需求如下:
爬取的结果没有问题了,我需要你根据现在的页面爬取逻辑帮我生成一个 python 的爬虫脚本
要求:
1. 废弃使用 Playwright 等 Webdriver 自动化工具的方式
2. 使用传统 Request + Beautiful 技术栈,简化爬取过程
3. 将生成的数据写入 `huggingface_models.json` 文件中。
执行提示词,生成爬虫脚本,并执行脚本将数据落地到 json 文件 验证 json 文件的结果,字段和数据条数是否正确(huggingface 共 100 页,每页 30 条数据,共落地录入 2997 条记录,字段正确),继续写入提示词
我希望将爬取的 json 文件,使用 mysql mcp 录入数据库的 `ai_model_api_list` 表中
录入时,为`json`中的每条数据增加字段`platform`其值固定为`huggingface`
忽略 downloads 字段,暂不进行处理
执行提示词,返回响应
我来帮你分析一下 JSON 文件中的数据数量。
File is too large, >2.00MB
看来这个文件比较大,让我们用 Python 脚本来分析一下文件内容。
输出了入库的 Python 脚本
检查生成 python 脚本中字段映射是否正确后,输入提示词
使用 mysql mcp 为我检查一下 `ai_model_api_list` 的表结构和数据插入脚本,确保我的脚本匹配数据库的表设计
得到响应反馈
好的,让我们使用这些连接信息来查看表结构:
`Called MCP tool connect_db`
现在让我们查看 ai_model_api_list 表的结构:
`Called MCP tool describe_table`
根据表结构,我需要更新我们的导入脚本。我注意到以下几点:
1. created_at 和 updated_at 字段有默认值,不需要我们手动设置 `current_time`
2. 需要添加一些必填字段:api_brief_en, status, translate status, is_official, is_commercial
3. ur1 字段有唯一约束(UNIQUE)
让我修改导入脚本:
主要更新内容:
1. 更新了数据库连接信息
2. 移除了 created_at 和 updated_at 的手动设置(因为它们有默认值)
3. 添加了必需的字段:
api_brief_en:从 readme_en 中提取前 255 个字符
status:默认值 0
translate status:默认值 0
is official:设为 0
(假设所有 HuggingFace 模型都是非官方的)
is_commercial:默认值 0
4. 添加了对重复 URL 的处理(因为 url 字段有唯一约束)
您可以直接运行这个更新后的脚本来导入数据:
`python AI_Spider/utils/import_to_mysql.py`
执行插库脚本,尝试将数据入库,得到反馈 个别数据的 readme_en 字段,超出长度限制,检查 log 找到对应 JSON 数据和其网址,核对数据,当前字段类型为 TEXT,huggingface 中,个别模型 readme 部分很长,确定数据超出字段长度限制,根据建议修改字段为 MEDIUMTEXT
插入数据成功,使用 mysql mcp 查询数据库状态,得到反馈
让我查询一下 `ai_model_api_list` 表中的数据总数:
目前表中共有 5,605 条数据。
我也可以帮您查看一下数据的基本分布情况:
数据按平台分布如下:
Hugging Face: 2,997 条
Replicate: 2,608 条
您需要了解其他方面的数据分布情况吗?比如按类别、更新时间等维度的统计?
总结
MCP 在当前实例中的实际意义
- 使用 Playwright MCP 自动化进行网页分析和验证爬虫逻辑,自动生成爬虫脚本(这里其实有一点风险,直接从 webdriver 转 Request 的方式,自动生成的脚本可能遇到动态数据爬取不到的情况,需要在实际操作中,根据结果,进一步验证分析,很幸运在当前实例中并没有遇到)
- 使用 MySql MCP 自动化检查生成的数据插入脚本是否与数据库设计相匹配,数据库字段设计是否能匹配落地数据的要求,根据提示修改插入脚本和表字段设计,快速简单验证数据入库结果(受限于网络环境,有时也并不一定快速)
最终代码
- huggingface.py
import requests
from bs4 import BeautifulSoup
from typing import Dict, List, Optional
from datetime import datetime
import time
import logging
from dataclasses import dataclass, asdict
import json
from pathlib import Path
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
@dataclass
class ModelInfo:
"""模型信息数据类"""
name: str
category: str
version_time: str
downloads: int
url: str = ""
readme_en: str = ""
class HuggingFaceSpider:
"""Hugging Face 模型爬虫"""
BASE_URL = "https://huggingface.co/models"
def __init__(self, output_dir: str = "data"):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
})
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def _parse_downloads(self, downloads_text: str) -> int:
"""解析下载数"""
if not downloads_text:
return 0
# 移除所有空白字符和点
downloads_text = downloads_text.strip().replace("•", "").strip()
if 'k' in downloads_text.lower():
return int(float(downloads_text.lower().replace('k', '')) * 1000)
elif 'm' in downloads_text.lower():
return int(float(downloads_text.lower().replace('m', '')) * 1000000)
try:
return int(downloads_text)
except ValueError:
return 0
def _get_model_detail(self, model_url: str) -> tuple[str, str]:
"""获取模型详情页的readme内容和分类
Returns:
tuple: (category, readme_content)
"""
try:
response = self.session.get(model_url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# 查找分类信息
category_element = soup.select_one('header div.mb-3.flex.flex-wrap.md\\:mb-4 a:nth-child(1) div span')
category = category_element.text.strip() if category_element else "未指定"
# 查找readme内容
readme_div = soup.select_one('div.model-card-content.prose')
readme_content = str(readme_div) if readme_div else ""
return category, readme_content
except Exception as e:
logging.error(f"获取模型 {model_url} 详情页时出错: {str(e)}")
return "未指定", ""
def _parse_model_info(self, article) -> Optional[ModelInfo]:
"""解析单个模型信息"""
try:
# 获取模型名称
name = article.find('h4').text.strip()
# 获取模型URL
url = f"https://huggingface.co/{name}"
# 获取更新时间
time_element = article.find('time')
version_time = time_element.get('datetime', '')
if version_time:
# 转换为指定格式
dt = datetime.fromisoformat(version_time.replace('Z', '+00:00'))
version_time = dt.strftime('%Y-%m-%d %H:%M:%S')
# 获取下载数
link_text = article.find('a').text
downloads_text = link_text.split('•')[-1].strip()
downloads = self._parse_downloads(downloads_text)
# 获取详情页内容和分类
category, readme_en = self._get_model_detail(url)
return ModelInfo(name, category, version_time, downloads, url, readme_en)
except Exception as e:
logging.error(f"解析模型信息时出错: {str(e)}")
return None
def get_page(self, page: int) -> List[ModelInfo]:
"""获取指定页面的模型信息"""
url = f"{self.BASE_URL}?p={page}&sort=trending"
try:
response = self.session.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
models = []
articles = soup.find_all('article', class_='overview-card-wrapper')
for article in articles:
model_info = self._parse_model_info(article)
if model_info:
print("name", model_info.name)
print("category", model_info.category)
print("version_time", model_info.version_time)
print("url", model_info.url)
print("downloads", model_info.downloads)
print("=" * 60)
models.append(model_info)
return models
except Exception as e:
logging.error(f"获取页面 {page} 时出错: {str(e)}")
return []
def save_to_json(self, models: List[ModelInfo], filename: str = "huggingface_models.json"):
"""保存模型信息到JSON文件"""
filepath = self.output_dir / filename
try:
# 将ModelInfo对象转换为字典列表
models_data = [asdict(model) for model in models]
# 保存为JSON文件,设置缩进以提高可读性
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(models_data, f, ensure_ascii=False, indent=2)
logging.info(f"数据已保存到: {filepath}")
except Exception as e:
logging.error(f"保存JSON文件时出错: {str(e)}")
def get_all_models(self, max_pages: int = 100) -> List[ModelInfo]:
"""获取所有页面的模型信息"""
all_models = []
for page in range(max_pages):
logging.info(f"正在爬取第 {page + 1} 页...")
models = self.get_page(page)
if not models:
break
all_models.extend(models)
# 添加延迟,避免请求过于频繁
time.sleep(2) # 增加延迟到2秒,因为现在要爬取详情页
return all_models
def main():
"""主函数"""
# 创建爬虫实例,指定输出目录
spider = HuggingFaceSpider(output_dir="data")
# 获取所有模型信息
models = spider.get_all_models()
# 生成带时间戳的文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"huggingface_models_{timestamp}.json"
# 保存到JSON文件
spider.save_to_json(models, filename)
if __name__ == "__main__":
main()