大牛分享:.Net实现拉勾网爬虫
优采云 发布时间: 2020-09-04 21:12.Net实现拉钩网采集器
几天前,我看到一个用.NET Core编写的采集器,我有点莫名其妙的兴奋。我一直在使用Gathering来搜寻有关Lagou的招聘信息。这个傻瓜式工具等同于用HTML模板页面标记DOM。节点,然后在跟踪节点信息的同时在浏览器窗口上模拟人类的浏览行为。它有很多优点,但缺点也很明显:爬行速度慢;麻烦的数据清理和转储;仅了解过程,但不了解原理,网站更改了模板或需要抓取其他网站来再现效率,而是自己编写程序。
然后自己实施?做吧!
首先了解需要提取的网页结构。对于搜索结果,您需要单击控件以显示分页。别那么麻烦。检查网络,发现每次单击下一页时,都会将异步POST请求发送到一个地址:
URL:https://www.lagou.com/jobs/positionAjax.json?px=new&needAddtionalResult=false
其请求数据为(以.Net搜索的第二页为例):first = false&pn = 2&kd = .NET
很明显,pn和kd分别在页码中传递并搜索关键词。
再次检查其响应消息,它在单个页面上返回所有作业信息,格式为JSON:
您可以使用JavaScriptSerializer类的DeserializeObject方法将序列反向转换为字典。
有关作业详细信息(每个作业的主页),返回html。 Html Agility Pack在该工具包之前用于解析html,但是据说AngleSharp具有更好的性能,我计划这次替换它。
我立即想到使用Socket作为客户端程序。我首先尝试.NET Core,但发现它缺少很多库,这太麻烦了。我仍然使用.NET Framework。我很快遇到302重定向问题和Https证书问题。诸如线程阻塞,套接字更难以处理,果断地放弃它,HttpWebRequest很简单等一系列问题,但是Post请求也会发生302错误,掩盖了普通浏览器的请求标头或重定向它无法解决,尝试更改为Get方法后,发现所有问题都可以避免,而我不由得感到高兴,偶然访问得太频繁了,结果如下:
这可以阻止我吗?您是如此困难,只需将整个车站关闭即可。
我碰巧有一个尚未过期的Azure帐户,因此我可以打开一个虚拟机来玩。
测试成功后,编写一个正式程序。我称之为入门级Lagoo 采集器。如果您使用更多它或将来遇到新问题,则必须对其进行升级。
根据面向对象的思想,该程序就像在不同的车床上构造零件,最后将它们组装成产品,整个过程得以简化。我的基本想法是,将单个采集器实例采集和一组关联关键词(某些关键词可能无法区分,例如C#和.Net),另存为单个xml文档(也可以保存到数据库,Excel,缓存在,我更习惯于另存为xml,然后映射到Excel文档),该过程使用Log4Net记录日志。
第一步:规定采集器材料获取方法:
创建一个类:LagouWebCrawler,定义其构造函数和注册字段:
class LagouWebCrawler
{
string CerPath;//网站证书本地保存地址
string XmlSavePath;//xml保存地址
string[] PositionNames;//关联关键词组
ILog LogToTxt;//Log4Net控制器
///
/// 引用拉勾职位采集器
///
/// 证书所在位置
/// xml文件写入地址/param>
/// 关联关键词组
/// Log4Net控制器
public LagouWebCrawler(string _cerPath, string _xmlSavePath,string [] _positionNames ,ILog log)
{
this.CerPath = _cerPath;
this.XmlSavePath = _xmlSavePath;
this.LogToTxt = log;
this.PositionNames = _positionNames;
}
第2步:设计采集器的行为
接下来,定义此采集器的行为。在采集器中,将主要功能用作其他功能的开始区域。主要函数名为CrawlerStart,它仅负责将搜索关键词组和json字符串拆分(读取(反序列化为字典))并写入最终的xml。它具有子功能,负责读取字典(数据清理)和将节点写入xml。该子函数名为JobCopyToXML,而xml及其节点的编写使用XDocument和XElement进行操作。由于必须从搜索列表中进入每个作业的主页以获取详细信息,并检查从Internet下载的数据,以清理一些会导致书写错误的控制字符,因此两个人负责网络爬网和特殊操作。字符。这两个函数调用名为GetHTMLToString和ReplaceIllegalClar的清洗方法。
2. 1主要功能:
main函数需要一些成员变量来存储XDocument和XElement对象,以及位置的统计信息和索引。同时,它还需要回调证书验证(我不知道这是什么,我没有时间直接从Internet学习和复制)。
XDocument XWrite;//一组关联词搜索的所有职位放入一个XML文件中
XElement XJobs;//XDocument根节点
List IndexKey;//寄存职位索引键,用于查重。
int CountRepeat = 0;//搜索结果中的重复职位数
int CountAdd = 0;//去重后的总职位数
///
/// 爬取一组关联关键词的数据,格式清洗后存为xml文档
///
/// int[0]+int[1]=总搜索结果数;int[0]=去重后的结果数;int[1]=重复数
public int[] CrawlerStart()
{
XWrite = new XDocument();
XJobs = new XElement("Jobs");//根节点
IndexKey = new List();
foreach (string positionName in PositionNames)//挨个用词组中的关键词搜索
{
for (int i = 1; i 0)//可能关键词搜不到内容
{
XWrite.Add(XJobs);//将根节点添加进XDocument
//XmlDocument doc = new XmlDocument();
//doc.Normalize();
XWrite.Save(XmlSavePath);
LogToTxt.Info("爬取了一组关联词,添加了" + CountAdd + "个职位,文件地址:" + XmlSavePath);
}
return new int[] { CountAdd, CountRepeat };
}
catch (Exception ex)
{
LogToTxt.Error("XDocument导出到xml时失败,文件:" + XmlSavePath + ",错误信息:" + ex);
return new int[] { 0,0};
}
return new int[] { CountAdd, CountRepeat };
}
///
/// 回调验证证书-总是返回true-跳过验证
///
private bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { return true; }
2. 2个子功能:
///
/// 将每个职位数据清洗后添加到XDocument对象的根节点下
///
private bool JobCopyToXML(Dictionary dt)
{
int id = Convert.ToInt32(dt["positionId"]);//职位详情页的文件名,当作索引键
if (IndexKey.Contains(id))//用不同关联词搜出的职位可能有重复。
{
CountRepeat++;// 新增重复职位统计
return true;
}
IndexKey.Add(id);//添加一个索引
XElement xjob = new XElement("OneJob");
xjob.SetAttributeValue("id", id);
string positionUrl = @"https://www.lagou.com/jobs/" + id + ".html";//职位主页
try
{
xjob.SetElementValue("职位名称", dt["positionName"]);
xjob.SetElementValue("薪酬范围", dt["salary"]);
xjob.SetElementValue("经验要求", dt["workYear"]);
xjob.SetElementValue("*敏*感*词*要求", dt["education"]);
xjob.SetElementValue("工作城市", dt["city"]);
xjob.SetElementValue("工作性质", dt["jobNature"]);
xjob.SetElementValue("发布时间", Regex.Match(dt["createTime"].ToString(), @"[\d]{4}-[\d]{1,2}-[\d]{1,2}").Value);
xjob.SetElementValue("职位主页", positionUrl);
xjob.SetElementValue("职位诱惑", dt["positionAdvantage"]);
string html = GetHTMLToString(positionUrl, CerPath);//从职位主页爬取职位和企业的补充信息
var dom = new HtmlParser().Parse(html);//HTML解析成IDocument,使用Nuget AngleSharp 安装包
//QuerySelector :选择器语法 ,根据选择器选择dom元素,获取元素中的文本并进行格式清洗
xjob.SetElementValue("工作部门", dom.QuerySelector("div.company").TextContent.Replace((string)dt["companyShortName"], "").Replace("招聘", ""));
xjob.SetElementValue("工作地点", dom.QuerySelector("div.work_addr").TextContent.Replace("\n", "").Replace(" ", "").Replace("查看地图", ""));
string temp = dom.QuerySelector("dd.job_bt>div").TextContent;//职位描述,分别去除多余的空格和换行符
temp = string.Join(" ", temp.Trim().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
xjob.SetElementValue("职位描述", string.Join("\n", temp.Split(new string[] { "\n ", " \n", "\n" }, StringSplitOptions.RemoveEmptyEntries)));
xjob.SetElementValue("企业官网", dom.QuerySelector("ul.c_feature a[rel=nofollow]").TextContent);
xjob.SetElementValue("企业简称", dt["companyShortName"]);
xjob.SetElementValue("企业全称", dt["companyFullName"]);
xjob.SetElementValue("企业规模", dt["companySize"]);
xjob.SetElementValue("发展阶段", dt["financeStage"]);
xjob.SetElementValue("所属领域", dt["industryField"]);
xjob.SetElementValue("*敏*感*词*页", @"https://www.lagou.com/gongsi/" + dt["companyId"] + ".html");
XJobs.Add(xjob);
CountAdd++;//新增职位统计
return true;
}
catch (Exception ex)
{
LogToTxt.Error("职位转换为XElement时出错,文件:"+ XmlSavePath+",Id="+id+",错误信息:"+ex);
Console.WriteLine("职位转换为XElement时出错,文件:" + XmlSavePath + ",Id=" + id + ",错误信息:" + ex);
return false;
}
}
2. 3 Web采集器:
///
/// Get方式请求url,获取报文,转换为string格式
///
private string GetHTMLToString(string url, string path)
{
Thread.Sleep(1500);//尽量模仿人正常的浏览行为,每次进来先休息1.5秒,防止拉勾网因为访问太频繁屏蔽本地IP
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.ClientCertificates.Add(new X509Certificate(path));//添加证书
request.Method = "GET";
request.KeepAlive = true;
request.Accept = "text/html, application/xhtml+xml, */*";
request.ContentType = "text/html";
request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
request.Credentials = CredentialCache.DefaultCredentials;//添加身份验证
request.AllowAutoRedirect = false;
byte[] responseByte = null;
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
using (MemoryStream _stream = new MemoryStream())
{
response.GetResponseStream().CopyTo(_stream);
responseByte = _stream.ToArray();
}
string html = Encoding.UTF8.GetString(responseByte);
return ReplaceIllegalClar(html);//进行特殊字符处理
}
catch (Exception ex)
{
LogToTxt.Error("网页:" + url + ",爬取时出现错误:" + ex);
Console.WriteLine("网页:" + url + ",爬取时出现错误:" + ex);
return "";
}
}
2. 4特殊字符处理:
private string ReplaceIllegalClar(string html)
{
StringBuilder info = new StringBuilder();
foreach (char cc in html)
{
int ss = (int)cc;
if (((ss >= 0) && (ss = 11) && (ss = 14) && (ss 0)
{
string str = xmlSavePath + ":用时" + sw.Elapsed + ";去重后的总搜索结果数=" + count[0] + ",搜索结果中的重复数=" + count[1];
Console.WriteLine(str);
}
else
{
Console.WriteLine("遇到错误,详情请检查日志");
}
Console.ReadKey();
关于Internet上Log4Net的配置有很多共享。我更改了日志格式,以使其更易于阅读:
采集整个网站的结果: