前言

考虑到硬盘空间不使用是一种极大的浪费现象,为了提高磁盘空间的利用率,想要存储更多学习资料在里面,抖音上有铺天盖地的小姐姐教我们跳舞,是极好的学习资源库。因此,出于学习目的,就想要把她们的学习资料保存到我的本地空间,免得哪一天她们被封了我想看也看不到学也学不到了。

工欲善其事 必先利其器

考虑到 GayHub 上应该有很多和我一样的好学人士,已经上传了他们下载学习资料的工具。经过一番检索和尝试,都以失败告终,原因应该是多样的,推测是他们的项目没跟上抖音网站的更新。

迫于无奈,只能尝试自己写一个。奈何自己前端知识极其匮乏,无法写出爬虫程序。

想起两年前用 RPA-Python 写过一个自动登陆校园网 的程序,这个库简单实用,直接操作浏览器,不需要懂什么逆向等乱七八糟的,会点击下载就行,于是想继续使用这个库。但这个库在 Linux 环境下需要特殊安装,所以还是算了吧。

image-20230716015057231

因为,实际上在控制浏览器这个需求里,更著名的库是 selenium

Ubuntu22 环境下安装 selenium

安装 selenium 在不同的操作系统下应该都是差不多的。但安装过程中我也才过一些坑,这里记录一下成功安装的过程。

  1. 安装 selenium

    pip install selenium
  2. 安装 chrome 浏览器

    我这里是图形界面环境,直接去官网下载 deb 包安装即可。

  3. 下载 chromedriver

    下载 chromedriver 之前首先检查 chrome 版本。可以在浏览器关于界面中进行查看,也可以在终端执行 google-chrome --version 来查看。

    输出如下:

    wgx@wgx-Lenovo:~$ google-chrome --version
    Google Chrome 114.0.5735.198

    接下来打开下载 chromedriver 的网址:https://chromedriver.chromium.org/downloads

    选择与浏览器对应的版本进行下载。这里我的浏览器版本太新了,没有能够完全对应的 chromedriver ,于是我下载了 114.0.5735.90 版本,只有最后一位小版本号对不上,应该不会有问题,事实证明确实如此。

    image-20230716020235327

    下载完成后,将 chromedriver 复制到 /usr/loacl/bin/ 目录下。

    执行下面代码进行测试,如果能够成功启动浏览器表示安装成功。

    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    
    options = Options()
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--no-sandbox")
    
    driver = webdriver.Chrome(options=options)
    input()
    driver.quit()

在浏览器上下载抖音视频

抖音网站上没有提供下载地址,需要 F12 在 video 标签中找到视频的地址。

以周姐的视频为例:

image-20230716141150877

至于这 3 个视频地址的区别,我还没发现。任意一个都可以下载视频。

我以为是存在清晰度的区别,但我下载了这三个地址的视频,大小都是一样的。

学会了手动下载,接下来就可以使用 selenium 控制浏览器自动下载了。

selenium 的基本操作

启动浏览器并打开一个网页

from selenium.webdriver.chrome.options import Options
from selenium import webdriver
options = Options()
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--no-sandbox")

driver = webdriver.Chrome(options=options)
driver.get("https://www.douyin.com")

获取页面中的元素

获取页面中的元素主要通过 find_element 或者 find_elements 方法,前者返回元素对象,后者返回元素对象的列表。

查找元素需要使用 By 类中提供的定位策略,以下是一些常用的 By 类方法及其解释:

  1. By.ID:通过元素的唯一标识符 id 来定位元素。

  2. By.NAME:通过元素的 name 属性来定位元素。

  3. By.XPATH:通过元素的 XPath 表达式来定位元素。XPath 是一种用于在 XML 或 HTML 中导航和定位元素的语言。

  4. By.CSS_SELECTOR:通过元素的 CSS 选择器来定位元素。CSS 选择器是一种通过样式规则匹配元素的方法。

  5. By.CLASS_NAME:通过元素的 class 属性来定位元素。

  6. By.TAG_NAME:通过元素的标签名称来定位元素。

  7. By.LINK_TEXT:通过元素的完整文本内容来定位超链接元素 <a>

  8. By.PARTIAL_LINK_TEXT:通过元素的部分文本内容来定位超链接元素 <a>

    下面是基本的示例代码:

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://www.example.com")

