文章目录

  • 1. 前言
  • 2. 数据源协议分析
  • 3. 使用轻量型数据库Sqlite存取数据
  • 3. 数据抓取函数
  • 4. 数据解析函数
  • 5. 抓取全部数据
  • 6. 数据检查清洗
  • 7. 数据可视化
  • 8. 组装在一起
  • 9. 效果展示
  • 10. 后记

1. 前言

济南,始终是一座不温不火、慢慢腾腾的城市,一如生活在她怀抱中的市井百姓:闲适、从容。也有人说她土气、落后,但我始终觉得,她很美,而且美得独一无二,美得沁人心脾。作为北方城市,济南有山、有水、有泉,背靠黄河,有深厚的历史和文化底蕴。生活在这座城市,我很荣幸。

空闲时间,我喜欢逛逛济南的大街小巷、看看济南的山山水水。曾经拍了很多的照片,也写过赞美她的诗。

你的美
弥散在清晨护城河氤氲的水面上
附着在午后玛瑙泉缓缓升起的气泡里你的美
回响在兴国禅寺的暮鼓晨钟里
飘荡在百年教堂的穹顶钟楼上你的美
渲染了九如山漫山的红叶
点亮了七星台璀璨的星空五龙潭的樱花,是你如花的笑靥
百花洲的垂柳,是你妙曼的身姿
长河落日,是你无尽的爱
鹊华烟雨,是你温柔的吻我将,深深地
永远地,爱你

但是,作为程序员,我觉得还是应该用数据对她说出我的爱。本文完整演示了从济南市城乡水务局网站爬取历年来趵突泉、黑虎泉地下水位数据,并绘制出水位变化曲线。全部代码涉及到sqlite、optparse、Requests、datetime、lxml、re、numpy、matplotlib等众多模块的使用, 希望能给ython初学者一点启发。

2. 数据源协议分析

这是提供济南市城乡水务局地下水位数据的网站,借助于FireFox提供的网络分析工具,我们很容易搞明白抓取数据的url和method,以及请求头和发送的数据,还可以查看应答的数据格式。详见下面的截图。

3. 使用轻量型数据库Sqlite存取数据

我选择使用Sqlite来保存数据,并提供数据查询服务。数据表只需要一个,结构很简单,只要日期、趵突泉水位、黑虎泉水位三个字段就OK。创建数据库连接对象的时候,构造函数会先检测数据库文件是否存在,如果不存在,则在连接之后,先调用建表方法_create_table(),创建数据表。我把全部的数据库代码贴在下面,看官可以复制并以 waterdb.py 为名保存成文件。

waterdb.py

import os
import sqlite3class WaterDB:"""水位数据库"""def __init__(self):"""构造函数"""fn_db = 'spring.db'is_db = os.path.exists(fn_db)self._conn = sqlite3.connect(fn_db)self._cur = self._conn.cursor()if not is_db:self._create_table()def _create_table(self):"""创建表spring,共3个字段:date(日期)、bt(趵突泉水位)、hh(黑虎泉水位)"""sql = '''CREATE TABLE spring(id INTEGER PRIMARY KEY AUTOINCREMENT,date DATE,bt REAL,hh REAL)'''self._execute(sql)self._conn.commit()def _execute(self, sql, args=()):"""运行SQ语句"""if isinstance(args, list):  # 批量执行SQL语句,此时parameter是list,其元素是tupleself._cur.executemany(sql, args)else:  # 单次执行SQL语句,此时parameter是tuple或者Noneself._cur.execute(sql, args)if sql.split()[0].upper() != 'SELECT':  # 非select语句,则自动执行commit()self._conn.commit()return self._cur.fetchall()def close(self):"""关闭数据库连接"""self._cur.close()self._conn.close()def append(self, data):"""插入水位数据"""sql = 'INSERT INTO spring (date, bt, hh) values (?, ?, ?)'self._execute(sql, data)def dedup(self):"""去除各个字段完全重复的数据,只保留id最小的记录"""self._execute('delete from spring where id not in(select min(id) from spring group by date, bt, hh)')def rectify(self, err_list):"""更新已知的日期错误"""for item in err_list:if item[3]:sql = 'update spring set date=? where date=? and bt=? and hh=?'self._execute(sql, (item[3], item[0], item[1], item[2]))else:sql = 'delete from spring where date=? and bt=? and hh=?'self._execute(sql, (item[0], item[1], item[2]))def fill(self, missing_list):"""补缺"""for item in missing_list:res = self._execute('select * from spring where date=? and bt=? and hh=?', (item[0], item[1], item[2]))if not res:sql = 'insert into spring (date, bt, hh) values (?, ?, ?)'self._execute(sql, item)def stat(self):"""统计信息:数据总数、最早数据日期、最新数据日期"""total = 0date_first = Nonedate_last = Noneres = self._execute('select date from spring order by date')if res:total = len(res)date_first = res[0][0]date_last = res[-1][0]return total, date_first, date_lastdef get_data(self, date1, date2=None):"""取得指定日期或日期范围的水位数据"""if date2:return self._execute('select * from spring where date>=? and date<=? order by date', (date1, date2))else:return self._execute('select * from spring where date= ?', (date1,))if __name__ == '__main__':pass

