网易云音乐听歌量爬虫(免登陆版)

  最近学习了一下爬虫的原理和基本的思路, 并且利用selenium + Python + Chrome 进行了一系列的爬虫.

  最开始的想法是, 想每天爬取自己网易云音乐的一些数据, 比如每天听歌量, 之类的.

  可以很轻松的利用selenium中的CSS selector 或者 XPath路径找到对应的HTML标签元素, 再通过鼠标等操作, 完成登陆, 进而爬取数据,

  但是, 后来发现网易云音乐的登陆有机器人检测, 在监视模式下无法通过selenium直接登陆. 参考了网上的各方意见之后, 找到解决的方法有两种:

    1. 通过get/post请求分析, 并修改header来提交表单完成登陆操作;(在requests模块中)

    2. 通过模拟鼠标移动来登陆(但听说这种方法现在也会直接被认定为机器人!)

  最终这个登陆问题也没有得到解决!!

  但是, 这并没有打消我爬虫的积极性. 所以就有了现在的免登陆爬虫:

  零、运行环境搭建:

    1. 运行环境

        我的爬虫是在win7下运行的,用到的爬虫软件有:

          ChromeDriver 70.0.3538.67

          Python 3.7.0

          Selenium 3.14.1

        当然, 如果你和我一样, 也是Firefox的忠实粉丝, 我也为你做了测试:

          geckodriver-v0.23.0-win64

          Firefox 60.0.1

          Python 3.7.0

          Selenium 3.14.1

        这么做的目的主要是因为, 一开始不知道Firefox也有headless模式(无头), 所以专门下载的Chrome.

    2. 爬虫部署:

        Win7下: 可以使用任务计划程序来定制爬虫; 我使用的是一个叫Ontimer的软件, 可以一键设置和管理所有自定义的计划, 并实现后台静默运行;

        Linux下: 我的服务器是CentOS6.9, 比较难搭建运行环境, 如果是CentOS7的操作系统, 可以使用crontab来部署你的爬虫!

  一、 取得并分析URL地址:

        取得的URL地址如:https://music.163.com/#/user/songs/rank?id=你的网易云音乐ID

        上面id=, 可以通过找到你的个人主页来得到(也可以修改id来访问别人的主页)

  二、设置请求头:

# 设置Chrome请求头(无头模式):
options = webdriver.ChromeOptions()
options.add_argument('--headless')

options.add_argument('lang=zh_CN.UTF-8') # 设置中文
options.add_argument('disable-infobars') #隐藏"Chrome正在受到自动软件的控制"
options.add_argument('--disable-gpu') #谷歌文档提到需要加上这个属性来规避bug
    # 更换头部
user_agent = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) " +
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"
    )
options.add_argument('user-agent=%s'%user_agent)

        这个是Chrome浏览器请求头设置的代码, 如果你用的是Firefox浏览器:

# 设置Chrome请求头(无头模式):
options = webdriver.FirefoxOptions()
options.add_argument('-headless')

options.add_argument('lang=zh_CN.UTF-8') # 设置中文
options.add_argument('disable-infobars') #隐藏"Chrome正在受到自动软件的控制"
options.add_argument('--disable-gpu') #谷歌文档提到需要加上这个属性来规避bug
    # 更换头部
user_agent = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) " +
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"
    )
options.add_argument('user-agent=%s'%user_agent)

        基本和Chrome一致.

        如果很不幸, 你用的是PhantomJS或者其他的浏览器驱动, 我这里并没有配置header的代码;

        (其实我是有PhantomJS配置的代码的, 但是由于听说PhantomJS要停止维护了, 所以在这里就不写了! 有需要的朋友请自行百度)

  三、配置并请求链接:

        browser = webdriver.Chrome(chrome_options=options)
        browser.get("https://music.163.com/#/user/songs/rank?id=" + user_id)

        在这里, 我通过一个str类型的变量user_id来实现了访问网易云音乐不同用户的页面;

  三'、预处理:

        browser.switch_to.frame("g_iframe")

        认真读过网易云音乐页面源代码之后, 发现, 其实他的页面是一个嵌入到body中的一个叫g_iframe的框架构成的;

        其实这也是我放弃requests 转而用selenium的最主要原因, 因为貌似requests只能在单个页面中寻找元素, 而无法处理iframe框架.

  四、寻找对应元素的xpath:

        xpath在类似于Chrome或者Firefox这种主流的浏览器里非常简单:

        按下F12, 找到对应的元素, 右键源代码, 复制xpath即可!

