查看全课大纲(第 48 / 51 节)
  1. 1.为什么人人都该学点编程?
  2. 2.计算机与程序设计:你的电脑到底在“想”什么?
  3. 3.Python 这门语言,到底有什么魔力?
  4. 4.Python 开发环境配置:如何拥有你的第一个“编程工具箱”?
  5. 5.如何安装一个更强大的代码编辑器?
  6. 6.汇率兑换1.0:你的第一个“钱生钱”小程序
  7. 7.你的第一行代码,藏着哪些秘密?
  8. 8.汇率兑换2.0(上):让程序看懂“CNY”和“USD”
  9. 9.汇率兑换程序,如何让它“聪明”地判断币种?
  10. 10.汇率兑换程序能一直运行,直到我喊停吗?
  11. 11.汇率兑换4.0:如何把“算钱”功能打包成积木?
  12. 12.汇率兑换5.0:给你的程序穿上“结构化”的西装
  13. 13.怎么用 Python 画一个会动的五角星?
  14. 14.如何让程序自动画出不同大小的五角星?
  15. 15.函数自己调用自己,会发生什么?
  16. 16.用递归画出神奇的分形树
  17. 17.Python 怎么计算你一天躺着不动能消耗多少热量?
  18. 18.如何让程序“活”起来,持续为你服务?
  19. 19.如何让 BMR 计算器更“聪明”?
  20. 20.当程序遇到“不按套路出牌”的用户怎么办?
  21. 21.怎么用 Python 模拟一个“滚雪球”式的存钱计划?
  22. 22.如何用列表记录每一周的存款?
  23. 23.如何让程序自己数数?—— 用 for 循环解放你的双手
  24. 24.52周存钱挑战4.0:你的存款计算器能有多灵活?
  25. 25.52周存钱挑战:怎么根据日期自动查存款?
  26. 26.Python 怎么判断一年中的第几天?
  27. 27.列表 vs 元组:哪个更适合存储月份天数?
  28. 28.用集合给月份分个队,换个思路算天数
  29. 29.把月份和天数“锁”在一起:用字典给数据配个对
  30. 30.Python 怎么判断密码强弱?
  31. 31.Python 怎么给密码设置“防沉迷”系统?
  32. 32.Python 怎么把密码保存到文件里?
  33. 33.Python 怎么读取文件里的密码?
  34. 34.Python 怎么把密码检查工具“打包”起来?
  35. 35.Python 怎么把文件操作也“打包”起来?
  36. 36.计算机怎么“扔骰子”?——用Python模拟随机事件
  37. 37.如何用 Python 模拟掷两个骰子?
  38. 38.怎么用 Python 把骰子轨迹画出来?
  39. 39.骰子点数分布长啥样?用直方图一眼看穿!
  40. 40.用科学计算库让骰子程序“起飞”
  41. 41.空气质量指数怎么算?手把手教你写一个AQI计算器
  42. 42.空气质量指数计算:从零开始,让程序看懂空气
  43. 43.如何读取空气质量数据文件并找出空气质量最好的五个监测点?
  44. 44.JSON 数据怎么变成 Excel 表格?—— 让数据换个“马甲”
  45. 45.Python 怎么让程序自己判断文件类型?
  46. 46.如何让程序自己上网查空气质量?
  47. 47.用美味的汤来解析网页:Beautiful Soup 入门
  48. 48.如何让爬虫自动抓取全国所有城市的空气质量?
  49. 49.爬到的数据怎么存下来?—— 把全国空气质量数据保存为CSV文件
  50. 50.用 pandas 玩转空气质量数据:找出全国空气最好和最差的城市
  51. 51.空气质量数据怎么“洗”干净,还能画成图?

如何让爬虫自动抓取全国所有城市的空气质量?

约 9 分钟

小象实战讲义 · Python 入门:8 个小项目边玩边学

上一节我们学会了用 BeautifulSoup 解析单个城市的空气质量数据,但每次都要手动输入城市拼音,效率太低了!这一节,我们要让爬虫变得更聪明——自动找到网站上所有城市的链接,然后一口气把全国城市的空气质量全部抓取下来!

学完这一节,你将得到一个真正的实用爬虫:只要运行一次程序,就能看到全国几百个城市的实时空气质量数据在屏幕上滚动输出,就像天气预报一样酷!

💡 核心导读

  • 本节新增:编写一个函数,自动从网站首页提取所有城市的中文名和拼音链接
  • 爬虫升级:从「单点抓取」升级到「批量抓取」,实现真正的自动化
  • 实战技巧:如何定位网页中的特定区域,提取我们需要的数据
  • 程序结构:把不同功能拆分成独立的函数,让代码更清晰

