V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
denonw
V2EX  ›  Python

Python 脚本分享 - 同步网易云音乐歌单到 qq 音乐

  •  
  •   denonw · 2017-06-19 13:24:17 +08:00 · 3974 次点击
    这是一个创建于 2722 天前的主题,其中的信息可能已经有所发展或是发生改变。

    上周做了个同步歌单的脚本(/t/369133), 在这里分享下写脚本的一些经验以及遇到的一些坑。 项目地址: https://github.com/Denon/syncPlaylist

    爬取流程

    整个爬虫的流程大致如下

    1. 爬取网易云音乐的歌单列表
    2. 登录 qq 音乐
    3. 搜索步骤 1 中找到的歌曲
    4. 添加到 qq 歌单中

    爬取网易云音乐的歌单列表

    这个步骤比较简单, 正常访问该歌单的手机端页面爬取就好, 观察页面可以发现所有歌曲都在<span class="detail">...</span> 里面. 那么直接用 requests + beautifulsoup 获取下就好.

    登录 qq 音乐

    其实一开始的思路是想通过发送模拟请求来登录的. 但看来看去没有找到怎么做(有人知道的话谢谢分享下). 考虑到后面比较复杂的操作, 最后就直接用 selenium 来做了. 用 selenium 来做就比较简单了. 唯一要注意的是, 登录 qq 的那个弹出框是在一个 iframe 里面.

    def login_qq():
        # 切换 iframe
        browser.switch_to.frame("frame_tips")
        wait.until(lambda browse: browser.find_element_by_id("switcher_plogin"))
        sleep(0.5)
        browser.find_element_by_id("switcher_plogin").click()
        user_input = browser.find_element_by_id("u")
        user_input.send_keys("account")
        pwd_input = browser.find_element_by_id("p")
        pwd_input.send_keys("password")
        submit = browser.find_element_by_id("login_button")
        submit.click()
        # 登录成功以后要切换回来
        browser.switch_to.default_content()
    

    搜索歌曲以及添加到歌单中

    这里找到 qq 音乐的搜索 url 然后把关键字填入就好. 搜索到歌曲以后, 我这里比较偷懒, 只选择把第一个搜索到的结果添加进去. 添加的操作实际上分为三步:

    1. 鼠标移到歌曲上
    2. 点击 '+' 图标
    3. 点击歌单 这里我测试了好几种方法. 最后发现还是直接使用 javascript 来操作成功率比较高. 另外一方面, 可以直接在浏览器 console 里面直接测试 js 操作, 测试比较方便
    def add_song():
        # 点击出歌单
        browser.execute_script("document.getElementsByClassName('songlist__list')[0].firstElementChild.getElementsByClassName('list_menu__add')[0].click()")
        sleep(0.5)
        # 通过 data-dirid 来选择歌单
        browser.find_element_by_css_selector("a[data-dirid='{}']".format(playlist_id)).click()
        return
    

    打包 exe 执行文件

    选择使用 py2exe 来打包. 这里有个坑就是由于我们用到了 selenium, selenium 里面的某些函数依赖了两个 js 文件, 需要把这两个 js 文件添加到打包的脚本里面

    from distutils.core import setup
    import py2exe
    from glob import glob
    
    setup(
        console=["run.py"],
        data_files=[
            (r'.', glob(r'D:\myproject\syncPlaylist\config.json')),
            (r'.', glob(r'D:\ProgramData\Anaconda3\envs\python27\Lib\site-packages\selenium\webdriver\remote\getAttribute.js')),
            (r'.', glob(r'D:\ProgramData\Anaconda3\envs\python27\Lib\site-packages\selenium\webdriver\remote\isDisplayed.js'))
        ]
    )
    

    关于重试

    在执行脚本过程中发现, 偶尔会出现点击登录以后 qq 登录还是没成功的情况, 以及添加歌曲时, 脚本偶尔会出错. 这里为了不中断整个脚本执行, 有必要加上重试这个操作, 因此写了一个重试的装饰器

    def retry(retry_times=0, exc_class=Exception, notice_message=None):
        """retry_times: 重试次数
        exc_class: 捕捉的异常 class
        notice_message: 发生异常时候输出的错误信息, 为 None 时则不输出
        """
        def wrapper(f):
            @functools.wraps(f)
            def inner_wrapper(*args, **kwargs):
                current = 0
                while True:
                    try:
                        return f(*args, **kwargs)
                    except exc_class as e:
                        if current >= retry_times:
                            raise RetryException()
                        if notice_message:
                            print notice_message
                        current += 1
            return inner_wrapper
        return wrapper
    

    总结以及剩余的问题

    说实话, 本来以为写这个脚本难度不是很大. 但前前后后差不多花了两三天的时间 T_T. 问题在于之前爬虫这方面不是很熟悉以及项目结构在一开始比较混乱(其实就是懒= =). 平时也比较少写这种技术分享的 blog, 有什么问题大家多多指教, 乐意接受批评.

    第 1 条附言  ·  2017-06-20 00:35:32 +08:00
    ... 看来大家都想从 qq 同步到网易去= =
    我这几天搞一个反向的吧
    16 条回复    2017-06-20 15:24:06 +08:00
    bearqq
        1
    bearqq  
       2017-06-19 14:22:59 +08:00
    for _ in range(0,retry_times):
    try:
    dosomething()
    break#执行成功,跳出 for
    exception:
    continue
    else:#for 执行完毕未跳出,即错误次数超出
    raise RetryException()
    return
    return "success"

    我一般这么 retry
    -。-
    denonw
        2
    denonw  
    OP
       2017-06-19 14:27:02 +08:00
    @bearqq 恩,这也是一种思路。 我用了 while, 你用的 for.
    bearqq
        3
    bearqq  
       2017-06-19 14:40:03 +08:00
    @denonw 格式果然崩了。。。
    之前没想过用 wrapper 来 retry,学习啦
    wq2016
        4
    wq2016  
       2017-06-19 16:21:48 +08:00
    有趣~
    crashX
        5
    crashX  
       2017-06-19 17:33:50 +08:00
    感觉功能做反了。
    natforum
        6
    natforum  
       2017-06-19 17:35:09 +08:00
    很强势
    HypoChen
        7
    HypoChen  
       2017-06-19 17:40:07 +08:00
    感觉功能做反了+1
    denonw
        8
    denonw  
    OP
       2017-06-19 17:43:30 +08:00
    @HypoChen
    @crashX
    后面会补上这个反向功能~
    willhunger
        9
    willhunger  
       2017-06-19 17:44:00 +08:00 via iPhone
    然而 QQ 音乐 web 端有添加网易云和虾米音乐的歌单的功能
    infun
        10
    infun  
       2017-06-19 17:46:15 +08:00
    QQ 音乐 那么难用。。。
    AsherG
        11
    AsherG  
       2017-06-19 17:49:36 +08:00 via iPhone
    感觉挺不错,关注一个
    denonw
        12
    denonw  
    OP
       2017-06-19 17:55:18 +08:00
    @willhunger 是的。。但是那个歌单我怎么都复制不出来= =. 所以只能自己搞一个了
    mingyun
        13
    mingyun  
       2017-06-19 23:52:35 +08:00
    感觉功能做反了 +1
    weaming
        14
    weaming  
       2017-06-20 00:12:53 +08:00
    感觉功能做反了 +1
    furch
        15
    furch  
       2017-06-20 10:59:33 +08:00
    感觉功能做反了 +1
    Antidictator
        16
    Antidictator  
       2017-06-20 15:24:06 +08:00
    网易云音乐的还没有 QQ 音乐的那么全。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1226 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 18:00 · PVG 02:00 · LAX 10:00 · JFK 13:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.