查看全课大纲(第 46 / 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.空气质量数据怎么“洗”干净,还能画成图?

如何让程序自己上网查空气质量?

约 20 分钟

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

还记得我们之前做的空气质量查询程序吗?数据都是预先准备好的文件。但空气质量是实时变化的,昨天的数据今天就没用了。这一节,我们要让程序自己上网查——输入城市名,它就能告诉你此刻的空气质量!就像给你的程序装上了“千里眼”,让它能实时看到全国各地的空气状况。

💡 核心导读

  • 网络爬虫是什么:让程序自动上网抓取信息的“机器人”
  • requests 模块:Python 里最简单的上网工具,一行代码就能获取网页
  • GET 请求:像在浏览器地址栏输入网址一样访问网页
  • 网页内容解析:从一大堆“代码”中找到我们需要的空气质量数值

从“看文件”到“看网页”

之前我们的程序都是读取本地文件(JSON 或 CSV),数据是静态的。但真实的空气质量数据每时每刻都在更新,我们需要实时获取。

怎么获取?当然不是让你每天手动去网站复制粘贴——那样太慢了,而且容易出错。我们需要的是网络爬虫。

什么是网络爬虫?

想象一下,你有一个不知疲倦的助手,它能:

  1. 按照你给的网址去访问网页
  2. 把网页内容全部“复制”下来
  3. 从里面找出你需要的信息

这个助手就是网络爬虫。它本质上是一个自动抓取互联网信息的程序。

# 对比一下两种方式:

# 旧方式:读取本地文件(数据可能已经过时)
import json
import os

# 本节需要 beijing_aqi.json(课程 resources 附件提供);为便于单独运行,先用示例数据建一份
with open('beijing_aqi.json', 'w', encoding='utf-8') as f:   # 示例数据:每次运行都重建,保证输出一致
    f.write('{"aqi": "51", "area": "北京", "pm25": "32", "pm10": "46"}')
with open('beijing_aqi.json', 'r', encoding='utf-8') as f:
    data = json.load(f)
print('北京AQI(文件数据):', data['aqi'])

# 新方式:实时从网上获取(数据永远是最新的)
# 这就是我们这节要学的!
北京AQI(文件数据): 51

上网第一步:requests 模块

Python 里有个超级好用的上网工具叫 requests(请求),它让我们用最简单的方式访问网页。

# 首先需要安装 requests(如果你还没安装的话)
# 在命令行输入:pip install requests

import requests

# 访问一个网页,简单到不可思议!
url = 'http://www.example.com'
response = requests.get(url, timeout=30)  # timeout=30 表示最多等30秒

print('状态码:', response.status_code)  # 200表示成功,404表示找不到网页
print('网页内容前500字符:', response.text[:500])
状态码: 200
网页内容前500字符: <!doctype html><html lang=en><head><meta charset=utf-8><title>Example Domain</title> …(后面还有几百字符,全是类似的标签和 CSS 样式,这里省略展示)

本节新增了什么? 我们引入了第一个新工具:requests 模块,它是 Python 里最简单的上网方式。

HTTP 状态码小知识

当你访问网页时,服务器会返回一个状态码告诉你结果:

  • 200:成功!网页正常打开了
  • 400:请求有问题(比如参数错了)
  • 404:最著名的错误——网页不存在
  • 500:服务器内部出错

我们主要关心 200,只要看到这个数字,就说明网页获取成功了。

实战:查询任意城市的空气质量

现在我们来做一个实用的工具:输入城市拼音,就能查到实时空气质量。

1. 找到目标网站

我们使用一个公开的空气质量网站作为例子:http://pm25.in/

在网站正常运营时:

  • 访问 http://pm25.in/beijing 能看到北京的空气质量
  • 访问 http://pm25.in/shanghai 能看到上海的空气质量
  • 访问 http://pm25.in/guangzhou 能看到广州的空气质量

规律是:网址后面直接加上城市拼音。

重要的现实情况:公开网站会变化、也会停止服务。课程更新时实测 pm25.in 的城市页已经返回 410 Gone(该页面已被永久移除),所以下面的代码现在拿不到真实 AQI 数字。这恰好是学习爬虫最好的一课:请求失败、页面变更才是常态。我们仍以这个网站为例走一遍完整流程(你可以把网址换成任何其他可访问的网页来验证),并在最后学会如何让程序在拿不到数据时不崩溃。

import requests

# 让用户输入城市拼音
city_pinyin = 'beijing'  # 原程序用 input(),这里用固定值方便演示

# 拼接完整的网址
url = 'http://pm25.in/' + city_pinyin
print('要访问的网址:', url)

# 获取网页内容
def get_html_text(url):
    """获取网页的文本内容"""
    r = requests.get(url, timeout=30)
    print('访问状态:', r.status_code)  # 成功时是 200;该站当前返回 410
    return r.text

# 调用函数获取网页
url_text = get_html_text(url)
print('网页内容长度:', len(url_text), '字符')
print('前200字符:', url_text[:200])

运行这段代码,你会看到服务器这次返回的 HTML 文本。当前 pm25.in 返回的是一个很短的 410 错误页(见下面输出,正文里写着 “It is gone!”)——这说明请求本身成功送达了,但请求的页面已经不存在。我们正是要学会读懂这种信号。

2. 从“原料”中提取我们需要的信息

网页内容是一大串 HTML 代码,我们需要从中找到空气质量数值。就像从一锅汤里捞出我们想吃的肉块。

先看看“肉块”在哪里

我们当前实际拿到的响应是 410 错误页——这是上面代码真实打印出来的内容:状态码 410、正文里写着 “It is gone!”,整个页面只有 254 个字符:

要访问的网址: http://pm25.in/beijing
访问状态: 410
网页内容长度: 254 字符
前200字符: <html><head><title>ERROR</title><style>body{font-family:"sans";background:#1f1e1a;color:#5f5e5a;text-align:center}h1{font-size:6em;color:#e6462e}</style></head><body><h2><br/>It is gone!<br/>So be gon

而在网站正常运营时,空气质量数值是藏在一段固定的 HTML 标签里的。用浏览器打开城市页、右键数值“检查”,能看到它以前的页面结构:

<div class="span12 data">
    <div class="span1">
      <div class="value">
            61          ← 空气质量数值就藏在这里
      </div>
    </div>
</div>

对比一下就明白:当前的 410 错误页里根本没有上面这个“包装盒”,所以接下来的查找必然落空——这正好能验证我们的查找逻辑在“找不到时”的行为。我们要做的,就是让程序在 HTML 里寻找“包装盒”,找到就取出数值,找不到就如实报告。

import requests

def get_html_text(url):
    """获取网页的文本内容"""
    r = requests.get(url, timeout=30)
    return r.text

def main():
    # 城市拼音(原程序用 input(),这里用固定值)
    city_pinyin = 'beijing'
    url = 'http://pm25.in/' + city_pinyin
    
    # 获取网页内容
    url_text = get_html_text(url)
    
    # 定义我们要找的"包装盒"特征
    aqi_div = '''<div class="span12 data">
        <div class="span1">
          <div class="value">
            '''
    
    # 在网页内容中查找这个"包装盒"
    index = url_text.find(aqi_div)
    
    if index == -1:
        print('找不到空气质量数据!')
        return
    
    # 计算数值开始的位置(跳过"包装盒"本身)
    begin_index = index + len(aqi_div)
    
    # 空气质量一般是2-3位数,我们先取2位
    end_index = begin_index + 2
    
    # 切片获取数值
    aqi_val = url_text[begin_index:end_index]
    
    print(f'{city_pinyin}的空气质量为:{aqi_val}')

# 运行主程序
main()

当前运行结果(网站已 410):

找不到空气质量数据!

这个结果是正确且符合预期的:程序没有崩溃,而是走完了“请求→在 HTML 中查找→找不到→友好提示”的完整分支。等你把网址换成一个正常返回数据的网页(或将来该站恢复),同一套代码就会在 else 分支里打印出真实数值。能优雅地处理“拿不到数据”,本身就是爬虫必备的健壮性。

3. 让程序更灵活

不过上面的程序有个小问题:如果空气质量是三位数(比如 120),我们只取两位就错了。让我们改进一下:

import requests

def get_html_text(url):
    r = requests.get(url, timeout=30)
    return r.text

def main():
    # 可以试试不同的城市
    cities = ['beijing', 'shanghai', 'guangzhou']
    
    for city in cities:
        url = 'http://pm25.in/' + city
        url_text = get_html_text(url)
        
        aqi_div = '''<div class="span12 data">
        <div class="span1">
          <div class="value">
            '''
        
        index = url_text.find(aqi_div)
        
        if index == -1:
            print(f'{city}:数据获取失败')
            continue
        
        begin_index = index + len(aqi_div)
        
        # 改进:找到数值结束的位置(遇到'<'标签结束)
        # 从开始位置往后找,找到第一个'<'为止
        end_index = url_text.find('<', begin_index)
        
        if end_index == -1:
            end_index = begin_index + 3  # 保险起见,最多取3位
        
        aqi_val = url_text[begin_index:end_index].strip()  # strip()去掉可能的空格
        
        print(f'{city}的空气质量为:{aqi_val}')

main()

当前运行结果(三个城市页都已 410):

beijing:数据获取失败
shanghai:数据获取失败
guangzhou:数据获取失败

看!循环、多城市、动态位数处理这些逻辑都写好了;只是因为目标站点当前不可用,每个城市都走进了“数据获取失败”分支。程序如实地逐个报告了失败,而不是带着错误数据往下跑——这种 continue 跳过失败项、继续处理下一个城市的写法,正是批量爬虫的标准做法。

网络爬虫的脆弱性(这一节最重要的体会)

刚才的经历就是最好的例子:目标网站直接返回了 410,我们准备好的提取逻辑一个数字都没拿到。 即便网站正常,这种字符串查找方法本身也很“脆弱”:

  1. 网站会失效或改版:页面一旦下线(410/404)或调整 HTML 结构,写死的 aqi_div 立刻就找不到了——这是爬虫最大的不确定性。
  2. 处理复杂:用 find() 和切片处理 HTML 比较麻烦,容易出错。
  3. 容错性差:网页稍有变化(比如多个空格),程序就可能失败。

所以写爬虫一定要做到两点:先看 status_code 是不是 200,再用 try...except 兜底(动手练一练第 2 题就是答案)。这就像用菜刀从一整只鸡上切下一块肉——能切下来,但不够优雅,容易切到手。

下一节预告:我们会学习一个更强大的工具——Beautiful Soup(美味的汤),它能像用筷子夹菜一样,优雅地从网页中提取我们需要的信息。

📝 动手练一练

  1. 扩展城市列表:修改程序,让它能查询你家乡的空气质量。你需要先到 http://pm25.in/ 网站上看看你的城市对应的拼音是什么。

  2. 错误处理:现在的程序如果输入不存在的城市(比如 xxx),会直接报错。请添加错误处理,让程序在找不到城市时友好地提示用户。

  3. 保存结果:让程序把查询结果保存到一个文本文件中,格式为“城市: 空气质量值”,每行一个城市。

👉 点击查看第2题参考答案
import requests

def get_html_text(url):
    try:
        r = requests.get(url, timeout=30)
        if r.status_code == 200:
            return r.text
        else:
            return None
    except:
        return None

def main():
    city_pinyin = 'xxx'  # 试试不存在的城市
    
    url = 'http://pm25.in/' + city_pinyin
    url_text = get_html_text(url)
    
    if url_text is None:
        print(f'无法访问{city_pinyin}的空气质量页面,请检查网络或城市名')
        return
    
    aqi_div = '''<div class="span12 data">
        <div class="span1">
          <div class="value">
            '''
    
    index = url_text.find(aqi_div)
    
    if index == -1:
        print(f'找不到{city_pinyin}的空气质量数据,可能城市名错误或网站改版')
        return
    
    begin_index = index + len(aqi_div)
    end_index = url_text.find('<', begin_index)
    
    if end_index == -1:
        end_index = begin_index + 3
    
    aqi_val = url_text[begin_index:end_index].strip()
    
    print(f'{city_pinyin}的空气质量为:{aqi_val}')

main()
无法访问xxx的空气质量页面,请检查网络或城市名

思路说明:

  1. 在 get_html_text 函数中添加 try...except 捕获网络错误
  2. 检查状态码,如果不是 200 就返回 None
  3. 在主程序中判断返回值,给出友好的错误提示
  4. 如果找不到特定的 HTML 结构,也提示可能的原因

本章小结

这一节我们让程序“活”了起来——它不再只读本地文件,而是能主动向互联网发起请求。虽然示例站点当前已经 410,但你掌握的是可以套用到任何网站的通用流程。

📋 行动清单

  • 运行今天的代码:亲眼看到 200(example.com)和 410(pm25.in)两种状态码的区别
  • 换个网站试试:把 url 换成你自己喜欢的任何网页,打印它的 status_code 和前 200 字符
  • 思考一下:如果网站改版或下线了,程序要怎样才能不崩溃?(提示:状态码判断 + try/except)

我们学会了用 requests 模块发起网络请求、读懂状态码、用字符串查找尝试提取信息,也亲身体会了爬虫的脆弱。下一节,我们会学习更优雅的网页解析方法 Beautiful Soup,并继续强化容错,让程序更强大、更稳定。

—— 小象教研组

配套资料 · 直接下载

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

  • 第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
    下载
看图文没弄懂?展开看本节视频讲解(约 20 分钟)
📺 正在播放小象学院官方高清录播(支持倍速与清晰度调节)