关注我,持续分享逻辑思维&管理思维; 可提供大厂面试辅导、及定制化求职/在职/管理/架构辅导;
有意找工作的同学,请参考博主的原创:《面试官心得--面试前应该如何准备》,《面试官心得--面试时如何进行自我介绍》《做好面试准备,迎接2024金三银四》。
推荐热榜内容:《架构实战--以海量存储系统讲解热门话题:分布式概念》
-------------------------------------正文----------------------------------------
博主曾经运营过一个段子网,当时写了个网络爬虫,抓取网上的段子到自己的网站数据库,以便我的段子网可以直接浏览这些内容。为便于观看,及便于让大家理解,以下代码我节选主要的内容,并在这次分享的过程中,加入了一些注释,以便大家理解。
using System; using System.Collections.Generic; using System.Text; using System.Collections; using xu_common.log; namespace data_collection.spider_core { class UrlSet { public UrlSet() { // 定义三个队列,准备抓取的URL集合,已经抓取的URL集合,错误的URL集合 // 这里需要注意,因为我是定向抓取,这里URL不会太多。如果是全网抓取,URL太多,要防止内存泄露 _going_to_parse = new ArrayList(); _already_parse = new ArrayList(); _error_link = new ArrayList(); } private static UrlSet __instance = null; public static UrlSet instance { get { if (__instance == null) { __instance = new UrlSet(); } return __instance; } } private ArrayList _going_to_parse = null; private ArrayList _already_parse = null; private ArrayList _error_link = null; // 判断URL是否抓取过,并根据参数add是否true来判断这个URL是否入库 private bool is_url_parsed(string url, bool add) { bool rv; lock (_already_parse.SyncRoot) { rv = _already_parse.Contains(url); if (!rv && add) _already_parse.Add(url); } return rv; } // 判断URL是否抓取过 private bool is_url_parsed(string url) { return is_url_parsed(url, false); } // 判断URL是否在待抓取列表,并根据参数add是否true来判断这个URL是否加入待抓取 private bool is_url_going_to_parse(string url, bool add) { bool rv; lock (_going_to_parse.SyncRoot) { rv = _going_to_parse.Contains(url); if (!rv && add) _going_to_parse.Add(url); } return rv; } // 判断URL是否在待抓取列表 private bool is_url_going_to_parse(string url) { return is_url_going_to_parse(url, false); } // 判断URL是否在错误URL列表,并根据add来确定是否要加入此列表 private bool is_url_error_lnk(string url, bool add) { bool rv; lock (_error_link.SyncRoot) { rv = _error_link.Contains(url); if (!rv && add) _already_parse.Add(url); } return rv; } private bool is_url_error_lnk(string url) { return is_url_error_lnk(url, false); } ////// 把一个Url加到待解析列表中. /// 如果已经解析过,返回-1; /// 如果是坏链,返回-2 /// 如果已经在待解析列表中,返回1. /// 否则加入待解析列表,并且返回0 /// /// ///>=0:OK, <0:ERROR public int add_going_parse_url(string url) { lock (_going_to_parse.SyncRoot) { if (is_url_parsed(url)) { return -1; } if (is_url_error_lnk(url)) { return -2; } if (is_url_going_to_parse(url, true)) return 1; //_going_to_parse.Add(url); return 0; } } ////// 添加一个已经抓取过的链接,如果此链接在待抓取或者坏链中,删除 /// 如果已经在抓取过列表中,返回-1,否则返回0 /// /// ///0:OK,-1:ERROR public int add_parsed_url(string url) { // already parse, not use to parse again. if (is_url_going_to_parse(url)) { _going_to_parse.Remove(url); } if (is_url_error_lnk(url)) { _error_link.Remove(url); //return -1; } if (is_url_parsed(url, true)) { return -1; } //_already_parse.Add(url); return 0; } ////// 添加一个错误的链接.如果该链接在待抓取列表中,删除(说明不应该抓取) /// /// ///0:OK; -1:ERROR public int add_error_url(string url) { if (is_url_going_to_parse(url)) { _going_to_parse.Remove(url); } /* if (is_url_parsed(url)) { return -2;//都已经解析过了,还加进这里去干嘛? never go to here,因为解析过的话,不可能再拿来解析,然后到这里的 } * */ if (is_url_error_lnk(url, true)) { return -1; } //_error_link.Add(url); return 0; } ////// 把代解析的第一个节点抓下来,成功,则url有值,并返回true, 不然返回错误 /// /// ///public bool pop_going_to_parse_url(ref string url) { url = ""; bool rv = false; lock (_going_to_parse.SyncRoot) { if (_going_to_parse.Count <= 0) { rv = false; } else { url = _going_to_parse[0].ToString(); _going_to_parse.RemoveAt(0); rv = true; } } return rv; } public int going_to_parse_url_num() { int ret = 0; lock (_going_to_parse.SyncRoot) { ret = _going_to_parse.Count; } return ret; } private string[] _no_parse_keyword = null; private int _no_parse_type = 3; public void SetNoParseKeyWord(string str, string split, int type) { _no_parse_keyword = xu_common.CommonOperator.Split(str, split); _no_parse_type = type; } public bool IsNoParse(string url) { LogMsg.LogError(url + ", no_parse_type="+ _no_parse_type.ToString()); if (_no_parse_type == 1) { for (int i = 0; i < _no_parse_keyword.Length; i++) { if (url.Contains(_no_parse_keyword[i])) return false; } return true; } else if(_no_parse_type==2) { for (int i = 0; i < _no_parse_keyword.Length; i++) { if (url.Contains(_no_parse_keyword[i])) return true; } return false; } return false; } #region write back to file: ToString public string GoingParseToString() { string ret = ""; int count = _going_to_parse.Count; for (int i = 0; i < count-1; i++) { ret += _going_to_parse[i].ToString() + "\r\n"; } if (count > 0) ret += _going_to_parse[count - 1].ToString(); _going_to_parse.Clear(); return ret; } public string AlreadyParsedToString() { string ret = ""; int count = _already_parse.Count; for (int i = 0; i < count - 1; i++) { ret += _already_parse[i].ToString() + "\r\n"; } if (count > 0) ret += _already_parse[count - 1].ToString(); _already_parse.Clear(); return ret; } public string ErrorUrlToString() { string ret = ""; int count = _error_link.Count; for (int i = 0; i < count - 1; i++) { ret += _error_link[i].ToString() + "\r\n"; } if (count > 0) ret += _error_link[count - 1].ToString(); _error_link.Clear(); return ret; } #endregion } }
以下代码是一个线程抓取文件(非网页内容,如图片、文件)的主要代码:设置一个URL,抓取后把URL放到已抓取队列,并保存网页内容到文件中; 抓取失败,则把URL放到错误队列。
有些注释掉的 MessageBox是博主当时调试用的,大家可忽略。
namespace data_collection.spider_core { // 继承的Task类是博主写的公共类,这里不是关键代码,不再贴出。 // 这里是一个子线程。需要设置抓取的URL和网页文件保存路径。为了加快抓取速度,需要启动多线程。 class DownFileTask : xu_common.thread.Task { public override void Run() { FileStream fileStream = new FileStream(_filepath, FileMode.Append|FileMode.Create, FileAccess.Write); Stream inStream = null; try { HttpWebRequest myre = (HttpWebRequest)WebRequest.Create(_url); if (fileStream.Length == myre.ContentLength) { //MessageBox.Show("你已完成下载该程序了", "ok"); return; } myre.AddRange(Convert.ToInt32(fileStream.Length));//接上次下载的字节开始下载文件 HttpWebResponse response = (HttpWebResponse)myre.GetResponse(); inStream = response.GetResponseStream(); //this.progressBar1.Maximum = total; //this.progressBar1.Minimum = 0; int length = 1024; byte[] buffer = new byte[1025]; int readerLength = 0, currentLength = 0; while ((readerLength = inStream.Read(buffer, 0, length)) > 0) { currentLength += readerLength; fileStream.Write(buffer, 0, readerLength); //this.progressBar1.Value = currentLength + countByte; fileStream.Flush(); } fileStream.Close(); inStream.Close(); //File.Delete(Application.StartupPath + @"\FileLength.txt"); //MessageBox.Show("down 成功", "ok"); // 抓取成功,这个URL放入已抓取队列 UrlSet.instance.add_parsed_url(_url); } catch (Exception ex) { xu_common.log.LogMsg.LogError("down file:" + _url + ", error.msg:" + ex.ToString()); // 抓取失败,这个URL放入已失败队列 UrlSet.instance.add_error_url(_url); } } public void SetUrl(string url) { _url = url; } private string _url; public void SetFilePath(string filepath) { _filepath = filepath; } private string _filepath; } }
以上是下载文件和图像的代码。
------------------------------------------
以下代码是下载网页的核心代码。
1.初始化代抓取的URL,设置浏览器代理(欺骗对应网站)。
2.下载URL对应的网页内容。
3.从URL的内容中解析结构化内容(根据规则),同时根据网页内容,解析到很多URL(网页的外链)。
3.1 如果URL是需要下载的文件或图片,则新起一个线程,用上面的代码下载。
3.2 如果URL是要继续抓取的网页URL,则放到待爬取链接。
注(重要):此处,抓取到的内容(保存为_content)的解析,这里博主写了一个规则处理器。所以下面代码一句话带过。规则处理器比较简单,根据下载的网站不同。比如你想下载 http://xx.com/a/1.html,网站的内容,一般是通用的,我们就可以设置一个规则:只要满足URL为http://xx.com/a/*.html的,则按以下规则处理:跳过………………等代码,直到找到内容开始处,这里需要我们实际看一下http://xx.com/a/1.html的源码,一般是或
using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Net; using xu_common.log; using System.Collections; using System.IO; using System.IO.Compression; using System.Security.Policy; namespace data_collection.spider_core { class WebGetHtml { // 初始化要抓取的网页URL public WebGetHtml(string parse_url) { _url = parse_url; } string _url = null; string _tag_base_url = null; string _title; string _base_url = null; string _base_top_url = null; string _content; private void GenBaseUrl() { if (_tag_base_url == null) { _base_top_url = global_var.RegTopUrl.Match(_url).Value; string not_param_url = null; int qPisition = this._url.IndexOf("?"); if (qPisition < 0) { not_param_url = _url; } else { not_param_url = _url.Substring(0, qPisition); } _base_url = Regex.Replace(not_param_url, "(?<=.*/)[^/]*$", "", RegexOptions.Compiled | RegexOptions.IgnoreCase); } else { _base_top_url = global_var.RegTopUrl.Match(_tag_base_url).Value; _base_url = _tag_base_url; } if (_base_url.EndsWith("/")) { _base_url = _base_url.Substring(0, _base_url.Length - 1); } } public class UrlType { public static int UrlTypeImg = 1; public static int UrlTypeFile = 2; public static int UrlTypeHtml = 3; public static int UrlTypeError = 4; public static int UrlTypeSelf = 5; public static int UrlTypeOtherFile = 6; }; // 检查URL类型 private int CheckUrl(string UrltoCheck) { if (Regex.IsMatch(UrltoCheck, "^#*$", RegexOptions.IgnoreCase | RegexOptions.Compiled)) return UrlType.UrlTypeError; else if (UrltoCheck == _url || (UrltoCheck + "/") == _url || UrltoCheck == (_url + "/")) return UrlType.UrlTypeSelf; else if (UrltoCheck.EndsWith(".css")) return UrlType.UrlTypeOtherFile; else if (UrltoCheck.EndsWith(".wmv")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".asf")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".mp3")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".avi")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".mpg")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".mpeg")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".rmvb")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".rm")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".doc")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".rar")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".zip")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".tar")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".xls")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".pdf")) return UrlType.UrlTypeFile; else if (UrltoCheck.EndsWith(".jpg")) return UrlType.UrlTypeImg; else if (UrltoCheck.EndsWith(".jpeg")) return UrlType.UrlTypeImg; else if (UrltoCheck.EndsWith(".ico")) return UrlType.UrlTypeImg; else if (UrltoCheck.EndsWith(".gif")) return UrlType.UrlTypeImg; else if (UrltoCheck.EndsWith(".bmp")) return UrlType.UrlTypeImg; else if (UrltoCheck.EndsWith(".png")) return UrlType.UrlTypeImg; else if (UrltoCheck.StartsWith("ftp://")) return UrlType.UrlTypeError; else if (UrltoCheck.StartsWith("telnet://")) return UrlType.UrlTypeError; else if (UrltoCheck.StartsWith("mms://")) return UrlType.UrlTypeError; else if (UrltoCheck.StartsWith("rstp://")) return UrlType.UrlTypeError; else if (UrltoCheck.StartsWith("mailto")) return UrlType.UrlTypeError; else if (UrltoCheck.StartsWith("javascript")) return UrlType.UrlTypeError; else return UrlType.UrlTypeHtml; } //确定URL是否属于要抓取的网站 private bool CheckUrlThisSite(string NewUrltoCheck) { //return Form1.instance.CheckUrlToParse(NewUrltoCheck); return global_var.Instance.IsInSite(NewUrltoCheck); } // 有些网页的链接不是以http开头,而是相对链接。需要处理这些不规则链接 private string GenUrl(string incomeUrl) { if (incomeUrl.StartsWith("http://")) return incomeUrl; else { /* * /x.aspx 直接是base里的(或者窗口TopUrl)顶级域名,加上x.aspx x.aspx 直接是base里的(或者窗口TopUrl)Url,加上x.aspx ./x.aspx 直接是base里的(或者窗口TopUrl)Url,加上x.aspx(同上) ../x.aspx 直接是base里的(或者窗口TopUrl)Url的上一层,加上x.aspx */ if (incomeUrl.StartsWith("/")) { string trueUrl = _base_top_url + incomeUrl; return trueUrl; } int parent_depth = 0; while (incomeUrl.StartsWith(".")) { if (incomeUrl.StartsWith("../")) { parent_depth += 1; incomeUrl.Substring(3, incomeUrl.Length - 3); } else if (incomeUrl.StartsWith("./")) { incomeUrl = incomeUrl.Substring(2, incomeUrl.Length - 2); } else return null; } string head_str = _base_url; if (parent_depth > 0) { for (int i = 0; i < parent_depth; i++) { int qposition = head_str.LastIndexOf("/"); if (qposition < 0) { // not_http_lenght_not_enough head_str = _base_top_url; break; } head_str = head_str.Substring(0, qposition); } } if (head_str.StartsWith("http:") && head_str.Length < "http://".Length) { //不是http开头的,长度不够的情况,在前面 not_http_lenght_not_enough 处理了 //说明:如果base_url=http://a.com/b/,但是有../../../a.html,没有足够的目录的情况下 //Url是指:http://a.com/b/a.html head_str = _base_top_url; } return head_str + "/" + incomeUrl; } } //下载网页内容 private bool WebGetContent() { HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(_url); // 设置浏览器代理 myRequest.UserAgent = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36"; //Encoding encode = System.Text.Encoding.GetEncoding(936); HttpWebResponse myResponse = (HttpWebResponse)myRequest.GetResponse(); //myResponse.ContentEncoding; //LogMsg.instance.LOG(LogMsg.DEBUG, "begin to parse response"); //LogMsg.LogDebug("begin to parse response"); if (myResponse.StatusCode != HttpStatusCode.OK) { LogMsg.LogError("get url:" + _url + ", error. status:"+myResponse.StatusDescription); return false; } Stream src_stream = myResponse.GetResponseStream(); StreamReader myStreamReader = null; if (myResponse.ContentEncoding != null && myResponse.ContentEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase)) { myStreamReader = new StreamReader(new GZipStream(src_stream, CompressionMode.Decompress), Encoding.Default); } else { myStreamReader = new StreamReader(src_stream, Encoding.Default); } ArrayList response_bytes_array = new ArrayList(); int i_byte = 0; while (true) { i_byte = myStreamReader.BaseStream.ReadByte(); if (i_byte == -1) break; response_bytes_array.Add(Convert.ToByte(i_byte)); } byte[] response_bytes = new byte[response_bytes_array.Count]; for(int i=0;i三、管理和调度任务
这里管理和调度任务不再赘述,如上面下载文件和图片一样,可以使用多线程进行下载。这些管理代码不难,且上面已有示例,下载网页的任务调度类似。保存在Url队列中,按顺序启动多线程去获取内容。
四、保存数据
这块代码比较隐私。博主在上面的代码处也有说明。是根据对应的网站,在获取网页源码内容后,需要自己写规则解析,解析到对应的内容后,直接调用DB操作类写入到数据库。
以上就是本次分享的内容。如果有兴趣的,欢迎关注博主私聊。
博主其它经典原创:《管理心得--工作目标应该是解决业务问题,而非感动自己》,《管理心得--如何高效进行跨部门合作》,《管理心得--员工最容易犯的错误:以错误去掩盖错误》,《技术心得--如何成为优秀的架构师》、《管理心得--如何成为优秀的架构师》、《管理心理--程序员如何选择职业赛道》。欢迎大家阅读。