3. 数据抓取函数

本项目用到了很多模块,导入方式先一并写在这里,后面就不逐一导入了。

import re, optparse
import requests
from datetime import datetime, timedelta
from lxml import etreeimport numpy as np
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as tickerfrom waterdb import WaterDBplt.rcParams['font.sans-serif'] = ['FangSong']  # 设置默认字体
plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像时'-'显示为方块的问题

我选择使用requests模块完成抓取。Requests 是用Python语言编写的,基于 urllib,但比 urllib 更加方便,也更加 pythonic。下面这个抓取函数,每次抓取45条数据,只要传递一个从最新数据起始的编号,就返回从该编号开始的45天的水位数据的xml文本。

def spider(id):"""抓取单页水位数据,返回html文本"""html = requests.post(url = 'http://jnwater.jinan.gov.cn/module/web/jpage/dataproxy.jsp?startrecord=%d&endrecord=%d&perpage=15'%(id, id+45),headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)'},data = {'col': '1','appid': '1','webid': '23','path': '/','columnid': '22802','sourceContentType': '1','unitid': '56254','permissiontype': '0'})return html.text

4. 数据解析函数

数据解析函数使用了lxml 解析模块。因为下载下来的原始数据不规范(有的把"月"写成了“目”,“日”写成了“曰”,甚至缺失),提取水位数据时,使用了正则表达式。

def parse_html(html):"""解析html文本,返回解析结果"""parse_html = etree.HTML(html)items = parse_html.xpath('/html/body/datastore/recordset/record')data = list()p_date = re.compile(r'(\d{4})\D+(\d{1,2})\D+(\d{1,2})')  # 匹配年月日数字部分的正则表达式for item in items:date, bt, hh = item.xpath('string(.)').strip().split('  ')[::2]year, month, day = p_date.findall(date)[0]date = '%s-%02d-%02d'%(year, int(month), int(day))bt, hh = bt[:-1].replace(',','.'), hh[:-1].replace(',','.')try:bt, hh = float(bt[:5]), float(hh[:5])data.append((date, bt, hh))except:passreturn data

5. 抓取全部数据

下面这个函数的功能是:抓取数据至指定日期,并解析入库。grab() 需要两个参数:water_db 是数据库连接对象,deadline表示抓取截止日期。网站的最早数据日期是2012年5月2日,首次抓取时,deadline = ‘2012-05-02’,当补齐数据时,deadline可以指定为数据库已有的最新数据日期。

def grab(water_db, deadline):"""抓取数据,每次一页(45条),直到页内包含截止日期"""id = 1flag = Truewhile flag:html = spider(id)data = parse_html(html)water_db.append(data)if deadline in [item[0] for item in data]:flag = Falseid += 45print('.', end='', flush=True)print()

6. 数据检查清洗

这个网站的数据有不少错误,有缺失,也有重复,因此数据抓取抓取完成后,需要对数据做清洗。有时候,我们也需要对数据的连续性做检查。我把这些功能封装在了一个函数里面。

