Python3爬取新版喜马拉雅音频,解决JS反爬

前言

应该也有一年了吧,之前也在简书CSDN上写过爬取喜马拉雅音频的文章,经历了一次喜马拉雅的改版,同时也更新了一波代码

最近为了喜欢的雪中,回去重新打算跑一下代码下载音频,这一跑不要紧,结果就发现喜马拉雅又改版了

得,又得重新写代码,且这次还加了JS反扒的手段,让我也好好学习了一把,嗯,下面进入正题

分析

初步分析

老样子,首先来看看我们要爬取的目标https://www.ximalaya.com/youshengshu/2684034/

image
image

像这样的882个音频,共计30页,每页一般标准的有30个,最后要将这882个音频保存到本地,那么我们最需要的是找到音频的源播放地址,我们不妨打开一个音频来看看,同时按F12打开开发者工具

首先我看了看https://www.ximalaya.com/youshengshu/2684034/2725352的网页源代码中,并没有相关的播放地址,所以我开始在开发者工具中找

image

页面刷新完之后的XHR中我没有找到明显的播放地址,然后我点了一下页面的播放按钮,之后XHR又跳出来好几条信息,随后我找到了

image

可以看到,src对应的m4a链接就是音频的源播放地址,我们只要拿到这个链接就行了

image

那么我们接下来就应该要访问上面这个链接,从而拿到音频的播放地址,但是当我们复制链接后去打开时会发现

image

[SIGN] no sign or wrong sign,是的,你很大几率会看到这个,没有sign或错误的sign,那么也就是说这个链接是打不开的?那么带上请求头试试?后来我用postman访问这个链接,带上请求头后还是没有得到结果,然后想了想,返回给我们的提示是sign

正好请求头中就有这个xm-sign,于是我重新试了一下,只带上这个xm-sign去访问,发现在一次尝试中拿到了之前看到的带有播放链接的response

xm-sign: dcf3736db17584cb0b7260c1fcb1f05f(45)1569231095030(64)1569231094006

那么下一步要解决的就是如何获得这个xm-sign

进阶分析

xm-sign并不在XHR中能够找到,所以我下一步的目标是在JS文件中找

image

找啊找,终于在上面的js文件中,找到了点头绪,出现了同样的xm-sign,这个过程对我来说是比较漫长的,因为需要在js文件中一个个的浏览过去,当然你通过fiddler抓包去找也是差不多的

然后为了明确知道xm-sign是怎么来的,我们就需要对这个js文件进行打断点调试,在控制台的Source中打开该Js文件

image

右键该JS文件,点击Open in Sources panel,或者鼠标轻放在该文件上,会出现该文件的路径,到控制台Source中找到它

image

找到该文件后点击打开,然后点击下方的花括号按钮,美化代码,再按ctrl + f搜索xm-sign,就可以定位到xm-sign

image

我们需要知道对应xm-sign的值t到底是怎么样的,接着在return e上打上断点,这样一来,当页面刷新运行到这里的时候会自动停止之后的JS代码,可以让我们来进行调试,如何通过Chrome调试可以参考这篇文章

打好断点后,刷新页面,等待一会,不用动别的,然后就可以看到下图

image

图中所示的xm-sign的值就是t的值,再往上一点,t的值就是由o.default产生的

n.interceptors.request.use(function(e) {
        if (e.url.indexOf("/revision") > -1) {
            var t = (0,
            o.default)();  //  然后点击 o.default 进入该方法中 
            e.headers = function(e) {
                for (var t = 1; t < arguments.length; t++) {
                    var r = null != arguments[t] ? arguments[t] : {};
                    t % 2 ? i(r, !0).forEach(function(t) {
                        u(e, t, r[t])
                    }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) : i(r).forEach(function(t) {
                        Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t))
                    })
                }
                return e
            }({}, e.headers, {
                "xm-sign": t
            })
        }
        return e
    });

接着我们点击o.default这个方法,进去看看这一串值到底如何生成,鼠标放在o.default上,点击出现的anonymous方法