这里以

歌曲名为例: /html/body/div[3]/div/div[2]/div/div[1]/ul/li[1]/div[2]/div[1]/div/span/a/b

  五、处理对应元素:

        xpath_musicname_left = "/html/body/div[3]/div/div[2]/div/div[1]/ul/li["
        xpath_musicname_right = "]/div[2]/div[1]/div/span/a/b"

        for i in range(1, 101):  
            # 歌曲名处理:
            try: # 排行榜内部不到一百首歌, 退出
                xpath_musicname = xpath_musicname_left + str(i) + xpath_musicname_right
                each_info["name"] = (browser.find_element_by_xpath(xpath_musicname))
                # 正则表达式,去掉歌名中的逗号, 并将"name"处理为str类型!:
                each_info["name"] = re.sub(",", "", each_info["name"].text)
            except:
                break

        通过查看多个元素的xpath可以找到规律, 将其分为'左边'和'右边', 从而插入相应的数量信息即可确定对应的xpath;

        再通过查看selenium的手册, 找到driver.find_element_by_xpath()函数, 可以返回一个Web类型的变量(.text为对应的字符串)

  六、处理数据:

        最后, 可以保存相应的数据, 不论是JSON, .csv, 还是.txt类型都是可以的!!!!

        我为了方便处理数据, 最终选择了将数据保存为.txt格式.

源代码:

Chrome:

#!/usr/bin/python 
#-*- coding: utf-8 -*-
import re
import sys
import datetime
from selenium import webdriver
from time import sleep
from selenium.webdriver.common.action_chains import ActionChains

def scrap_music(user_id):
    try:
        # 打开网页: 
        browser = webdriver.Chrome(chrome_options=options)
        browser.get("https://music.163.com/#/user/songs/rank?id=" + user_id)

        # selenium预处理: (转换到iframe内部)
        browser.switch_to.frame("g_iframe")

        xpath_musicname_left = "/html/body/div[3]/div/div[2]/div/div[1]/ul/li["
        xpath_musicname_right = "]/div[2]/div[1]/div/span/a/b"

        xpath_artist_left = "/html/body/div[3]/div/div[2]/div/div[1]/ul/li["
        xpath_artist_right = "]/div[2]/div[1]/div/span/span/span"
        
        xpath_artist_text_left = ""
        xpath_artist_text_right = ""

        xpath_playtimes_left = "/html/body/div[3]/div/div[2]/div/div[1]/ul/li["
        xpath_playtimes_right = "]/div[3]/span"

        # 用列表类型来整合toplist, each_info存放每一个的表单
        toplist = list()
        each_info = dict()

        # 时间戳处理:
        year = datetime.datetime.now().strftime("%Y")
        month = datetime.datetime.now().strftime("%m")
        day = datetime.datetime.now().strftime("%d")
        hour = datetime.datetime.now().strftime("%H")
        now_time = month + u"/" + day + u":" + hour + u"h"

        for i in range(1, 101):  
            # 歌曲名处理:
            try: # 排行榜内部不到一百首歌, 退出
                xpath_musicname = xpath_musicname_left + str(i) + xpath_musicname_right
                each_info["name"] = (browser.find_element_by_xpath(xpath_musicname))
                # 正则表达式,去掉歌名中的逗号, 并将"name"处理为str类型!:
                each_info["name"] = re.sub(",", "", each_info["name"].text)
            except:
                break
            # 歌唱家处理:
            try: # 第一个try 用来判断是否为100首歌, 若通过, 说明有歌, 这个try 用来判断歌手是链接形式还是文本形式给出!
                xpath_artist = xpath_artist_left + str(i) + xpath_artist_right
                each_info["artist"] = (browser.find_element_by_xpath(xpath_artist).get_attribute("title"))
            except:
                break
            # 播放次数(百分比)处理: (由于未登录, 所以采用默认排名末位的歌曲仅播放一次, 并通过第一与末尾元素的比例来判定播放次数)
            xpath_playtimes = xpath_playtimes_left + str(i) + xpath_playtimes_right
            each_info["playtime_percent"] = (browser.find_element_by_xpath(xpath_playtimes).get_attribute("style"))
            
            # 正则表达式将非数字的字符全部转化为空字符, 完成数字的提取:
            each_info["playtime_percent"] = int(re.sub("\D", "", each_info["playtime_percent"]))
            
            # 排行榜序列索引
            each_info["index"] = i
              
            # 处理时间戳  
            each_info["time"] = now_time
            
            # 加入处理后的数据到列表toplist中:
            toplist.append(each_info)
            each_info = dict()
            
            
        # 播放次数(真实数据)处理: (由于未登录, 所以采用默认排名末位的歌曲仅播放一次, 并通过第一与末尾元素的比例来判定播放次数)
        # 处理首个和末尾单元的播放次数:
        toplist[-1]["playtimes"] = 1

        if toplist[-1]["playtime_percent"] == 0: # 如果网页上最后一名的长度为0, 则第一位的播放量应当大于100, 选取为101(预测)
            toplist[0]["playtimes"] = 101
        else:
            toplist[0]["playtimes"] = (toplist[0]["playtime_percent"] / toplist[-1]["playtime_percent"]) * toplist[-1]["playtimes"]

        # 处理所有其他的次数:
        for i in toplist:
            if i["playtime_percent"] == toplist[-1]["playtime_percent"]:
                i["playtimes"] = 1
            else:
                i["playtimes"] = int((i["playtime_percent"] / 100.0) * toplist[0]["playtimes"])

        # 注: 以上计算方法仅仅是预测, 所以会有一定的误差!    

        # 输出到文件中:
        # 打开文件追加a模式
        filename = user_id + ".txt"
        fileout = open(filename, "a+", encoding='utf-8')
        #写入数据:
        for i in toplist:
            fileout.writelines(i["name"] + "," + i["artist"] + "," + str(i["playtimes"]) + "," + i["time"] + "\n")    
            
    finally:
        # 关闭文件和窗口:
        fileout.close()
        browser.close()