def spring_verify(water_db):"""数据检查"""# 去除重复数据water_db.dedup()# 更新已知的日期错误err_list = [('2010-10-10', 28.22, 28.17, '2017-10-10'),('2012-01-31', 28.57, 28.51, '2013-01-31'),('2012-12-03', 28.88, 28.86, '2016-12-03'),('2012-12-30', 28.68, 28.66, '2016-12-30'),('2014-09-09', 28.24, 28.18, '2015-09-09'),('2015-02-25', 27.99, 27.92, '2018-02-25'),('2016-02-17', 28.50, 28.46, '2017-02-17'),('2016-06-03', 28.09, 28.02, '2014-06-03'),('2017-02-14', 27.98, 27.91, '2018-02-14'),('2017-07-19', 28.25, 28.20, None)]water_db.rectify(err_list)# 补缺missing_list = [('2014-03-11', 28.52, 28.42),('2016-11-05', 28.95, 28.96),('2016-11-22', 28.90, 28.89),('2017-03-29', 28.09, 28.03)]water_db.fill(missing_list)# 数据检查lost_list = list()  # 数据缺失记录repeat_dict = dict()  # 数据重复记录total, date_first, date_last = water_db.stat()  # 数据总数、最早数据日期、最新数据日期if date_first and date_last:date_start = datetime.strptime(date_first, '%Y-%m-%d')date_stop = datetime.strptime(date_last, '%Y-%m-%d')while date_start <= date_stop:date = date_start.strftime('%Y-%m-%d')result = water_db.get_data(date)if len(result) == 0:  # 数据缺失lost_list.append(date)elif len(result) > 1:  # 数据重复repeat_dict.update({date: [(item[2],item[3]) for item in result]})date_start += timedelta(days=1)print('------------------------------------------')print(u'*** 数据检查报告 ***')print('------------------------------------------')print(u' * 数据总数:%d条'%total)if date_first and date_last:print(u' * 最早日期: %s'%date_first)print(u' * 最新日期: %s'%date_last)print(u' * 缺失数据:%d条'%len(lost_list))for item in lost_list[:15]:print(u'   - %s'%item)if len(lost_list) > 15:print(u'   - ...')print(u' * 重复数据:%d天'%len(repeat_dict))for date in repeat_dict:print(u'   - %s: '%date)for item in repeat_dict[date]:try:print(u'     > %.02f, %.02f'%(item[0], item[1]))except:print(date, item)print()

7. 数据可视化

数据可视化,稍微麻烦一点。我设计的功能是这样的:根据给出的日期范围,绘制水位变化曲线。如果日期范围不超过一年,还可以同时绘制历史同期数据。这个工作分成两个函数,一个处理数据,一个使用matplotlib绘图。

处理数据函数需要4个参数:数据库连接对象、开始日期、截止日期、是否需要历史同期数据。