image

我们就自动跳转到了下一个JS文件,同样格式化后我们就看到了下图所示的function部分

image
function() {
    return function(t) {
        var e = Date.now();  // 当前时间戳
        return ("{ximalaya-" + t + "}(" + Le(100) + ")" + t + "(" + Le(100) + ")" + e).replace(/{([\w-]+)}/, function(t, e) {
            return Ue(e)
        })
    }(Ne() ? Date.now() : window.XM_SERVER_CLOCK || 0)
}

这个方法最终返回的就是我们的xm-sign的值,而return部分就是

("{ximalaya-" + t + "}(" + Le(100) + ")" + t + "(" + Le(100) + ")" + e).replace(/{([\w-]+)}/, function(t, e) {return Ue(e)}

取消之前的断点,我们可以在下图中再打断点,再刷新页面,等待,调试

image

可以看到

image

这里面t就是时间戳,这个我们可以在前面的找到对应的服务器时间戳,请求这个即可https://www.ximalaya.com/revision/time

Le(100)是上面的一个函数,能生产100以内的随机数,如下,e就是当前的时间戳

function Le(t) {
    return ~~(Math.random() * t)  
    // ~~ 代表双非按位取反运算符,对于正数,它向下取整;对于负数,向上取整;非数字取值为0
}

replace(/{([\w-]+)}/, function(t, e) {return Ue(e)},replace就是把{ximalaya-" + t + "}部分替换成Ue(e)的值

我们接着看Ue(),同样鼠标放在Ue上,点击出现的anonymous(t, n)方法

image
 t.exports = function(t, n) {
                if (null == t)
                    throw new Error("Illegal argument " + t);
                var r = e.wordsToBytes(i(t, n));
                // 以下就是返回的值
                return n && n.asBytes ? r : n && n.asString ? o.bytesToString(r) : e.bytesToHex(r)
            }

我们在这里也可以打个断点看看返回的是什么,并且右键编辑断点时要打印的数据,我在这里就设置在控制台打印当前返回的内容

image
image
image

然后这个箭头就会变橙色,我们取消其他的断点,再次刷新页面,等待一会,调试

image

可以看到t = "ximalaya-1569237828683", n = undefined,并且控制台打印有15de4b221c3cb112d4d8200ccf094a8e这样的一串字符

image

根据经验猜测这可能是md5码,于是我们尝试使用在线转换工具,将ximalaya-1569237828683转换为MD5编码格式的内容,结果如下

image

结果正确,现在我们可以得出结论,参数xm-sign的值其实就是

<span id="inline-purple">MD5(ximalaya-服务器时间戳) + (100以内的随机数) + 服务器时间戳 + (100以内的随机数) + 现在的时间戳</span>

代码

解决了上面的JS反爬问题,我们来看看实际代码,这是主要的爬取起始部分

# 传入专辑的ID,xm_fm_id
def get_fm(self, xm_fm_id):
    # 根据有声书ID构造url
    fm_url = self.base_url + '/youshengshu/{}'.format(xm_fm_id)
    print(fm_url)
    r_fm_url = self.s.get(fm_url, headers=self.header)
    # 获取书名
    fm_title = re.findall('<h1 class="title _leU">(.*?)</h1>', r_fm_url.text, re.S)[0]
    print('书名:' + fm_title)
    # 新建有声书ID的文件夹
    fm_path = self.make_dir(xm_fm_id)
    # 取最大页数
    max_page = re.findall(r'<input type="number" placeholder="请输入页码" step="1" min="1" '
                          r'max="(\d+)" class="control-input _bfuk" value=""/>', r_fm_url.text, re.S)
    if max_page and max_page[0]:
        for page in range(1, int(max_page[0]) + 1):
            print('第' + str(page) + '页')
            # 获取当前时间对应的 xm-sign 添加到请求头中
            self.get_sign()
            # 访问链接
            r = self.s.get(self.base_api.format(xm_fm_id, page), headers=self.header)
            # print(json.loads(r.text))
            r_json = json.loads(r.text)
            for audio in r_json['data']['tracksAudioPlay']:
                # 获取json中的每个音频的标题以及播放源地址
                audio_title = str(audio['trackName']).replace(' ', '')
                audio_src = audio['src']
                # 交给下载的方法
                self.get_detail(audio_title, audio_src, fm_path)
            # 每爬取1页,30个音频,休眠3秒
            time.sleep(3)
    else:
        print(os.error)

这是构造xm-sign的方法,用到了Python的hashlib

def __init__(self):
    self.base_url = 'https://www.ximalaya.com'
    self.base_api = 'https://www.ximalaya.com/revision/play/album?albumId={}&pageNum={}&sort=0&pageSize=30'
    self.time_api = 'https://www.ximalaya.com/revision/time'
    self.header = {
        'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0'
    }
    self.s = requests.session()

def get_time(self):
    """
    获取服务器时间戳
    :return:
    """
    r = self.s.get(self.time_api, headers=self.header)
    return r.text

def get_sign(self):
    """
    获取sign: md5(ximalaya-服务器时间戳)(100以内随机数)服务器时间戳(100以内随机数)现在时间戳
    :return: xm_sign
    """
    nowtime = str(round(time.time() * 1000))
    # 得到服务器时间戳
    servertime = self.get_time()
    # 构造 xm-sign
    sign = str(hashlib.md5("ximalaya-{}".format(servertime).encode()).hexdigest()) + "({})".format(
        str(round(random.random() * 100))) + servertime + "({})".format(str(round(random.random() * 100))) + nowtime
    # 添加到请求头
    self.header["xm-sign"] = sign
    # print(sign)
    # return sign

这是保存音频的部分

def get_detail(self, title, src, path):
    # 请求源地址的链接,得到response
    r_audio_src = self.s.get(src, headers=self.header)
    # 构造保存路径
    m4a_path = path + title + '.m4a'
    if not os.path.exists(m4a_path):
        with open(m4a_path, 'wb') as f:
            # 写入
            f.write(r_audio_src.content)
            print(title + '保存完毕...')
    else:
        print(title + 'm4a已存在')

成果

image

后续

当然这只是喜马拉雅非付费的音频专辑,如果是付费后的专辑则需要另一套更加复杂的JS破解方法,我搞了半天,先拿手机抓包试了试,到最后发现还是得破解如何构造最后的下载地址

网上也有那种软件,这个就不多说了

需要代码的可以去我的GitHub,传送门

参考

python模拟喜马拉雅js,全过程突破xm-sign,轻松爬取音频数据
python爬取喜马拉雅音频,突破xm-sign校验反爬(爬虫)
爬虫之突破xm-sign校验反爬
喜马拉雅音频下载工具
Chrome 开发者工具代码行断点调试
js中~~和 | 的妙用

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

推荐阅读更多精彩内容

  • 前前言 喜马拉雅已经更换标签,我重新更新了下代码,思路还是如此,需要的可以扫一下文末公众号二维码(本人会在上面发表...
    不存在的一角阅读 1,688评论 0 2
  • 上一篇我们重点介绍了如何把爬取到的图片下载下来。没错,如果你还记得的话,我们使用的是urlretrieve这个Py...
    joyousluoo阅读 8,687评论 0 3
  • 在我的家乡,米粉有两种,一种叫榨粉,或叫圆粉(桂林米粉),另一种叫切粉(有点像广东肠粉切条)。 我喜欢吃切粉,理由...
    玩子世家阅读 2,380评论 2 2
  • 有时候播放rtmp流时会出现莫名其妙的播放不出来的情况,这时候就需要对报文进行分析,wireshark无疑是不错的...
    FlyingPenguin阅读 3,810评论 0 2
  • 因为有光,才有阴影。光越强大,阴影越深。人性的光和阴影,在这个复杂世界里,淋漓尽致的发挥着。我们成不了别人世界里的...
    王小垂阅读 176评论 0 0