最近發現一些快手的作者,作品還不錯,出於學習研究的目的,決定看一下怎麼爬取資料。現在網上有一些爬蟲工具,不過大部分都失效了,或者不開源。於是自己就寫了一個小工具。先看一下成果:
軟體只需要填寫作者uid以及網頁版的請求Cookie,即可實現自動下載,下載目錄在程式根目錄下的Download資料夾。
由於快手的風控比較厲害,軟體也做了應對措施。不過需要使用者點選軟體中的提示文字,複製貼上到瀏覽器,把請求的json儲存到本地檔案。使用軟體提供的解析本地json按鈕解析下載即可。如果返回的json檔案很短或者沒有資料,需要在快手的任意一個頁面重新整理一下,也就是告訴快手風控,現在是正常瀏覽,沒有機器人的行為。
下面說一下構建整個App的思路。
1. 快手網頁端準備
-
開啟https://live.kuaishou.com/ ,在頂部搜尋你要爬取的作者暱稱,進入作者主頁。也可以從App端分享作者的主頁連結,貼上進來。作者主頁載入完成後,位址列的地址一定要是類似:https://live.kuaishou.com/profile/xxxxxx。 後面的xxxxxx就是作者的user id。這個記住,複製出來,後面會用到。
-
按F12開啟瀏覽器的開發者工具(我之前就說過開發者工具是好東西,研究爬蟲必備,一定要好好學習)。
-
選擇開發者工具頂部的“網路”,“全部”,如圖所示。在請求列表中找到user id,點選它,右面就會出來請求的標頭。裡面有個Cookie,需要記住,複製出來。如果沒有的話,記得重新整理頁面。
-
在列表裡面可以看到很多請求,我們需要從中找到網頁端展示作品列表的那條請求,即public開頭的,或者直接在左上角搜尋public,即可過濾絕大部分無關請求。這個請求的響應資料裡面有作者作品的完整json響應。
你可以右擊它,在新標籤頁面開啟,開啟後位址列會顯示完成的瀏覽器請求地址。這個網址需要記住,後續會用到。那個count預設是12或者20,我們用到時候,直接拉滿,9999即可。
2. Postman攔截請求,模擬請求,並生成C#請求程式碼
-
安裝postman interceptor攔截器,安裝地址https://chromewebstore.google.com/detail/postman-interceptor/aicmkgpgakddgnaphhhpliifpcfhicfo 不得不說,這又是一個神器,搭配開發者工具,理論上可以搞定幾乎所有的爬蟲需求了。
-
開啟Postman,點選右下角的Start Proxy,
開啟攔截後,重新回到網頁版作者主頁,重新整理一下頁面,等頁面載入完成後,點選停止攔截。否則列表會一直增多,因為他會攔截電腦的所有網路請求。這時Postman攔截器就會攔截到一大堆請求,同理,找到public請求,或者在左上角輸入public,即可過濾出來我們需要的。
點選這個請求連結
這是Postman會開啟一個新的視窗,包含了請求這個連結的所有引數以及標頭資訊。
點選Postman最右面的程式碼工具即可生成我們需要的程式碼。你可以選擇C#、python、js、curl等等。
3. 使用WPF寫介面以及下載邏輯
- 新建WPF工程,為了介面好看,這次我用了開源的WPF UI,之前用過HandyControl、MicaWPF,這些都是不錯的UI控制元件庫。
下載使用了開源的Downloader,請求使用了RestSharp,解析Json使用NewtonsoftJson,另外推薦一個免費的圖示庫FlatIcon。
介面如下:
點選檢視程式碼
<ui:FluentWindow
x:Class="KuaishouDownloader.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:KuaishouDownloader"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="MainWindow"
Width="900"
Height="760"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowCornerPreference="Default"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TitleBar Title="快手作者主頁作品爬取" Height="32" />
<ui:Button
x:Name="themeButton"
Grid.Row="1"
Width="32"
Height="32"
Margin="0,0,8,0"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Click="Theme_Click"
CornerRadius="16"
FontSize="24"
Icon="{ui:SymbolIcon WeatherMoon48}"
ToolTip="切換主題" />
<ui:SnackbarPresenter
x:Name="snackbarPresenter"
Grid.Row="1"
VerticalAlignment="Bottom" />
<StackPanel
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border
Width="200"
Height="200"
HorizontalAlignment="Center"
CornerRadius="100">
<ui:Image
x:Name="imgHeader"
Width="200"
Height="200"
CornerRadius="100" />
</Border>
<ui:TextBlock
x:Name="tbNickName"
Margin="0,12,0,0"
HorizontalAlignment="Center" />
<StackPanel Margin="0,12,0,0" Orientation="Horizontal">
<ui:TextBlock
Width="60"
Margin="0,12,0,0"
VerticalAlignment="Center"
Text="uid" />
<ui:TextBox
x:Name="tbUid"
Width="660"
Height="36"
VerticalContentAlignment="Center"
ToolTip="App進入作者主頁,分享主頁-複製連結,用瀏覽器開啟連結,位址列一般變為https://www.kuaishou.com/profile/xxxxxx/開頭的,複製xxxxxx過來" />
</StackPanel>
<StackPanel Margin="0,12,0,0" Orientation="Horizontal">
<ui:TextBlock
Width="60"
VerticalAlignment="Center"
Text="cookie" />
<ui:TextBox
x:Name="tbCookie"
Width="660"
Height="36"
VerticalContentAlignment="Center"
ToolTip="利用瀏覽器開發者工具,從網路-請求標頭中獲取" />
</StackPanel>
<StackPanel
Margin="0,12,0,0"
HorizontalAlignment="Center"
Orientation="Horizontal">
<ui:Button
x:Name="btnDownload"
Height="32"
Appearance="Primary"
Click="Download_Click"
Content="開始下載"
CornerRadius="4 0 0 4"
ToolTip="預設下載到程式根目錄下,檔案日期為作品釋出日期" />
<ui:Button
x:Name="btnParseJson"
Height="32"
Appearance="Primary"
Click="ParseJson_Click"
Content="..."
CornerRadius="0 4 4 0"
ToolTip="解析從web或者postman儲存的json資料" />
</StackPanel>
<TextBlock
Width="700"
Margin="0,12,0,0"
Foreground="Gray"
MouseDown="CopyUrl"
Text="被快手風控不要慌,瀏覽器開啟快手網頁版,掃碼登陸,點選我複製網址,貼上到瀏覽器開啟。開啟後如果有很長很長的json資料返回,就對了。複製json儲存到本地json檔案,然後用第二個按鈕解析json資料即可下載。"
TextWrapping="Wrap" />
<Expander Margin="0,12,0,0" Header="更多選項">
<StackPanel Orientation="Horizontal">
<CheckBox
x:Name="cbAddDate"
Margin="12,0,0,0"
VerticalAlignment="Center"
Content="檔名前加上日期"
IsChecked="True"
ToolTip="檔名前面加上類似2024-01-02 13-00-00的標識,方便排序" />
<CheckBox
x:Name="cbLongInterval"
Margin="12,0,0,0"
VerticalAlignment="Center"
Content="增加作品下載延時"
IsChecked="True"
ToolTip="預設勾選,作品間下載延時5~10秒。取消勾選1~5秒隨機,可能被風控" />
</StackPanel>
</Expander>
</StackPanel>
<StackPanel
Grid.Row="1"
Margin="0,0,0,-2"
VerticalAlignment="Bottom">
<TextBlock x:Name="tbProgress" HorizontalAlignment="Center" />
<ProgressBar x:Name="progress" Height="8" />
</StackPanel>
<ui:Button
x:Name="infoButton"
Grid.Row="1"
Width="32"
Height="32"
Margin="0,0,8,8"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Click="Info_Click"
CornerRadius="16"
FontSize="24"
Icon="{ui:SymbolIcon Info28}"
ToolTip="鳴謝" />
<ui:Flyout
x:Name="flyout"
Grid.Row="1"
HorizontalAlignment="Right">
<ui:TextBlock Text="鳴謝: 
1. Microsoft Presentation Foundation
2. WPF-UI
3. RestSharp
4. Newtonsoft.Json
5. Downloader
6. Icon from FlatIcon" />
</ui:Flyout>
</Grid>
</ui:FluentWindow>
- 後臺邏輯沒有使用MVVM,就是圖方便。
點選檢視程式碼
using KuaishouDownloader.Models;
using Newtonsoft.Json;
using RestSharp;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows;
using Wpf.Ui;
using Wpf.Ui.Controls;
namespace KuaishouDownloader
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
{
string downloadFolder = AppContext.BaseDirectory;
SnackbarService? snackbarService = null;
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
snackbarService = new SnackbarService();
snackbarService.SetSnackbarPresenter(snackbarPresenter);
if (File.Exists("AppConfig.json"))
{
var model = JsonConvert.DeserializeObject<AppConfig>(File.ReadAllText("AppConfig.json"));
if (model != null)
{
tbUid.Text = model.Uid;
tbCookie.Text = model.Cookie;
}
}
}
private void Theme_Click(object sender, RoutedEventArgs e)
{
if (Wpf.Ui.Appearance.ApplicationThemeManager.GetAppTheme() == Wpf.Ui.Appearance.ApplicationTheme.Light)
{
themeButton.Icon = new SymbolIcon(SymbolRegular.WeatherSunny48);
Wpf.Ui.Appearance.ApplicationThemeManager.Apply(Wpf.Ui.Appearance.ApplicationTheme.Dark);
}
else
{
themeButton.Icon = new SymbolIcon(SymbolRegular.WeatherMoon48);
Wpf.Ui.Appearance.ApplicationThemeManager.Apply(Wpf.Ui.Appearance.ApplicationTheme.Light);
}
}
private async void Download_Click(object sender, RoutedEventArgs e)
{
try
{
btnDownload.IsEnabled = false;
btnParseJson.IsEnabled = false;
if (string.IsNullOrEmpty(tbUid.Text) || string.IsNullOrEmpty(tbCookie.Text))
{
snackbarService?.Show("提示", $"請輸入uid以及cookie", ControlAppearance.Caution, null, TimeSpan.FromSeconds(3));
return;
}
var json = JsonConvert.SerializeObject(new AppConfig() { Uid = tbUid.Text, Cookie = tbCookie.Text }, Formatting.Indented);
File.WriteAllText("AppConfig.json", json);
var options = new RestClientOptions("https://live.kuaishou.com")
{
Timeout = TimeSpan.FromSeconds(15),
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
};
var client = new RestClient(options);
var request = new RestRequest($"/live_api/profile/public?count=9999&pcursor=&principalId={tbUid.Text}&hasMore=true", Method.Get);
request.AddHeader("host", "live.kuaishou.com");
request.AddHeader("connection", "keep-alive");
request.AddHeader("cache-control", "max-age=0");
request.AddHeader("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Google Chrome\";v=\"127\", \"Chromium\";v=\"127\"");
request.AddHeader("sec-ch-ua-mobile", "?0");
request.AddHeader("sec-ch-ua-platform", "\"Windows\"");
request.AddHeader("upgrade-insecure-requests", "1");
request.AddHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
request.AddHeader("sec-fetch-site", "none");
request.AddHeader("sec-fetch-mode", "navigate");
request.AddHeader("sec-fetch-user", "?1");
request.AddHeader("sec-fetch-dest", "document");
request.AddHeader("accept-encoding", "gzip, deflate, br, zstd");
request.AddHeader("accept-language", "zh,en;q=0.9,zh-CN;q=0.8");
request.AddHeader("cookie", tbCookie.Text);
request.AddHeader("x-postman-captr", "9467712");
RestResponse response = await client.ExecuteAsync(request);
Debug.WriteLine(response.Content);
var model = JsonConvert.DeserializeObject<KuaishouModel>(response.Content!);
if (model == null || model?.Data?.List == null || model?.Data?.List?.Count == 0)
{
snackbarService?.Show("提示", $"獲取失敗,可能觸發了快手的風控機制,請等一段時間再試。", ControlAppearance.Danger, null, TimeSpan.FromSeconds(3));
return;
}
await Download(model!);
}
finally
{
btnDownload.IsEnabled = true;
btnParseJson.IsEnabled = true;
}
}
private async void ParseJson_Click(object sender, RoutedEventArgs e)
{
try
{
btnDownload.IsEnabled = false;
btnParseJson.IsEnabled = false;
var dialog = new Microsoft.Win32.OpenFileDialog();
dialog.Filter = "Json檔案(.Json)|*.json";
bool? result = dialog.ShowDialog();
if (result == false)
{
return;
}
var model = JsonConvert.DeserializeObject<KuaishouModel>(File.ReadAllText(dialog.FileName)!);
if (model == null || model?.Data?.List == null || model?.Data?.List?.Count == 0)
{
snackbarService?.Show("提示", $"不是正確的json", ControlAppearance.Caution, null, TimeSpan.FromSeconds(3));
return;
}
await Download(model!);
}
finally
{
btnDownload.IsEnabled = true;
btnParseJson.IsEnabled = true;
}
}
private async Task Download(KuaishouModel model)
{
progress.Value = 0;
progress.Minimum = 0;
progress.Maximum = (double)model?.Data?.List?.Count!;
snackbarService?.Show("提示", $"解析到{model?.Data?.List?.Count!}個作品,開始下載", ControlAppearance.Success, null, TimeSpan.FromSeconds(5));
imgHeader.Source = new System.Windows.Media.Imaging.BitmapImage(new Uri(model?.Data?.List?[0]?.Author?.Avatar!));
tbNickName.Text = model?.Data?.List?[0]?.Author?.Name;
string pattern = @"\d{4}/\d{2}/\d{2}/\d{2}";
for (int i = 0; i < model?.Data?.List!.Count; i++)
{
DateTime dateTime = DateTime.Now;
string fileNamePrefix = "";
var item = model?.Data?.List[i]!;
Match match = Regex.Match(item.Poster!, pattern);
if (match.Success)
{
dateTime = new DateTime(int.Parse(match.Value.Split("/")[0]), int.Parse(match.Value.Split("/")[1]),
int.Parse(match.Value.Split("/")[2]), int.Parse(match.Value.Split("/")[3]), 0, 0);
if (cbAddDate.IsChecked == true)
fileNamePrefix = match.Value.Split("/")[0] + "-" + match.Value.Split("/")[1] + "-" + match.Value.Split("/")[2]
+ " " + match.Value.Split("/")[3] + "-00-00 ";
}
downloadFolder = Path.Combine(AppContext.BaseDirectory, "Download", item?.Author?.Name! + "(" + item?.Author?.Id! + ")");
Directory.CreateDirectory(downloadFolder);
switch (item?.WorkType)
{
case "single":
case "vertical":
case "multiple":
{
await DownLoadHelper.Download(item?.ImgUrls!, dateTime, downloadFolder, fileNamePrefix);
}
break;
case "video":
{
await DownLoadHelper.Download(new List<string>() { item?.PlayUrl! }, dateTime, downloadFolder, fileNamePrefix);
}
break;
}
progress.Value = i + 1;
tbProgress.Text = $"{i + 1} / {model?.Data?.List!.Count}";
Random random = new Random();
if (cbLongInterval.IsChecked == true)
await Task.Delay(random.Next(5000, 10000));
else
await Task.Delay(random.Next(1000, 5000));
}
snackbarService?.Show("提示", $"下載完成,共下載{model?.Data?.List!.Count}個作品", ControlAppearance.Success, null, TimeSpan.FromDays(1));
}
private void CopyUrl(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (string.IsNullOrEmpty(tbUid.Text))
{
snackbarService?.Show("提示", "請輸入uid以及cookie", ControlAppearance.Caution, null, TimeSpan.FromSeconds(3));
return;
}
Clipboard.SetText($"https://live.kuaishou.com/live_api/profile/public?count=9999&pcursor=&principalId={tbUid.Text}&hasMore=true");
snackbarService?.Show("提示", "複製完成,請貼上到瀏覽器開啟", ControlAppearance.Success, null, TimeSpan.FromSeconds(3));
}
private void Info_Click(object sender, RoutedEventArgs e)
{
flyout.IsOpen = true;
}
}
}
- 下載類,下載完檔案後,將檔案的日誌修改為發表日誌,方便排序以及資料分析。
點選檢視程式碼
public static async Task Download(List<string> urls, DateTime dateTime, string downloadFolder, string fileNamePrefix)
{
string file = string.Empty;
try
{
var downloader = new DownloadService();
foreach (var url in urls)
{
Uri uri = new Uri(url);
file = downloadFolder + "\\" + fileNamePrefix + Path.GetFileName(uri.LocalPath);
if (!File.Exists(file))
await downloader.DownloadFileTaskAsync(url, file);
//修改檔案日期時間為發博的時間
File.SetCreationTime(file, dateTime);
File.SetLastWriteTime(file, dateTime);
File.SetLastAccessTime(file, dateTime);
}
}
catch
{
Debug.WriteLine(file);
Trace.Listeners.Add(new TextWriterTraceListener(downloadFolder + "\\_FailedFiles.txt", "myListener"));
Trace.TraceInformation(file);
Trace.Flush();
}
}
- 原始碼分享
完整版程式碼已上傳到Github https://github.com/hupo376787/KuaishouDownloader ,喜歡的點一下Star謝謝。
4. 下載使用
開啟https://github.com/hupo376787/KuaishouDownloader/releases/tag/1.0,點選下載zip檔案,解壓縮後,就可以像開頭那樣使用了。