[数据采集与融合技术]第二次大作业
作业
作业①
要求:在中国气象网(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: 数据存数据库比终端输出方便多了