15  爬虫实例 1:岭南学院教师名录整理

从一个网址出发,通过与 AI 的多轮对话,逐步形成规范提示词,最终生成可运行代码。

0. 开始之前

安装依赖

📦 环境准备
pip install requests beautifulsoup4 pandas openpyxl
  • requests:发送 HTTP 请求,获取网页内容
  • beautifulsoup4:解析 HTML,提取目标字段
  • pandas:整理数据,输出 Excel
  • openpyxlpandas 输出 .xlsx 格式的底层依赖

设定 AI 工作模式

大多数 AI 的默认行为是「收到任务 → 立即给出完整答案」。对于爬虫任务,这意味着:你说「帮我写爬虫」,它会直接生成一段可能完全不符合需求的代码,而不是先问清楚你要什么。

在发出第一条消息之前,先把以下角色设定提示词完整复制,单独发给 AI(ChatGPT / DeepSeek / 豆包均适用),收到「明白」后再开始对话:

📋 角色设定提示词

接下来我们要合作完成一个网页爬虫任务。请你扮演一位爬虫开发引导者,遵守以下规则:

  1. 不要直接生成代码,除非我明确说「请生成代码」
  2. 每次回复只问我一到两个最关键的问题,等我回答后再继续
  3. 引导我依次提供:目标网址、需要的字段、数据用途、HTML 结构(需我去 DevTools 查)、是否有翻页、输出格式——但不要一次全问
  4. 信息足够后,先把收集到的内容整理成结构化提示词给我确认,我确认后再生成代码
  5. 如果我的信息有歧义或可能导致代码出错,请追问,不要自行假设

收到后请回复「明白,请告诉我你的爬虫任务」。

1. 与 AI 的对话过程

下面是一个零基础学生发送角色设定后,与 AI 完成这项任务的完整对话记录。

阅读方式:通读全部六轮对话,理解每轮在解决什么问题。操作步骤(如何使用 DevTools)集中在第 2 节,读完对话再去操作。

🧑 学生·第 1 轮

我想爬取这个网页的信息:https://lingnan.sysu.edu.cn/Faculty,帮我写一个 Python 爬虫。(附上截图)

岭南学院教师名录页面截图
🤖 AI·第 1 轮

好的,我看到这是中山大学岭南学院的教师名录页面。在写代码之前,我需要先了解几件事。

第一个问题:你想提取哪些信息,打算用这份数据做什么?

从截图来看,每位教师的卡片里包含姓名、职称、邮箱、研究方向,部分教师还有职务信息(如「院长助理」)。你需要全部提取,还是只要其中几个字段?了解用途有助于我决定输出格式。

💡 本轮要点:AI 没有直接写代码,而是先问「要什么」和「做什么用」——对应第一章提示词八要素中的要素②(数据字段)和要素⑤(输出要求)。

🧑 学生·第 2 轮
  • 我需要的字段:姓名、职称、教研室、职务、邮箱、研究方向,还有个人主页链接。
  • 输出:整理一份名录存档,方便查阅;输出成一份 Excel + 一个 Markdown 文档。
🤖 AI·第 2 轮

明白了。现在我需要了解网页的 HTML 结构,才能告诉代码去哪里找数据。

请做一件事:在 Chrome 中打开这个页面,对着任意一位教师的姓名右键 → 选「检查」,开发者工具会弹出并高亮对应的 HTML。找到包含该教师完整信息的外层 <div>,右键 → Copy → Copy outerHTML,把复制的内容发给我。

具体操作方法见本节末尾的「DevTools 操作指引」。

💡 本轮要点:AI 需要 HTML 结构才能写出正确的选择器,但它看不到网页——必须让你去 DevTools 里取。这正是第一章 1.4 节介绍 DevTools 的实战应用。

🧑 学生·第 3 轮

我找到了,是这样的:

<div class="infors">
  <h3>
    <a href="/faculty/LuDi" target="_blank" hreflang="zh-hans">卢荻</a>
    <span>教授</span>
  </h3>
  <p class="one-line">政治经济学教研室</p>
  <p class="one-line">ludi26@mail.sysu.edu.cn</p>
  <p class="text-light two-line">
    <b>研究方向:</b>中国经济发展
  </p>
