自研、掌握核心科技?这我可不敢吹,我老老实实说我用了个Chromium内核组件。
为了统计一些数据,一条条复制粘贴肯定是够累的。用爬虫吧,自己还不精通,而且现在好多数据都需要登录才能请求,或者有些需要滑滚动条才显示。
比如csdn社区的打卡记录,一个月的如何快速的统计出来呢?
- 我想控制网页的请求
- 我想控制请求结果
- 我想给网页中硬塞点JS
- 我想模拟输入、模拟按键
- 我想自动翻页、拉滚动条、自动抓取数据
-
在Chrome浏览器中,可以通过安装扩展插件进行一些“特殊操作”。比如CSDN的浏览器插件就很强大,可以参考我的另一篇文章。
自己弄个浏览器,将这些都实现。
☆☆☆一定要注意,通过自动方式请求,一定要控制频率,我一般每个请求之间都会停顿5秒以上,文明抓数据,不能给别人和自己造成麻烦。☆☆☆
曾经号称打破漂亮国垄断的而大火的“hongxin”浏览器,最终被爆出实际是基于Chromium内核。其实我们也可以弄一个。做桌面软件,微软的Winform和Wpf那肯定是很方便的,也有对应的.Net组件方便将Chromium应用在Winform和Wpf程序中。
CefSharp 允许您在 .NET 应用程序中嵌入 Chromium。 它是 Marshall A. Greenblatt 围绕 Chromium 嵌入式框架 (CEF) 的轻量级 .NET 包装器。 大约 30% 的绑定是用 C++/CLI 编写的,这里的大部分代码是 C#。 它可以在 C# 或 VB 或任何其他 CLR 语言中使用。 CefSharp 提供 WPF 和 WinForms Web 浏览器控件实现。
CefSharp 是 BSD 许可的,因此它可以在专有和免费/开源应用程序中使用。
1. 新建项目
CefSharp 提供 WPF 和 WinForm支持,所以新建哪种项目都行,当然Wpf的可以做的更加漂亮一些。
本例以WinForm为例,新建一个WinForm项目:
设置项目名称,例如MyChrome
添加CefSharp组件,在Nuget中搜索CefSharp.Winforms, 由于本例是建的.Net core项目, 所以选择CefSharp.Winforms.NETCore, 安装即可
在Program.cs中进行初始化:
public static int Main(string[] args)
{
#if ANYCPU
CefRuntime.SubscribeAnyCpuAssemblyResolver();
#endif
//For Windows 7 and above, best to include relevant app.manifest entries as well
Cef.EnableHighDPISupport();
var settings = new CefSettings()
{
//By default CefSharp will use an in-memory cache, you need to specify a Cache Folder to persist data
CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache")
};
//Perform dependency check to make sure all relevant resources are in our output directory.
Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null);
var browser = new BrowserForm();
Application.Run(browser);
return 0;
}
新建一个Form,例如MainForm,进入设计器:
在工具箱里可以看到已经有了相应组件:
第二个就是浏览器组件,可以拖一个到Form中进行配置。
具体可配置项比较多,可以参考GitHub中相关的配置文档:https://github.com/cefsharp/CefSharp。
在GitHub库中,也提供了相应的Example,如果想快速学习一下,这是非常好的例子。
访问一下CSDN,大概如下图:
2. 功能体验
在Chrome浏览器中,可以通过安装扩展插件进行一些“特殊操作”。比如CSDN的浏览器插件就很强大,可以参考我的另一篇文章:油#猴是什么猴?又一门新的编程语言?卷不动了呀。
浏览器插件都这么强大了,直接使用Chromium则能获得更大的主动权。
2.1 DevTools
要处理网页,怎么能少了DevTools,平时做前端开发,F12键肯定不少按。在这里也依然方便。在上图的地址栏中可以看到我自定义了三个按钮,其中一个就是DevTools,按钮对应的代码也很简单:
browser.ShowDevTools();
2.2 执行Js语句
来个最简单的例子:
browser.ExecuteScriptAsync("alert('sdsdd')");
2.3 模拟按键
有时候通过Js模拟输入、点击按钮等操作比较复杂,因为你不知道实际在页面中输入的时候,页面背地里又有什么操作,所以如果简单的给字段赋值可能是没用的。那么怎么模拟键盘输入呢?
比如想输入这样一句话:"没事看看CSDN"
string str = "没事看看CSDN";
foreach (char item in str)
{
browser.GetBrowserHost().SendKeyEvent((int)258u, (int)item, 0);
}
这些都是基础功能,通过CefSharp,你可以控制请求数据,修改响应结果,发挥你的想象,你还想干什么。下面通过一个简单例子看一下。
3. 统计CSDN社区的打卡记录
需求分析:每一篇都有如下图这样的几页打卡记录
而每个月都有差不多30篇这样的文章,如何快速的统计出来呢?
思路就是:
- 读取每一篇打卡贴的链接
- 逐一打卡每一篇打卡贴
- 获取打卡贴第一页的打卡记录
- 逐一翻到下一页,获取各页的打卡记录
3. 1 获取所有打卡贴
首先关键字搜索,获取打卡贴列表
// 获取所有链接的JS
string script = @"Array.from(document.getElementsByClassName('user-tabs user-tabs-search')[0].getElementsByClassName('long-text-title')).map(x => ( x.href));";
// 执行JS代码,返回一个JavascriptResponse
JavascriptResponse response1 = await browser.EvaluateScriptAsync(script);
dynamic arr = response1.Result;
// 遍历结果集合,将所有URL存储到一个静态集合中
foreach (dynamic row in arr)
{
RecordManager.Urls.Add(row.ToString());
}
为了直观,弹出一个对话框,将URL显示出来:
GetCSDN getCSDN = new GetCSDN();
getCSDN.browser = browser;
getCSDN.ShowDialog();
这里定义了一个静态类来控制分析操作。
public static class RecordManager
{
// 是否开始分析
public static bool IsStart { get; set; } = false;
// 分析到第几页
public static int Index { get; set; } = -1;
// 获取下一页地址
public static string GetNextUrl()
{
Index = Index + 1;
if (Urls.Count == Index)
{
return "";
}
return Urls[Index];
}
// URL列表
public static List<string> Urls = new List<string>();
// 获取到的打卡记录列表
public static List<Record> RecordList { get; set; } = new List<Record>();
// 分析完之后的回调函数
public static Action callback;
}
3.2 逐一打开各个页面并获取结果
上一图右边做了个开始按钮,点击将逐一开始分析。
系统提供了多种Handler用于浏览器各种操作的处理,这里要分析请求操作,所以用到RequestHandler
public class CustomRequestHandler: RequestHandler
{
protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
{
return new CustomResourceRequestHandler();
}
}
主要是指定了另一个专门用于处理请求的CustomResourceRequestHandler,二者关系如下图,后者才是分析的主角:
public class CustomResourceRequestHandler : ResourceRequestHandler
{
private readonly MemoryStream memoryStream = new MemoryStream();
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
{
// 将请求响应结果放到MemoryStream中
return new CefSharp.ResponseFilter.StreamResponseFilter(memoryStream);
}
protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength)
{
// 只分析打卡列表的请求结果
if (!RecordManager.IsStart || !(request.Url.ToLower().StartsWith("https://bizapi.csdn.net/community-cloud/v1/community/task/list") && request.Method.ToLower().Equals("get")))
{
return;
}
var bytes = memoryStream.ToArray();
string pages = string.Empty;
string page = request.Url.Substring(request.Url.IndexOf("page=") + 5, 1);
var str = System.Text.Encoding.UTF8.GetString(bytes);
JObject obj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(str);
List<Record> list = (List<Record>)obj["data"]["finish"]["list"].ToObject(typeof(List<Record>));
pages = obj["data"]["finish"]["pages"].ToString();
if (list.Count > 0)
{
RecordManager.RecordList.AddRange(list);
}
if (pages.Equals(page))
{
string url = RecordManager.GetNextUrl();
if (string.IsNullOrEmpty(url))
{
RecordManager.callback();
}
else
{
// 休息5秒,再请求下一篇文章
Thread.Sleep(5000);
browser.MainFrame.LoadUrl(url);
}
}
else
{
// 休息5秒,再请求下一页
Thread.Sleep(5000);
string js = $"document.getElementsByClassName('number')[{int.Parse( page)}].click();";
browser.MainFrame.ExecuteJavaScriptAsync(js);
}
}
}
3.3 展示结果
3.4 导出结果
讲结果导出到Excel:
//创建工作薄
var workbook = new HSSFWorkbook();
//创建表
var table = workbook.CreateSheet("data");
int i = 0;
RecordManager.RecordList.ForEach(record => {
var row = table.CreateRow(i);
var cell = row.CreateCell(0);
cell.SetCellValue(record.finishTime);
var cell1 = row.CreateCell(1);
cell1.SetCellValue(record.userName);
var cell2 = row.CreateCell(2);
cell2.SetCellValue(record.nickName);
i++;
});
using (var fs = File.OpenWrite(@"d:/test/1.xls"))
{
workbook.Write(fs); //向打开的这个xls文件中写入mySheet表并保存。
Console.WriteLine("生成成功");
}