㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟

  我长期专注 Python 爬虫工程化实战,主理专栏 《Python爬虫实战》:从采集策略反爬对抗,从数据清洗分布式调度,持续输出可复用的方法论与可落地案例。内容主打一个“能跑、能用、能扩展”,让数据价值真正做到——抓得到、洗得净、用得上

  📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间:如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
  
💕订阅后更新会优先推送,按目录学习更高效💯~

0️⃣ 前言(Preface)

  • 一句话说明:今天我们要用 Python (Requests + BeautifulSoup),遍历开源示例代码聚合站点的分页,提取项目卡片信息,最终产出一份结构化的本地代码库索引表。

  • 读完能获得什么

    1. 掌握 卡片式列表页 的通用抓取与 HTML 解析技巧。
    2. 学会处理多标签(Tags)的文本清洗与合并。
    3. 打通“网页抓取”到“API 数据富化”的进阶架构思路。

1️⃣ 摘要(Abstract)

  • 项目概述:本项目针对公开的开源项目目录网站,通过模拟 HTTP 请求遍历分页。利用 BS4 精准定位项目卡片 DOM 节点,提取项目名、编程语言、标签集、简介及仓库地址等 6 大核心维度,最终输出去重后的 CSV 数据集,为后续接入 GitHub API 提供基础数据底座。

  • 核心收益

    1. 建立个人专属的“轮子库”导航字典。
    2. 提升对脏数据(如缺失简介、相对路径链接)的容错处理能力。

2️⃣ 背景与需求(Why)

  • 为什么要爬

    • 知识聚合:优秀的代码示例往往散落在几百个分页里,找起来犹如大海捞针。爬下来集中管理,找代码快人一步。
    • 自动化基础:有了这个基础索引(特别是拿到了仓库链接),后续可以写脚本自动克隆项目,或者调用 API 分析代码活跃度。
  • 目标字段清单

    1. project_name(项目名称)
    2. language(主要编程语言,如 Python, Go, TS)
    3. tags(技术标签,如 Web, Async, ORM)
    4. update_time(页面上展示的更新时间)
    5. repo_link(仓库原始链接,极度重要🌟)
    6. description(一句话简介)

3️⃣ 合规与注意事项(必写)⚠️

做开源社区的爬虫,一定要保持极客的优雅与克制。

  • robots.txt:抓取前检查 domain.com/robots.txt,避开禁止抓取的路由。
  • 频率控制:开源聚合站往往是开发者用爱发电部署的(比如挂在 Vercel 或 GitHub Pages 上),不要用高并发去攻击人家!设置 time.sleep(2),做个文明人。
  • 数据使用:抓取的数据仅供个人学习、检索使用,请勿用于倒卖或构建垃圾 SEO 站。尊重开源,Keep Open Source Awesome

4️⃣ 技术选型与整体流程(What/How)

  • 技术流派静态 HTML 爬取(Requests + BeautifulSoup)

    • 理由:绝大多数代码示例目录站点(如基于 Hugo/Jekyll 构建的静态站)都是纯 HTML 输出,无需 Selenium 这把牛刀,直接请求源码速度最快。
  • 整体流程

    [构造分页 URL 集合: page=1 to N] 
         ⬇️
    [循环:请求列表页 HTML] ➡ (失败重试机制)
         ⬇️
    [解析层: 定位 <div class="project-card">] 
         ⬇️
    [提取内部字段: 语言, 标签拼接, 清洗链接] ➡ (容错: 缺失字段补 None)
         ⬇️
    [数据存储 (CSV) & 去重]
    

5️⃣ 环境准备与依赖安装(可复现)

  • Python 版本:推荐 Python 3.8+

  • 依赖安装

    pip install requests beautifulsoup4 pandas
    
  • 项目结构

    example_code_spider/
    ├── spider_main.py
    └── data/
        └── open_source_examples_index.csv  # 英文命名,避免路径报错
    

6️⃣ 核心实现:请求层(Fetcher)

import requests
import time
import random

# 构造一个看起来像真实浏览器的请求头
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.5'
}

def fetch_page(url, retries=3):
    """带重试机制的页面抓取器"""
    try:
        # 爬虫的修养:随机延时 1.5 - 3 秒
        time.sleep(random.uniform(1.5, 3.0))
        
        print(f"🌍 正在请求: {url}")
        response = requests.get(url, headers=HEADERS, timeout=10)
        
        # 遇到 429 限流时的特殊处理
        if response.status_code == 429:
            print("🛑 触发网站限流 (429)! 休眠 15 秒后重试...")
            time.sleep(15)
            return fetch_page(url, retries - 1)
            
        response.raise_for_status() # 非 200 状态码直接抛出异常
        return response.text
        
    except requests.exceptions.RequestException as e:
        if retries > 0:
            print(f"⚠️ 请求异常: {e},剩余重试次数: {retries}")
            return fetch_page(url, retries - 1)
        print(f"❌ 放弃抓取 {url}")
        return None

