原理:
模拟浏览器行为,请求目标资源,获取到需要的内容后,视需求再进一步提炼(一般使用字符串处理得到需要的信息)。
需要具备哪些知识:
- HTTP通信原理:需要理解HTTPRequest,HTTPResponse,HTTPHeader,Cookie,Session,Content-Type,Form提交,编码,谓词(一般只需要用到GET和POST),HTTP状态码。
- 前端知识:主要用来分析网页,不需要了解多深,只需要知道基本的HTML元素,HTML元素的id、name、tag等属性,对CSS有大概的了解;需要懂得使用浏览器DevTools查看网页元素,查看Network中的通信,查看Request和Response的详情;某些场景里,需要对JS有一定了解。
- 一种编程语言:需要了解如何通过httpclient发送Get和Post请求,并获取服务器响应,需要懂得如何进行字符串处理(对正则表达式有个基本了解,能够实际使用起来的话,帮助会很大)。
场景一:批量下载网站上的二进制文件,这种需求不需要额外处理,直接保存到文件就完成任务了;
场景二:希望提取网页中的某些信息,这种需求需要在得到网页源码后,通过字符串处理,摘出所需要的内容,这部分工作的重点不光是如何获取到网页源码,还在于如何对HTML源码进行解析,精确提取关键信息。
demo:
- 爬虫一:直接抓取页面
- 爬虫二:带user-agent检测的页面
- 爬虫三:带翻页的列表
- 爬虫四:要求登录的页面
- 爬虫五:利用终极利器Selenium执行批处理操作
爬虫一:直接抓取页面
- 网站页面:http://nginx.org/
- 目标内容:看看nginx news有哪些目录
- 步骤一:踩点
首先要做的一件事,就是分析目标网页,查看资源是如何呈现的,这一步完成后,才知道应该使用哪种技术去下载页面,以及如何去提取网页中的内容。
通过网页分析我们可以发现,news目录位于body -> div#main -> div#menu下面,而这个div#menu又包含了其它的一些信息,比如语言链接,和下面的about等不相关的栏目。
- 步骤二:下载页面
下载页面的方法挺多,命令行工具有curl, wget等等,或者支持http交互的语言(纯TCP也能实现,但是需要自己构造HTTP消息发到服务器,同时也要额外解析一下服务器返回的响应内容),这个地方八仙过海各显神通了。 - 步骤三:提取内容
static void Main(string[] args)
{
Regex.Matches(
Regex.Match(
new WebClient().DownloadString("http://nginx.org/"),
"(?<=id=\"menu.*?news).*</div>").Value,
"<a href=\"(.*?)\">(\\d{4})</a>").
Select(m => $"Tag: {m.Groups[2]}\tLink: {m.Groups[1]}").
ToList().ForEach(s => Console.WriteLine(s));
Console.ReadLine();
}
爬虫二:带user-agent检测的页面
- 网站页面:https://www.ip.cn/
User-Agent是浏览器向网站发送的一个身份标识,这个标识记录了浏览器的类型、操作系统环境、内核、版本等信息。服务器可以根据这些信息做相应的动作,比如识别搜索引擎蜘蛛(正规搜索引擎蜘蛛都会主动向网站表明自己的身份)、统计本站访客的操作系统占比、PC/移动占比、根据不同的浏览器展示不同的外观、根据不同的操作系统跳转到对应系统版本的软件下载链接等等。
这个参数设计之初,其目的就是为了提升用户浏览体验(以及浏览器厂商之间的恶性竞争,正因为这个原因,现在user-agent已经不算一个很重要的东西了),而网站加入user-agent检测,也可以初步区分出访客是机器人还是真实用户(user-agent可以伪造,所以这个不是决定性证据,会有漏网的情况,但如果没有user-agent,则可以100%断定不是正常访客)。
ip.cn这个网站就是如此,如果没有user-agent,它就不会正常输出内容,而是报一个403错误提示。
- 目标信息:查看本机的公网IP信息(地址、位置、geoip记录)。通过查看网页源码就可以发现这三条信息被三个<code>标签所包含。
- 下载和提取
WebClient client = new WebClient();
client.Headers["user-agent"] =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
string result = client.DownloadString("https://www.ip.cn");
var matches = Regex.Matches(result, "(?<=<code>).*?(?=</code>)");
for (int i = 0; i < 3; i++)
Console.WriteLine(
new string[] { "ip", "位置", "geoip" }[i] +$"\t{matches[i].Value}");
Console.ReadLine();
爬虫三:带翻页的列表
有的时候,我们会碰到抓取某个栏目所有页面的需求,例如“新闻”栏目下所有新闻,这种场景里,往往新闻会有数十上百条,新闻列表也会有数十页之多。碰到这种需求,需要拆解一下任务:第一步,获取所有新闻列表页面,并保存新闻链接;第二步按新闻链接访问页面,抓取实际的新闻页面。
- 网站页面:https://blog.centos.org/
- 抓取内容:2020年的所有博文(只提取文章标题、分类、作者、时间、评论数量,文章内容我就不保存了)
- 完整的月份列表可以在任一个页面的右侧找到,因为只是演示,所以只抓取2020年一月到五月的数据。
- 第一步,访问blog首页拿到所有Archive,并提取2020年的月份;
- 第二步,访问每个月的列表页面并读取每个列表页面的新闻链接;
如果当前列表页面有“Older Posts”这个链接,说明不止一页,需要继续访问下一页,直到不再出现“Older Posts”,说明到达该最后一页列表;
- 第三步,到这一步,手里就已经有了所需月份的、所有新闻页面的链接,慢慢后台批量处理吧。
class Program
{
static void Main(string[] args)
{
var result = GetContent("https://blog.centos.org");
if (!result.succ)
{
Console.WriteLine("无法打开网站。");
}
else
{
string html =
Regex.Match(result.code, "archives-2.*?</ul>", RegexOptions.Singleline).Value;
var months =
Regex.Matches(html, "(?<=<li><a href=').*?(?='>.*?</a>)").
Select(m => m.Value).Where(url => url.Contains("/2020/"));
Console.WriteLine("栏目列表:\r\n" + string.Join("\r\n", months) + "\r\n");
foreach (var month in months)
{
var _month = month;
Task.Run(() =>
{
List<string> news = new List<string>();
GetMonth(_month, ref news);
news.ForEach(u => GetNews(u));
Console.WriteLine($"!!!!!!!!!!{_month}完成。\r\n");
});
}
}
Console.ReadLine();
}
static (string code, bool succ) GetContent(string url)
{
WebClient client = new WebClient();
client.Headers["user-agent"] =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
try
{
return (client.DownloadString(url), true);
}
catch (Exception)
{
return ("", false);
}
}
/// <summary>
/// 获取月份所有新闻的链接
/// </summary>
/// <param name="url">指定月份的新闻首页</param>
/// <returns></returns>
static void GetMonth(string url, ref List<string> news)
{
var result = GetContent(url);
if (result.succ)
{
var matches = Regex.Matches(
result.code,
"(?<=boxh3.*?<a href=\\\")https://blog.*?(?=\\\")");
foreach (Match m in matches) news.Add(m.Value);
if (result.code.Contains("id=\"older-posts\""))
{
string nextpage =
Regex.Match(result.code, "(?<=older-posts.*?)https://.*?(?=\")").Value;
GetMonth(nextpage, ref news);
}
}
else
{
GetMonth(url, ref news); //失败时,重新来一遍
}
}
/// <summary>
/// 根据新闻URL,获取标题、作者、日期、分类、回复数量
/// </summary>
/// <param name="url"></param>
static void GetNews(string url)
{
var html = GetContent(url);
if (html.succ)
{
string segment =
Regex.Match(html.code, "loophead.*?(?<=</div>)", RegexOptions.Singleline).Value;
string title =
Regex.Match(segment, "(?<=<h2.*?>).*?(?=<)").Value;
string date =
Regex.Match(segment, "(?<=class=\\\"postcalendar.*?>).*?(?=<)").Value.Trim();
string author =
Regex.Match(segment, "(?<=class=\\\"postauthor.*?>).*?(?=<)").Value.Trim();
string category =
Regex.Match(segment, "(?<=class=\\\"postcategory.*?><a.*?>).*?(?=<)").Value.Trim();
string comments =
Regex.Match(segment, "\\d+\\s+Comments").Value;
if (string.IsNullOrEmpty(comments)) comments = "0";
Console.WriteLine(
$"URL:\t{url}\r\nTITLE:\t{title}\r\nDATE:\t{date}\r\nAUTHOR:\t{author}\r\n" +
$"CATEGORY:\t{category}\r\nCOMM:\t{comments}\r\n");
}
else
{
GetNews(url);
}
}
}
爬虫四:要求登录的页面
- 测试站点:某scm-manager站点
- 目标内容:获取服务器所有代码库
- 步骤一:登录服务器,获取SESSION_ID;
- 步骤二:访问代码库列表,获取内容。
class Program
{
static void Main(string[] args)
{
string urlbase = "http://192.168.2.72:8080";
WebClient client = new WebClient();
client.Headers["Content-Type"] = "application/x-www-form-urlencoded";
string result =
client.UploadString(
$"{urlbase}/scm/api/rest/authentication/login",
"username=scmadmin&password=scmadmin");
string sessionid = Regex.Match(
client.ResponseHeaders["Set-Cookie"],
"JSESSIONID=.*?;", RegexOptions.IgnoreCase).Value;
Console.WriteLine($"sessionid: {sessionid}\r\n");
client.Headers["Cookie"] = sessionid;
client.Headers["Accept"] = "application/json;charset=UTF-8";
var repos =
JsonConvert.DeserializeObject<List<repo>>(
client.DownloadString($"{urlbase}/scm/api/rest/repositories"));
repos.ForEach(
r => Console.WriteLine($"库名:\t{r.name}\r\n类型:\t{r.type}\r\n" +
$"创建:\t{DateTime.Parse("1970/1/1 08:00").AddMilliseconds(r.creationDate)}\r\n"));
Console.ReadLine();
}
}
class repo
{
public long creationDate { get; set; }
public string name { get; set; }
public string type;
}
爬虫五:几乎什么都能抓、但并不仅仅用于爬虫的终极利器Selenium
selenium是一套webdriver组件,支持Chrome, Firefox, IE, PhantomJS。本文以Chrome做演示,还是用爬虫四中的网站为例,登录并创建五个repo。
因为网站HTML代码由服务器源源不断向webdriver控制的浏览器主动推送过来,所以无论以什么频率,在什么时间查看浏览器里的源代码,都只是读取已经保存下来的本地资源,服务器无法感知,没有任何负面影响。
由于需要渲染页面,和不断地检测页面加载是否完成,这种方式效率更低,但是由于它使用的是真实浏览器,而不再是“模拟”,并且可以精准地进行页面交互,selenium能够更轻松地胜任更多更复杂的任务,一般自动化测试都倾向于使用它,而爬虫则更多地考虑直接用http client模拟。
在使用selenium时,首先需要对业务全程有充分的了解,知道每一步会遇到什么,如何处理,弄清楚以后再将所需的操作翻译成浏览器的操作指令(本质上就是用自己偏爱的语言编写操作浏览器的宏)。
而操作指令所包含的内容,就是根据条件进行判断和操作:在何种条件下,找到哪个/哪些网页元素,进行何种操作。这一步的关键在于如何找到目标元素,selenium可以通过LinkText,Tagname, classname, Id, XPath等各种手段定位元素,具体根据何种方式定位,根据实际情况选择即可,往往有多种方式可以实现相同的目的。
Nuget库:Selenium.WebDriver、Selenium.WebDriver.ChromeDriver
操作流程:
- 打开页面并等待加载完成(通过关键的网页元素是否存在来判断,比如确定登录页面是否加载完成,就可以拿用户名和密码输入框,以及登录按钮作依据);
- 填入用户名、密码,点击登录按钮;
- 点击Repositories链接,进入代码库列表页面;
- 点击Add按钮打开表单;
- 填写表单中的库名、库类型;
- 点击Ok按钮提交。
class Program
{
static void Main(string[] args)
{
IWebDriver browser = new ChromeDriver();
browser.Navigate().GoToUrl("http://192.168.2.72:8080/scm/");
//网页加载过程中循环查看源码,直到出现用户名输入框、密码输入框和登录按钮为止认为
//加载完成,这个认定标准由自己按需制定
while (!browser.PageSource.Contains("id=\"loginButton\"") ||
!browser.PageSource.Contains("type=\"password") ||
!browser.PageSource.Contains("id=\"username")) Thread.Sleep(2000);
//填表,提交
browser.FindElement(By.Id("username")).SendKeys("scmadmin");
browser.FindElement(By.Name("password")).SendKeys("scmadmin");
Thread.Sleep(500);//填完必填项后,等半秒,提交按钮解除禁用状态再点击
browser.FindElement(By.ClassName("x-btn-text")).Click();
//点击进入Repositories界面
while (!browser.PageSource.Contains("Repositories</a>")) Thread.Sleep(2000);
browser.FindElement(By.LinkText("Repositories")).Click();
for (int i = 0; i < 5; i++)
{
//点击提交按钮
while (!browser.PageSource.Contains("resources/images/add.png")) Thread.Sleep(2000);
browser.FindElement(By.Id("repositoryAddButton")).FindElement(By.TagName("tbody")).
FindElements(By.TagName("button"))[0].Click();
while (!browser.PageSource.Contains("id=\"repositoryName\"") ||
!browser.PageSource.Contains("id=\"repositoryType") ||
!browser.PageSource.Contains(">Ok</button>")) Thread.Sleep(2000);
//填写库名
browser.FindElement(By.Id("repositoryName")).SendKeys(
"repo-test-" + DateTime.Now.ToString("yyMMddHHmmss"));
browser.FindElement(By.Id("repositoryType")).Click();
while (!browser.PageSource.Contains("x-combo-list-item")) Thread.Sleep(2000);
//点击下拉菜单里的Subversion
string repotype = (i & 1) == 1 ? "Subversion" : "Git";
browser.FindElements(By.ClassName("x-combo-list-item")).
Where(e => e.Text == repotype).First().Click();
Thread.Sleep(500);//填完必填项后,等半秒,提交按钮解除禁用状态再点击
browser.FindElement(By.XPath("//button[text()='Ok']")).Click();
Thread.Sleep(2000);
}
}
}