[数据采集与融合技术]第二次大作业

作业

作业①

要求:在中国气象网(http://www.weather.com.cn)给定城市集的7日天气预报,并保存在数据库。

输出信息:

  • 序号地区日期天气信息温度
    1 北京 7日(今天) 晴间多云,北部山区有阵雨或雷阵雨转晴转多云 31℃/17℃
    2 北京 8日(明天) 多云转晴,北部地区有分散阵雨或雷阵雨转晴 34℃/20℃
    3 北京 9日(后台) 晴转多云 36℃/22℃
    4 北京 10日(周六) 阴转阵雨 30℃/19℃
    5 北京 11日(周日) 阵雨 27℃/18℃
    6......        

思路:

1. url构建

经测试,各城市的url是通过其对应的标识码进行区分;

因此,可以通过标识码进行url的构建。

代码实现上,使用dict对城市和对应的标识码进行映射,通过输入城市名称实现请求:

cityCode = {
    "北京": "101010100",
    "上海": "101020100",
    "广州": "101280101",
    "深圳": "101280601"
}
if __name__ == '__main__':
    cityName = "北京"
    # 获取html
    url = f'http://www.weather.com.cn/weather/{cityCode[cityName]}.shtml'

2.  发送请求

这里使用的是requests库进行实现:

response = requests.get(url)
response.encoding = "utf-8"
html = response.text

3. 构建soup对象并提取信息

对页面进行解析,发现该网页数据较为规整,目标数据均位于class = "t clearfix"ul下的各个li中:

 使用css进行选择ul,并遍历其中的li数据:

# 构建soup对象
soup = BeautifulSoup(html, "lxml")

# 解析表格
ul = soup.find('ul', attrs={"class": "t clearfix"})
for li in ul.find_all('li'):
    day = li.find('h1').text
    msg = li.find_all('p')[0].text
    temp = li.find_all('p')[1].text
    temp = clear_data(temp)
    print(cityName, day, msg, temp)

4. 数据库保存

这里我使用pymysql构建了一个极简的mysql数据库连接类(CURD均未进行封装):

import pymysql


# mysql连接极简封装
class DB:
    host = '127.0.0.1'
    port = 3306
    user = 'root'
    passwd = '******'

    def __init__(self):
        self.connection = pymysql.connect(host=self.host, port=self.port, user=self.user, passwd=self.passwd)
        self.driver = self.connection.cursor()

在爬虫开始前,应先连接数据库,并且创建数据表(内容包括城市,时间,天气信息,温度):

from mysql import DB

# 创建数据库表单
db = DB()
db.driver.execute('use spider_test')
db.driver.execute('drop table if exists weather')
sql_create_table = """ CREATE TABLE `weather` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `city` varchar(20) DEFAULT NULL,
  `udate` varchar(20) DEFAULT NULL,
  `msg` varchar(20) DEFAULT NULL,
  `temp` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8; 
"""
db.driver.execute(sql_create_table)

最后,对每条数据进行插入操作(异常回滚已省略):

sql_insert = f'insert into weather(city, udate, msg, temp) values ("{cityName}", "{day}", "{msg}", "{temp}")'
db.driver.execute(sql_insert)
db.connection.commit()

5. 运行结果

作业②

要求:用requests和自选提取信息方法定向爬取股票相关信息,并存储在数据库中。

思路:

1. 抓包

由于该网页属于动态渲染网页,数据通过ajax进行动态渲染,因此需要进行抓包。

打开F12并刷新,获取浏览器发送的js请求。

如下进行解析:

  1. 随便复制一个股票名称;

  2. 启动搜索,粘贴复制的股票名称;

  3. 搜索栏中出现一个(或多个 -> 随着时间会进行额外的数据刷新请求)含有该页所有股票代码信息的请求接口;

  4. 右侧Response中可以预览到所有数据。

 2. 参数解析

上步中获取到的接口url如下:

http://67.push2.eastmoney.com/api/qt/clist/get?cb=jQuery112407972169804676412_1634974778877&pn=1&pz=20&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&fid=f3&fs=m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23&fields=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18,f20,f21,f23,f24,f25,f22,f11,f62,f128,f136,f115,f152&_=1634974778878

可以看到,在url后,跟着很多get参数,可以对参数进行修改来完善爬虫。

因此我进行了一些尝试(请求经过验证,均与实际页面相匹配):

首先,最先看到的是pn和pz两个参数。

在后端编写接口中,一般都会使用page_num和page_size对应页码和分页大小,pn和pz则是这两个变量的缩写。

然后是cb=jQuery.......参数。

jQuery是基于js的一个框架,虽然对前端这块不太了解,但是一般情况,前后端分离项目需有一定的请求规范,而爬取到的接口细看是在Restful API的基础上添加一层jQuery函数封装。

可以大胆推测,该接口是专门对jQuery进行的适配,而用于控制这个适配可能性最高的参数就是cb参数(因为里面出现了jQuery)。

删除后,接口数据使用json格式进行返回(这后端接口不规范):

很直观的看出,data中的diff对应是一个list类型数据,启动包含的 f* 就是我们所需要数据。 接着是数据范围限定

经过人工比对,各字段和参数名对应表如下:

股票代码 f12
名称 f14
最新报价 f2
涨跌幅 f3
涨跌额 f4
成交量 f5
成交额 f6
振幅 f7
最高 f15
最低 f16
今开 f17
昨收 f18

最后是其他一些未知作用的参数,可能是后端进行埋点使用,逐个进行筛选,去掉不必要参数,最后,参数添加如下:

    # 需要的参数,其他的参数就不需要请求了
    needParams = ['f12', 'f14', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f15', 'f16', 'f17', 'f18']
    fields = ",".join(needParams)   # 合并成fields参数

    # 输入分页相关参数限定爬取数量和范围,fid为股票种类(f3为目的),fields限定返回参数
    # 和爬取到的接口相比,去掉了cb=jQuery....参数,就可以直接返回json格式数据
    url = f'http://60.push2.eastmoney.com/api/qt/clist/get' \
          f'?pn={pageNum}' \
          f'&pz={pageSize}' \
          f'&po=1&np=1&fltt=2&invt=2' \
          f'&fid={fid}&fs=m:1+s:2' \
          f'&fields={fields}'

(po=1&np=1&fltt=2&invt=2作用未知,但是删除将导致请求未空)

3. 数据库构建

db = DB()
db.driver.execute('use spider_test')
db.driver.execute('drop table if exists money')
sql_create_table = """ CREATE TABLE `money` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `code` varchar(64) DEFAULT NULL,
      `name` varchar(64) DEFAULT NULL,
      `zxbj` varchar(64) DEFAULT NULL,
      `zdf` varchar(64) DEFAULT NULL,
      `zde` varchar(64) DEFAULT NULL,
      `cjl` varchar(64) DEFAULT NULL,
      `cje` varchar(64) DEFAULT NULL,
      `zf` varchar(64) DEFAULT NULL,
      `high` varchar(64) DEFAULT NULL,
      `low` varchar(64) DEFAULT NULL,
      `jk` varchar(64) DEFAULT NULL,
      `zs` varchar(64) DEFAULT NULL,
      PRIMARY KEY (`id`)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8; 
"""
db.driver.execute(sql_create_table)
原则上,数据库字段名不应该出现中文字符,因此,数据库命名看着有些随意。
插入操作和作业一中大同小异,这里就不再展示了。

4. 结果展示

当前只创建一张数据表进行实验,如有实际需求需要使用按时间对表进行划分,不能把所有时间段数据杂在一张表内(不然没有意义)。

另外,仅保存源数据信息,方便数据分析或可视化操作,数据转义(加%或加万、亿等)不应该在数据库中进行。

作业③

爬取中国大学2021主榜(https://www.shanghairanking.cn/rankings/bcur/2021)所
有院校信息,并存储在数据库中,同时将浏览器F12调试分析的过程录制Gif加入至博客中。

输出信息:

排名学校总分
1 清华大学 969.2

思路:

1. 抓包

该网页的翻页实现比较特殊,是通过一次性返回所有数据,然后通过js函数进行动态实现。

和第二题的抓包一样,采用搜索的方式获取消息来源。

由于福州大学不在第一页,我们可以通过使用搜索"南方小清华""福州大学"进行搜索,很快就找到了数据来源于一个payload.js脚本:

2. 解析网页

nameList = re.findall(r'univNameCn:"(.*?)"', html, re.S)
scoreList = re.findall(r'score:(.*?),', html, re.S)

使用正则表达式,可以快速提取参数,但是发现相同分数下,排名会使用参数进行替代,如:

观察请求到的js脚本,发现其结构为一个函数,前方函数头存有参数名,结尾则存有参数值。

因此,只要获取参数名和参数值的对应表,对异常数据进行替代,就解决上述出现的问题。

3. 参数提取

仍然使用正则表达式提取出参数名(keys)和参数值(values),并使用split(",")进行分割,并结合转换为dict类型:

keys = re.findall(r'function\((.*?\))', html)[0].split(',')
values = re.findall(r'\((.*?)\)', html)[-1]
params = dict(zip(keys, values))

然而,出现了元素对应错误的问题,打印keys和values长度,发现两者长度相差1:

检查数据发现在一个字符串中,存在","对分割进行了干扰:

 

 这种情况,解决方法很多,我选择的是一个很“Python”的方法,使用eval函数进行解析:

1. 先将字符串进行处理,把一些类型换成Python的表达方式(如:true->True, null->None)等;

2. 将字符串用中括号"包起来";

3. 直接使用eval转为list形式

    values = eval("[" +
                  values.replace("true", "True").replace("false", "False").replace("null", "None")
                  + "]")

输出看到,参数已经一一对应:

(eval虽然好使,但争议较大,在使用不当容易成“烂代码”)

 4. 结果展示

编写一个简单的替换代码,实现参数替换:

 

数据库构建上,和上面区别不大,但是要注意,排名rank存在重复,因此不能作为主键,需要另外使用id字段进行区分。

 

 另外,对于同分排行进行一个同排行处理,可以看到,排名和分数均实现了对应:

 抓包GIF:

代码地址

https://gitee.com/mirrolied/spider_test

心得

1. 在本次作业中,使用到了mysql数据库对爬取到的信息进行了存储,使用pymysql进行了连接,并使用原生sql语句对数据库进行了操作,和以前常用orm的session操作数据库的有一定的区别。也算是复习了数据库的相关操作;

2. 接口的类型五花八门,而且总有人不按套路出牌,整一些奇奇怪怪的渲染方式。但是可见即可爬,合理运用浏览器开发者工具的过滤器可以事半功倍,快速锁定目标报文。

3. 针对不同的返回格式,可以灵活选用数据提取的方式(第一题:CSS,第二题:json,第三题:re),对不同解析方法均需要熟练掌握,以便在需要时使用。

4. 后面可以适当学习一些前端知识,目前对js的了解还比较基础,对于第三题将数据丢js脚本再返回的操作还是第一次见(一时半会还判断不出来这是前后端分离还是耦合)。

PS: 数据存数据库比终端输出方便多了

posted @ 2021-10-23 17:37  mirrorlied  阅读(20)  评论(0编辑  收藏  举报