Python3爬虫实战: requests+正则爬取音乐网站Allmusic

本文同时发布至我的个人博客,点击进入我的个人博客阅读。本博客供技术交流与经验分享,可自由转载。转载请附带原文链接,感谢!

项目背景

AllMusic 是一个关于音乐的元数据资料库,在1991年由流行文化维护者Michael Erlewine与数学家兼哲学博士Vladimir Bogdanov创立,目的是成为音乐消费者的导览。AllMusic New Releases 是 AllMusic 为用户提供的一项内容推荐服务,以周为频次向用户推荐本周的新音乐/新专辑,甚至你可以通过邮件的形式来订阅该内容。

对于中重度音乐爱好者或习惯聆听新音乐的人群,AllMusic New Releases 提供了很优秀的推荐服务。作为一个严谨专业音乐资料库,AllMusic 提供十分专业且全面的音乐信息。于此同时,AllMusic 有一个由若干专家乐评人组成的内容团队,每周推荐的都是一些比较具有音乐性或话题性的专辑,同时也提供十分专业的乐评

作为一名 AllMusic 的用户,我对其中的内容质量十分满意,但是使用过程中还是有一些不好的体验:

  1. 由于服务器架设在国外,虽然没有被墙,但是网页加载十分缓慢。
  2. AllMusic 在去年接入广告服务,需要安装对应的广告插件才能正常访问。

学习了 Python 的基本爬虫技术后,我决定尝试一下通过爬虫技术来规避这个问题。基本思路是:爬取最近10周的 AllMusic New Releases 的内容,获取专辑图片(地址)、艺术家、专辑名、风格、厂牌、评分等基本信息,并以文本形式存储于本地,下次需要查看时可以直接查看本地文件。

功能实现

一个原始的爬虫实现可以分为:抓取页面 —> 信息提取 —> 格式化输出/存储,同时,由于我们需要处理10个页面,所以引入线程池来实现多线程爬虫能一定程度地优化爬虫性能。有了基本的方向之后就可以开始编写程序,这里我们使用最原始的步进式编程策略来完成。

(一)抓取单个页面

Python 中关于实现页面抓取的一般有 urllibrequests, 这里我们选择 API 更加简洁的requests

def getOnePage(url, headers):
    try:
        rp = requests.get(url=url, headers=headers)
        if rp.status_code == 200:
            return rp.text
        return None
    except RequestException as e:
        print('Request Exception')
        return None

getOnePage()主体上是一个try...except...结构,调用requests.get()获取指定 url 的 html 代码,并以字符串的形式返回;若获取失败则获取函数抛出的RequestException异常,同时要注意 Allmusic 会检查 get 方法的请求头,所以我们需要传入headers请求头参数。

(二)信息析取

这里我们需要爬取两方面的信息:一是我们需要获取的New Releases 的内容;二是需要从网页中获取日期信息来构成url(当然也可以直接通过算法计算,Allmusic 的更新日期是每周的周五)。

析取 New Releases 中的内容

使用Chrome的开发者工具分析我们需要爬取的网页,观察我们关心的字段内容及其所在的标签。这里我们使用正则表达式匹配来解析,当然你也可以选择 BeautifulSoup、Pyquery 等网页解析库。

def parseOnePage(html):
    # use regular expression to get the specified information we want. It do not work well on 'artist' file, so we
    # we have to process 'artist' after it
    pattern = re.compile('album-cover">.*?img src="(.*?)".*?artist">(.*?)</div>.*?title">.*?>(.*?)</a>.*?label">(.*?)</div>.*?styles">.*?>(.*?)</a>.*?allmusic-rating rating-allmusic-(\d+)">.*?headline-review">(.*?)<div.*?author">(.*?)</div>', re.S)
    items = re.findall(pattern, html)
    for item in items:
        # process the 'artist',in order to remove the html code such as '<a>xxx</a>', I use re.split() func.
        artist = item[1].strip()
        artist = re.split('<.*?>', artist)
        artist = ''.join(artist).strip()
        yield {
            'cover': item[0],
            'artist': artist,
            'title': item[2],
            'label': item[3].strip(),
            'styles': item[4],
            'allmusic-rating': item[5],
            'review': item[6].strip(),
            'author': item[7].strip()[2:],
        }

这里使用正则表达式来解决确实带来了一定的麻烦,问题在于在匹配artist字段时由于html格式上的不统一给匹配语法带来了麻烦,无法直接用一次正则匹配解决。这里最后采用的方法是“先扩大匹配范围,然后再在后续处理中过滤不需要的内容”这种思路。首先,第一次通过pattern规则匹配,我们获得类似如下格式的artist字段:

...
{'artist': '<a href="http://www.allmusic.com/artist/jefre-cantu-ledesma-mn0002000220">Jefre Cantu-Ledesma</a>'}
{'artist': 'Various Artists'}
{'artist': '<a href="http://www.allmusic.com/artist/peacers-mn0003408245">Peacers</a>'}
...

进而,使用re.split('<.*?>', artist),过滤标签即可获得文本内容:

...
{'artist': 'Jefre Cantu-Ledesma'}
{'artist': 'Various Artists'}
{'artist': 'Peacers'}
...

正则表达式的用法技巧性比较强,不停地试错和调试然后灵活地调用方法才能比较高效地解决问题。正常匹配之后,我们可以尝试添加如下main()函数测试单网页的爬取是否正常。

def main():
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
        'Host': 'www.allmusic.com'
    }
    url = 'http://www.allmusic.com/newreleases' 
    print(parseOnePage(getOnePage(url, headers)))
    
if __name__ == '__main__':
  main()

获取日期信息

若单网页能正常爬取,那么我们可以开始考虑爬取多个网页。首先分析这些网页url规律:

http://www.allmusic.com/newreleases/20170818
http://www.allmusic.com/newreleases/20170811
http://www.allmusic.com/newreleases/20170804
...

不难想到,我们只要获取所有的日期并以’YYYYMMDD‘的形式添加在基础url上,就可以得到最终的url。通过一下方法,我们可以从网页中获取日期信息:

def getDate(html):
    # get the most recent date and save as a 'datetime'
    pattern = re.compile('week-filter">.*?value="(.*?)".*?selected">', re.S)
    selecteDate = re.findall(pattern, html)[0]
    selecteDatetime = datetime.strptime(selecteDate, '%Y%m%d')
    date = []
    # Allmusic update its information per week so we get information one time for every 7 days. The way to realize it
    # is changing the end of url(such as /20170818 to 20170811)
    for i in range(10):
        i_timedelta = timedelta(7 * i, 0, 0)
        last_datetime = selecteDatetime - i_timedelta
        date.append(datetime.strftime(last_datetime, '%Y%m%d'))
    return date

类似地,也是使用正则匹配的方法。另外,这里我在获取第一个日期字符串后,将其转为datetime对象,以使用datetime的相关方法来计算得出剩余九个需要获取的日期。

(三)静态本地存储

def writeDown(content):
    with open('AllmusicNewReleasesLast10Week.txt', 'a', encoding='utf-8') as f:
        f.write(json.dumps(content, ensure_ascii=False) + '\n')
        f.close()

写入txt文件中,实现本地存储。

(四)多线程爬取

修改main()函数与文件入口,将爬取10个网页的线程加入线程池中,进行多线程爬取:

def main(offset):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome' +
                      '/60.0.3112.90 Safari/537.36',
        'Host': 'www.allmusic.com'
    }
    url = 'http://www.allmusic.com/newreleases'
    date = getDate(getOnePage(url, headers))
    new_url = url + '/' + date[offset]
    for i in parseOnePage(getOnePage(new_url, headers)):
        print(i)
        writeDown(i)

        
if __name__ == '__main__':
    pool = Pool()
    pool.map(main, [i for i in range(10)])

在控制台输出,引入多线程后爬取时间缩短了2-3秒左右,性能明显提升。

项目总结

第一次写爬虫程序,选择了使用 requests + 正则的实现方案,主要是为了巩固基础技术。正则表达式虽然强大,但是在实现过程中确实会遇到困难。或许使用 BeautifulSoup 一个简单的标签选择就可以实现的解析,用正则来实现可能会繁琐许多,工具选择确实对实现效率有很大影响。当然,熟练地使用正则表达式,也能在很多时候很巧妙地解决问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,418评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,593评论 18 139
  • /*** 调整窗口的透明度* @param from>=0&&from<=1.0f* @param to>=0&&...
    Dragon_Boat阅读 961评论 0 1
  • 曾经写过一篇文章,谈及PPT90天践行自己的遗憾,有遗憾是否还接着遗憾吗?所有的遗憾最后都成为自己心里不敢触及的一...
    化化0422阅读 528评论 1 12
  • 今天,一大早来到公司,早会都没来得及开就送胶水去公务员小区客户家施工,回来之后经理回来开早会,开完早会就去参加车展...
    邓承友阅读 120评论 0 0