开场图

相信大家在YouTube和Bilibili上都看过类似的视频。这种画面在国外被称为Bar Chart Race,搭配大气磅礴的BGM,会营造出一种“跌宕起伏”的身临其境的感觉。许多此类视频都获得了相当多的观看次数。

由于这类视频的流行,网上已经有专门的制作工具,而且都以NO-CODING作为营销点,这进一步导致了这种视频的“泛滥”视频类型。不过,作为一个喜欢折腾的数据分析工程师,我还是习惯用手敲代码来实现。

数据源

有大量可用数据。这次我们就聚焦热点话题。以近期成为上班族热点的股市为主题,我们将历年排名靠前的个股列出来。通过动态排名图展示10只A股股票。

由于是股市数据,所以可以直接在证券交易所官网查询相关数据。果然,在上交所官网的数据栏目,有一个供投资者“市值排名”的查询入口(http://www.sse.com.cn/market/stockdata/marketvalue/主要的/)。点击进入,您会看到我们的“十大股票市值排名”报告,您可以通过日期过滤框进行查询。

数据源已经确定,接下来需要梳理工作流程。

数据流分析

网站分析

网页更改日期查询后,URL没有变化改变了,但页面没有改变。没有刷新,初步判断是通过Ajax异步更新的。在Chrome浏览器中,右键inspect,查看Network模块下的JS标签。

此时再次切换查询日期,就会发现真正的请求URL(如http://query.sse.com.cn/ marketdata/tradedata/queryTopMktValByPage.do?&jsonCallBack=jsonpCallback12925&isPagination=true&searchDate=2021-01-01&_=1610296018800),可见请求URL需要我们配置以下参数:

jsonCallBack: 之后不传入测试并不会影响 isPagination: truesearchDate: 查询 date_: 时间戳,不传入也不影响

点击请求 URL 最后,我们可以使用右侧面板上的 Preview 和 Response 标签来帮助我们检查是否request中有爬虫返回的数据。

数据捕获

Requests库用于捕获它。 Requests 库是 Python 中最简单、最容易使用的 HTTP 库。我们可以使用它构造一个 URL 请求并获取其响应。

一般来说,要构建 HTTP 请求,您需要需要传入请求头(header)、请求地址、请求方法(GET或POST等)和HTTP协议版本。另外,根据前面的网站分析,我们还需要向URL中传入参数。 Requests库提供了params关键字参数,它允许我们用字典来配置URL所需的参数。

导入请求\nparams = params = {\n "isPagination": "true",\n "searchDate": "2021-01-11"\n}\n\nheaders = {\n " Referer": "http://www.sse.com.cn/market/stockdata/marketvalue/",\n "Accept-Encoding": "gzip, deflate",\n "Connection": "keep-alive", \n "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS //query.sse.com.cn/marketdata/tradedata/queryTopMktValByPage.do"\n\nresponse = requests.get(url, headers = headers , params = params)\n\nprint(response.text)\n

response.text最终输出的是一个嵌套的JSON字符串,里面隐藏了我们想要的市值、排名等数据结果

然后,使用正则表达式拦截response.text的输出结果,拦截目标数据

# continue\nimport re\ntext = response. text\nresult = re.search('"result":\\[(.*?)\\]', text).group(1)\ntemp = {}\ nstock_info = re.findall('"market": "(.*?)",.*?"产品A":"(.*?)",.*?"产品名称":"(.*?)",.*?"排名":(.*?) \\}', result, re.DOTALL)\n\n\nf = open(file_path + '/stock_history_market_value.csv', 'a+', newline = '')\ nprint('写作:', trade_date)\nwriter = csv.DictWriter(f, ['year', 'trade_date', 'code', 'stock_name', 'market_value', 'rank'])\n\nstock_info 中的信息:\n temp = {\n "year ": 2021,\n "交易日期": "2021-01-11",\n "代码": 信息[1],\n "股票名称": 信息[ 2],\n "市场价值": 信息[0] ,\n "rank": info[3]\n }\n print(temp)\n writer.writerow(temp)\nprint('Completed', trade_date )\n

执行后完成后,你会发现程序目录下多了一个文件stock_history_market_value.csv

自从动态排名图需要往年的数据,需要将上面写的csv步骤封装到spider_market_value函数中以便复用。考虑到数据量,这里只采集历年(2000年以来)每个月最后一天的数据。另外,执行命令也封装成函数,方便参数传递和执行。

def get_monthly_market_value(year):\n# 如果参数是今年,则取本月之前每个月最后一天的市值排名,本月的市值排名将采用脚本时间的前一天\ n 如果year == datetime.date.today().year:\n this_month = datetime.date.today().month\n for Month in range(1, this_month+1 ):\n 如果月份 == 日期时间。日期.今天 ().月份:\n trade_date = (datetime.date.today() -Timedelta(days = 1).strftime('%y-%m-%d')\n Spider_market_value de_date)\n else:\n trade_date = str(年) + '-'+ str(month) + '-' + str(calendar.monthRane(year,month)[1]) \nspider_market_Value(year,trade_date)\n# 如果参数是日历年,则取上的市值排名每月的最后一天\n 否则:\n 对于范围 (1, 13) 中的月份: trade_date = str(year) + '-' + str(month) + '- ' + str(calendar.monthrange(year, Month)[1])\n                Spider_market_value(year, trade_date)\n

给get_monthly_market_v,如果年份中传入alue(year),则可以抓取对应年份每个月的数据并汇总写入进入 stock_history_market_value.csv 文件。

这样,数据部分就准备好了。

绘图可视化

在生成动态图之前,首先检查所使用的库和函数的使用情况。本文将以经典可视化库matplotlib中的animation.FuncAnimation为例。调用之前需要了解一下这个方法中的参数或者der确认下一步的准备工作。

animation.FuncAnimation的主要参数可以从官网文档查看:

fig - 传入canvas对象,可以通过fig创建,ax = plt.subplots(); func - 每帧更新调用的(绘图)函数(例如下面要创建的新的draw_barchart()函数)frames - func函数的参数,作为帧序列,图例将依赖它动态变化

\n# 给每只股票随机一个 Color\nrandom.seed(444)\nget_colors = lambda n: list(map(lambda i:"#" +"%06x" % random.randint(0x111111, 0xffffff),range( n)))\ncolors = get_colors( df['code'].nunique())\n\ncodecolors = dict()\nuni_code = set(df['code'])\n对于代码,zip 中的颜色(uni_code, color):\ncodecolors[code] = color\n\n\ndef draw_barchart(trade_date):\n plt.rcParams['font.sans-serif'] = ['微软雅黑']\n plt.rcParams['animation .embed_limit'] = 2**128\n\n#读取当天的数据\ndf_date = df [df [df_date '] == trade_date] \ n df_date = df_date.sort_values(by = ['m mArket_value'], ascending = true) \ n \ n# 绘制之前必须先清除画布每次绘图,否则图像会重叠 \n ax.clear() \n\n# ['stock_name'].astype(str), df_date['market_value'], color = [codecolors[c] for c in df_date[ 'code']])\n  \n  # 标记复制\n  dx = df_date['market_value'].max()/200\n for i, (value, code) in enumerate(zip(df_date['market_value'] , df_date['stock_name'].astype(str))):\n ax.text (值-dx, i, 代码, 大小 = 14, 重量 = 600, ha = '右', va = '底部')\ n            ax.text(value+dx, i, f'{value:,.0f} ', size = 14, ha = 'left', va = 'bottom')\n \n \n # 标记帧日期\n ax.text(1, 0.45, trade_date.split('-')[0] + '- ' + trade_date.split('-')[1], 变换 = ax.transAxes, color = '#777777', 尺寸= 46, ha = 'right')\n \n \n # 标记轴标签\n ax.text(0, 1.06, "市值(万元)", transform = ax.transAxes, size = 12, color = '#777777')\n \n \n # 设置('top')的位置\n \n #设置X轴坐标的颜色和字体大小\n ax.tick_params(axis = 'x', color = '# 777777')\n ax.xaxis.set_major_formatter(ticker.StrMethodFormatter('{x:,.0f}'))\n                                                                                                              主要',轴 = 'x',线型 = '-')\n ax.set_axisbelow(True )\n \n # 设置标题\n ax.text(0.3, 1.05, '历年市值排名前10的股票', transform = ax.transAxes, size = 48, Weight = 600, ha = 'left') \n \n # 删除边框\n plt.box(False)\n\nfig, ax = plt.subplots(figsize=(22, 10 ))\nanimator = Animation.FuncAnimation(fig, draw_barchart,frames = trade_date_list, Interval = 125)\nHTML(animator.to_jshtml())\n

使用draw_barchart()作为数据更新函数,月份作为frames帧序列,执行上面的语句,稍等片刻,会出现文章开头的动态排名图:

动画的流畅程度不仅取决于FuncAnimation的iterval参数(用于设置换帧的时间间隔),还取决于每帧数据之间的间隙。间隙越小,逐帧播放就越流畅,其原理与皮影戏相同,因此,如果想要获得更流畅的动画,可以考虑每天或者每周捕获目标数据当然,要处理的数据量会更大,运行时间和性能问题也需要考虑,不妨多调试测试一下。