</div>
🤖 AI·第 3 轮

结构很清晰。我能看出:姓名在 h3 > a,职称在 h3 > span,主页链接是 href 属性(需拼接域名),研究方向在 p.two-line(去掉「研究方向:」前缀)。

但有一个问题class="one-line"<p> 标签出现了两次——第一个是「政治经济学教研室」,第二个是邮箱。同一个 class 对应不同内容,代码无法靠 class 名直接区分(这是第一章误区⑥提到的情况)。

另外,截图里「许准」教授有「院长助理」这个职务字段。请再复制一下许准的 HTML,我需要对比两人的结构,确认职务字段是否影响其他字段的位置。

💡 本轮要点:AI 从截图中发现「院长助理」,主动要求第二个样例——因为只有一个样例时,无法发现字段可能缺失的情况(对应第一章误区⑥)。

🧑 学生·第 4 轮

好的,许准的是这样:

<div class="infors">
  <h3>
    <a href="/faculty/XuZhun" target="_blank" hreflang="zh-hans">许准</a>
    <span>教授</span>
  </h3>
  <p class="one-line">政治经济学教研室</p>
  <p class="one-line">院长助理</p>
  <p class="one-line">xuzh237@mail.sysu.edu.cn</p>
  <p class="text-light two-line">
    <b>研究方向:</b>政治经济学、发展经济学、思想史
  </p>
</div>
🤖 AI·第 4 轮

两段 HTML 对比,结构差异很清楚了:

字段 卢荻(无职务) 许准(有职务)
教研室 第 1 个 p.one-line 第 1 个 p.one-line
职务 ❌ 不存在 第 2 个 p.one-line
邮箱 第 2 个 p.one-line 第 3 个 p.one-line

不能靠位置区分,要靠内容特征:含 @ 的是邮箱,其余的是职务。

还有两个问题

  1. 除了 Excel,是否考虑同时输出一份 Markdown 文件?名录类文档用 Markdown 格式更便于共享和查阅(飞书、Notion、GitHub 均可直接渲染)。
  2. 页面顶部有「政治经济学教研室」等分类标签——切换标签时 URL 有没有变化?请试一下,确认是否需要翻页。

💡 本轮要点:AI 主动建议增加 Markdown 输出(要素⑤),并追问翻页问题(要素①)——不确认翻页逻辑,代码可能只抓到当前筛选下的教师,而非全部名录。

🧑 学生·第 5 轮

Markdown 也要,好主意。

翻页:我切换了几个教研室标签,URL 没有变化,一直是 lingnan.sysu.edu.cn/Faculty。滑到底部也没有「下一页」按钮。

🤖 AI·第 5 轮

URL 不变、无翻页按钮,说明所有教师数据在同一个 HTML 页面里,教研室切换只是前端 JavaScript 的显示/隐藏,不影响爬取——只需请求一次页面。

现在信息已经足够,先把对话内容整理成提示词,你确认后我再生成代码:

📋 整理后的完整提示词

请帮我用 Python 写一个爬虫,完成以下任务。本代码仅用于课堂教学演示。

目标网站https://lingnan.sysu.edu.cn/Faculty,所有教师在同一页面,无需翻页,一次请求即可获取全部数据。

采集对象:每一位教师的基本信息,每条记录对应一位教师。

数据格式:数据直接写在 HTML 中(模式 A)。以下是两条真实 HTML 样例,注意结构不同——卢荻无职务字段,许准有:

<!-- 样例 A:无职务 -->
<div class="infors">
  <h3>
    <a href="/faculty/LuDi" target="_blank">卢荻</a>
    <span>教授</span>
  </h3>
  <p class="one-line">政治经济学教研室</p>
  <p class="one-line">ludi26@mail.sysu.edu.cn</p>
  <p class="text-light two-line"><b>研究方向:</b>中国经济发展</p>
</div>

