网络数据爬取实例教程

  • 前言
  • 爬取数据用的类浏览器
  • 找到我们需要的数据
    • 使用DOM提取数据
    • 使用正则表达式解析数据
  • 2018年趵突泉会停止喷涌吗
    • URL分析
    • 网页下载
    • 数据解析
    • 爬取全部数据
    • 数据保存与检索的考量
    • 绘制水位变化曲线图
    • 数据分析

前言

一般而言,网络数据爬取是指基于http/https/ftp协议的数据下载——翻译成白话,就是从特定网页上获取我们需要的数据。想象一个浏览网页的过程,大致可以分为两个步骤:

  1. 在浏览器地址栏输入网址,打开这个网页
  2. 用眼睛找到我们需要的信息

事实上,从网上爬取数据的过程和我们浏览网页的过程是一样的,同样也包含这两个步骤,只是工具略有不同而已。

  1. 使用相当于“浏览器”的组件下载网址(URL)对应的网页(源码)
  2. 使用技术手段从下载的网页(源码)上找到我们需要的数据

爬取数据用的类浏览器

python有两个内置的模块urllib和urllib2,可以用来作为爬取数据用的“浏览器”,pycurl也是一个不错的选择,可以应对更复杂的要求。

我们知道,http协议共有8种方法,真正的浏览器至少支持两种请求网页的方法:GET和POST。相对于urllib2而言,urllib模块只接受字符串参数,不能指定请求数据的方法,更无法设置请求报头。因此,urllib2被视为爬取数据所用“浏览器”的首选。

这是urllib2模块最简单的应用:

import urllib2
response = urllib2.urlopen('http://xufive.sdysit.com/')
if response.code == 200:
    html = response.read() # html就是我们所请求的网页的源码
    print html
else:
    print 'Response error.'

urllib2.urlopen除了可以接受字符串参数,还可以接受urllib2.Request对象。这意味着,我们可以灵活地设置请求的报头(header)。

urllib2.urlopen的构造函数原型如下:

urllib2.urlopen(url[, data[, timeout[, cafile[, capath[, cadefault[, context]]]]])

urllib2.Request的构造函数原型如下:

urllib2.Request(url[, data][, headers][, origin_req_host][, unverifiable])

找到我们需要的数据

目标数据往往以千变万化的各种形式隐匿于我们下载到的网页源码中,这就需要我们首先分析网页源码,找出目标数据的分布规律,然后再选择相应的方式获取。

使用DOM提取数据

Beautiful Soup做为python的第三方库,可以帮助我们从网页源码中找到我们需要的数据。Beautiful Soup可以从一个HTML或者XML提取数据,它包含了简单的处理、遍历、搜索文档树、修改网页元素等功能。安装非常简单(如果没有解析器,也一并安装):

pip install beautifulsoup4
pip install lxml

下面的例子演示了如何从http://xufive.sdysit.com上找到我们需要的信息。

import urllib2
from bs4 import BeautifulSoup

response = urllib2.urlopen('http://xufive.sdysit.com/')
if response.code == 200:
    html = response.read()
    soup = BeautifulSoup(html, 'lxml')

    div = soup.find('div') # 找出第一个div节点
    print div # 查看div标签的全部信息
    print div.text # 打印div标签的内容
    print div.attrs # 打印div标签的全部属性
    print div['style'] # 打印div标签的样式

    divs = soup.find_all('div') # 找出所有的div节点
    for div in divs: # 遍历所有的div节点
        print div

    for tr in divs[1].find_all('tr'): # 遍历所有的tr
        for td in tr.find_all('td'):
            print td.text
else:
    print 'Response error.'

使用正则表达式解析数据

有时候,目标数据隐身于大段的文本中,无法透过html标签直接获取;或者,相同的标签数量众多,而目标数据只占其中的一小部分。此时一般要借助于正则表达式了。下面的代码可以直接把年月日提取出来(提示:处理中文时,html源码和匹配模式必须使用utf-8编码,否则运行出错):

#-*- coding: utf-8 -*-
import re

html = u"""
    2011年10月15日
    2011年10月15日
    2012年5月9日
    2015年11月23日
    2015年7月8日
    2016年12月5日
    2017年3月22日
    2017年6月17日
"""
pattern  = re.compile(r'(\d{4})年(\d{1,2})月(\d{1,2})日')
data = pattern.findall(html.encode('utf-8'))
print data

2018年趵突泉会停止喷涌吗?

济南市城乡水务局网站(http://jnwater.gov.cn)每天都会发布趵突泉和黑虎泉的地下水位,并且可以查询到更早期的水位数据。本教程的目标任务是:

  • 从这个网站上下载全部的水位数据
  • 绘制指定时间段的水位变化曲线
  • 分析济南市地下水位变化规律,对2018年泉水是否会停止喷涌做出预判

URL分析

简单操作几次,我们就会发现,自2012年5月2日开始,济南市城乡水务局网站每天都会发布趵突泉和黑虎泉的地下水位数据,全部数据采用分页方式显示,每页显示20天,页面编号从1开始,第1页的URL为:

http://www.jnwater.gov.cn/list.php?catid=101&page=1

网页下载

如果使用前面的代码下载这个网页,就会发现出错了。这多少有些令人气馁,为什么会这样呢?原来,济南市城乡水务局网站做了一点手脚,限制了非浏览器访问网站。用刚才的代码访问和用浏览器访问有什么区别吗?当然,这就是请求报头中的User-Agent(用户代理)。当浏览器访问网站时,会在请求报头中声明自己是什么类型的浏览器,运行什么样的操作系统上。我们只需要在在请求报头中增加User-Agent,就可以骗过服务器了。下面是一个抓取指定页面水位数据的函数。

import urllib2

def get_data_by_page(page):
    url = 'http://www.jnwater.gov.cn/list.php?catid=101&page=%d'%page
    req = urllib2.Request(url, '', {'User-Agent':'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'})
    response = urllib2.urlopen(req)
    if response.code == 200:
        html = response.read()
        return html
    else:
        return False

数据解析

查看下载的网页源码,不难发现,趵突泉和黑虎泉的地下水位数据在html中的是这样的:

<tr>
  <td width="25%" align="center" bgcolor="#DAE9F8" class="s14">日期td>
  <td width="15%" align="center" bgcolor="#DAE9F8"><span class="s14">趵突泉水位span>td>
  <td width="14%" align="center" bgcolor="#DAE9F8"><span class="s14">黑虎泉水位span>td>
tr>
<tr>
  <td align="center" bgcolor="#DAF8E8" class="s14">2017年10月2日td>
  <td align="center" bgcolor="#DAF8E8" class="s14">28.20米td>
  <td align="center" bgcolor="#DAF8E8" class="s14">28.15米td>
tr>
<tr>
  <td align="center" bgcolor="#DAF8E8" class="s14">2017年10月1日td>
  <td align="center" bgcolor="#DAF8E8" class="s14">28.16米td>
  <td align="center" bgcolor="#DAF8E8" class="s14">28.10米td>
tr>

结合网页实际显示效果,很容易找到水位数据的提取条件:tr标签包含3个td标签,且td标签的背景色(bdcolor)为绿色(#DAF8E8)。继续完善我们的下载函数,使之能够解析出我们需要的数据,而不是返回一个网页源码。

def get_data_by_page(page):
    data = list()
    p_date = re.compile(r'(\d{4})\D+(\d{1,2})\D+(\d{1,2})')

    url = 'http://www.jnwater.gov.cn/list.php?catid=101&page=%d'%page
    req = urllib2.Request(url, '', {'User-Agent':'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'})
    response = urllib2.urlopen(req)

    if response.code == 200:
        html = response.read()
        soup = BeautifulSoup(html, 'lxml')
        for tr in soup.find_all('tr'):
            tds = tr.find_all('td')
            if len(tds) == 3 and 'bgcolor' in tds[0].attrs and tds[0]['bgcolor'] == '#DAF8E8':
                year, month, day = p_date.findall(tds[0].text.encode('utf8'))[0]
                baotu = float(tds[1].text[:-1])
                heihu = float(tds[2].text[:-1])
                data.append(['%s-%02d-%02d'%(year, int(month), int(day)), baotu, heihu])

    return data

爬取全部数据

根据网站上数据分页的数量,循环调用get_data_by_page函数,并把返回结果汇总,就得了全部水位数据。这是一个简单的工作,但实际上并容易做好,这是因为:

  • 并不是每次的网页请求都会成功
  • 数据并不总是和我们期望的一致,解析过程会出现异常
  • 日期错误、数据缺失

目前已知的错误有:

  • 2015年9月9日,错写为2014年9月9日
  • 2014年6月3日,错写为2016年6月3日
  • 2013年1月31日,错写为2012年1月31日
  • 2012年10月30日数据缺失
  • 2014年3月11日数据缺失

最好的方法是,从最近的数据日期开始,使用datetime模块的timedelta对象,逐天处理,日期错误的,改正日期,缺失数据的,用前后两天的平均值补齐。

数据保存与检索的考量

待续

绘制水位变化曲线图

待续

数据分析

待续

你可能感兴趣的:(python论道)