1. 前言

2月15日,CSDN 联合PyCon中国、wuhan2020、xinguan2020 等力量,举办以「抗击疫情,开发者在行动」为主题的2020 Python开发者日·线上技术峰会,围绕Python在疫情中的具体落地应用与项目,为广大Python开发者、爱好者揭秘代码的力量。

当接到主办方邀请的时候,我的第一感觉是压力和责任。因为这个活动的背景是当前疫情,各行各业都在援助湖北,所有的目光都聚焦在武汉,武汉牵动着亿民众的心。那一刻,我情不自禁,在ppt上写出了下面的文字:

在这里插入图片描述
这是一场纯粹的公益活动,没有牵扯到任何的利益关系。参与者可以扫码后选择免费加入,或者支付19元加入,完全自愿。如果有收入的话,所有的收入将会由主办方捐献给急需援助的地区。
在这里插入图片描述
本文所有的代码,已上传至我的Github:https://github.com/xufive/2020Pyday,如有需要,请自行下载。

2. 关于爬虫,我们必须了解的一些概念

爬虫大概是Pythoneer最先接触、使用最多的技术之一,看似简单,却涉及到网络通讯、应用协议、html/CSS/js、数据解析、服务框架等多个技术领域,初学者往往不容易驾驭,甚至很多人把爬虫等同为某个流行的爬虫库,比如scrapy等。我认为,概念是理论的基石,思路是代码的先驱。弄清楚基本概念、原理,再去编写使用爬虫,会有事半功倍的效果。

2.1 爬虫的定义

  • 定义1:爬虫(crawler)是指一段自动抓取互联网信息的程序,从互联网上抓取对于我们有价值的信息。
  • 定义2:爬虫也叫网络蜘蛛(spider),是一种用来自动浏览万维网的网络机器人。

仔细体会,两者有细微的差异:前者倾向于爬取特定对象,后者倾向于全站或全网搜索。

2.2 爬虫的法律风险

作为一种计算机技术,爬虫本身在法律上并不被禁止,但利用爬虫技术获取数据这一行为具有违法甚至是犯罪风险:

  • 超负荷爬取,从而导致网站瘫痪或不能访问
  • 非法侵入计算机信息系统
  • 爬取个人信息
  • 侵犯隐私
  • 不正当竞争

2.3 从爬虫应用场景理解爬虫类型

  • 聚焦网络爬虫:针对特定对象或目标,通常是一过性的
  • 增量式网络爬虫:仅爬取增量部分,这意味着爬取是频繁的或周期性的活动
  • 深层网络爬虫:针对那些内容不能通过静态链接获取的、隐藏在登录、搜索等表单后的,只有用户提交必要信息才能获得的数据
  • 通用网络爬虫:又称全网爬虫,主要为门户站点搜索引擎和大型 Web 服务提供商采集数据

2.4 爬虫的基本技术和爬虫框架

一个基本的爬虫框架,至少要包含三个部件:调度服务器、数据下载器、数据处理器,分别对应着爬虫的三个基本技术:调度服务框架、数据抓取技术、数据预处理技术。

在这里插入图片描述
上图是我们近年来一直使用的一个框架,比基本框架多了一个管理平台,用于配置下载任务、监视系统个部件工作状况、监视数据到达情况、均衡各节点负载、分析下载数据的连续性、补齐或重新下载数据等。

3. 数据抓取技术

通常情况下,我们使用标准模块urllib,或者第三方模块requests/pycurl等模块抓取数据,有时候也使用自动化测试工具selenium模块。当然,也有很多封装好的框架可用,比如,pyspider/scrapy等等。抓取数据的技术要点包括:

  • 构造并发送请求:方法、报头、参数、cookie文件
  • 接收并解读应答:应答代码、应答类型、应答内容、编码格式
  • 一次数据抓取,往往由多次请求-应答组成

3.1 腾讯NPC疫情数据下载