#-----------------------------------------  程序开始处 -------------------------------------------#

# 设置Chrome请求头(无头模式):
options = webdriver.ChromeOptions()
options.add_argument('--headless')

options.add_argument('lang=zh_CN.UTF-8') # 设置中文
options.add_argument('disable-infobars') #隐藏"Chrome正在受到自动软件的控制"
options.add_argument('--disable-gpu') #谷歌文档提到需要加上这个属性来规避bug
    # 更换头部
user_agent = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) " +
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"
    )
options.add_argument('user-agent=%s'%user_agent)

usr_id = []
for each_id in usr_id:
    scrap_music(str(each_id))
# 退出程序:
#    sys.exit(0)


Firefox:

#!/usr/bin/python 
#-*- coding: utf-8 -*-
import re
import sys
import datetime
from selenium import webdriver
from time import sleep
from selenium.webdriver.common.action_chains import ActionChains

def scrap_music(user_id):
    try:
        # 打开网页: 
        browser = webdriver.Firefox(firefox_options=options)
        browser.get("https://music.163.com/#/user/songs/rank?id=" + user_id)

        # selenium预处理: (转换到iframe内部)
        browser.switch_to.frame("g_iframe")

        xpath_musicname_left = "/html/body/div[3]/div/div[2]/div/div[1]/ul/li["
        xpath_musicname_right = "]/div[2]/div[1]/div/span/a/b"

        xpath_artist_left = "/html/body/div[3]/div/div[2]/div/div[1]/ul/li["
        xpath_artist_right = "]/div[2]/div[1]/div/span/span/span"
        
        xpath_artist_text_left = ""
        xpath_artist_text_right = ""

        xpath_playtimes_left = "/html/body/div[3]/div/div[2]/div/div[1]/ul/li["
        xpath_playtimes_right = "]/div[3]/span"

        # 用列表类型来整合toplist, each_info存放每一个的表单
        toplist = list()
        each_info = dict()

        # 时间戳处理:
        year = datetime.datetime.now().strftime("%Y")
        month = datetime.datetime.now().strftime("%m")
        day = datetime.datetime.now().strftime("%d")
        hour = datetime.datetime.now().strftime("%H")
        now_time = month + u"/" + day + u":" + hour + u"h"

        for i in range(1, 101):  
            # 歌曲名处理:
            try: # 排行榜内部不到一百首歌, 退出
                xpath_musicname = xpath_musicname_left + str(i) + xpath_musicname_right
                each_info["name"] = (browser.find_element_by_xpath(xpath_musicname))
                # 正则表达式,去掉歌名中的逗号, 并将"name"处理为str类型!:
                each_info["name"] = re.sub(",", "", each_info["name"].text)
            except:
                break
            # 歌唱家处理:
            try: # 第一个try 用来判断是否为100首歌, 若通过, 说明有歌, 这个try 用来判断歌手是链接形式还是文本形式给出!
                xpath_artist = xpath_artist_left + str(i) + xpath_artist_right
                each_info["artist"] = (browser.find_element_by_xpath(xpath_artist).get_attribute("title"))
            except:
                break
            # 播放次数(百分比)处理: (由于未登录, 所以采用默认排名末位的歌曲仅播放一次, 并通过第一与末尾元素的比例来判定播放次数)
            xpath_playtimes = xpath_playtimes_left + str(i) + xpath_playtimes_right
            each_info["playtime_percent"] = (browser.find_element_by_xpath(xpath_playtimes).get_attribute("style"))
            
            # 正则表达式将非数字的字符全部转化为空字符, 完成数字的提取:
            each_info["playtime_percent"] = int(re.sub("\D", "", each_info["playtime_percent"]))
            
            # 排行榜序列索引
            each_info["index"] = i
              
            # 处理时间戳  
            each_info["time"] = now_time
            
            # 加入处理后的数据到列表toplist中:
            toplist.append(each_info)
            each_info = dict()
            
            
        # 播放次数(真实数据)处理: (由于未登录, 所以采用默认排名末位的歌曲仅播放一次, 并通过第一与末尾元素的比例来判定播放次数)
        # 处理首个和末尾单元的播放次数:
        toplist[-1]["playtimes"] = 1

        if toplist[-1]["playtime_percent"] == 0: # 如果网页上最后一名的长度为0, 则第一位的播放量应当大于100, 选取为101(预测)
            toplist[0]["playtimes"] = 101
        else:
            toplist[0]["playtimes"] = (toplist[0]["playtime_percent"] / toplist[-1]["playtime_percent"]) * toplist[-1]["playtimes"]

        # 处理所有其他的次数:
        for i in toplist:
            if i["playtime_percent"] == toplist[-1]["playtime_percent"]:
                i["playtimes"] = 1
            else:
                i["playtimes"] = int((i["playtime_percent"] / 100.0) * toplist[0]["playtimes"])

        # 注: 以上计算方法仅仅是预测, 所以会有一定的误差!    

        # 输出到文件中:
        # 打开文件追加a模式
        filename = user_id + ".txt"
        fileout = open(filename, "a+", encoding='utf-8')
        #写入数据:
        for i in toplist:
            fileout.writelines(i["name"] + "," + i["artist"] + "," + str(i["playtimes"]) + "," + i["time"] + "\n")    
            
    finally:
        # 关闭文件和窗口:
        fileout.close()
        browser.close()