<!-- 样例 B:有职务 -->
<div class="infors">
  <h3>
    <a href="/faculty/XuZhun" target="_blank">许准</a>
    <span>教授</span>
  </h3>
  <p class="one-line">政治经济学教研室</p>
  <p class="one-line">院长助理</p>
  <p class="one-line">xuzh237@mail.sysu.edu.cn</p>
  <p class="text-light two-line"><b>研究方向:</b>政治经济学、发展经济学、思想史</p>
</div>

字段识别规则(请严格按此逻辑解析): - h3 > a 文本为姓名href 拼接 https://lingnan.sysu.edu.cn 后为个人主页链接 - h3 > span 文本为职称 - class="one-line"<p> 标签按顺序处理:第一个为教研室;之后含 @ 的为电子邮箱;不含 @ 的为职务 - classtwo-line<p> 内容去掉「研究方向:」前缀后为研究方向 - 字段不存在时填入空字符串,不跳过整条记录

需要提取的字段(共 7 个): 姓名职称教研室职务研究方向电子邮箱个人主页链接

技术要求:使用 requests + BeautifulSoupUser-Agent 设为真实 Chrome 值;请求失败时打印错误并终止;解析单条逻辑封装为函数 parse_faculty(card)

输出: - Excel 文件:lingnan_faculty.xlsx,中文字段名 - Markdown 文件:lingnan_faculty.md,列表格式,邮箱和链接用尖括号包裹(可点击),样式如下:

- **许准**|教授
  - 教研室:政治经济学教研室
  - 职务:院长助理
  - 研究方向:政治经济学、发展经济学、思想史
  - 邮箱:<xuzh237@mail.sysu.edu.cn>
  - 主页:<https://lingnan.sysu.edu.cn/faculty/XuZhun>

代码要求:加中文注释;先只打印前 3 条,确认字段正确后再保存完整文件。

💡 本轮要点:AI 在生成代码前先整理提示词、等待确认——这是本章最重要的工作习惯。对话中散落的信息在这一步汇聚成标准格式,也让你看清「提示词是怎么从对话里生长出来的」。


🧑 学生·第 6 轮

好的,请生成代码。

2. DevTools 操作指引

本节对应第 3 轮对话中 AI 要求「复制 HTML」的操作步骤。如果你已经熟悉 DevTools,可以跳过。

DevTools 的基本用法见第一章 1.4 节。这里只补充一个新动作:如何复制单个元素的完整 HTML

第一步:打开 DevTools,定位目标元素

  • Windows:F12Ctrl+Shift+C(直接进入元素选择模式)
  • Mac:Command+Option+ICommand+Shift+C

用箭头选择器点击页面上任意一位教师的姓名,Elements 面板会自动高亮对应的 <a> 标签。

📷 截图插入点 A:在 lingnan.sysu.edu.cn/Faculty 页面,用箭头选择器点击「卢荻」后,Elements 面板高亮 <a> 标签、周围可见 <div class="infors"> 完整结构的截图。

第二步:向上选取外层 <div>

Elements 面板高亮的是最内层的 <a> 标签。在面板里点击上方的 <div class="infors">,选中包含该教师全部信息的外层块。

📷 截图插入点 B:在 Elements 面板中,点击 <a> 标签上方的 <div class="infors">,整个教师信息块被蓝色高亮的截图。

第三步:复制 outerHTML

<div class="infors">右键 → Copy → Copy outerHTML,即可复制该教师的完整 HTML 代码,直接粘贴给 AI。

📷 截图插入点 C:右键菜单展开「Copy」子菜单,「Copy outerHTML」选项高亮的截图。

💡 为什么要复制外层 <div>,而不是直接复制 <a> 标签?

<a> 标签只包含姓名和链接,AI 看不到邮箱、研究方向等其他字段。复制包含完整信息的外层块,AI 才能分析字段之间的结构关系,写出正确的解析逻辑。

3. 代码

# 安装依赖包
!pip install openpyxl
import requests
from bs4 import BeautifulSoup
import pandas as pd


# ── 请求头:模拟真实浏览器,避免被服务器拒绝 ──
HEADERS = {
    'User-Agent': (
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
        'AppleWebKit/537.36 (KHTML, like Gecko) '
        'Chrome/120.0.0.0 Safari/537.36'
    )
}