def get_plot_data(water_db, start, stop, history):"""取得绘图数据"""# 数据日期范围:start_date ~ stop_datetotal, date_first, date_last = water_db.stat()  # 数据总数、最早数据日期、最新数据日期start_date = datetime.strptime(start, '%Y%m%d') if options.start else datetime.strptime(date_first, '%Y-%m-%d')stop_date = datetime.strptime(stop, '%Y%m%d') if options.end else datetime.strptime(date_last, '%Y-%m-%d')total_days = (stop_date-start_date).days + 1# 日期序列:result['date']result = dict()result.update({'date': [start_date+timedelta(days=i) for i in range(total_days)]})result.update({'line': list()})# 判断是否包含2月29日leap = 0for d in result['date']:if d.month == 2 and d.day == 29:leap = 1# 确定是否需要历史同期数据if total_days > (365 + leap):  # 日期范围超过一年,则忽略历史同期history = 0# 以日期序列的年份作为名称start_y, stop_y = start_date.year, stop_date.yearname = '%d'%start_y if start_y == stop_y else '%d-%d'%(start_y, stop_y)# 取得数据日期范围内的数据d, bt, hh = list(), list(), list()for item in water_db.get_data(start_date.strftime('%Y-%m-%d'), stop_date.strftime('%Y-%m-%d')):d.append(item[1])bt.append(item[2])hh.append(item[3])# 水位数据对齐日期序列,无数据则补np.nana = [0 for i in range((datetime.strptime(d[0], '%Y-%m-%d')-start_date).days)]b = [0 for i in range((stop_date-datetime.strptime(d[-1], '%Y-%m-%d')).days)]bt, hh = np.array(a+bt+b), np.array(a+hh+b)bt[bt==0] = np.nanhh[hh==0] = np.nanresult['line'].append({'name':name, 'bt':bt, 'hh':hh})# 取得历史同期数据for i in range(history):start_y, start_m, start_d = start_date.year-i-1, start_date.month, start_date.daystop_y, stop_m, stop_d = stop_date.year-i-1, stop_date.month, stop_date.daystar_str, stop_str = '%d-%02d-%02d'%(start_y,start_m,start_d), '%d-%02d-%02d'%(stop_y,stop_m,stop_d)start_d = datetime.strptime(star_str, '%Y-%m-%d')stop_d = datetime.strptime(stop_str, '%Y-%m-%d')if stop_str < '2012-05-02':breakname = '%d'%start_y if start_y == stop_y else '%d-%d'%(start_y, stop_y)days = (stop_d-start_d).days + 1d, bt, hh = list(), list(), list()for item in water_db.get_data(star_str, stop_str):d.append(item[1])bt.append(item[2])hh.append(item[3])if days > total_days:  # 历史同期范围内有2月29日,则需要剔除该日leap = False for i in range(len(d)):if '-02-29' in d[i]:leap = Truebreakif leap:d.pop(i)bt.pop(i)hh.pop(i)elif days < total_days:  # 日期序列内有2月29日,则历史同期需要在对应位置插入一个nanleap = False for i in range(1, len(d)):if '-03-01' in d[i]:leap = Truebreakif leap:d.insert(i, '')bt.insert(i, 0)hh.insert(i, 0)y0, m0, d0 = d[0].split('-')y1, m1, d1 = d[-1].split('-')d0 = datetime.strptime('%d-%s-%s'%(start_date.year, m0, d0), '%Y-%m-%d')d1 = datetime.strptime('%d-%s-%s'%(stop_date.year, m1, d1), '%Y-%m-%d')a = [0 for i in range((d0-start_date).days)]b = [0 for i in range((stop_date-d1).days)]bt, hh = np.array(a+bt+b), np.array(a+hh+b)bt[bt==0] = np.nanhh[hh==0] = np.nanresult['line'].append({'name':name, 'bt':bt, 'hh':hh})return result

绘图函数使用 get_plot_data() 返回的数据绘图。当需要绘制历史同期时,默认只使用黑虎泉水位数据(mode=True),若mode为False,则使用趵突泉水位数据。

def plot(data, mode):"""绘图"""plt.figure('WaterLevel', facecolor='#f4f4f4', figsize=(15, 8))plt.title(u'济南地下水位变化曲线图', fontsize=20)plt.grid(linestyle=':')plt.annotate(u'单位:米', xy=(0,0), xytext=(0.1,0.9), xycoords='figure fraction')plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))plt.gca().xaxis.set_major_locator(ticker.MultipleLocator(len(data['date'])/20))if len(data['line']) == 1:plt.plot(data['date'], data['line'][0]['bt'], color='#ff7f0e', label=u'趵突泉')plt.plot(data['date'], data['line'][0]['hh'], color='#2ca02c', label=u'黑虎泉')else:        for item in data['line']:if mode:plt.plot(data['date'], item['hh'], label=u'黑虎泉(%s)'%item['name'])else:plt.plot(data['date'], item['bt'], label=u'趵突泉(%s)'%item['name'])plt.legend(loc='best')    plt.gcf().autofmt_xdate()plt.show()

8. 组装在一起

我打算使用 optparse 模块,构造了一个linux风格的使用界面,以常规GNU/POSIX语法指定选项。函数 parse_args() 实现了这个规划。

