如何利用维基百科的数据可视化当代音乐史

翻译校对:丁雪 吴怡雯 程序验证修改:李小帅

“我相信马塞勒斯·华莱士,我的丈夫,你的老板吩咐你带我出门做我想做的任何事。现在,我想跳舞,我要赢,我想得到那个奖杯,把舞跳好来!”

《黑色追缉令》是我一直以来最喜欢的电影。令人惊奇的故事情节、演员、表演以及导演会让我想要前去影院观看,当别人问起“你看过这部电影吗?”,我可以打破僵局。电影中最具标志性的场景可能是乌玛•瑟曼和约翰•特拉沃尔塔在杰克兔子餐厅的舞池跳扭扭舞的那段。虽然这可能是乌玛•瑟曼最经典的舞蹈场景,但约翰•特拉沃尔塔似乎根本停不下来,在电影《迈克》、《发胶》、《黑色追缉令》、《油脂》、《周末夜狂热》和《都市牛郎》中约翰所饰演的角色总是梳着锃亮的大背头、乌黑的头发、极富本性地跳着舞。

虽然很多人可能会笑约翰在舞池中央跟着迪斯科音乐跳舞的场景,但扪心自问,所有酷酷的舞蹈电影是否都注定是相同的。随着时间流逝我们是否还会被《魅力四射》(Bring it On,美国系列青春校园电影——译者注)和《街舞少年》(Stompthe Yard)中的音乐所感动?如果看一看这些年最流行音乐风格的变化趋势(如下图),大众对流行乐偏好的变化似乎没有迪斯科的节奏那么快。

◆ ◆ ◆

可视化

通过分析Billboard年终榜单中前100首歌曲,我们可以根据每年Billboard上最流行歌曲所代表的音乐风格的份额来量化现代音乐的走向。图中我们可以看出,迪斯科(Disco)只有短短十几年的光辉,从90年代以来饶舌(Rap)和嘻哈(Hip-Hop)音乐风格才持续出现。有趣的是,本世纪初随着历史的重复,饶舌和嘻哈音乐处于巅峰,迪斯科的变动与流行音乐中一些最低份额的流派保持一致。慢摇滚(Soft Rock)和硬摇滚(HardRock)的光景甚至比迪斯科更糟糕,在2005年完全灭绝。相反的是,麦当娜在2005年的复兴单曲继续延续了迪斯科的影响力,在2010年后,我们被火星哥(Bruno Mars)和魔力红(Maroon 5)的歌洗脑。

这一可视化视图是如何绘制而成的?

维基百科是一座金矿,里面有列表,列表里面套着列表,甚至被套着的列表里面还套着列表。其中一个列表恰巧是Billboard最热门的100首单曲,它使我们能够很容易地浏览维基百科的数据。在快速查看网址后,我们能够简单地生成页面,从中爬取数据,这样更简单。我们从为程序加载必要的模块和参数开始。

#iPython 内联查看画图并导入必要的包

import numpy as np

import pandas as pd

import seaborn as sns

import pylab as pylab

import matplotlib.pyplot as plt

import requests, cPickle, sys, re, os

from bs4 import BeautifulSoup as bs

import logging

logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',level=logging.INFO)

requests = requests.Session()

# 设置画板大小

pylab.rcParams['figure.figsize'] = 32, 16

接着程序脚本利用我们在网址中找到的模式,尝试从页面中提取所有可能存在的链接。

# 定义一个从维基百科表格中抓取相关信息的函数

如果没有返回NaN

def tryInstance(td, choice):

try:

# 歌曲只有一个维基百科链接,但是歌手可能有许多链接。

我们创建一个选择标志, #用来决定抓取文本信息还是链接信息

if (choice == 0):

return td.text

elif (choice == 1):

links = [x['href'] for x in td.findAll('a')]

if (len(links) != 0):

return links

else:

return float('NaN')

except:

return float('NaN')

#找到页面的第一个table,尽量抓取所有表格行的信息

pandaTableHeaders = ['year', 'pos', 'song','artists', 'song_links', 'artist_links']

headers = {'User-Agent': 'Mozilla/5.0 (X11;Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116Safari/537.36'}