BASE_URL = 'https://lingnan.sysu.edu.cn'
LIST_URL = f'{BASE_URL}/Faculty'


def make_abs_url(href: str) -> str:
    """
    将相对链接转为绝对链接。
    """
    if not href:
        return ''

    href = href.strip()
    if href.startswith('http://') or href.startswith('https://'):
        return href
    if href.startswith('/'):
        return BASE_URL + href

    return BASE_URL + '/' + href


def parse_faculty(card) -> dict:
    """
    解析单个教师信息块,返回标准字段字典。
    字段缺失时填空字符串。
    """
    result = {
        '姓名': '',
        '职称': '',
        '教研室': '',
        '职务': '',
        '研究方向': '',
        '电子邮箱': '',
        '个人主页链接': ''
    }

    # ── 1. 姓名 + 个人主页链接 + 职称 ──
    h3 = card.find('h3')
    if h3:
        a_tag = h3.find('a')
        if a_tag:
            result['姓名'] = a_tag.get_text(strip=True)
            href = a_tag.get('href', '')
            result['个人主页链接'] = make_abs_url(href)

        # 有些页面把职称放在 h3 内的 span 里
        span = h3.find('span')
        if span:
            result['职称'] = span.get_text(strip=True)
        else:
            # 如果没有 span,则尝试从 h3 的纯文本中剥离姓名,提取剩余部分作为职称
            h3_text = h3.get_text(' ', strip=True)
            if result['姓名'] and h3_text.startswith(result['姓名']):
                title = h3_text.replace(result['姓名'], '', 1).strip()
                result['职称'] = title

    # ── 2. 处理 p 标签:教研室 / 职务 / 邮箱 ──
    # 原网页中通常:
    # 第一个 one-line 是教研室
    # 含 @ 的是邮箱
    # 其他 one-line 往往是职务
    one_lines = card.find_all('p', class_='one-line')

    for i, p in enumerate(one_lines):
        text = p.get_text(' ', strip=True)
        if not text:
            continue

        if i == 0 and not result['教研室']:
            result['教研室'] = text
        elif '@' in text:
            result['电子邮箱'] = text
        else:
            # 可能存在多个非邮箱 one-line,合并为“职务”
            if result['职务']:
                result['职务'] += ';' + text
            else:
                result['职务'] = text

    # ── 3. 研究方向 ──
    two_line = card.find('p', class_='two-line')
    if two_line:
        raw = two_line.get_text(' ', strip=True)
        raw = raw.replace('研究方向:', '').replace('研究方向:', '').strip()
        result['研究方向'] = raw

    return result


def deduplicate_faculty(records: list) -> list:
    """
    对教师记录去重。
    优先使用“个人主页链接”作为唯一标识;
    若主页链接为空,则退化为“姓名 + 邮箱”。
    """
    seen = set()
    clean_records = []

    for row in records:
        # 优先使用主页链接去重
        if row['个人主页链接']:
            key = ('url', row['个人主页链接'])
        else:
            key = ('fallback', row['姓名'], row['电子邮箱'])

        if key not in seen:
            seen.add(key)
            clean_records.append(row)

    return clean_records