#-----------------------------------------  程序开始处 -------------------------------------------#

# 设置Chrome请求头(无头模式):
options = webdriver.FirefoxOptions()
options.add_argument('-headless')

options.add_argument('lang=zh_CN.UTF-8') # 设置中文
options.add_argument('disable-infobars') #隐藏"Chrome正在受到自动软件的控制"
options.add_argument('--disable-gpu') #谷歌文档提到需要加上这个属性来规避bug
    # 更换头部
user_agent = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) " +
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"
    )
options.add_argument('user-agent=%s'%user_agent)

usr_id = []
for each_id in usr_id:
    scrap_music(str(each_id))
# 退出程序:
#    sys.exit(0)


    注:

        需要注意的是, 在处理并计算网易云音乐播放次数的时候, 如果没有登陆, 则播放的次数是不会被显示的!!!

       而, 可以观察到: 其实每一个播放的歌曲后面都有一定的阴影面积, 通过计算, 可以发现, 这些都是和第一位播放量的比例.

        所以不妨认为播放排行榜中的最后一首歌的播放次数为1次, 从而推测出(注意: 这里也仅仅是推测!!!)第一位播放量;

        所以对于想要精确播放量的朋友, 现在可以关闭这个网页了, 因为精确的数据是本爬虫算法无法获取的.(笑)

        但是经过本人测试, 对于一般的用户而言, 基本上相差很小!!(至少对于我数据可视化来说, 这些数据造成的误差可以忽略了!)

       

        大概就是这样吧, 本人也是最近才开始学习爬虫, 有什么错误的地方也请大家批评指正, 也欢迎大家一起交流!

 

你可能感兴趣的:(Python,Python爬虫)