渡里妮娜

小萌新初探M站,笨方法实现自动签

238浏览 9天前 软件教程 MA108948

这是其实是一篇严谨的技术文

多年以后,面对着蒸蒸日上的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站服务器升级中被永久归档,从赛博世界彻底清除......

所幸,在那一刻,他终于明白了,“签到要走正道哦”,所昭示的因果

大家大多数都是靠着自己抢的签到呢喵,慎用自动签到喵

未经作者允许,禁止转载
#签到 #M站 #新人 #脚本 #软件 #萌新
30