def parse_args():"""获取参数"""parser = optparse.OptionParser()help = u"检查数据"parser.add_option('-v', '--verify', action='store_const', const='verify', dest='cmd', default='verify', help=help)help = u"补齐数据"parser.add_option('-f', '--fix', action='store_const', const='fix', dest='cmd', help=help)help = u"绘制水位线变化图"parser.add_option('-p', '--plot', action='store_const', const='plot', dest='cmd', help=help)help = u"选择绘图开始日期(格式为YYYYMMDD),默认最早数据日期"parser.add_option('-s', '--start', action="store", default=None, help=help)help = u"选择绘图结束日期(格式为YYYYMMDD),默认最新数据日期"parser.add_option('-e', '--end', action="store", default=None, help=help)help = u"设置是否绘制历史同期数据 (参数为数字),默认不绘制"parser.add_option('-H', action="store", dest="history", default=0, help=help)help = u"选择趵突泉,默认选择黑虎泉"parser.add_option('-b', action="store_false", dest="mode", default=True, help=help)return parser.parse_args()

用户界面如下:

PS > py -3 .\jnspring.py -h
Usage: jnspring.py [options]Options:-h, --help            show this help message and exit-v, --verify          检查数据-f, --fix             补齐数据-p, --plot            绘制水位线变化图-s START, --start=START选择绘图开始日期(格式为YYYYMMDD),默认最早数据日期-e END, --end=END     选择绘图结束日期(格式为YYYYMMDD),默认最新数据日期-H HISTORY            设置是否绘制历史同期数据 (参数为数字),默认不绘制-b                    选择趵突泉,默认选择黑虎泉

主程序:

main():options, args = parse_args()  # 获取命令和参数if options.cmd == 'verify':  # 检查数据water_db = WaterDB()spring_verify(water_db)water_db.close()elif options.cmd == 'fix':  # 补齐数据water_db = WaterDB()deadline = water_db.stat()[2]  # 最新数据日期if not deadline:deadline ='2012-05-13'grab(water_db, deadline)spring_verify(water_db)water_db.close()elif options.cmd == 'plot':  # 数据可视化water_db = WaterDB()data = get_plot_data(water_db, options.start, options.end, int(options.history))plot(data, mode=options.mode)water_db.close()

9. 效果展示

PS > py -3 .\jnspring.py -p -s20190101 -e20191231 -H3 -b

PS > py -3 .\jnspring.py -p -s20130101 -e20131231

10. 后记

近期有很多朋友通过私信咨询有关python学习问题。为便于交流,我在CSDN的app上创建了一个小组,名为“python作业辅导小组”,面向python初学者,为大家提供咨询服务、辅导python作业。欢迎有兴趣的同学扫码加入。

CSDN 不止为我们提供了这样一个交流平台,还经常推出各类技术交流活动。近期我将在 GeekTalk 栏目,和 Python 新手共同探讨如何快速成长为基础扎实、功力强大的程序员。CSDN 还为这个活动提供了一些纪念品。如果有兴趣,请扫码加入,或者直接点此进入。