def main():
    # ── Step 1:请求页面 ──
    print(f'正在请求:{LIST_URL}')
    resp = requests.get(LIST_URL, headers=HEADERS, timeout=20)
    resp.raise_for_status()
    resp.encoding = 'utf-8'

    # ── Step 2:解析 HTML ──
    soup = BeautifulSoup(resp.text, 'html.parser')

    # 仍然先按原有 class 抓取,但随后必须去重
    cards = soup.find_all('div', class_='infors')
    print(f'原始抓取到 {len(cards)} 个信息块')

    # ── Step 3:解析全部记录 ──
    all_faculty_raw = []

    for card in cards:
        info = parse_faculty(card)

        # 过滤明显无效记录:
        # 至少要有姓名;同时主页链接中不应是列表页本身
        if not info['姓名']:
            continue

        if info['个人主页链接'].rstrip('/') == LIST_URL.rstrip('/'):
            continue

        all_faculty_raw.append(info)

    print(f'解析后得到 {len(all_faculty_raw)} 条原始教师记录')

    # ── Step 4:去重 ──
    all_faculty = deduplicate_faculty(all_faculty_raw)
    print(f'去重后保留 {len(all_faculty)} 位教师')

    # ── Step 5:预览前 5 条 ──
    print('\n【前 5 条预览】')
    for row in all_faculty[:5]:
        for k, v in row.items():
            print(f'  {k}{v}')
        print()

    # ── Step 6:转为 DataFrame ──
    df = pd.DataFrame(all_faculty)

    # 可选:按姓名排序,便于查阅
    df = df.sort_values(by='姓名', kind='stable').reset_index(drop=True)

    # ── Step 7:输出 Excel;若环境缺 openpyxl,则退回 CSV ──
    try:
        df.to_excel('lingnan_faculty.xlsx', index=False)
        print(f'✅ Excel 已保存:lingnan_faculty.xlsx(共 {len(df)} 条)')
    except ModuleNotFoundError:
        print('⚠️ 未安装 openpyxl,无法写入 xlsx,改为输出 CSV')
        df.to_csv('lingnan_faculty.csv', index=False, encoding='utf-8-sig')
        print(f'✅ CSV 已保存:lingnan_faculty.csv(共 {len(df)} 条)')

    # ── Step 8:输出 Markdown ──
    md_lines = ['# 中山大学岭南学院教师名录', '']

    for row in all_faculty:
        title_line = f"- **{row['姓名']}**"
        if row['职称']:
            title_line += f"|{row['职称']}"
        md_lines.append(title_line)

        if row['教研室']:
            md_lines.append(f"  - 教研室:{row['教研室']}")
        if row['职务']:
            md_lines.append(f"  - 职务:{row['职务']}")
        if row['研究方向']:
            md_lines.append(f"  - 研究方向:{row['研究方向']}")
        if row['电子邮箱']:
            md_lines.append(f"  - 邮箱:<{row['电子邮箱']}>")
        if row['个人主页链接']:
            md_lines.append(f"  - 主页:<{row['个人主页链接']}>")

        md_lines.append('')

    with open('lingnan_faculty.md', 'w', encoding='utf-8') as f:
        f.write('\n'.join(md_lines))

    print('✅ Markdown 已保存:lingnan_faculty.md')


if __name__ == '__main__':
    main()
正在请求:https://lingnan.sysu.edu.cn/Faculty
原始抓取到 249 个信息块
解析后得到 249 条原始教师记录
去重后保留 83 位教师

【前 5 条预览】
  姓名:毕青苗
  职称:副教授
  教研室:政治经济学教研室
  职务:
  研究方向:中国经济增长,营商环境建设
  电子邮箱:biqm@mail.sysu.edu.cn
  个人主页链接:https://lingnan.sysu.edu.cn/faculty/BiQingmiao

  姓名:才国伟
  职称:教授
  教研室:国际经济与区域经济教研室
  职务:
  研究方向:经济增长与发展、国际经济与政策、 数字经济与政策等
  电子邮箱:caiguow@mail.sysu.edu.cn
  个人主页链接:https://lingnan.sysu.edu.cn/faculty/CaiGuowei

  姓名:蔡荣鑫
  职称:副教授
  教研室:公司金融教研室
  职务:
  研究方向:企业并购、私募股权与风险投资、经济增长与贫困问题
  电子邮箱:cairx@mail.sysu.edu.cn
  个人主页链接:https://lingnan.sysu.edu.cn/faculty/CaiRongxin

  姓名:陈川
  职称:副教授
  教研室:计量经济与数据科学教研室
  职务:
  研究方向:联邦学习、机器学习鲁棒性、LLM可靠性、图机器学习、社交网络分析
  电子邮箱:chenchuan@mail.sysu.edu.cn
  个人主页链接:https://lingnan.sysu.edu.cn/faculty/ChenChuan

  姓名:陈刚
  职称:教授
  教研室:国际经济与区域经济教研室
  职务:
  研究方向:物流工程、运营管理、数字经济
  电子邮箱:lnscheng@mail.sysu.edu.cn
  个人主页链接:https://lingnan.sysu.edu.cn/faculty/ChenGang

