Python爬取静态网站:以历史天气为例

发布时间:2022-04-13 阅读 286

Stata连享会   主页 || 视频 || 推文 || 知乎 || Bilibili 站

温馨提示: 定期 清理浏览器缓存,可以获得最佳浏览体验。

New! lianxh 命令发布了:
随时搜索推文、Stata 资源。安装:
. ssc install lianxh
详情参见帮助文件 (有惊喜):
. help lianxh
连享会新命令:cnssc, ihelp, rdbalance, gitee, installpkg

课程详情 https://gitee.com/lianxh/Course

课程主页 https://gitee.com/lianxh/Course

⛳ Stata 系列推文:

PDF下载 - 推文合集

作者:王颖 (四川大学)
邮箱wangyingchn@outlook.com


目录


数据获取是实证研究的第一步。随着互联网数据的指数级增长,网络数据成为重要且常用的数据源。网络爬虫也因此成为获取数据的重要方式。但是我们通常会觉得爬虫非常复杂,不知道从何下手。为此,本文将通过实际的爬取案例介绍,来帮助大家掌握相关知识。

1. 静态网页和动态网页

网页类型包括静态网页和动态网页。简单来说,静态网页是指数据直接存储在网页的 html 中,不论用户是否请求了数据,数据就 “静止” 在那里。动态网页的数据则被 “藏” 起来了,用户每次请求后,动态网页才会有一个向远程数据库请求数据的“动作”,再把数据显示出来,但用户无法直接从网页的 html 中获取数据。

这里不细说静态网页和动态网页的官方定义,只说两个最明显的区别,以方便大家在分析网页时进行区分:

  • 从源代码看,静态网页的数据直接存在网页的源代码中,动态网页的数据不会出现在网页源代码中。也就是说,在查看网页源代码时,可以看到网页中显示数据的就是静态网页,反之就是动态网页;
  • 从网址特征看,静态网页的数据不会 “动”,所以一个页面就是一个网址,翻页时网址会变化。动态网页自己会 “动”,所以哪怕请求新的数据 (如翻页),网址也不会变化。

直观来说,翻页时网址变化的网站就是静态网站,反之就是动态网站。比如微博评论、bilibili 评论这样一直下滑会一直出现新的数据,但是网址不变的,就是动态网站。

2. 静态网页爬取的思路

这次我们先聊聊如何爬取静态网页的数据。由于静态网页结构比较简单,可以直接通过获取网页源代码得到数据,所以爬取比较简单。有了目标网站后,静态网页数据的爬取可以分为四步:

  • 分析网页结构
  • 请求网页数据
  • 解析网页数据
  • 储存最终数据

如果涉及多页的数据,还涉及到分析网址翻页规律和进行循环:

  • 分析单页网页结构
  • 分析网址翻页规律,获取所有网址
  • 循环请求、获取并解析网页数据
  • 储存最终数据

小提示:多页循环时,可以先爬取单页数据,成功后再循环爬取部分数据 (比如 10 页),没问题再爬取完整数据。

3. 案例之爬取历史天气

接下来用一个简单的实战案例,来具体介绍如何通过 Python 爬取静态网页的数据。主要内容包括:

  • 如何分析网页结构
  • 如何请求静态网页数据
  • 如何解析表格型数据
  • 如何把数据实时存入 csv 文件
  • 如何循环爬取多页数据

3.1 分析网页结构

分析网页结构是爬取数据的第一步,也是重中之重。在本案例中,我们需要爬取「天气网」的历史数据。以北京市 2022 年 3 月天气的「网页」为例,进行网页结构分析。首先,我们需要的数据如下图左所示,包括每天的日期、最高气温、最低气温、天气、风向。

然后,在浏览器页面右键查看网页源代码,并在源代码中找到对应的数据 (如下图右)。这也是前面提到的静态网页第一个特征:页面上显示的数据都可以在源代码中找到。

小提示:静态网页的结构都比较简单,我们不需要太多精力去分析网页结构,只需要在源代码中验证一下是否有需要的数据即可。

3.2 请求网页数据

由于数据都藏在源代码中,我们只需要请求网页内容,即把源代码下载到本地,进行分析。关于网页请求,我们需要用到 Python 爬虫中常用包 requests

