一般而言,网络数据爬取是指基于http/https/ftp协议的数据下载——翻译成白话,就是从特定网页上获取我们需要的数据。想象一个浏览网页的过程,大致可以分为两个步骤:
事实上,从网上爬取数据的过程和我们浏览网页的过程是一样的,同样也包含这两个步骤,只是工具略有不同而已。
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])
目标数据往往以千变万化的各种形式隐匿于我们下载到的网页源码中,这就需要我们首先分析网页源码,找出目标数据的分布规律,然后再选择相应的方式获取。
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
济南市城乡水务局网站(http://jnwater.gov.cn)每天都会发布趵突泉和黑虎泉的地下水位,并且可以查询到更早期的水位数据。本教程的目标任务是:
简单操作几次,我们就会发现,自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函数,并把返回结果汇总,就得了全部水位数据。这是一个简单的工作,但实际上并容易做好,这是因为:
目前已知的错误有:
最好的方法是,从最近的数据日期开始,使用datetime模块的timedelta对象,逐天处理,日期错误的,改正日期,缺失数据的,用前后两天的平均值补齐。
待续
待续
待续