极简数据抓取教程:山水济南,Say I love you with data相关推荐

  1. Uipath 数据抓取教程

    Uipath是RPA的老大,其教程等都比较完善,但在使用过程中,由于其教程基本上都是从英文版简单翻译过来,导致在国内不是很好使用. 本人对数据抓取教程进行一个优化. 原教程:使用数据抓取的示例(htt ...

  2. python教程怎么抓起数据_介绍python 数据抓取三种方法

    三种数据抓取的方法正则表达式(re库) BeautifulSoup(bs4) lxml *利用之前构建的下载网页函数,获取目标网页的html,我们以https://guojiadiqu.bmcx.co ...

  3. 【RPA入门教程】数据抓取功能使用教学(一)

    UiBot 0.7 版新增加了[数据抓取]功能,这项功能可以方便获取网页中的相似元素,将相似元素的数据采集到数组中,比如各种电商网站(淘宝.京东.拼多多等)的商品分类.商品列表信息(商品名.价格等), ...

  4. 【RPA入门教程】UiBot数据抓取功能使用教学(二)

    数据抓取功能使用说明 点击 UiBot 编辑器工具栏的[数据抓取]按钮,打开数据抓取工具 数据抓取工具需要先选取一个目标,点击选择目标按钮即可. 这个目标就是要采集的数据字段,如果要采集商品名,则先选 ...

  5. python asyncio教程_在Python3中使用asyncio库进行快速数据抓取的教程

    web数据抓取是一个经常在python的讨论中出现的主题.有很多方法可以用来进行web数据抓取,然而其中好像并没有一个最好的办法.有一些如scrapy这样十分成熟的框架,更多的则是像mechanize ...

  6. 掘金站内用户和文章排行分析 | 数据抓取和排序实现

    文章教你如何做掘金站内数据抓取,数据解析,最后形成排序后的排名. 项目起因是我突然想看看掘金站内有哪些优质作者,为了不错过每一个大佬,我选择直接抓取站内所有的文章信息找到作者并进行排名.各位关注 + ...

  7. 查询数据 抓取 网站数据_有了数据,我就学会了如何在几个小时内抓取网站,您也可以...

    查询数据 抓取 网站数据 I had a shameful secret. It is one that affects a surprising number of people in the da ...

  8. WebFetch 是无依赖极简网页爬取组件

    WebFetch 是无依赖极简网页爬取组件,能在移动设备上运行的微型爬虫. WebFetch 要达到的目标: 没有第三方依赖jar包 减少内存使用 提高CPU利用率 加快网络爬取速度 简洁明了的api ...

  9. js读取http chunk流_极简 Node.js入门 教程双工流

    点击上方蓝字关注我们 小编提示: 本文是由 ICBU 的谦行小哥哥出品,我们会持续发出极简 Node.js入门 教程,敬请期待哦,文中有比较多的演示代码建议横屏阅读 双工流就是同时实现了 Readab ...

  10. b站视频详情数据抓取,自动打包并发送到指定邮箱(单个或者群发)

    BiLiBiLi Time: 2020年11月6日19:44:58 Author: Yblackd BiLiBiLi BiLiBiLi 介绍 软件架构 安装教程 使用说明 源码下载 BiLiBiLi ...

最新文章

  1. MyBatis中使用流式查询避免数据量过大导致OOM
  2. 只有程序员才能读懂的西游记,又看了一遍西游记!
  3. 北航、旷视联合,打造最强实时语义分割网络
  4. 皮一皮:所以这也是大数据的一种?
  5. 【Python】聊聊Pandas的前世今生
  6. 【机器学习基础】机器学习模型的度量选择(上)
  7. 【Linux安全】安全口令策略设置
  8. P1078 文化之旅[最短路]
  9. Java 数据结构(链表LinkedList增删改查、数组Vector、获取Vector最大值、交换Vector两成员位置、栈的实现、压栈出栈实现反转、队列Queue)
  10. 查看openfrie是否连接mysql_openfire连接mysql数据库的字符集问题解决
  11. android html模板下载地址,Android HTML模板
  12. C#与OC交互方法中的ong参数的兼容性问题
  13. 安卓开发-开发环境搭建
  14. QCA9531方案双通道嵌入式无线AP模块应用选型参考
  15. 加入飞桨特殊兴趣小组(PPSIG),点亮AI时代的梦想
  16. 酷狗音乐应用在计算机里怎么拖出来,电脑如何使用酷狗音乐剪辑音乐|电脑使用酷狗音乐剪辑音乐的方法...
  17. 南京2级计算机成绩查询,南京审计大学教务管理系统登录入口、成绩查询网上选课查分...
  18. (翻译)标签云(Tag Cloud)
  19. 2021年压力容器作业(R)移动式压力容器充装(R2)考试题库解析
  20. 巴菲特的答卷:年净利润腰斩,百亿美元“错误”,但这些重仓股收益颇丰

热门文章

  1. amoeba mysql下载_amoeba for mysql
  2. redis实现的分布式锁为啥要设置过期时间?
  3. crx插件转换火狐插件_关于Firefox插件
  4. matlab画简谐振动图,基于MATLAB的简谐振动合成图形的动态演示.pdf
  5. 自动驾驶 4-5 自行车模型的横向动力学 Lateral Dynamics of Bicycle Model
  6. java碳纤维山地车车架咋样_自行车碳纤维车架值得买吗?它有哪些优缺点?老骑手来给你答案!...
  7. 空城计课件软件测试,空城计课件公开课.ppt
  8. 阿里巴巴十周年有感----宗教的盛宴
  9. Ubuntu-Chrome 更新Flash插件
  10. Ubuntu/Debian安装护眼软件f.lux indicator applet