# 导入模块
import requests

# 输入网址。这里还是以北京市 2022 年 3 月的天气为例进行单页爬取。
url = "https://lishi.tianqi.com/beijing/202203.html"

# 伪装一下,让服务器以为是正常浏览,而不是爬虫
# 静态网页通常反爬不严格,所以只要通过 User-Agent 伪装成浏览器即可
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) \
     Chrome/77.0.3865.120 Safari/537.36"
}

# 请求数据,使用 get 方法请求数据
response = requests.get(url, headers=headers)
response

如果返回 <Response [200]> 则代表请求数据成功。如果返回 403 或 404 则说明请求不成功,可能需要检查电脑网络是否通畅、目标网址是否可以正常访问、headers 是否有正确设置等。

3.3 解析网页数据

请求成功后,我们要再次回到源代码中,以查看数据结构。

可以看到,在网页源代码中数据是以 html 格式存储的。首先,最外面的 div 标签包裹了整个表格,里面的 div 标签包裹了表头。接着,一个 ul 标签包裹了所有的行内容,其中每一行是一个 li 标签,每列具体数据是 div 标签。

因此,我们需要的是每个 li 标签里所有 div 标签里的数据。这里,我们使用 Python 中 bs4 包的 BeautifulSoup 进行网页数据解析。

# 导入模块
from bs4 import BeautifulSoup

soup = BeautifulSoup(response.text, "html.parser")  # 由于是通过 html 格式存储的,所以用 “html.parser” 进行解析
data_table = soup.find('ul', class_="thrui").find_all("li")  # 找到包裹表内容的 ul 标签,找到里面所有的 li 标签

weather_list = []  # 构造空列表以存储数据
for li in data_table[1:]:  # 循环获取每行的数据(li 标签)
    th_list = li.find_all('div')  # 获取每行的每个数据 (li 标签下的 div 标签)
    weather = {
        'date': th_list[0].get_text(),  # 获取第一个 div 标签中的内容,命名为 “date”
        'temp_high': th_list[1].get_text(),
        'temp_low': th_list[2].get_text(),
        'weather': th_list[3].get_text(),
        'wind': th_list[4].get_text(),
        'url': response.url  # 爬取时通常可以顺便保存一下当页的网址,方便溯源和排查错误
    }  # 每行数据存储在一个字典中
    weather_list.append(weather)  # 所有行的数据存入一个列表中

小提示:在爬取表格型数据时,如果列数很多,像上面一样单个获取再存储就比较麻烦。这里再提供一种适用于爬取表格型数据的更简洁的方式,需要用到 Python 中另一个强大的模块 numpy

import numpy as np
weather_list = []
for li in data_table[1:]:     
    th_list = li.find_all('div')
    for th in th_list:
        s = th.get_text()  # 循环获取 div 标签数据
        weather_list.append("".join(s.split()))  # 直接把获取的 div 标签全部存在一个列表中
result = np.array(weather_list).reshape(-1, 5)   # 通过 numpy 直接转为多行 5 列的数据表
# 最后再转化为数据框并重命名列即可
# 这种方式虽然方便,但可能不太直观,也可能出错(比如有的数据不全,就容易错位)
# 此外,还有很多不同的解析数据的方式,可以多去尝试

3.4 储存爬取数据

把数据解析好之后,数据就可以通过更加结构化的形式进行存储,通常可以使用 txt、excel、csv 等格式。由于无格式、比 txt 更直观、可以用 excel 打开,一般比较推荐用 csv 格式储存。

# 导入模块
import csv

# 保存数据的文件路径
save_path = 'weather.csv'

# 将数据写入 csv
with open(save_path, 'a', newline='', encoding='utf-8') as fp:
    csv_header = ['date', 'temp_high', 'temp_low', 'weather', 'wind', 'url']  # 设置表头,即列名
    csv_writer = csv.DictWriter(fp, csv_header)  
    if fp.tell() == 0:
        csv_writer.writeheader()  # 如果文件不存在,则写入表头;如果文件已经存在,则直接追加数据不再次写入表头。
    csv_writer.writerows(weather_list)  # 写入数据

