使用Python爬虫技术获取动态网页数据简洁方法与实践案例

1,831 阅读6分钟

网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。Python语言,就可以作为爬虫算法的编写语言。

1. 网络爬虫技术概述

对于非软件专业开发人员来说,网络爬虫技术还是比较复杂的。对于专业B/S结构软件开发人员,网络爬虫软件开发技术是围绕Http协议展开的,主要涉及到知识点有URL、Http请求与Webservice、数据格式。

对于非软件专业开发人员来说,一般合法获取动态数据,可以简单的理解为机器人自动通过浏览器查询网页,自动记录获取的数据,也就是如下图所示发请求(Request)及返回(Respone)过程,和分析网页记录数据过程。 在这里插入图片描述

1.1. Http网络处理过程

Http网络处理过程简化描述是以下步骤: 客户端(例如浏览器Chrome、自己开发的Python的程序)通过URL发http请求,服务端响应请求,返回HTML+CSS+JavaScipt代码(含数据),而JavaScipt代码中可能含有URL请求,动态的再请求,直到加载完成整个网页。

我们常说爬虫其实就是在这一堆的动态HTTP请求中,找到能给我们返回需要数据的URL链接,无论是网页链接还是App抓包得到的API链接,我们获取后,模拟客户端发送一个请求包,得到一个返回包,我们再针对返回包解析数据。主要内容有:

  • URL
  • 请求方法(POST, GET)
  • 请求包headers
  • 请求包内容
  • 返回包headers

http请求由请求行,消息报头,请求正文三部分构成。 在这里插入图片描述

请求方法说明
GET请求获取Request-URI所标识的资源
POST在Request-URI所标识的资源后附加新的数据
HEAD请求获取由Request-URI所标识的资源的响应消息报头
PUT请求服务器存储一个资源,并用Request-URI作为其标识
DELETE请求服务器删除Request-URI所标识的资源
TRACE请求服务器回送收到的请求信息,主要用于测试或诊断
CONNECT保留将来使用
OPTIONS请求查询服务器的性能,或者查询与资源相关的选项和需求

HTTP响应也由三部分组成,包括状态行,消息报头,响应正文。 在这里插入图片描述 我们关心的是响应正文,直接截取我们需要的数据。当然,常用的方法是解析响应的页面,很多时候更完整。

1.2. 数据解析

http请求返回格式就是http响应,有固定格式,但是数据体可能多种多样。常用有四种方式:

  • 正则表达式
  • requests-html
  • BeautifulSoup
  • lxml的XPath

2. 爬取成品油零售价格数据实践案例

我们偶尔需要获取互联网上某些数据,例如需要最近今年成品油零售价格数据。

2.1. 找到数据源