稍加分析,我们就能很容易地从腾讯的疫情实时追踪站点上,得到它的数据服务url:

https://view.inews.qq.com/g2/getOnsInfo

以及3个QueryString参数:

  • name: disease_h5
  • callback: 回调函数
  • _: 精确到毫秒的时间戳

接下来,就是水到渠成了:

>>> import time, json, requests
>>> url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
>>> data = json.loads(requests.get(url=url).json()['data'])
>>> data.keys()
dict_keys(['lastUpdateTime', 'chinaTotal', 'chinaAdd', 'isShowAdd', 'chinaDayList', 'chinaDayAddList', 'dailyNewAddHistory', 'dailyDeadRateHistory', 'confirmAddRank', 'areaTree', 'articleList'])
>>> d = data['areaTree'][0]['children'] >>> [item['name'] for item in d]
['湖北', '广东', '河南', '浙江', '湖南', '安徽', '江西', '江苏', '重庆', '山东’, …, '香港', '台湾', '青海', '澳门', '西藏']
>>> d[0]['children'][0]
{'name': '武汉', 'today': {'confirm': 1104, 'suspect': 0, 'dead': 0, 'heal': 0, 'isUpdated': True}, 'total': {'confirm': 19558, 'suspect': 0, 'dead': 820, 'heal': 1377, 'showRate': True, 'showHeal': False, 'deadRate': 4.19, 'healRate': 7.04}}

更详细的说明,请参考《Python实战:抓肺炎疫情实时数据,画2019-nCoV疫情地图》

3.2 Modis数据下载

Modis是搭载在TERRA和AQUA遥感卫星上的一个重要的传感器,是卫星上唯一将实时观测数据通过x波段向全世界直接广播,并可以免费接收数据并无偿使用的星载仪器。光谱范围广:共有36个波段,光谱范围从0.4μm-14.4μm。下载Modis数据的步骤如下:

  1. 以GET方式请求https://urs.earthdata.nasa.gov/home
  2. 从应答中解析出解析token
  3. 构造表单,填写用户名密码以及token
  4. 以POST方式请求https://urs.earthdata.nasa.gov/login
  5. 记录cookie
  6. 以GET方式请求文件下载页面
  7. 从应答中解析出文件下载的url
  8. 下载文件

在这里插入图片描述
下面,我们在Python IDLE中以交互方式使用requests模块完成整个流程。当然,同样的功能,也可以使用pycurl模块实现。我在Github上同时提供了requests和pycurl两种实现代码。

>>> import re
>>> from requests import request
>>> from requests.cookies import RequestsCookieJar
>>> resp = request('GET', 'https://urs.earthdata.nasa.gov/home')
>>> pt = re.compile(r'.*<input type="hidden" name="authenticity_token" value="(.*)" />.*')
>>> token = pt.findall(resp.text)[0]
>>> jar = RequestsCookieJar()
>>> jar.update(resp.cookies)
>>> url = 'https://urs.earthdata.nasa.gov/login'
>>> forms = {“username”: “linhl”, “redirect_uri”: “”, “commit”: “Log+in, “client_id”: “”, “authenticity_token”: token, “password”:*********"}
>>> resp = request('POST', url, data=forms, cookies=jar)
>>> resp.cookies.items()
[('urs_user_already_logged', 'yes'), ('_urs-gui_session', '4f87b3fd825b06ad825a666133481861')]
>>> jar.update(resp.cookies)
>>> url = 'https://ladsweb.modaps.eosdis.nasa.gov/archive/allData/6/MOD13Q1/2019/321/MOD13Q1.A2019321.h00v08.006.2019337235356.hdf'
>>> resp = request('GET', url, cookies=jar)
>>> pu = re.compile(r'href="(https://ladsweb.modaps.eosdis.nasa.gov.*hdf)"')
>>> furl = pu.findall(resp.text)[0]
>>> furl= furl.replace('&amp;', '&')
>>> resp = request('GET', furl, cookies=jar)
>>> with open(r'C:\Users\xufive\Documents\215PyCSDN\fb\modis_demo.hdf', 'wb') as fp:
	fp.write(resp.content)

我们下载的Modis数据,是HDF格式的。HDF(Hierarchical Data File),意为多层数据文件,是美国国家高级计算应用中心(National Center for Supercomputing Application, NCSA)为了满足各种领域研究需求而研制的一种能高效存储和分发科学数据的新型数据格式 。HDF可以表示出科学数据存储和分布的许多必要条件。HDF ,以及另一种数据格式文件netCDF, 不仅仅美国人在用,我们中国也在用,尤其是空间科学、大气科学、地球物理等领域,几乎所有的数据分发,都依赖这两种格式的文件。

这是刚才下载的hdf数据文件的庐山真面貌:
在这里插入图片描述

3.3 夸克AI搜索NPC疫情数据

夸克AI搜索NPC疫情数据,这个站点无法使用常规的手段抓取数据,网页源代码和页面显示的内容也完全不搭调(未渲染)。面对这样的网站,我们还有什么技术手段吗?别担心,我给大家介绍一个有趣的数据抓取技术:只要通过浏览器地址栏可以访问的数据,都可以抓到,真正做到“可见即可抓”。

可见即可抓的实现,依赖于selenium模块。实际上,selenium并不是专门用于数据抓取的工具,而是一个用于测试网站的自动化测试工具,支持各种浏览器包括Chrome、Firefox、Safari等主流界面浏览器。用selenium抓取数据,并不是一个通用的方法,因为它仅支持GET方法(当然,也有一些扩展技术可以帮助selenium实现POST,比如安装seleniumrequests模块)。

>>> from selenium import webdriver
>>> from selenium.webdriver.chrome.options import Options
>>> opt = Options()
>>> opt.add_argument('--headless')
>>> opt.add_argument('--disable-gpu')
>>> opt.add_argument('--window-size=1366,768')
>>> driver = webdriver.Chrome(options=opt)
>>> url = 'https://broccoli.uc.cn/apps/pneumonia/routes/index?uc_param_str=dsdnfrpfbivesscpgimibtbmnijblauputogpintnwktprchmt&fromsource=doodle'
>>> driver.get(url)
>>> with open(r'd:\broccoli.html', 'w') as fp:
	fp.write(driver.page_source)
	
247532
>>> driver.quit()

关于selenium模块的安装和使用,更多详细信息,请参考介绍一种有趣的数据抓取技术:可见即可抓

4. 数据预处理技术

4.1 常见的预处理技术

数据预处理需要考虑的问题:

  • 数据格式是否规范?
  • 数据是否完整?
  • 不符合规范、不完整的数据如何处理?
  • 如何保存?如何分发?

基于上述考虑,产生了如下预处理技术:

  • xml/html数据解析
  • 文本数据解析
  • 数据清洗、校验、去重、补缺、插值、标准化
  • 数据存储和分发

4.2 解析示例:地磁指数(dst)

地磁指数,是描述一时间段内地磁扰动强度的一种分级指标。在中低纬度的观测站使用的地磁指数称之为Dst 指数,这个指数每小时量测一次,主要是量测地磁水平分量的强度变化。这个站点提供了Dst 指数下载,页面提供上个月每一天每个小时的Dst 指数。全部流程如下:

  1. 抓取html页面,使用requests
  2. 从html中解析出文本数据,保存成数据文件,使用bs4
  3. 解析文本数据,保存成二维数据表,使用正则表达式

我们还是在Python IDLE中以交互方式实现这个流程:

>>> import requests
>>> html = requests.get('http://wdc.kugi.kyoto-u.ac.jp/dst_realtime/lastmonth/index.html')
>>> with open(r'C:\Users\xufive\Documents\215PyCSDN\dst.html', 'w') as fp:
	fp.write(html.text) 

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html.text, "lxml")
>>> data_text = soup.pre.text
>>> with open(r'C:\Users\xufive\Documents\215PyCSDN\dst.txt', 'w') as fp:
	fp.write(data_text)

>>> import re
>>> r = re.compile('^\s?(\d{1,2})\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$', flags=re.M)
>>> data_re = r.findall(data_text)
>>> data = dict()
>>> for day in data_re:
	data.update({int(day[0]):[int(day[hour+1]) for hour in range(24)]})

5. 数据深加工技术

5.1 数据可视化

数据可视化,是数据的视觉表现,旨在借助于图形化手段,清晰有效地传达与沟通信息。它是一个处于不断演变之中的概念,其边界在不断地扩大。事实上,数据可视化也被认为是数据挖掘的手段之一。

Matplotlib是Python旗下最有影响力的2D绘图库,它提供了一整套和Matlab相似的命令API,十分适合交互式地进行制图。而且也可以方便地将它作为绘图控件,嵌入GUI应用程序中。matplotlib 可以绘制多种形式的图形包括普通的线图,直方图,饼图,散点图以及误差线图等;可以比较方便的定制图形的各种属性比如图线的类型,颜色,粗细,字体的大小等;它能够很好地支持一部分TeX排版命令,可以比较美观地显示图形中的数学公式。

虽然matplotlib主要专注于绘图,并且主要是二维的图形,但是它也有一些不同的扩展,能让我们在地理图上绘图,让我们把Excel和3D图表结合起来。在matplotlib的世界里,这些扩展叫做工具包(toolkits)。工具包是一些关注在某个话题(如3D绘图)的特定函数的集合。比较流行的工具包有Basemap、GTK 工具、Excel工具、Natgrid、AxesGrid和mplot3d等。

Pyecharts也是一个很棒的绘图库,特别是它的Geo地理坐标系功能强大,使用方便。我是从它的js版本echarts开始认识它的。不过,Pyecharts的缺点也很突出:一是版本更迭没有延续性,二是不支持TeX排版命令。尤其是第2个问题,严重制约了Pyecharts的发展空间。

数据3D的可视化方面,推荐选择PyOpenGL,此外还有VTK / Mayavi / Vispy等可供选择。我自己也有一个3D库,已经在https://github.com/xufive/wxgl开源。详情请参考开源我的3D库WxGL:40行代码将疫情地图变成三维地球模型

在这里插入图片描述

关于Matplotlib,请参考我的一篇博文:数学建模三剑客MSN。关于Basemap,我最近在Python实战:抓肺炎疫情实时数据,画2019-nCoV疫情地图一文中有较详细的应用实例。

5.2 数据挖掘

所谓数据挖掘,是指从大量的数据中通过算法揭示出隐含的、先前未知的并有潜在价值的信息的过程。数据挖掘是一种决策支持过程,它主要基于统计学、数据库、可视化技术、人工智能、机器学习、模式识别等技术,高度自动化地分析数据。

下面,我们以全国每天确诊NCP人数变化曲线为例,简单演示曲线拟合技术。曲线拟合多用于趋势预测,常用的拟合方法有最小二乘曲线拟合、目标函数拟合等。

# -*- coding: utf-8 -*-

import time, json, requests
import numpy as np
import matplotlib.pyplot as plt
from scipy import optimize

plt.rcParams['font.sans-serif'] = ['FangSong']  # 设置默认字体
plt.rcParams['axes.unicode_minus'] = False  # 解决保存图像时'-'显示为方块的问题

def get_day_list():
    """获取每日数据"""
    
    url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
    data = json.loads(requests.get(url=url).json()['data'])['chinaDayList']
    return [(item['date'], item['confirm']) for item in data]

def fit_exp():
    """拟合"""
    
    def func(x, a, b):
        return np.power(a, (x+b)) # 指数函数y = a^(x+b)
        
    _date, _y = zip(*get_day_list())
    _x = np.arange(len(_y))
    x = np.arange(len(_y)+1)
    
    fita, fitb = optimize.curve_fit(func, _x, _y, (2,0))
    y = func(x, fita[0], fita[1]) # fita即为最优拟合参数
    
    plt.plot(_date, _y, label='原始数据')
    plt.plot(x, y, label='$%0.3f^{x+%0.3f}$'%(fita[0], fita[1]))
    plt.legend(loc='upper left')
    plt.gcf().autofmt_xdate() # 优化标注(自动倾斜)
    plt.grid(linestyle=':') # 显示网格
    plt.show()

if __name__ == '__main__':
    fit_exp()

拟合效果如下:
在这里插入图片描述
当前全国一心,抗击病毒,疫情发展已经逐渐趋于稳定,我们使用指数函数作为拟合目标,在后期的偏差会越来越大,但在初期,该拟合方式对于趋势预估有一定的参考价值。

5.3 数据服务

所谓数据服务,就是提供数据供爬虫抓取。Python有很多成熟的web框架,比如,Django,Tornado,Falsk等,都可以很轻松地实现数据服务。当然,除了服务框架,数据服务也离不开数据库。因为时间限制,这里就简单演示一下最经济的数据服务器吧:

PS D:\XufiveGit\2020Pyday\fb> python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

试试看,浏览器变成了文件浏览器。

6. 调度服务框架

前面讲过,一个基本的爬虫框架,至少要包含三个部件:调度服务器、数据下载器、数据处理器。我们就用这三个部件,演示一个最小的爬虫框架。

6.1 调度服务模块

APScheduler 是我最喜欢的一个用于调度服务的模块,其全称是 Advanced Python Scheduler。这是一个轻量级的 Python 定时任务调度框架,功能非常强大。APScheduler 有很多种触发器,下面的代码中,使用了cron触发器,这是APScheduler 中最为复杂的触发器,支持cron语法,可以设定非常复杂的触发方式。

6.2 迷你爬虫框架

全部代码,包括注释,只有五十几行,却能实现每10分钟从腾讯疫情数据服务站点抓取一次数据,并解析保存为csv格式的数据文件。

# -*- coding: utf-8 -*-

import os, time, json, requests
import multiprocessing as mp
from apscheduler.schedulers.blocking import BlockingScheduler

def data_obtain():
    """获取数据"""
    
    url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
    with open('fb/ncp.txt', 'w') as fp:
        fp.write(requests.get(url=url).json()['data'])
    
    print('Obtain OK')

def data_process():
    """处理数据"""
    
    while True:
        if os.path.isfile('fb/ncp.txt'):
            with open('fb/ncp.txt', 'r') as fp:
                data = json.loads(fp.read())
            
            with open('fb/ncp.csv', 'w') as fp:
                for p in data['areaTree'][0]['children']:
                    fp.write('%s,%d,%d,%d,%d\n'%(p['name'], p['total']['confirm'], p['total']['suspect'], p['total']['dead'], p['total']['heal']))
            
            os.remove('fb/ncp.txt')
            print('Process OK')
        else:
            print('No data file')
        
        time.sleep(10)

if __name__ == '__main__':
    # 创建并启动数据处理子进程
    p_process = mp.Process(target=data_process) # 创建数据处理子进程
    p_process.daemon = True  # 设置子进程为守护进程
    p_process.start() # 启动数据处理子进程
    
    # 创建调度器
    scheduler = BlockingScheduler() 
    
    # 添加任务
    scheduler.add_job(
        data_obtain,            # 获取数据的任务
        trigger = 'cron',       # 设置触发器为cron     
        minute = '*/1',         # 设置每分钟执行一次
        misfire_grace_time = 30 # 30秒内没有执行此job,则放弃执行
    )
    
    # 启动调度服务
    scheduler.start() 
Logo

长江两岸老火锅,共聚山城开发者!We Want You!

更多推荐