爬虫的「导航系统」:先找到所有城市

我们的目标:让爬虫自己「看地图」

想象一下,你要去一个陌生的城市旅游,但手里只有一张标着「景点」的地图,却不知道具体有哪些景点。我们的爬虫现在就是这种情况——我们知道怎么获取单个城市的空气质量(上节课的 get_city_aqi 函数),但不知道有哪些城市可以抓取。

本节我们要做的:给爬虫装上一个「导航系统」,让它先到网站首页,找到「所有城市」这个列表,然后记下每个城市的名字和对应的链接地址。

import requests
from bs4 import BeautifulSoup

def get_all_cities():
    """
    获取网站上所有城市的中文名和拼音链接
    返回一个列表,每个元素是 (城市名, 城市拼音) 的元组
    """
    # 空气质量网站的首页,这里列出了所有城市
    url = 'http://pm25.in/'
    
    # 发送请求获取网页内容
    r = requests.get(url, timeout=30)
    
    # 用BeautifulSoup解析网页
    soup = BeautifulSoup(r.text, 'lxml')
    
    # 准备一个空列表,用来存放城市信息
    city_list = []
    
    # 关键步骤1:找到包含所有城市的那个div区域
    # 网站上有两个class为'bottom'的div,我们要取第二个
    city_div = soup.find_all('div', {'class': 'bottom'})[1]
    
    # 关键步骤2:在这个div里找到所有的<a>标签(每个城市都是一个链接)
    city_link_list = city_div.find_all('a')
    
    # 关键步骤3:遍历每个链接,提取我们需要的信息
    for city_link in city_link_list:
        # 获取城市的中文名称(链接的文本内容)
        city_name = city_link.text
        
        # 获取城市的拼音(链接的href属性,去掉开头的斜杠)
        # 例如:href="/beijing" → 取"beijing"
        city_pinyin = city_link['href'][1:]
        
        # 把城市名和拼音作为一个元组,添加到列表中
        city_list.append((city_name, city_pinyin))
    
    return city_list

# 测试一下我们的函数
cities = get_all_cities()
print(f"找到了 {len(cities)} 个城市")
print("前5个城市:", cities[:5])
找到了 381 个城市
前5个城市: [('阿坝州', 'aba'), ('阿克苏地区', 'akesu'), ('阿拉尔', 'alaer'), ('阿拉善盟', 'alashanmeng'), ('阿勒泰地区', 'aletai')]

看!我们的爬虫已经成功找到了381个城市,而且正确提取了每个城市的中文名和拼音。这个「导航系统」工作正常!

注:上面的数字与城市名是课程录制时的一次示例输出。城市总数和排序可能随网站调整而变化,以你运行时的实时结果为准。

组装完整的爬虫:批量抓取所有城市

把两个功能「拼接」起来

现在我们有:

  1. 导航系统:get_all_cities() - 知道有哪些城市
  2. 数据抓取器:get_city_aqi() - 知道怎么抓取单个城市的数据

接下来就像组装乐高积木一样,把这两个功能拼接起来:

import requests
from bs4 import BeautifulSoup

# 1. 数据抓取器(上节课的函数)
def get_city_aqi(city_pinyin):
    """
    获取指定城市的空气质量数据
    """
    url = 'http://pm25.in/' + city_pinyin
    r = requests.get(url, timeout=30)
    soup = BeautifulSoup(r.text, 'lxml')
    
    # 找到所有空气质量指标的div
    div_list = soup.find_all('div', {'class': 'span1'})
    
    city_aqi = []
    # 提取前8个指标(PM2.5, PM10, CO等)
    for i in range(8):
        div_content = div_list[i]
        caption = div_content.find('div', {'class': 'caption'}).text.strip()  # 指标名称
        value = div_content.find('div', {'class': 'value'}).text.strip()      # 指标数值
        city_aqi.append((caption, value))
    
    return city_aqi

# 2. 导航系统(本节新增的函数)
def get_all_cities():
    """
    获取所有城市信息
    """
    url = 'http://pm25.in/'
    city_list = []
    r = requests.get(url, timeout=30)
    soup = BeautifulSoup(r.text, 'lxml')
    
    city_div = soup.find_all('div', {'class': 'bottom'})[1]
    city_link_list = city_div.find_all('a')
    
    for city_link in city_link_list:
        city_name = city_link.text
        city_pinyin = city_link['href'][1:]
        city_list.append((city_name, city_pinyin))
    
    return city_list