小提示:爬取时,建议爬一页存一页。如果完全爬取完再一次写入,很可能会遇到循环爬取过程中出错,导致已爬取的数据无法成功储存。这里写入 csv 时用的编码格式是 utf-8,所以打开数据时也要用对应的编码格式,否则可能会出现乱码。

3.5 循环爬取数据

通常,我们需要的不仅仅是单页的数据,而是网站上的所有或大部分数据,所以需要多页循环爬取。在成功爬取一页数据之后,多页爬取就很简单了。只需要分析网址的变化规律,生成所有需要的网址,再把单页爬取重复多次就可以了。

首先,来分析一下网址的变化规律。

bejing
https://lishi.tianqi.com/beijing/202203.html
https://lishi.tianqi.com/beijing/202202.html
https://lishi.tianqi.com/beijing/202201.html
https://lishi.tianqi.com/beijing/202112.html

shanghai
https://lishi.tianqi.com/shanghai/202203.html
https://lishi.tianqi.com/shanghai/202202.html

可以看到,网址在变化。这也是前面提到静态网页的另一个特征,网址不变数据不 “动”,数据要 “动” 网址就变。 回到例子中,只有两个地方在变化。一个是城市变化时,网址的城市代码部分变化,这里是城市的汉语拼音。另一个是时间变化时,网址的时间代码部分变化,这里是年月的 6 位数字。

知道了网址的变化规律后,我们就可以通过循环来爬取多页数据。这里有两种思路,一种是先根据网址变化规律,一次性生成所有要爬取的网址,再在网址中循环获取数据。另一种是根据网址变化规律,生成一个网址,爬取一个网址,不断循环。两种方法都可以,这里使用第一种,先生成所有网址。

url_pattern = 'https://lishi.tianqi.com/{}/{}.html'  # 网址的基本结构,有变化的两个部分用 {} 替代,后面循环补充

city_list = ['beijing', 'shanghai']      # 构造需要爬取的城市的列表
years = [x for x in range(2020, 2022)]   # 使用列表生成式生成年份列表
months = [str(x).zfill(2) for x in range(1, 13)] # 生成月份列表,zfill 函数补充两位数
month_list = [str(year) + str(month) for year in years for month in months]  # 年月循环拼在一起

url_list = []              # 空列表用于存储所有网址
for c in city_list:       # 先循环城市
    for m in month_list:  # 再循环时间
        url_list.append(url_pattern.format(c, m))  # 通过 format 函数生成网址

生成了所有的网址后,把前面单页爬取循环到不同的网址下,就可以获取所有的数据了。

4. 完整代码

在实际过程中,通常会把每个需要重复操作的步骤都构造成函数,方便在循环中调用。这里的完整代码是通过构造函数的方式来进行的,函数的主体部分就是前面每个步骤的具体代码。

# -*- coding: utf-8 -*-
# Author: W.Y.
# Email: wangyingchn@outlook.com
# Date: 2022/4/4

# 导入模块
import csv  # 用于存储数据
import time  # 用于时间间隔避免过频繁的请求
import requests  # 用于请求数据
from bs4 import BeautifulSoup  # 用于解析数据

# 请求数据
def get_response(url):
    # 伪装一下,让服务器以为是正常浏览,而不是爬虫。
    # 静态网页通常反爬不严格,所以只要通过 User-Agent 伪装成浏览器即可。
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
        (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"
    }
    # 请求数据。使用 get 方法请求数据
    response = requests.get(url, headers=headers)
    return response

# 解析数据
def parse_data(response):
    soup = BeautifulSoup(response.text, "html.parser")  # 由于是通过 html 格式存储的,所以用 “html.parser” 进行解析
    data_table = soup.find('ul', class_="thrui").find_all("li")  # 找到包裹表内容的 ul 标签,找到里面所有的 li 标签
    weather_list = []  # 构造空列表以存储数据
    for li in data_table[1:]:  # 循环获取每行的数据(li 标签)
        th_list = li.find_all('div')  # 获取每行的每个数据 (li 标签下的 div 标签)
        weather = {
            'date': th_list[0].get_text(),  # 获取第一个 div 标签中的内容,命名为 “date”
            'temp_high': th_list[1].get_text(),
            'temp_low': th_list[2].get_text(),
            'weather': th_list[3].get_text(),
            'wind': th_list[4].get_text(),
            'url': response.url  # 爬取时通常可以顺便保存一下当页的网址,方便溯源和排查错误
            # 'city': response.url.split('/')[3]  # 如果需要增加一列城市,也可以通过 url 来获取
        }   # 每行数据存储在一个字典中
        weather_list.append(weather)  # 所有行的数据存入一个列表中
    return weather_list