cookies = {'cookie': 'CP=H2;WMF-Last-Access=16-Apr-2016; GeoIP=CN:01:Xuancheng:30.95:

118.76:v4;enwikimwuser-sessionId=588324132f65192a'}

def scrapeTable(year):

#创建url路径,用BeautifulSoup解析页面内容,创建列表用来存储表数据

url ='https://en.wikipedia.org/wiki/Billboard_Year-End_Hot_100_singles_of_'+str(year)

soup =bs(requests.get(url, headers=headers, cookies=cookies).content)

table = []

#由于文本格式的不同,我们针对4种特例使用不同的code来创建临时souptable变量

souptable= soup.find('table')

if (year in [2006, 2012, 2013]):

souptable = soup.findAll('table')[1]

elif (year in [2011]):

souptable = soup.findAll('table')[4]

#从上面迭遍历程序得到的table中收集每个表格行的信息

for pos, tr in enumerate(souptable.findAll('tr')):

tds = tr.findAll('td')

if (len(tds) > 0):

toAppend = [

year, pos,

tryInstance(tds[-2], 0),tryInstance(tds[-1], 0),

tryInstance(tds[-2], 1),tryInstance(tds[-1], 1)

]

table.append(toAppend)

#创建并返回表数据的数据框形式

df = pd.DataFrame(table)

df.columns = pandaTableHeaders

return df

#遍历所有可能的年份,序列化存储,方便以后使用

dfs =pd.DataFrame(pandaTableHeaders).set_index(0).T

for year in xrange(1956, 2016):

print year,

dfs = dfs.append(scrapeTable(year))

cPickle.dump(dfs.reset_index().drop('index',axis=1), open('wikipediaScrape.p', 'wb'))

借助存储在数据帧中的所有链接,我们可以加载每个维基百科页面,并从每一页右上角信息表中提取信息。不幸的是,当所有这些信息表的长度不同,有不同的 HTML 嵌套和不完整数据时,这些数据会变得特别混杂(竟然没有人将Gorillaz 音乐进行归类?!)。

为了解决这一问题,我们在代码中查找表对象,并将其作为字符串保存并在之后的分析进行加载。这样做的优点是加倍的,它可以让我们从一次运行中收集所有必要的信息;同时,也帮助我们从用户的定义中对音乐流派关键词进行分类。

#从wikipediaScrape.p文件中加载数据框,创建新的列,边抓取信息边填充

dfs =cPickle.load(open('wikipediaScrape.p', 'rb'))

subjects =['Genre', 'Length', 'Producer', 'Label', 'Format', 'Released', 'B-side']

for subject insubjects:

dfs[subject] = float('NaN')

# 与上面的tryInstance函数类似,尽可能抓取更多信息

# 捕获缺失异常,使用NaNs替代缺失值

# 另外,还有一个问题是tables难于管理。其内容可能存在或不存在,可能有错别字

# 或不同的名字。

def extractInfoTable(url):

infoTable = []

#捕获表头、表行和页面异常

try:

soup = bs(requests.get(url, headers=headers, cookies=cookies).content)

for tr in soup.find('table').findAll('tr'):

try:

header = tr.find('th').text

if (header == 'Music sample'):

# Music sample表示信息表的结束,如果满足条件中断循环以节省时间

break

try:

# 如果表头不是Musicsample,收集”tr”对象中所有可能的信息

trs = tr.findAll('td')

infoTable.append([header,trs])

except:

noTrsFound = True

except:

noHeaderFound = True

except:

noPageFound = True

#如果subjects列表中存在记录,保存HTML字符串形式

infoColumns = []

for subject in subjects:

instanceBool = False

for header, info in infoTable:

if (subject in header):

infoColumns.append([subject,str(info)])

instanceBool = True

break

if (not instanceBool):

infoColumns.append([subject,float('NaN')])

#返回所有抓取的信息

return infoColumns

#对数据帧中所有的歌曲使用scraping函数

forsongIndex in xrange(0,dfs.shape[0]):

printsongIndex, dfs.ix[songIndex].year, dfs.ix[songIndex].song

try:

# 获取链接

song_links =['https://en.wikipedia.org' + x for x in dfs.ix[songIndex].song_links]

# 抽取信息

logging.info('extract info')

infoTable =extractInfoTable(song_links[0])

# 存储index和subjectstore信息

for idx, subject in enumerate(subjects):

dfs.loc[:,(subject)].ix[songIndex]= str(infoTable[idx][1])

#每100首歌曲序列化保存

if (songIndex % 100 == 0):

cPickle.dump(dfs.reset_index().drop('index', axis=1), open('full_df.p','wb'))

except(TypeError):

print 'NaN link found'

# 保存所有的数据帧信息

cPickle.dump(dfs.reset_index().drop('index',axis=1), open('full_df.p', 'wb'))

现在,我们开始对所有HTML字符串进行分析。当音乐流派可以被识别时,我们就可以抽取关键词列表,之后将它们分入“脏列表”(脏,表示数据还未被清洗——译者注)。这一列表充满了错别字、名称不统一的名词、引用等等。

#创建流派字典,比如,对于“folk”和“country”范围的分析则认为是相同的音乐流#派

genreList= {

'electronic': ['electronic'],

'latin' : ['latin'],

'reggae' : ['reggae'],

'pop' : ['pop'],

'dance' : ['dance'],

'disco' : ['disco', 'funk'],

'folk' : ['folk', 'country'],

'r&b' : ['r&b'],

'blues' : ['blues'],

'jazz' : ['jazz'],

'soul' : ['soul'],

'rap' : ['rap', 'hip hop'],

'metal' : ['metal'],

'grunge' : ['grunge'],

'punk' : ['punk'],

'alt' : ['alternative rock'],

'soft rock' : ['soft rock'],

'hard rock' : ['hard rock'],

}

#加载数据帧并抽取相关的流派

# 添加“dirty”列,名单包括HTML元素

# “ dirty”列包含的错别字、引用等记录都会导致异常发生,但是我们感兴趣的是从

# 混乱的字符串中抽取相关的关键字,通过简单匹配所有的小写实例,计数最后的

#“pop”流派个数

df =cPickle.load(open('full_df.p', 'rb'))

defextractGenre(x):

sx = str(x)

try:

dirtyList = [td.text.replace('\n', '')for td in BeautifulSoup(sx).findAll('td')]

return dirtyList

except:

return float('NaN')

df['Genre']= df['Genre'].apply(extractGenre)

# 打印df['Genre']

最后我们为每首歌所代表的音乐流派创建标志列,使绘制图片更加容易。

#添加”key”列,如果key是流派字典的键值则为1,否则为0。拷贝数据帧,使

#用.loc[(tuple)]函数以避免切片链警告。

for keyin genreList.keys():

df[key] = 0

dfs =df.copy()

# 对于genreList字典中每个流派匹配字符串,如果能匹配,则标志指定列,以便能够在后面输出布尔结果

forgenre in genreList:

ans=0

for idx in xrange(0, df.shape[0]):

if (len(df.loc[(idx,'Genre')]) > 0):

if (any([x indf.loc[(idx,'Genre')][0].lower() for x in genreList[genre]])):

dfs.loc[(idx, genre)] = 1

ans+=1

print genre, ans

sys.stdout.flush()

cPickle.dump(dfs,open('genre_df.p', 'wb'))

◆ ◆ ◆

微调变量后导出数据

df =cPickle.load(open('genre_df.p', 'rb'))

defaverageAllRows(gdf):

# 添加”sums”列

gdf['sums'] = gdf.sum(axis=1)

#对数据帧的每列除以”sums”列,添加精度1e-12,排除分母为零的情况

logging.info('averageAllRows')

for col in gdf.columns:

gdf[col] =gdf[col].divide(gdf['sums']+1e-12)

#返回数据帧并丢弃”sums”列

return gdf.drop('sums', axis=1)

pylab.rcParams['figure.figsize']= 32, 16

gdf =pd.DataFrame()

for g ingenreList.keys():

gdf[g] = df.groupby('year')[g].sum()

# 自定义打印顺序

gl2 = [

'jazz', 'blues', 'folk', 'soul', 'pop','disco', 'rap', 'soft rock',

'hard rock', 'dance', 'r&b', 'alt','latin', 'reggae', 'electronic', 'punk',

'grunge', 'metal',

]

#对数据帧重新排序并对所有行求平均

gdf =gdf[gl2]

gdf =averageAllRows(gdf)

# 创建百分比条形图

ax =gdf.plot(kind='bar', width=1,stacked=True, legend=False, cmap='Paired',linewidth=0.1)

ax.set_ylim(0,1)

ax.legend(loc='centerleft', bbox_to_anchor=(1, 0.5))

locs,labels = plt.xticks()

plt.setp(labels,rotation=90)

plt.show()

◆ ◆ ◆

最后的输出

◆ ◆ ◆

编后语

由于程序是对1956年-2016年期间的Wiki年度热门歌手页面的爬取,处理过程很耗时,因此,我们将1956-2016时间段分成了6部分,每部分包含了跨度为10年的年度热门歌手页面的处理。具体方法是将”for year in xrange(1956, 2016)”程序修改为” foryear in xrange(1956, 1966)”等。您也可以使用我们训练好的模型进行验证,模型文件genre_df.p已按照年份保存到对应目录了,在加载模型文件的目录地址一定不要写错了。

df =cPickle.load(open('./06_16/genre_df.p', 'rb'))

defaverageAllRows(gdf):

# 添加”sums”列

gdf['sums'] = gdf.sum(axis=1)

#对数据框的每列除以”sums”列,添加精度1e-12,排除分母为零的情况

logging.info('averageAllRows')

for col in gdf.columns:

gdf[col] =gdf[col].divide(gdf['sums']+1e-12)

#返回数据框并丢弃“sums”列

return gdf.drop('sums', axis=1)

pylab.rcParams['figure.figsize']= 32, 16

gdf =pd.DataFrame()

for g ingenreList.keys():

gdf[g] = df.groupby('year')[g].sum()

# 自定义打印顺序

gl2 = [

'jazz', 'blues', 'folk', 'soul', 'pop','disco', 'rap', 'soft rock',

'hard rock', 'dance', 'r&b', 'alt','latin', 'reggae', 'electronic', 'punk',

'grunge', 'metal',

]

#对数据框重新排序并对求平均

gdf =gdf[gl2]

gdf =averageAllRows(gdf)

# 创建百分比条形图

ax =gdf.plot(kind='bar', width=1,stacked=True, legend=False, cmap='Paired',linewidth=0.1)

ax.set_ylim(0,1)

ax.legend(loc='centerleft', bbox_to_anchor=(1, 0.5))

locs,labels = plt.xticks()

plt.setp(labels,rotation=90)

plt.show()

维基百科真是个宝,里面有很多可以挖掘的内容,欢迎读者朋友们也试试,给我们投投稿,谢谢!

原文链接:http://www.microbrewdata.com/defining-a-modern-history-of-music-using-wikipedia-data/

原文发布于微信公众号 - 大数据文摘(BigDataDigest)

原文发表时间:2016-06-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FreeBuf

逆向分析智能窗帘频射协议

近来我热衷于对家庭自动化设备的破解,然后将它们添加到我的Homekit集成包之中。这事情要从几个月前说起,当时我爸订购了大批量的RAEX 433MHz射频电动窗...

32150
来自专栏数据小魔方

高级筛选到底有多“高级”!

今天跟大家分享excel筛选功能中隐藏的高级筛选功能! excel中的筛选窗口中,一直隐藏着一个不起眼的小菜单——高级:(如下图) ? 按照微软软件一贯风格,藏...

36150
来自专栏偏前端工程师的驿站

JS魔法堂:不完全国际化&本地化手册 之 理論篇

前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求——国际化&本地化。熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"...

25180
来自专栏闻道于事

使用ichartjs生成图表

官网:http://www.ichartjs.com/   ichartjs 是一款基于HTML5的图形库。使用纯javascript语言, 利用HTML5的c...

62770
来自专栏深度学习之tensorflow实战篇

计算机常用算法对照表整理

常用对照: NLP CRF算法: 中文名称条件随机场算法,外文名称conditional random field algorithm,是一种数学算法,是2...

52750
来自专栏前端侠2.0

css3的transform造成z-index无效, 附我的牛逼解法

我想锁表头及锁定列。昨天新找的方法是用css3的transform,这个应该在IE9以上都可以的。

72230
来自专栏ACM小冰成长之路

51Nod-1615-跳跃的杰克

ACM模版 描述 ? 题解 这个题代码炒鸡简单,只要想通了就好了。 这里我们贪心的想,尽量向靠近的方向跳,如果跳过了,我们考虑超过的距离是奇数还是偶数,如果是偶...

22660
来自专栏懂啵的蟒络空间

哈希现金(Hashcash)与“工作量证明”

“哈希现金(Hashcash)是一种用于防止垃圾电子邮件和拒绝服务攻击的工作量证明系统,最近以其在比特币(以及其他加密货币)挖矿算法中的应用而闻名,由Adam ...

576100
来自专栏思考的代码世界

Python网络数据采集之处理自然语言|第07天

12540
来自专栏IT可乐

深入理解计算机系统(5.1)------优化程序性能

  你能获得的对程序最大的加速比就是当你第一次让它工作起来的时候。   在讲解如何优化程序性能之前,我们首先要明确写程序最主要的目标就是使它在所有可能的情况下都...

257100

扫码关注云+社区

领取腾讯云代金券