7️⃣ 核心实现:解析层(Parser)

这是爬虫的核心逻辑。我们要把整个列表页切碎,精准提取每一张“项目卡片”里的数据。

from bs4 import BeautifulSoup
from urllib.parse import urljoin

def parse_project_cards(html, base_url):
    """解析页面,提取项目卡片数组"""
    if not html:
        return []
        
    soup = BeautifulSoup(html, 'html.parser')
    projects_data = []
    
    # 假设每个项目都被包裹在一个 class 为 "example-card" 的 div 中
    cards = soup.find_all('div', class_='example-card')
    
    for card in cards:
        try:
            # 1. 项目名与仓库链接 (通常包裹在卡片头部的 a 标签)
            title_tag = card.find('h3', class_='card-title').find('a')
            project_name = title_tag.get_text(strip=True) if title_tag else "Unknown_Project"
            
            raw_link = title_tag['href'] if title_tag and 'href' in title_tag.attrs else ""
            # 解决相对路径问题,将其转换为完整的绝对链接
            repo_link = urljoin(base_url, raw_link)
            
            # 2. 编程语言
            lang_tag = card.find('span', class_='lang-badge')
            language = lang_tag.get_text(strip=True) if lang_tag else "Mixed"
            
            # 3. 提取多个标签并合并 (例如:[React, TypeScript, Admin])
            tags_container = card.find('div', class_='tags-list')
            tags = []
            if tags_container:
                # 提取所有标签文本并用逗号拼接
                tags = [t.get_text(strip=True) for t in tags_container.find_all('span', class_='tag')]
            tags_str = ", ".join(tags) if tags else "None"
            
            # 4. 简介容错提取
            desc_tag = card.find('p', class_='description')
            description = desc_tag.get_text(strip=True) if desc_tag else "No description provided."
            
            # 5. 更新时间
            time_tag = card.find('time')
            update_time = time_tag['datetime'] if time_tag and 'datetime' in time_tag.attrs else (time_tag.get_text(strip=True) if time_tag else "Unknown")
            
            # 组装字典
            projects_data.append({
                'project_name': project_name,
                'language': language,
                'tags': tags_str,
                'update_time': update_time,
                'repo_link': repo_link,
                'description': description
            })
            
        except AttributeError as e:
            # 单个卡片结构异常,不影响其他卡片
            print(f"🐛 解析单张卡片失败,跳过: {e}")
            continue
            
    return projects_data

8️⃣ 数据存储与导出(Storage)

抓取的数据建议存为 CSV,不仅查阅方便,而且非常适合日后用 Pandas 导入进行二次处理(配合 API 补数)。

import pandas as pd
import os

def save_to_csv(data_list, filepath="data/open_source_examples_index.csv"):
    if not data_list:
        print("📭 没有提取到任何数据。")
        return
        
    # 确保 data 目录存在
    os.makedirs(os.path.dirname(filepath), exist_ok=True)
    
    df_new = pd.DataFrame(data_list)
    
    # 增量保存策略:如果文件已存在,先读取旧数据,再与新数据合并去重
    if os.path.exists(filepath):
        df_old = pd.read_csv(filepath)
        df_combined = pd.concat([df_old, df_new], ignore_index=True)
    else:
        df_combined = df_new
        
    # 🌟 去重策略:按“仓库链接 (repo_link)”去重,确保库里只有一个唯一索引
    df_combined.drop_duplicates(subset=['repo_link'], keep='last', inplace=True)
    
    # 存盘,指定 utf-8-sig 防止 Excel 打开乱码
    df_combined.to_csv(filepath, index=False, encoding='utf-8-sig')
    print(f"💾 数据已安全着陆!当前索引库总计: {len(df_combined)} 个示例项目。")

9️⃣ 运行方式与结果展示(必写)

这是拼装积木的最后一步。