✅ Excel 已保存:lingnan_faculty.xlsx(共 83 条)
✅ Markdown 已保存:lingnan_faculty.md
提示词

AI 生成的原始代码一共输出了 249 条教师记录,我反馈了这个信息后,AI 修正了代码,得到了正确的结果。

4. 验证:看到什么才算正确

代码运行后,Step 3 会打印前 3 条预览。对照以下标准判断是否继续:

检查项 期望结果 若不符合
姓名列 全为中文姓名 若出现邮箱或职称,字段严重错位,检查 parse_faculty 逻辑
邮箱列 @,格式如 xxx@mail.sysu.edu.cn 若为空或出现姓名,@ 识别逻辑有误
教师总数 几十至一百多条 若只有个位数,find_all('div', class_='infors') 的 class 名可能写错
职务列 少数教师有值,多数为空 若全部为空,检查职务识别逻辑;全部有值则可能把教研室误判为职务
💡 「职务为空」是正常现象

大多数教师没有行政职务,职务 列为空不是错误。检查的目的是区分「正常缺失」(无职务)和「解析错位」(职务被错误地识别为邮箱,或邮箱被丢失)。

5. 如果结果不对:发起调试对话

字段出错时,不要从头重写提示词,而是把具体错误现象告诉 AI。有效的调试消息包含三件事:

① 错误现象(哪列出了什么问题)
② 已做的验证(你确认了什么)
③ 中间结果(打印出的原始数据或 HTML)

示例

代码跑通了,但「职务」列全部为空。我去页面确认过,许准的卡片里确实有「院长助理」。

我打印了许准对应的 one_lines 内容:

政治经济学教研室
院长助理
xuzh237@mail.sysu.edu.cn

三行都在,但职务列还是空的。

这三件事让 AI 能精准定位问题,而不是重写整段代码。信息越具体,修复越快。

6. 复盘

六轮对话,每轮解决了什么:

轮次 学生提供了什么 AI 追问了什么 对应要素
第 1 轮 网址 + 截图 要哪些字段?做什么用? ②⑤
第 2 轮 字段清单 + 用途 去 DevTools 取 HTML
第 3 轮 一段 HTML 发现结构不一致,要第二段
第 4 轮 第二段 HTML 建议加 Markdown;追问翻页 ①⑤
第 5 轮 确认无翻页 整理提示词,等待确认
第 6 轮 确认提示词 生成代码
💡 规律:AI 追问 = 填写提示词模板的空格

对比这次对话和第一章 1.7 节的提示词通用模板,你会发现:AI 每次追问,恰好对应模板里缺失的一个要素

理解这个规律后,下次可以反过来操作:先自己对照模板把空格填好,再发给 AI,减少来回追问的轮数,第一次就拿到更可用的代码。

课堂讨论

  1. 第 3 轮中,如果学生只贴了卢荻的 HTML(没有许准的),AI 生成的代码会在哪里出错?出错的现象是什么?

  2. 调试消息的三要素(错误现象、已做验证、中间结果)中,哪一项最容易被忽略?忽略它会带来什么后果?

  3. 本例的页面只需请求一次,不需要设置请求间隔。如果改为爬取每位教师的详情页,提示词里需要新增哪些内容?

  4. 浏览输出结果 lingnan_faculty.md 时,发现 曾燕 老师的信息显示格式有误。请分析原因,并撰写提示词,让 AI 优化代码。

     - **曾燕**|教授
       - 教研室:保险与金融工程教研室
       - 研究方向:数字(普惠)金融:创新、风险与监管
    
     数字经济(发展趋势、社会效应、风险与安全)
    
     数字保险:创新、风险与监管
    
     平台经济:定价、风险管理、监管
    
     金融工程:定价、资产配置
    
     风险管理:风险测度、风险传染、风险管理策略
    
     保险精算:保险公司投资、分红与再保险策略等
       - 邮箱:<zengy36@mail.sysu.edu.cn>
       - 主页:<https://lingnan.sysu.edu.cn/faculty/ZengYan>