# 储存数据
def save_data(weather_list, save_path):
    with open(save_path, 'a', newline='', encoding='utf-8') as fp:
        csv_header = ['date', 'temp_high', 'temp_low', 'weather', 'wind', 'url']  # 设置表头,即列名
        csv_writer = csv.DictWriter(fp, csv_header)
        if fp.tell() == 0:
            csv_writer.writeheader()  # 如果文件不存在,则写入表头;如果文件已经存在,则直接追加数据不再次写入表头
        csv_writer.writerows(weather_list)  # 写入数据

# 构造网址
def generate_urls():
    url_pattern = 'https://lishi.tianqi.com/{}/{}.html'  # 网址的基本结构,有变化的两个部分用 {} 替代,后面循环补充
    city_list = ['beijing', 'shanghai']  # 构造需要爬取的城市的列表
    years = [x for x in range(2020, 2022)]  # 使用列表生成式生成年份列表
    months = [str(x).zfill(2) for x in range(1, 13)]  # 生成月份列表,zfill 函数补充两位数
    month_list = [str(year) + str(month) for year in years for month in months]  # 年月循环拼在一起
    url_list = []  # 空列表用于存储所有网址
    for c in city_list:  # 先循环城市
        for m in month_list:  # 再循环时间
            url_list.append(url_pattern.format(c, m))  # 通过 format 函数生成网址
    return url_list

# 定义爬取函数
def crawler(url, save_path):
    response = get_response(url)    # 请求数据
    results = parse_data(response)  # 解析数据
    save_data(results, save_path)   # 存储数据
    print(f'成功爬取数据:{url}')

if __name__ == '__main__':
    urls = generate_urls()     # 构造所有网址
    save_file = 'weather.csv'  # 保存数据的文件路径
    for u in urls:     # 在网址中循环
        time.sleep(2)  # 每次爬取休息 2 秒,以免太过频繁的请求
        crawler(u, save_file)  # 进行爬取

5. 相关推文

Note:产生如下推文列表的 Stata 命令为:
lianxh 爬虫, m
安装最新版 lianxh 命令:
ssc install lianxh, replace

相关课程

免费公开课

最新课程-直播课

专题 嘉宾 直播/回看视频
最新专题 文本分析、机器学习、效率专题、生存分析等
研究设计 连玉君 我的特斯拉-实证研究设计-幻灯片-
面板模型 连玉君 动态面板模型-幻灯片-
面板模型 连玉君 直击面板数据模型 [免费公开课,2小时]
  • Note: 部分课程的资料,PPT 等可以前往 连享会-直播课 主页查看,下载。

课程主页

课程主页

关于我们

  • Stata连享会 由中山大学连玉君老师团队创办,定期分享实证分析经验。
  • 连享会-主页知乎专栏,700+ 推文,实证分析不再抓狂。直播间 有很多视频课程,可以随时观看。
  • 公众号关键词搜索/回复 功能已经上线。大家可以在公众号左下角点击键盘图标,输入简要关键词,以便快速呈现历史推文,获取工具软件和数据下载。常见关键词:课程, 直播, 视频, 客服, 模型设定, 研究设计, stata, plus, 绘图, 编程, 面板, 论文重现, 可视化, RDD, DID, PSM, 合成控制法

连享会小程序:扫一扫,看推文,看视频……

扫码加入连享会微信群,提问交流更方便

✏ 连享会-常见问题解答:
https://gitee.com/lianxh/Course/wikis

New! lianxhsongbl 命令发布了:
随时搜索连享会推文、Stata 资源,安装命令如下:
. ssc install lianxh
使用详情参见帮助文件 (有惊喜):
. help lianxh