# 3. 主程序:把两个功能组合起来
def main():
    """
    主函数:批量抓取所有城市的空气质量
    """
    print("开始获取城市列表...")
    city_list = get_all_cities()
    print(f"共找到 {len(city_list)} 个城市")
    
    print("\n开始抓取空气质量数据(这可能需要几分钟)...")
    print("=" * 50)
    
    # 遍历每个城市,获取其空气质量
    for city in city_list:
        city_name = city[0]      # 城市中文名
        city_pinyin = city[1]    # 城市拼音
        
        try:
            # 调用数据抓取器
            city_aqi = get_city_aqi(city_pinyin)
            
            # 只显示PM2.5的数据(你也可以显示全部8个指标)
            pm25_value = None
            for item in city_aqi:
                if "PM2.5" in item[0]:
                    pm25_value = item[1]
                    break
            
            # 输出结果
            if pm25_value:
                print(f"{city_name}: PM2.5 = {pm25_value}")
            else:
                print(f"{city_name}: 数据获取失败")
                
        except Exception as e:
            # 如果某个城市抓取出错,跳过继续下一个
            print(f"{city_name}: 抓取出错 - {str(e)[:30]}...")

# 运行主程序
main()

运行后会逐行打印每个城市的 PM2.5(示例输出,数值随网站实时变化;抓取几百个城市需要几分钟):

开始获取城市列表...
共找到 381 个城市

开始抓取空气质量数据(这可能需要几分钟)...
==================================================
阿坝州: PM2.5 = 12
阿克苏地区: PM2.5 = 33
阿拉尔: PM2.5 = 27
阿拉善盟: PM2.5 = 17
阿勒泰地区: PM2.5 = 8
阿里地区: PM2.5 = 4
安康: PM2.5 = 28
安庆: PM2.5 = 30
安顺: PM2.5 = 16
安阳: PM2.5 = 35
鞍山: PM2.5 = 28
...
(后续城市数据继续输出)

太酷了! 我们的爬虫现在可以自动:

  1. 访问网站首页,找到所有城市
  2. 依次访问每个城市的页面
  3. 提取并显示PM2.5数据
  4. 即使某个城市出错,也会继续处理下一个

程序设计的艺术:为什么要把功能拆开?

像搭积木一样写代码

你可能注意到了,我们的程序被分成了三个部分:

  • get_city_aqi() - 专门负责「怎么抓一个城市」
  • get_all_cities() - 专门负责「有哪些城市」
  • main() - 负责「指挥」这两个函数工作

这种设计有三大好处:

1. 代码更清晰:每个函数只做一件事,容易理解和维护

# 就像乐高说明书:
# 步骤1:拼装轮子(get_all_cities)
# 步骤2:拼装车身(get_city_aqi)  
# 步骤3:组合成车(main)

2. 容易调试:如果某个城市数据抓取失败,我们知道问题出在 get_city_aqi;如果城市列表获取失败,我们知道问题在 get_all_cities

3. 方便复用:下次我们想抓取其他网站的数据,可以直接复用 get_all_cities 的思路,只需要修改解析网页的部分

网页解析的「精确制导」

在 get_all_cities() 函数中,有一个关键技巧:

# 网页里有两个 class="bottom" 的 div,城市列表在第二个里,所以用 [1]:
city_div = soup.find_all('div', {'class': 'bottom'})[1]
city_link_list = city_div.find_all('a')  # 再在这个区域里找所有城市链接

为什么是 [1](取第二个)而不是 [0](取第一个)?

如果你打开网站首页查看源代码,会发现有两个 class="bottom" 的div:

  • 第一个:可能是网站的页脚、版权信息等
  • 第二个:才是真正包含所有城市列表的区域

这就像在人群中找人——如果只说「找穿红衣服的人」,可能会找到好几个;我们需要更精确的描述:「找穿红衣服、戴眼镜、手里拿书的那个人」。

爬虫的「容错机制」:让程序更健壮

现实世界的网络并不完美

在实际运行中,你可能会遇到:

  • 某个城市页面暂时打不开
  • 网络连接突然中断
  • 网站结构发生变化

我们的程序通过 try...except 来处理这些意外:

# 在批量抓取的 for 循环里,用 try...except 包住每个城市的抓取动作:
for city in city_list:
    city_name = city[0]
    city_pinyin = city[1]
    try:
        city_aqi = get_city_aqi(city_pinyin)  # 正常抓取
        print(f"{city_name}: PM2.5 = {city_aqi}")
    except Exception as e:
        # 单个城市失败不影响其他城市,记录后继续
        print(f"{city_name}: 抓取出错 - {str(e)[:30]}...")