全国油价数据中心(data.eastmoney.com/cjsj/oil_de… 在这里插入图片描述

2.2. 找到网页数据查询接口

Fiddler是一种常见的抓包分析软件,我们可以利用Fiddler详细的对HTTP请求进行分析,并模拟对应的HTTP请求。

使用Fiddler时,本地终端浏览器和服务器之间所有的Request,Response都将经过Fiddler,由Fiddler进行转发。由于所有的数据都会经过Fiddler,所以Fiddler能够截获这些数据没实现网络数据抓包。如下图为Fiddler抓取网络链接请求情况。 在这里插入图片描述

跟踪、分析网络请求,发现如下链接是获取数据的接口: 在这里插入图片描述 在这里插入图片描述 通过跟踪监控请求,在WebForms请求表单中,找到日期数据,也就是接口API的变量。 在这里插入图片描述 感兴趣,可以自己再找下日期变量列表的API。

2.3. 分析返回数据格式

分析网页太麻烦了,直接截取数据方式分析爬取结果。

数据格式:

jQuery11230774292942777187_1622422491627(
{"version":"cafaf0743a45b7965a243c937f23dea5",
"result":{"pages":1,"data":[
{"DIM_ID":"EMI01641328","DIM_DATE":"2021/2/1","V_0":5.8,"V_92":6.13,"V_95":6.59,"V_89":5.74,"CITYNAME":"安徽","ZDE_0":0.06,"ZDE_92":0.06,"ZDE_95":0.06,"ZDE_89":0.06,"QE_0":5.74,"QE_92":6.07,"QE_95":6.53,"QE_89":5.68},
{"DIM_ID":"EMI01521389","DIM_DATE":"2021/2/1","V_0":5.81,"V_92":6.16,"V_95":6.56,"V_89":5.76,"CITYNAME":"北京","ZDE_0":0.06,"ZDE_92":0.06,"ZDE_95":0.07,"ZDE_89":0.05,"QE_0":5.75,"QE_92":6.1,"QE_95":6.49,"QE_89":5.71},
{"DIM_ID":"EMI01641332","DIM_DATE":"2021/2/1","V_0":5.77,"V_92":6.14,"V_95":6.55,"V_89":5.71,"CITYNAME":"福建","ZDE_0":0.06,"ZDE_92":0.06,"ZDE_95":0.06,"ZDE_89":0.05,"QE_0":5.71,"QE_92":6.08,"QE_95":6.49,"QE_89":5.66}
],"count":25},"success":true,"message":"ok","code":0}
);

2.4. Python实现抓取数据

本案例使用了requests模块,需要单独安装,如下所示:

pip install requests
# -*- coding: utf-8 -*-
'''
Created on 2021年5月31日

@author: 肖永威
'''
import requests
import re
import json
from datetime import datetime
import pandas as pd
from time import sleep
import random 


class Crawler(object):
    def __init__(self):
        # 东方财富接口
        self.url = 'http://datacenter-web.eastmoney.com/api/data/get'
        # 调价日期接口API
        self.params_date = 'callback=jQuery112300671446287155848_1622441721838&type=RPTA_WEB_YJ_RQ&sty=ALL&st=dim_date&sr=-1&token=894050c76af8597a853f5b408b759f5d&p=1&ps=5000&source=WEB&_=1622441721839'
        # 油价接口API
        self.params_price = 'callback=jQuery11230774292942777187_1622422491627&type=RPTA_WEB_YJ_JH&sty=ALL&filter=(dim_date%3D%27$date$%27)&st=cityname&sr=1&token=8&p=1&ps=100&source=WEB&_=1622422491638'

    # 取调价时间
    def getDates(self,start_date):
        dates_json = self._getResponse(self.params_date)
        self.dates = []
        start_date = datetime.strptime(start_date, "%Y-%m-%d").date()
        for dates in dates_json:
            dim_date = dates['dim_date']
            dim_date = dim_date.replace('/','-')
            if datetime.strptime(dim_date, "%Y-%m-%d").date() >= start_date:
                self.dates.append(dim_date)
                
        print (self.dates)
        
    # 取价格
    def getOilprice(self):
        self.pricedatas = []
        k = len(self.dates)
        i = 0
        for dates in self.dates:
            params_price = self.params_price.replace('$date$', dates)
            prices_json = self._getResponse(params_price)
            self.pricedatas.extend(prices_json)
            sleep(random.randint(0,3))
            i = i + 1
            print('完成:{:.2%}'.format(i/k))
        
        self.df = pd.DataFrame(self.pricedatas)
        #self.df.to_csv('price20210531.csv',encoding='utf_8_sig')
        print(self.pricedatas)    
        
    # 取API返回值
    def _getResponse(self,params):
        r = requests.get(self.url,params) 
        # 正则表达式,获取括号里的JSON数据
        p = re.compile(r'[(](.*?)[)]', re.S)
        jsondata = re.findall(p,r.text)
        # 返回数据为Json格式
        result = json.loads(jsondata[0])
           
        return result['result']['data']  

        
def main():
    Oil_prices = Crawler()
    
    tmp = Oil_prices.getDates('2008-03-01')
    Oil_prices.getOilprice()
    
if __name__ == '__main__':
    main()

2.5. 数据解析

由于是直接截取API返回数据,处理起来相对简单些,直接使用正则方式,匹配出内嵌JSON数据。

其中,代码片段:

        r = requests.get(self.url,params) 
        # 正则表达式,获取括号里的JSON数据
        p = re.compile(r'[(](.*?)[)]', re.S)
        jsondata = re.findall(p,r.text)
        # 返回数据为Json格式
        result = json.loads(jsondata[0])
           
        return result['result']['data']

3. 总结

关于爬虫技术,如果仅仅是提高获取数据速度和方便性,实际上也是比较简单的,从IT软件视角来看,都是很成熟的技术和协议,如果你会JSP、前后端分离开发、SOA技术,合法的爬虫将会很简单。重点是找到请求的URL,识别接口API和参数变量。

推荐多了解Webservice接口技术,提高解析数据效率。

参考:

《python爬虫之requests的使用》 博客园 , lweiser ,2019年6月

《深入理解HTTP协议》 知乎 ,零壹技术栈,2018年9月

《通过fiddler抓取HTTP协议的数据包 | 详细介绍fiddler使用过程。西边人西说测试》 CSDN博客 ,西边人细说测试 ,2018年3月

《Fiddler抓HTTP和HTTPS(手机/电脑)数据包步骤》 CSDN博客 ,梦想成为大佬的菜鸟 ,2019年11月