if __name__ == "__main__":
    print("🚀 开源示例代码雷达启动!")
    
    BASE_URL = "https://example-directory-site.com"
    # 假设 URL 结构为 /examples?page=N
    target_url_template = f"{BASE_URL}/examples?page={{}}" 
    
    all_extracted_projects = []
    
    # 假设我们要抓取前 3 页
    for page_num in range(1, 4):
        print(f"\n📄 正在扫描第 {page_num} 页...")
        target_url = target_url_template.format(page_num)
        
        # 1. 抓取 HTML
        # html_content = fetch_page(target_url)
        
        # --- 为了演示可运行性,这里采用 Mock 数据 ---
        html_content = f"""
        <div class="example-card">
            <h3 class="card-title"><a href="/repo/github/user/demo{page_num}">Awesome Demo {page_num}</a></h3>
            <span class="lang-badge">Python</span>
            <div class="tags-list"><span class="tag">FastAPI</span><span class="tag">Redis</span></div>
            <p class="description">A boilerplate for FastAPI backend.</p>
            <time datetime="2023-10-0{page_num}">Oct 0{page_num}, 2023</time>
        </div>
        """
        # ----------------------------------------
        
        # 2. 解析卡片
        projects = parse_project_cards(html_content, BASE_URL)
        print(f"   -> 本页发现 {len(projects)} 个宝藏项目")
        
        all_extracted_projects.extend(projects)
    
    # 3. 存储结果
    save_to_csv(all_extracted_projects)
    print("🏁 阶段性任务完成!")

📊 CSV 示例结果展示:

project_name language tags update_time repo_link description
Awesome Demo 1 Python FastAPI, Redis 2023-10-01 https://…/repo/…/demo1 A boilerplate for FastAPI backend.
Awesome Demo 2 Python FastAPI, Redis 2023-10-02 https://…/repo/…/demo2 A boilerplate for FastAPI backend.

🔟 常见问题与排错(老鸟经验)🛠️

  1. 标签(Tags)解析出来全黏在一起了

    • 问题soup.get_text() 有时候会把 <span>Vue</span><span>Vite</span> 变成 VueVite
    • 解法:像教程中那样,用 find_all 找到每个单独的 span,再将文本提取到列表中,最后用 ", ".join(list) 拼接,干净利落。
  2. 提取的链接点不开(404)

    • 问题:网页源码写的 href="/user/repo",你直接存下来,浏览器无法识别。
    • 解法:一定要引入 from urllib.parse import urljoin。使用 urljoin(base_url, raw_link) 自动将相对路径转换为绝对路径。
  3. 抓到一堆空壳(None/Unknown)

    • 排错:很多示例项目年久失修,作者根本没写简介,或者没有时间标签。所以 if tag else "Default" 的容错判断在爬虫里是保命符。

1️⃣1️⃣ 进阶优化:GitHub API 补数(满分操作🌟)

这个思路真的太棒了,必须要详细展开!

静态网页上的信息往往是不完整的(比如没有 Stars 数,没有最新的 Commit 时间)。我们可以利用刚才存下来的 repo_link,做二次富化(Enrichment)

  1. 写一个新脚本读取刚才的 open_source_examples_index.csv
  2. 正则匹配提取出 ownerrepo_name(如从 https://github.com/tiangolo/fastapi 提取出 tiangolofastapi)。
  3. 调用 GitHub API:GET https://api.github.com/repos/{owner}/{repo_name}
  4. 填补高阶数据:拿到真实的 stargazers_count(星标数)、forks_count(克隆数)、pushed_at(真实最后更新时间)。
  5. 注意:GitHub API 没 Token 只能请求 60次/小时,记得申请个 Personal Access Token 放在 Headers 里,能提升到 5000次/小时!

1️⃣2️⃣ 总结与延伸阅读

  • 复盘:今天我们不仅写出了一个静态 HTML 卡片解析器,还学习了容错提取、路径拼装和基于唯一特征(URL)的数据去重方案。这为你构建个人代码资产库打下了第一根地基。
  • 延伸:如果你想做一个在线的“示例代码搜索引擎”,可以尝试把这份 CSV 数据导入到 Algolia 或者轻量级的 MeiliSearch 中,再用 Vue 写个简单的搜索框,一个媲美官方的优秀开源社区导航站就诞生啦!

🌟 文末

好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

✅ 专栏持续更新中|建议收藏 + 订阅

墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以“入门 → 进阶 → 工程化 → 项目落地”的路线持续更新,争取让每一期内容都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?

评论区留言告诉我你的需求,我会优先安排实现(更新)哒~


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


✅ 免责声明

本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。

使用或者参考本项目即表示您已阅读并同意以下条款:

  • 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
  • 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
  • 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
  • 使用或者参考本项目即视为同意上述条款,即 “谁使用,谁负责” 。如不同意,请立即停止使用并删除本项目。!!!
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