这就像给爬虫戴上了「安全帽」——即使撞到障碍物(出错),也不会整个程序崩溃,而是记录下来继续前进。

📝 动手练一练

现在轮到你来改进这个爬虫了!试试下面这些挑战:

  1. 速度优化:现在的程序是一个城市接一个城市地抓取,速度比较慢。你能想到什么办法让程序运行更快吗?(提示:可以同时处理多个城市)

  2. 数据丰富:现在只显示了PM2.5数据,修改程序让它显示所有8个空气质量指标(PM2.5、PM10、CO、NO2、O3、SO2、温度、湿度)

  3. 结果筛选:只显示PM2.5数值超过50的城市(空气质量较差的城市)

👉 点击查看第3题的参考答案
# 思路:在输出前判断PM2.5值是否大于50

import requests
from bs4 import BeautifulSoup

# 为了示例简洁,这里只写关键部分
# 假设我们已经有了城市列表和获取AQI的函数

# 模拟几个城市的数据(实际使用时用真实的get_city_aqi函数)
def get_city_aqi_demo(city_pinyin):
    """模拟函数,返回模拟的AQI数据"""
    # 这里用固定数据模拟,实际应该从网站抓取
    demo_data = {
        'beijing': [('PM2.5', '65'), ('PM10', '120')],
        'shanghai': [('PM2.5', '45'), ('PM10', '80')],
        'guangzhou': [('PM2.5', '55'), ('PM10', '90')],
        'shenzhen': [('PM2.5', '38'), ('PM10', '70')]
    }
    return demo_data.get(city_pinyin, [('PM2.5', '0')])

# 模拟城市列表
city_list = [('北京', 'beijing'), ('上海', 'shanghai'), 
             ('广州', 'guangzhou'), ('深圳', 'shenzhen')]

print("PM2.5超过50的城市:")
print("=" * 30)

for city_name, city_pinyin in city_list:
    city_aqi = get_city_aqi_demo(city_pinyin)
    
    # 找到PM2.5的值
    pm25_value = 0
    for item in city_aqi:
        if "PM2.5" in item[0]:
            pm25_value = int(item[1])  # 转换为整数以便比较
            break
    
    # 只显示PM2.5 > 50的城市
    if pm25_value > 50:
        print(f"{city_name}: PM2.5 = {pm25_value} (空气质量较差)")
PM2.5超过50的城市:
==============================
北京: PM2.5 = 65 (空气质量较差)
广州: PM2.5 = 55 (空气质量较差)

本章小结

恭喜!在这一节中,你的爬虫完成了重要升级:

🎯 本节成果

  • ✅ 学会了批量抓取:从只能抓一个城市,升级到能自动抓取全国所有城市
  • ✅ 掌握了网页解析的精确定位:用 find_all()[1] 精确找到需要的网页区域
  • ✅ 理解了函数拆分的好处:把大任务拆成小函数,让代码更清晰、更易维护
  • ✅ 实现了简单的容错处理:用 try...except 让程序更健壮

📋 行动清单

学完立刻可以做的3件事:

  • 运行完整程序:把本节最后的完整代码复制运行,看看你的城市空气质量如何
  • 尝试改进:完成「动手练一练」的第2题,让程序显示更多空气质量指标
  • 观察思考:运行程序时观察输出,哪些城市的空气质量比较好?哪些比较差?

预告:现在数据只是在屏幕上显示,下一节我们要把这些宝贵的数据保存到文件中,这样就能随时查看、分析,甚至做成图表!

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第9章代码包:空气质量指数计算(含全部版本 10 个 .py)
    ZIP · 64KB
    下载
  • 数据集:aqi.csv(各城市 AQI 长表)
    772B
    下载
  • 数据集:beijing_aqi.json(北京监测点)
    3KB
    下载
  • 数据集:bottom10_aqi.csv(空气质量最差 10 城)
    412B
    下载
  • 数据集:china_city_aqi.csv(全国城市 AQI)
    13KB
    下载
  • 数据集:shanghai_aqi.json(上海监测点)
    3KB
    下载
  • 数据集:top10_aqi.csv(空气质量最好 10 城)
    372B
    下载
  • 数据集:top5_aqi.json(空气质量最好 5 城)
    1007B
    下载
看图文没弄懂?展开看本节视频讲解(约 9 分钟)
📺 正在播放小象学院官方高清录播(支持倍速与清晰度调节)