# 使用 By.ID 定位元素
element_by_id = driver.find_element(By.ID, "element_id")

# 使用 By.XPATH 定位元素
element_by_xpath = driver.find_element(By.XPATH, "//div[@class='example']")

# 使用 By.NAME 定位元素
element_by_name = driver.find_element(By.NAME, "element_name")

# 使用 By.CSS_SELECTOR 定位元素
element_by_css = driver.find_element(By.CSS_SELECTOR, ".example-class")

# 使用 By.CLASS_NAME 定位元素
element_by_class = driver.find_element(By.CLASS_NAME, "example-class")

# 使用 By.TAG_NAME 定位元素
element_by_tag = driver.find_element(By.TAG_NAME, "div")

# 使用 By.LINK_TEXT 定位超链接元素
element_by_link_text = driver.find_element(By.LINK_TEXT, "Click here")

# 使用 By.PARTIAL_LINK_TEXT 定位超链接元素
element_by_partial_link_text = driver.find_element(By.PARTIAL_LINK_TEXT, "here")

在网页中按键盘

在网页中按键盘依赖于 Keys 类,常见的按键如下:

  1. Keys.ENTER:模拟 Enter 键。
  2. Keys.RETURN:与 Keys.ENTER 相同,模拟 Enter 键。
  3. Keys.TAB:模拟 Tab 键。
  4. Keys.ESCAPE:模拟 Escape 键。
  5. Keys.SPACE:模拟空格键。
  6. Keys.BACK_SPACE:模拟退格键。
  7. Keys.DELETE:模拟删除键。
  8. Keys.UP:模拟方向键上。
  9. Keys.DOWN:模拟方向键下。
  10. Keys.LEFT:模拟方向键左。
  11. Keys.RIGHT:模拟方向键右。
  12. Keys.HOME:模拟 Home 键。
  13. Keys.END:模拟 End 键。
  14. Keys.PAGE_UP:模拟 Page Up 键。
  15. Keys.PAGE_DOWN:模拟 Page Down 键。
  16. Keys.CONTROL:模拟 Control 键。
  17. Keys.SHIFT:模拟 Shift 键。
  18. Keys.ALT:模拟 Alt 键。

Keys类的使用方法依赖于 send_keys() 方法,该方法用于将指定按键输入到特定的网页元素上。例如:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://www.example.com")

# 使用 By.ID 定位元素
element_by_id = driver.find_element(By.ID, "element_id")

# 按下 Enter 键
element_by_id.send_keys(Keys.ENTER)

鼠标操作

在 Selenium 中,要实现鼠标点击一个元素,你需要使用 ActionChains 类。ActionChains 类提供了一组用于模拟鼠标操作的方法,包括点击、双击、拖放等。

以下是一个示例,展示如何使用 ActionChains 类来实现鼠标点击一个元素:

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://www.example.com")

# 找到需要点击的元素
element_to_click = driver.find_element(By.ID,"element_id")

# 创建 ActionChains 对象
actions = ActionChains(driver)

# 执行鼠标点击操作
actions.click(element_to_click).perform()

获取元素的属性

以上这些内容可以完成基本的浏览器操作了,目前距离实现我的学习目标还差最后一步,就是如何获得标签的属性,也即获取 source 标签下的 src 属性。

实际上只需简单一个方法 get_attribute()

例如:

source.get_attribute("src")

完整代码

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
import requests
import os
import time 

options = Options()
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--no-sandbox")

driver = webdriver.Chrome(options=options)


