小萌新初探M站,笨方法实现自动签
这是其实是一篇严谨的技术文
多年以后,面对着蒸蒸日上的M站,早已无人问津的老妮娜会回想起自己第一次在M站发文(新人报到)的那个下午......
不能自动签到吗。。。
真的不能吗?!(你在燃什么)
以下为正文部分
—————————————————————分割线—————————————————————
显然,要实现自动签到,我们有两种方案:
1. 模拟客户端向服务器签到接口发送HTTP请求。
2. 请一个人每天帮我签到
现在需要的时对比方案可行性:
对于方案一,网上有大量开源的实现方案,基本都是先用抓包工具(如Fiddler)窃听客户端签到时发的HTTP请求;接着分析它的数据结构,比如发现是靠token和日期字段验证;最后找个开源脚本(比如Python+requests库),稍微改改后部署到服务器上定时跑,从此服务器就成了你的全自动签到替身。这种方法需较复杂的分析过程,且投入较大(其实是我不会)所以,我决定采用方案二
(那你为什么要给方案一加粗啊喂)
问:M站签到分几步?(参考把大象放进冰箱)
第一步:打开签到页(喵御宅每日签到)
第二步:点击”立即签到“
第三步:关闭签到页
这么简单的任务,咱用得着雇人?
咱 !没!钱!
那么,对于一个没有编程基础的人来说,有没有什么编程语言,可以又全能(api多),又方便(无需复杂运行环境),还容易上手(脚本化)地实现自动签到呢?有的,兄弟,有的,那就是:
PowerShell
(噔噔咚)
作为微软亲儿子级别的脚本语言,其不仅支持丰富的命令,与Win API交互,还保留了脚本语言小巧简单的特点,是实现自动签到脚本的不二之选
同时,在学习PowerShell的过程中(是的我是第一次学),我又发现了Selenium这个Web自动化测试工具。那么,一个基于PowerShell和Selenium的自动签到脚本,堂堂诞生(感觉好像很厉害的样子)
以下是一些PowerShell基础知识:
1. 在PowerShell中,我们以“$变量名”的形式声明和引用变量
2. 在PowerShell中,我们以“function 函数名 { param([参数类型]参数, ...) [return 返回值/ 函数名 = 返回值](此中括号内的内容可选)}”的格式声明函数
3. 遇到不会的命令,可以在命令行中输入”help 关键词“来获取帮助;关键词可以为命令名或可能在命令(PowerShell命令统一用"动词-名词”的格式)中出现的词;加上“-Full”获取详细内容,加上“-Examples”获取代码示例
有了这些基础后,让我们考虑脚本如何满足刚才的三步:
第一步:打开签到页(喵御宅每日签到)
声明个全局变量先:$Url = "https://www.mfuns.net/member/sign"
导入Selenium模块:Import-Module Selenium -ErrorAction Stop
配置Edge选项:
$edgeOptions = New-Object OpenQA.Selenium.Edge.EdgeOptions
$edgeOptions.AcceptInsecureCertificates = $true
启动浏览器并创建对应web驱动:
$service=[OpenQA.Selenium.Edge.EdgeDriverService]::CreateDefaultService()
$service.HideCommandPromptWindow = $true
$driver = New-Object OpenQA.Selenium.Edge.EdgeDriver($service, $edgeOptions)
$driver.Navigate().GoToUrl($Url)
至此我们已成功打开签到页
第二步:点击”立即签到“
在点击前,我们必须找到“立即签到”按钮,但脚本没法向我们一样看见网页,咋办?
Selenium 提供了 8 种主要的元素定位方式,他们分别是:Id(通过元素的 id 属性定位),Xpath(
通过 XML 路径定位),CssSelector(通过 CSS 选择器定位),Classname(通过元素的 class 属性定位),Name(通过元素的 name 属性定位),LinkText(通过超链接的精确文字定位),PartialLinkText(通过超链接的部分文字定位),TagName(通过HTML标签名定位)
这么多名词,看不懂咋办?没事,咱直接拿例子实操:
$signButton = $null
$ButtonIdentifier = "立即签到"
# 策略1: 通过XPath查找
$signButton = $driver.FindElement([OpenQA.Selenium.By]::XPath("//button[contains(text(), '$ButtonIdentifier')]"))
# 策略2: 通过CSS选择器查找
$signButton = $driver.FindElement([OpenQA.Selenium.By]::CssSelector("button.n-button.n-button--primary-type.n-button--large-type"))
# 策略3: 通过ClassName查找
$signButton = $driver.FindElement([OpenQA.Selenium.By]::ClassName("__button-1cvdmx0-llmp n-button n-button--primary-type n-button--large-type"))
整体模板大致如此,下面讲下这些用于定位的字符串是哪来的:
Edge打开喵御宅每日签到,按F12,我们可以选择想看的控件:
然后双击:
注意到class="__button-1cvdmx0-llmp n-button n-button--primary-type n-button--large-type n-button--disabled"和<span class="n-button__content">立即签到</span>
class对应内容便是ClassName查找法对应的字符串;“立即签到”则是Xpath查找的ButtonIdentifier;我们取“__button-1cvdmx0-llmp n-button n-button--primary-type n-button--large-type n-button--disabled"中看起来不那么随机的,网站更新后不大可能变化的部分用”."连接得到”n-button.n-button--primary-type.n-button--large-type“,再在最前面加上”button."表示控件类型,最终得到向CssSelector输入的"button.n-button.n-button--primary-type.n-button--large-type“
我们可以这样检验:按下Ctrl+f,打开搜索框
然后搜索刚才得到的字符串:
如果可以与刚才的控件唯一对应,则为有效字符串
接下来我们只需$SignButton.Click()模拟点击操作即可实现签到
第三步:关闭签到页
这个简单:
$driver.Quit()
$global:Driver = $null
即可关闭打开的浏览器
所以我们成功了?
等等,还没完!
如果我们直接这么运行就会发现,我们进入签到界面后是未登录状态
查询后发现:
Selenium每次启动时都会创建一个全新的、独立的浏览器实例,使用临时的干净配置文件,不会共享用户日常使用的浏览器中的任何会话、Cookie或缓存数据
原来如此,我们还要模拟登录操作,好在签到页刚好有登录按钮,我们只需要像刚才一样找到按钮,点击,找到输入框,输入文本,点击登录。具体实现文末给出
把安装过程也自动化!
为了方便和我一样的新人使用此脚本,我把下载Selenium及前置环境的功能也内置到脚本中了
不过由于执行权限问题,一切都基于CurrentUser
这带来了一个问题:
这样会重复安装NuGet和Selenium。脚本会在退出前把安装的东西删除吗?如果不会,有没有办法重复利用或在退出前把下载的东西删除?
我不会解决,希望可以得到大佬答复
最后,设置Windows任务计划程序
打开"任务计划程序"
创建基本任务:
名称: "每日自动签到"
触发器: 每天指定时间
操作: "启动程序"
程序或脚本: powershell.exe
添加参数: -ExecutionPolicy -File "C:\路径\到\AutoSign.ps1"
即可实现每天自动启动脚本签到
完整脚本:
# 该脚本自动检测并安装Selenium环境,然后打开Edge浏览器执行签到操作
param(
[string]$Url = "https://www.mfuns.net/member/sign"
)
# 1. 定义全局变量
$global:Driver = $null
$global:WebDriverPath = "C:\Users\$env:USERNAME\assemblies\MicrosoftWebDriver.exe"
$global:WebDriverDir = "C:\Users\$env:USERNAME\assemblies\"
$global:EdgeVersion = $null
$global:LogFile = "C:\Users\$env:USERNAME\log.txt"
$global:Username="你的用户名"
$global:Password="你的密码"
# 2. 日志记录函数
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "$timestamp [$Level] $Message"
Write-Host $logEntry
Add-Content -Path $global:LogFile -Value $logEntry -ErrorAction SilentlyContinue
}
# 3. 环境检测与安装函数
function Test-SeleniumEnvironment {
Write-Log "检测Selenium环境..."
# 检查Selenium模块是否已安装
$seleniumInstalled = Get-Module -ListAvailable -Name Selenium
if (-not $seleniumInstalled) {
Write-Log "Selenium模块未安装" "WARNING"
return $false
}
# 检查WebDriver是否存在
if (-not (Test-Path $global:WebDriverPath)) {
Write-Log "WebDriver未找到: $global:WebDriverPath" "WARNING"
return $false
}
Write-Log "Selenium环境正常" "SUCCESS"
return $true
}
function Install-SeleniumEnvironment {
Write-Log "开始安装Selenium环境..."
try {
# 安装NuGet package provider
Write-Log "安装NuGet package provider..."
$nugetInstalled = Install-PackageProvider -Name "NuGet" -Force -Scope CurrentUser -ErrorAction Stop
if ($nugetInstalled) {
Write-Log "NuGet安装成功" "SUCCESS"
}
# 安装Selenium模块
Write-Log "安装Selenium模块..."
Install-Module -Name Selenium -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop
Write-Log "Selenium模块安装成功" "SUCCESS"
# 设置执行策略
Write-Log "设置执行策略..."
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
Write-Log "执行策略设置成功" "SUCCESS"
# 下载并配置Edge WebDriver
Write-Log "配置Edge WebDriver..."
$global:EdgeVersion = Get-EdgeBrowserVersion
if (-not $global:EdgeVersion) {
throw "无法获取Edge浏览器版本"
}
# 创建目标目录
if (-not (Test-Path $global:WebDriverDir)) {
New-Item -Path $global:WebDriverDir -ItemType Directory -Force | Out-Null
}
# 下载WebDriver
$driverDownloaded = Download-EdgeWebDriver -Version $global:EdgeVersion
if (-not $driverDownloaded) {
throw "WebDriver下载失败"
}
Write-Log "Selenium环境安装完成" "SUCCESS"
return $true
}
catch {
Write-Log "环境安装失败: $($_.Exception.Message)" "ERROR"
return $false
}
}
function Get-EdgeBrowserVersion {
try {
Start-Process -FilePath msedge.exe
Start-Sleep -Seconds 1
$edgeProcesses = Get-Process -Name msedge -ErrorAction SilentlyContinue
if ($edgeProcesses) {
$version = $edgeProcesses[0].FileVersion
Write-Log "通过进程信息检测到Edge浏览器版本: $version" "INFO"
return $version
}
throw "无法自动检测Edge版本,请手动安装WebDriver"
}
catch {
Write-Log "获取浏览器版本失败: $($_.Exception.Message)" "ERROR"
return $null
}
}
function Download-EdgeWebDriver {
param([string]$Version)
try {
$driverUrl = $null
if ([Environment]::Is64BitOperatingSystem)
{
$driverUrl="https://msedgedriver.microsoft.com/$Version/edgedriver_win64.zip"
}
else
{
$driverUrl="https://msedgedriver.microsoft.com/$Version/edgedriver_win32.zip"
}
Write-Log "下载WebDriver: $driverUrl" "INFO"
# 下载ZIP文件
$zipPath = Join-Path $env:TEMP "edgedriver.zip"
Invoke-WebRequest -Uri $driverUrl -OutFile $zipPath -ErrorAction Stop
# 解压ZIP文件到临时目录
$tempExtractPath = Join-Path $env:TEMP "edgedriver_temp"
if (Test-Path $tempExtractPath) {
Remove-Item $tempExtractPath -Recurse -Force
}
New-Item -ItemType Directory -Path $tempExtractPath -Force | Out-Null
Write-Log "解压WebDriver..." "INFO"
Expand-Archive -Path $zipPath -DestinationPath $tempExtractPath -Force
# 查找解压后的msedgedriver.exe并重命名为MicrosoftWebDriver.exe
$sourceDriverPath = Join-Path $tempExtractPath "msedgedriver.exe"
if (-not (Test-Path $sourceDriverPath)) {
# 如果不在根目录,尝试在子目录中查找
$driverFile = Get-ChildItem -Path $tempExtractPath -Recurse -Filter "msedgedriver.exe" | Select-Object -First 1
if ($driverFile) {
$sourceDriverPath = $driverFile.FullName
} else {
throw "解压后的ZIP文件中未找到msedgedriver.exe"
}
}
# 复制并重命名文件到目标位置
Copy-Item -Path $sourceDriverPath -Destination $global:WebDriverPath -Force
Write-Log "WebDriver已复制并重命名为: $global:WebDriverPath" "SUCCESS"
# 清理临时文件
Remove-Item $tempExtractPath -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
return $true
}
catch {
Write-Log "下载WebDriver失败: $($_.Exception.Message)" "ERROR"
return $false
}
}
# 4. 签到功能模块
function Start-Browser {
param([string]$Url)
try {
Write-Log "启动浏览器..." "INFO"
# 导入Selenium模块
Import-Module Selenium -ErrorAction Stop
# 配置Edge选项
$edgeOptions = New-Object OpenQA.Selenium.Edge.EdgeOptions
$edgeOptions.AcceptInsecureCertificates = $true
# 启动浏览器
$service = [OpenQA.Selenium.Edge.EdgeDriverService]::CreateDefaultService($global:WebDriverDir, "MicrosoftWebDriver.exe")
$service.HideCommandPromptWindow = $true
$global:Driver = New-Object OpenQA.Selenium.Edge.EdgeDriver($service, $edgeOptions)
$global:Driver.Navigate().GoToUrl($Url)
Write-Log "浏览器启动成功,已访问: $Url" "SUCCESS"
return $true
}
catch {
Write-Log "浏览器启动失败: $($_.Exception.Message)" "ERROR"
return $false
}
}
function Wait-RandomDelay {
param(
[int]$MinDelay = 0,
[int]$MaxDelay = 300
)
$delay = Get-Random -Minimum $MinDelay -Maximum $MaxDelay
Write-Log "随机等待 $delay 秒..." "INFO"
Start-Sleep -Seconds $delay
}
function Find-UsernameInput {
param([string]$InputIdentifier = "请输入用户名/手机号/邮箱")
try {
Write-Log "查找账号输入框..." "INFO"
# 多种查找策略
$usernameInput = $null
# 策略1: 通过XPath查找
try {
$usernameInput = $global:Driver.FindElement([OpenQA.Selenium.By]::XPath("//input[@placeholder='$InputIdentifier']"))
Write-Log "通过XPath找到账号输入框" "SUCCESS"
}
catch {
# 策略2: 通过CSS选择器查找
try {
$usernameInput = $global:Driver.FindElement([OpenQA.Selenium.By]::CssSelector("input[autocomplete='username']"))
Write-Log "通过CSS选择器找到账号输入框" "SUCCESS"
}
catch {
throw "未找到账号输入框"
}
}
return $usernameInput
}
catch {
Write-Log "查找账号输入框失败: $($_.Exception.Message)" "ERROR"
return $null
}
}
function Find-PasswordInput {
param([string]$InputIdentifier = "请输入密码")
try {
Write-Log "查找密码输入框..." "INFO"
# 多种查找策略
$passwordInput = $null
# 策略1: 通过XPath查找
try {
$passwordInput = $global:Driver.FindElement([OpenQA.Selenium.By]::XPath("//input[@placeholder='$InputIdentifier']"))
Write-Log "通过XPath找到密码输入框" "SUCCESS"
}
catch {
# 策略2: 通过CSS选择器查找
try {
$passwordInput = $global:Driver.FindElement([OpenQA.Selenium.By]::CssSelector("input[type='password']"))
Write-Log "通过CSS选择器找到密码输入框" "SUCCESS"
}
catch {
throw "未找到密码输入框"
}
}
return $passwordInput
}
catch {
Write-Log "查找密码输入框失败: $($_.Exception.Message)" "ERROR"
return $null
}
}
function Find-LoginButton {
try {
Write-Log "查找登录按钮..." "INFO"
# 多种查找策略
$loginButton = $null
# 策略1: 通过CSS选择器查找
try {
$loginButton = $global:Driver.FindElement([OpenQA.Selenium.By]::CssSelector("button.n-button.n-button--primary-type.n-button--small-type.button.signin"))
Write-Log "通过CSS选择器找到登录按钮" "SUCCESS"
}
catch {
# 策略2: 通过ClassName查找
try {
$loginButton = $global:Driver.FindElement([OpenQA.Selenium.By]::ClassName("__button-1cvdmx0-lsmp n-button n-button--primary-type n-button--small-type button signin"))
Write-Log "通过ClassName找到登录按钮" "SUCCESS"
}
catch {
throw "未找到登录按钮"
}
}
return $loginButton
}
catch {
Write-Log "查找登录按钮失败: $($_.Exception.Message)" "ERROR"
return $null
}
}
function Find-LargeLoginButton {
try {
Write-Log "查找大登录按钮..." "INFO"
# 多种查找策略
$loginButton = $null
# 策略1: 通过CSS选择器查找
try {
$loginButton = $global:Driver.FindElement([OpenQA.Selenium.By]::CssSelector("button.n-button.n-button--primary-type.n-button--medium-type.n-button--block"))
Write-Log "通过CSS选择器找到大登录按钮" "SUCCESS"
}
catch {
# 策略2: 通过ClassName查找
try {
$signButton = $global:Driver.FindElement([OpenQA.Selenium.By]::ClassName("__button-1cvdmx0-ilmmp n-button n-button--primary-type n-button--medium-type n-button--block"))
Write-Log "通过ClassName找到大登录按钮" "SUCCESS"
}
catch {
throw "未找到大登录按钮"
}
}
return $loginButton
}
catch {
Write-Log "查找大登录按钮失败: $($_.Exception.Message)" "ERROR"
return $null
}
}
function Invoke-LoginAction {
param (
[string]$Username,
[string]$Password
)
$loginButton = Find-LoginButton
$usernameInput = Find-UsernameInput
$passwordInput = Find-PasswordInput
try {
if ($loginButton -eq $null) {
throw "未找到登录按钮"
}
Write-Log "准备登录..." "INFO"
$loginButton.Click()
# 等待操作完成
Start-Sleep -Seconds 3
if (($usernameInput = Find-UsernameInput) -eq $null -or ($passwordInput = Find-PasswordInput) -eq $null) {
throw "未找到输入框"
}
$usernameInput.SendKeys($Username)
$passwordInput.SendKeys($Password)
# 等待操作完成
Start-Sleep -Seconds 3
$largeLoginButton = Find-LargeLoginButton
if ($largeLoginButton -eq $null) {
throw "未找到登录按钮"
}
$largeLoginButton.Click()
Write-Log "登录操作执行成功" "SUCCESS"
return $true
}
catch {
Write-Log "登录操作失败: $($_.Exception.Message)" "ERROR"
return $false
}
}
function Find-SignButton {
param([string]$ButtonIdentifier = "立即签到")
try {
Write-Log "查找签到按钮..." "INFO"
# 多种查找策略
$signButton = $null
# 策略1: 通过XPath查找
try {
$signButton = $global:Driver.FindElement([OpenQA.Selenium.By]::XPath("//button[contains(text(), '$ButtonIdentifier')]"))
Write-Log "通过XPath找到签到按钮" "SUCCESS"
}
catch {
# 策略2: 通过CSS选择器查找
try {
$signButton = $global:Driver.FindElement([OpenQA.Selenium.By]::CssSelector("button.n-button.n-button--primary-type.n-button--large-type"))
Write-Log "通过CSS选择器找到签到按钮" "SUCCESS"
}
catch {
# 策略3: 通过ClassName查找
try {
$signButton = $global:Driver.FindElement([OpenQA.Selenium.By]::ClassName("__button-1cvdmx0-llmp n-button n-button--primary-type n-button--large-type"))
Write-Log "通过ClassName找到签到按钮" "SUCCESS"
}
catch {
throw "未找到签到按钮"
}
}
}
return $signButton
}
catch {
Write-Log "查找签到按钮失败: $($_.Exception.Message)" "ERROR"
return $null
}
}
function Invoke-SignAction {
param([object]$SignButton)
try {
if ($SignButton -eq $null) {
throw "签到按钮为空"
}
Write-Log "点击签到按钮..." "INFO"
$SignButton.Click()
# 等待操作完成
Start-Sleep -Seconds 3
Write-Log "签到操作执行成功" "SUCCESS"
return $true
}
catch {
Write-Log "签到操作失败: $($_.Exception.Message)" "ERROR"
return $false
}
}
function Close-Browser {
try {
if ($global:Driver -ne $null) {
Write-Log "关闭浏览器..." "INFO"
$global:Driver.Quit()
$global:Driver = $null
Write-Log "浏览器已关闭" "SUCCESS"
}
}
catch {
Write-Log "关闭浏览器时出错: $($_.Exception.Message)" "WARNING"
}
}
# 5. 主执行流程
function Start-AutoSign {
param([string]$Url)
Write-Log "=== 自动签到流程开始 ===" "INFO"
try {
# 检测环境
$envReady = Test-SeleniumEnvironment
if (-not $envReady) {
Write-Log "环境未就绪,开始自动安装..." "WARNING"
$installSuccess = Install-SeleniumEnvironment
if (-not $installSuccess) {
throw "环境安装失败,无法继续执行"
}
}
# 启动浏览器
$browserStarted = Start-Browser -Url $Url
if (-not $browserStarted) {
throw "浏览器启动失败"
}
$loginSuccess = Invoke-LoginAction -Username $global:Username -Password $global:Password
if (-not $loginSuccess) {
throw "登录操作失败"
}
# 随机延迟
Wait-RandomDelay -MinDelay 5 -MaxDelay 10
# 查找并点击签到按钮
$signButton = Find-SignButton -ButtonIdentifier "立即签到"
if ($signButton -eq $null) {
throw "无法找到签到按钮"
}
# 执行签到
$signSuccess = Invoke-SignAction -SignButton $signButton
if (-not $signSuccess) {
throw "签到操作失败"
}
# 等待操作完成
Wait-RandomDelay -MinDelay 3 -MaxDelay 5
Write-Log "=== 自动签到流程完成 ===" "SUCCESS"
return $true
}
catch {
Write-Log "自动签到流程失败: $($_.Exception.Message)" "ERROR"
return $false
}
finally {
# 关闭浏览器
Close-Browser
}
}
# 6. 脚本执行入口
try {
Write-Log "脚本启动" "INFO"
# 执行自动签到
$success = Start-AutoSign -Url $Url
if ($success) {
Write-Log "自动签到任务执行成功" "SUCCESS"
exit 0
} else {
Write-Log "自动签到任务执行失败" "ERROR"
exit 1
}
}
catch {
Write-Log "脚本执行过程中发生未预期的错误: $($_.Exception.Message)" "ERROR"
Write-Log "错误堆栈: $($_.Exception.StackTrace)" "ERROR"
exit 1
}
—————————————————————分割线—————————————————————
文后记:
沉迷于自动签到的小妮娜忽视了与站友的互动,最终也未能逃脱那“被遗忘”的宿命
当最后一位好友也清空了他的聊天记录,其人所留下的最后一片数字疆土——那个名为“@孤木落”的主页,便在下一轮M站服务器升级中被永久归档,从赛博世界彻底清除......
所幸,在那一刻,他终于明白了,“签到要走正道哦”,所昭示的因果
大家大多数都是靠着自己抢的签到呢喵,慎用自动签到喵