def download(config):
    user_count = 1
    login = False

    for user in config['user_list']:
        # 打开主页
        driver.get("https://www.douyin.com/user/"+user)
        driver.implicitly_wait(10)
        time.sleep(5)
        if not login:
            input("登陆后请关闭提示框,然后按回车键:")
            login = True

        ## 创建存储目录
        dir_path = f"{config['save_to']}/{user}"
        if not os.path.exists(dir_path):
            os.mkdir(dir_path)

        # 点击第一个视频
        first = driver.find_element(By.XPATH,'/html/body/div[2]/div[1]/div[2]/div[3]/div/div/div[3]/div[2]/div/div[2]/ul/li[1]/div/a/div')
        first.click()
        driver.implicitly_wait(5)
        time.sleep(2)

        # 跳过向导
        if user_count == 1:
            next_btn = driver.find_element(By.TAG_NAME,"body")
            next_btn.send_keys(Keys.DOWN)

        # 打开列表
        xpath = '//*[@id="sliderVideo"]/div[1]/div[2]/button'
        open_list = driver.find_element(By.XPATH,xpath)
        open_list.click()

        if user_count == 1:
            input("即将开始正式下载,手动选择清晰度后,按回车键:")

        video_name = 0 
        last_src = ""
        while True:
            driver.implicitly_wait(2)
            ul = driver.find_element(By.XPATH,'//*[@id="relatedVideoCard"]/div/div[3]/div/div/div[2]/div/div[1]/div/ul')
            lis = ul.find_elements(By.TAG_NAME,'li')
            li_size = len(lis)
            if(video_name == li_size):
                ul = driver.find_element(By.XPATH,'//*[@id="relatedVideoCard"]/div/div[3]/div/div/div[2]/div/div[1]/div/ul')
                lis = ul.find_elements(By.TAG_NAME,'li')
                li_size = len(lis)
                if(video_name == li_size):
                    print(f"第{user_count}个用户下载完毕,共下载{li_size}个视频")
                    break
            lis[video_name].click()
            video = driver.find_element(By.XPATH,'//*[@id="slideMode"]/div[1]/div[1]/div/xg-video-container/video')
            try:
                source = video.find_element(By.TAG_NAME,"source")
                if source == last_src:
                    driver.refresh()
                    driver.implicitly_wait(5)
                    xpath = '//*[@id="sliderVideo"]/div[1]/div[2]/button'
                    open_list = driver.find_element(By.XPATH,xpath)
                    open_list.click()
                    continue
                else:
                    last_src = source
            except Exception as e:
                print(e)
                driver.refresh()
                driver.implicitly_wait(5)
                xpath = '//*[@id="sliderVideo"]/div[1]/div[2]/button'
                open_list = driver.find_element(By.XPATH,xpath)
                open_list.click()
                continue
            response = requests.get(source.get_attribute("src"))
            if response.status_code == 200:
                content = response.content
                with open(f"{dir_path}/{video_name+1}.mp4", "wb") as f:
                    f.write(content)
                    video_name += 1
                    print(f"第{video_name}个下载完成!")
            else:
                print(f"第{video_name}个下载失败!")
        user_count += 1

if __name__ == '__main__':

    config = {
        'save_to': ".",
        'user_list': [
            'MS4wLjABAAAAYIN1c9oIG_oCZuDe2rYbRdfXs86P9hMhoFW4pMq3FZU',
            'MS4wLjABAAAAbgCnupO_NGaTAmzWnXSivCeHWrOe0wC2ZcpNvVoQfEk'
        ]
    }

    download(config)

    # 关闭浏览器
    driver.quit()

使用方法

文章开头已经用视频演示了使用方法,这里补充图文说明。

首先,代码中的 config 字典两个字段分别表示保存路径和用户列表,用户列表中填写的内容是下载用户抖音主页的链接。

image-20230716150204959

修改后,运行代码。

启动浏览器后,首先遇到验证码中间页,这里手动验证。

image-20230716150525763

然后进入登录页,这里还是手动扫码登录。

image-20230716150618465

登录后,有时会遇到下面的提示框,这个框需要手动关闭。

image-20230716150829940

这里其实可以做成自动的,但是我调试的时候不是每次都出,所以就没有编写在代码里。这里的另一种解决方案是干脆睡眠5秒钟。

接下来回到终端,输入回车键,让程序继续执行。

image-20230716150940948

接下来手动选择清晰度后,继续在终端回车。

image-20230716151330353

OK!学习资料已经源源不断地流入本地磁盘了😋。