用C#爬虫来抓取网页并解析
C# 网络爬虫整理
一、前言
在学了C#的网络爬虫之后,深感C#的强大,和爬虫的有趣,在这里将自己的心得体会记下来,以便日后的学习和回顾。这里有两个程序,首先是一个简单的抓取网页程序,将网页抓取下来之后用正则表达式进行解析,从而得到相应的信息。
二、C#网页爬虫
这里有几个要点:
第一:抓取的源
当我们想抓网页,首先就要知道该网页的具体内容,包含的主要信息,之后我们对信息进行处理,可以确定我们要抓取的网页数量,有两种抓取方法,一种是深度优先,一种是广度优先,最终抓取出所有自己想要的网页。
第二,对源的处理
我们知道我们抓取的是整个网页,有很多信息是我们不需要的,因此有两种处理方法,一种是用正则表达式来处理,另外一种是对DOM(Document Object Model)结构的数据用XPath函数来处理.
第三:处理过程中我们要用到线程
也就是异步任务,因此我们需要对其进行学习,理解async和await这一对孪生兄弟的用法。
1 a) 只有在async方法里面才能使用await操作符; 2 b) await操作符是针对Task对象的; 3 c) 当方法A调用方法B,方法B方法体内又通过await调用方法C时,如果方法C内部有异步操作,则方法B会等待异步操作执行完,才往下执行;但方法A可以继续往下执行,不用再等待B方法执行完。
1 static void Main(string[] args)
2
3 {
4
5 Test();
6
7 Console.WriteLine("Test End!");
8
9 Console.ReadLine();
10
11 }
12
13 static async void Test()
14
15 {
16
17 await Test1();
18
19 Console.WriteLine("Test1 End!");
20
21 }
22
23 static Task Test1()
24
25 {
26
27 Thread.Sleep(1000);
28
29 Console.WriteLine("create task in test1");
30
31 return Task.Run(() =>
32
33 {
34
35 Thread.Sleep(3000);
36
37 Console.WriteLine("Test1");
38
39 });
40
41 }
相当于代码:
1 static void Main(string[] args)
2
3 {
4
5 Test();
6
7 Console.WriteLine("Test End!");
8
9 Console.ReadLine();
10
11 }
12
13 static void Test()
14
15 {
16
17 var test1=Test1();
18
19 Task.Run(() =>
20
21 {
22
23 test1.Wait();
24
25 Console.WriteLine("Test1 End!");
26
27 });
28
29 }
30
31 static Task Test1()
32
33 {
34
35 Thread.Sleep(1000);
36
37 Console.WriteLine("create task in test1");
38
39 return Task.Run(() =>
40
41 {
42
43 Thread.Sleep(3000);
44
45 Console.WriteLine("Test1");
46
47 });
48
49 }
第四:在C#中大量的出现lambda表达式,我们要对其有深刻的理解和认识。
比如:
1 cityCrawler.OnStart += (s, e) => 2 { 3 Console.WriteLine("爬虫开始抓取地址:" + e.Uri.ToString()); 4 };
我们只有深刻的认识了lambda表达式,才能更好的使用和理解它。
第五:
我们的爬虫是怎么伪造浏览器来进行抓包的,如果大量的抓包会被服务器警觉,我们要采用代理来解决这一问题。
第六:
对EventHandler的认识,它的构造有两个参数,一个是当前的上下文,一个是具体的对象(这个对象是我们自己创建的,在该委托的模板中进行传递)。
第七:并发处理。
1 Parallel.For(0, 2, (i) => 2 { 3 var hotel = hotelList[i]; 4 hotelCrawler.Start(hotel.Uri); 5 });
而For函数的定义如下:
1 // 摘要:
2
3 // 执行 for(在 Visual Basic 中为 For)循环,其中可能会并行运行迭代。
4
5 //
6
7 // 参数:
8
9 // fromInclusive:
10
11 // 开始索引(含)。
12
13 //
14
15 // toExclusive:
16
17 // 结束索引(不含)。
18
19 //
20
21 // body:
22
23 // 将为每个迭代调用一次的委托。
24
25 //
26
27 // 返回结果:
28
29 // 包含有关已完成的循环部分的信息的结构。
30
31 public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int> body);
第八:计时函数
var watch = newStopwatch(); watch.Start(); 。。。。 watch.Stop() var milliseconds = watch.ElapsedMilliseconds;
第九:伪造浏览器
1 public async Task<string> Start(Uri uri,string proxy=null)
2
3 {
4
5 return await Task.Run(() =>
6
7 {
8
9 var pageSource = string.Empty;
10
11 try
12
13 {
14
15 if (this.OnStart != null) this.OnStart(this, newOnStartEventArgs(uri));
16
17 var watch = newStopwatch();
18
19 watch.Start();
20
21 var request = (HttpWebRequest)WebRequest.Create(uri);
22
23 request.Accept = "*/*";
24
25 request.ServicePoint.Expect100Continue = false;//加快载入速度
26
27 request.ServicePoint.UseNagleAlgorithm = false;//禁止Nagle算法加快载入速度
28
29 request.AllowWriteStreamBuffering = false;//禁止缓冲加快载入速度
30
31 request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");//定义gzip压缩页面支持
32
33 request.ContentType = "application/x-www-form-urlencoded";//定义文档类型及编码
34
35 request.AllowAutoRedirect = false;//禁止自动跳转
36
37 //设置User-Agent,伪装成Google Chrome浏览器
38
39 request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36";
40
41 request.Timeout = 5000;//定义请求超时时间为5秒
42
43 request.KeepAlive = true;//启用长连接
44
45 request.Method = "GET";//定义请求方式为GET
46
47 if (proxy != null)request.Proxy = newWebProxy(proxy);//设置代理服务器IP,伪装请求地址
48
49 request.CookieContainer = this.CookiesContainer;//附加Cookie容器
50
51 request.ServicePoint.ConnectionLimit = int.MaxValue;//定义最大连接数
52
53 using (var response = (HttpWebResponse)request.GetResponse()) {//获取请求响应
54
55 foreach (Cookie cookie in response.Cookies) this.CookiesContainer.Add(cookie);
56
57 //将Cookie加入容器,保存登录状态
58
59 if (response.ContentEncoding.ToLower().Contains("gzip"))//解压
60
61 {
62
63 using (GZipStream stream = newGZipStream(response.GetResponseStream(), CompressionMode.Decompress))
64
65 {
66
67 using (StreamReader reader = newStreamReader(stream, Encoding.UTF8))
68
69 {
70
71 pageSource = reader.ReadToEnd();
72
73 }
74
75 }
76
77 }
78
79 elseif (response.ContentEncoding.ToLower().Contains("deflate"))//解压
80
81 {
82
83 using (DeflateStream stream = newDeflateStream(response.GetResponseStream(), CompressionMode.Decompress))
84
85 {
86
87 using (StreamReader reader = newStreamReader(stream, Encoding.UTF8))
88
89 {
90
91 pageSource = reader.ReadToEnd();
92
93 }
94
95 }
96
97 }
98
99 else
100
101 {
102
103 using (Stream stream = response.GetResponseStream())//原始
104
105 {
106
107 using (StreamReader reader = newStreamReader(stream, Encoding.UTF8))
108
109 {
110
111
112
113 pageSource= reader.ReadToEnd();
114
115 }
116
117 }
118
119 }
120
121 }
122
123 request.Abort();
124
125 watch.Stop();
126
127 var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;//获取当前任务线程ID
128
129 var milliseconds = watch.ElapsedMilliseconds;//获取请求执行时间
130
131 if (this.OnCompleted != null) this.OnCompleted(this, newOnCompletedEventArgs(uri, threadId, milliseconds, pageSource));
132
133 }
134
135 catch (Exception ex)
136
137 {
138
139 if (this.OnError != null) this.OnError(this, newOnErrorEventArgs(uri,ex));
140
141 }
142
143 return pageSource;
144
145 });
146
147 }
这是我们爬虫的主体部分,我们伪造浏览器,设置好一定的参数,进行访问服务器,得到结果然后解析结果,并显示。整个过程是非常恰当的。
第十:对Task状态的掌控。
1 public OnCompletedEventArgs(Uri uri, int threadId, long milliseconds, string pageSource)
2
3 {
4
5 this.Uri = uri;
6
7 this.ThreadId = threadId;
8
9 this.Milliseconds = milliseconds;
10
11 this.PageSource = pageSource;
12
13 }
14
15 public OnErrorEventArgs(Uri uri,Exception exception)
16
17 {
18
19 this.Uri = uri;
20
21 this.Exception = exception;
22
23 }
24
25 public OnStartEventArgs(Uri uri)
26
27 {
28
29 this.Uri = uri;
30
31 }
我们有三种状态,起始态,完成态,出错态。并且将它们扩展为委托事件,在程序中使用,非常的抽象和方便。
1 public eventEventHandler<OnStartEventArgs> OnStart;//爬虫启动事件 2 public eventEventHandler<OnCompletedEventArgs> OnCompleted;//爬虫完成事件 3 public eventEventHandler<OnErrorEventArgs> OnError;//爬虫出错事件
1 cityCrawler.OnStart += (s, e) =>
2
3 {
4
5 Console.WriteLine("爬虫开始抓取地址:" + e.Uri.ToString());
6
7 };
8
9 cityCrawler.OnError += (s, e) =>
10
11 {
12
13 Console.WriteLine("爬虫抓取出现错误:" + e.Uri.ToString() + ",异常消息:" + e.Exception.Message);
14
15 };
16
17 cityCrawler.OnCompleted += (s, e) =>
18
19 {
20
21 //使用正则表达式清洗网页源代码中的数据
22
23 var links = Regex.Matches(e.PageSource, @"<a[^>]+href=""*(?<href>/hotel/[^>\s]+)""\s*[^>]*>(?<text>(?!.*img).*?)</a>", RegexOptions.IgnoreCase);
24
25 foreach (Match match in links)
26
27 {
28
29 var city = newCity
30
31 {
32
33 CityName = match.Groups["text"].Value,
34
35 Uri = newUri("http://hotels.ctrip.com" + match.Groups["href"].Value
36
37 )
38
39 };
40
41 if (!cityList.Contains(city)) cityList.Add(city);//将数据加入到泛型列表
42
43 Console.WriteLine(city.CityName + "|" + city.Uri);//将城市名称及URL显示到控制台
44
45 }
46
47 Console.WriteLine("===============================================");
48
49 Console.WriteLine("爬虫抓取任务完成!合计 " + links.Count + " 个城市。");
50
51 Console.WriteLine("耗时:" + e.Milliseconds + "毫秒");
52
53 Console.WriteLine("线程:" + e.ThreadId);
54
55 Console.WriteLine("地址:" + e.Uri.ToString());
56
57 };
第十一:代理服务器和测试服务器。
//测试代理IP是否生效:http://1212.ip138.com/ic.asp
//测试当前爬虫的User-Agent:http://www.whatismyuseragent.net
三、加强版的网络爬虫
在简单版的基础上,这次我们不是直接伪造浏览器上网了,而是使用相应的工具来帮助我们进行网页解析。
首先我们需要四个DLL:
其次我们还是先定义一个接口类:
1 public interface ICrawler 2 3 { 4 5 eventEventHandler<OnStartEventArgs> OnStart;//爬虫启动事件 6 7 eventEventHandler<OnCompletedEventArgs> OnCompleted;//爬虫完成事件 8 9 eventEventHandler<OnErrorEventArgs> OnError;//爬虫出错事件 10 11 Task Start(Uri uri, Script script, Operation operation); //启动爬虫进程 12 13 }
然后,我们需要用到PlantomJS和Selenium这两个工具,前者是用来对webkit进行渲染,后者是用来自动化测试,让服务器感觉到就像是真人一样的在操作网页。
1 private PhantomJSOptions _options;//定义PhantomJS内核参数 2 3 private PhantomJSDriverService _service;//定义Selenium驱动配置 4 5 public StrongCrawler(string proxy = null) 6 7 { 8 9 this._options = newPhantomJSOptions();//定义PhantomJS的参数配置对象 10 11 this._service = PhantomJSDriverService.CreateDefaultService(Environment.CurrentDirectory); 12 13 //初始化Selenium配置,传入存放phantomjs.exe文件的目录 14 15 _service.IgnoreSslErrors = true;//忽略证书错误 16 17 _service.WebSecurity = false;//禁用网页安全 18 19 _service.HideCommandPromptWindow = true;//隐藏弹出窗口 20 21 _service.LoadImages = false;//禁止加载图片 22 23 _service.LocalToRemoteUrlAccess = true;//允许使用本地资源响应远程 URL 24 25 _options.AddAdditionalCapability(@"phantomjs.page.settings.userAgent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36"); 26 27 if (proxy != null) 28 29 { 30 31 _service.ProxyType = "HTTP";//使用HTTP代理 32 33 _service.Proxy = proxy;//代理IP及端口 34 35 } 36 37 else 38 39 { 40 41 _service.ProxyType = "none";//不使用代理 42 43 } 44 45 }
之后就该我们的主线程了Task异步操作:
1 public async Task Start(Uri uri,Script script, Operation operation) 2 3 { 4 5 awaitTask.Run(() => 6 7 { 8 9 if (OnStart != null) this.OnStart(this, newOnStartEventArgs(uri)); 10 11 var driver = newPhantomJSDriver(_service, _options);//实例化PhantomJS的WebDriver 12 13 try 14 15 { 16 17 var watch = DateTime.Now; 18 19 driver.Navigate().GoToUrl(uri.ToString());//请求URL地址 20 21 if (script != null) driver.ExecuteScript(script.Code, script.Args); 22 23 //执行Javascript代码 24 25 if (operation.Action != null) operation.Action.Invoke(driver); 26 27 var driverWait = newWebDriverWait(driver, TimeSpan.FromMilliseconds(operation.Timeout));//设置超时时间为x毫秒 28 29 if (operation.Condition != null) driverWait.Until(operation.Condition); 30 31 var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId; 32 33 //获取当前任务线程ID 34 35 var milliseconds = DateTime.Now.Subtract(watch).Milliseconds; 36 37 //获取请求执行时间; 38 39 var pageSource = driver.PageSource;//获取网页Dom结构 40 41 this.OnCompleted.Invoke(this, newOnCompletedEventArgs(uri, threadId, milliseconds, pageSource, driver)); 42 43 } 44 45 catch (Exception ex) 46 47 { 48 49 this.OnError.Invoke(this, newOnErrorEventArgs(uri, ex)); 50 51 } 52 53 finally 54 55 { 56 57 driver.Close(); 58 59 driver.Quit(); 60 61 } 62 63 }); 64 65 }
在这里与简单的不同,首先我们没有返回值,其次,我们用了驱动PhantomJSDriver来代替我们自己构造的http请求,也进行了一定的参数设置。其次我们的事件都用了Invoke()方法来调用,它的作用是让主线程来执行相应的操作,从而避免死锁。
Phantom JS是一个服务器端的 JavaScript API 的 WebKit。其支持各种Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG.
selenium官方加上第三方宣布支持的驱动有很多种;除了PC端的浏览器之外,还支持iphone、Android的driver.
PC端的driver都是基于浏览器的,主要分为2种类型:
一种是真实的浏览器driver
比如:safari、ff都是以插件形式驱动浏览器本身的;ie、chrome都是通过二进制文件来驱动浏览器本身的;这些driver都是直接启动并通过调用浏览器的底层接口来驱动浏览器的,因此具有最真实的用户场景模拟,主要用于进行web的兼容性测试使用。
一种是伪浏览器driver
selenium支持的伪浏览器包括htmlunit、PhantomJS;他们都不是真正的在浏览器、都没有GUI,而是具有支持html、js等解析能力的类浏览器程序;这些程序不会渲染出网页的显示内容,但是支持页面元素的查找、JS的执行等;由于不进行css及GUI渲染,所以运行效率上会比真实浏览器要快很多,主要用在功能性测试上面。
htmlunit是Java实现的类浏览器程序,包含在selenium server中,无需驱动,直接实例化即可;其js的解析引擎是Rhino.
PhantomJS是第三方的一个独立类浏览器应用,可以支持html、js、css等执行;其驱动是Ghost driver在1.9.3版本之后已经打包进了主程序中,因此只要下载一个主程序即可;其js的解析引擎是chrome 的V8。
再来看主函数,这里我们定义了一个Operation类,为的就是模拟正常人的操作,让selenium来执行。
1 var operation = newOperation 2 3 { 4 5 Action = (x) => { 6 7 //通过Selenium驱动点击页面的“酒店评论” 8 9 x.FindElement(By.XPath("//*[@id='commentTab']")).Click(); 10 11 }, 12 13 Condition = (x) => { 14 15 //判断Ajax评论内容是否已经加载成功 16 17 return x.FindElement(By.XPath("//*[@id='commentList']")).Displayed && x.FindElement(By.XPath("//*[@id='hotel_info_comment']/div[@id='commentList']")).Displayed && !x.FindElement(By.XPath("//*[@id='hotel_info_comment']/div[@id='commentList']")).Text.Contains("点评载入中"); 18 19 }, 20 21 Timeout = 5000 22 23 };
最后是解析方法:
1 private static void HotelCrawler(OnCompletedEventArgs e) { 2 3 //Console.WriteLine(e.PageSource); 4 5 //File.WriteAllText(Environment.CurrentDirectory + "\\cc.html", e.PageSource, Encoding.UTF8); 6 7 var hotelName = e.WebDriver.FindElement(By.XPath("//*[@id='J_htl_info']/div[@class='name']/h2[@class='cn_n']")).Text; 8 9 var address = e.WebDriver.FindElement(By.XPath("//*[@id='J_htl_info']/div[@class='adress']")).Text; 10 11 var price = e.WebDriver.FindElement(By.XPath("//*[@id='div_minprice']/p[1]")).Text; 12 13 var score = e.WebDriver.FindElement(By.XPath("//*[@id='divCtripComment']/div[1]/div[1]/span[3]/span")).Text; 14 15 var reviewCount = e.WebDriver.FindElement(By.XPath("//*[@id='commentTab']/a")).Text; 16 17 var comments = e.WebDriver.FindElement(By.XPath("//*[@id='hotel_info_comment']/div[@id='commentList']/div[1]/div[1]/div[1]")); 18 19 var currentPage =Convert.ToInt32(comments.FindElement(By.XPath("div[@class='c_page_box']/div[@class='c_page']/div[contains(@class,'c_page_list')]/a[@class='current']")).Text); 20 21 var totalPage = Convert.ToInt32(comments.FindElement(By.XPath("div[@class='c_page_box']/div[@class='c_page']/div[contains(@class,'c_page_list')]/a[last()]")).Text); 22 23 var messages = comments.FindElements(By.XPath("div[@class='comment_detail_list']/div")); 24 25 var nextPage = Convert.ToInt32(comments.FindElement(By.XPath("div[@class='c_page_box']/div[@class='c_page']/div[contains(@class,'c_page_list')]/a[@class='current']/following-sibling::a[1]")).Text); 26 27 Console.WriteLine(); 28 29 Console.WriteLine("名称:" + hotelName); 30 31 Console.WriteLine("地址:" + address); 32 33 Console.WriteLine("价格:" + price); 34 35 Console.WriteLine("评分:" + score); 36 37 Console.WriteLine("数量:" + reviewCount); 38 39 Console.WriteLine("页码:" + "当前页(" + currentPage + ")" + "下一页(" + nextPage + ")" + "总页数(" + totalPage + ")" + "每页(" + messages.Count + ")"); 40 41 Console.WriteLine(); 42 43 Console.WriteLine("==============================================="); 44 45 Console.WriteLine(); 46 47 Console.WriteLine("点评内容:"); 48 49 foreach (var message in messages) 50 51 { 52 53 Console.WriteLine("帐号:" + message.FindElement(By.XPath("div[contains(@class,'user_info')]/p[@class='name']")).Text); 54 55 Console.WriteLine("房型:" + message.FindElement(By.XPath("div[@class='comment_main']/p/a")).Text); 56 57 Console.WriteLine("内容:" + message.FindElement(By.XPath("div[@class='comment_main']/div[@class='comment_txt']/div[1]")).Text.Substring(0,50) + "...."); 58 59 Console.WriteLine(); 60 61 Console.WriteLine(); 62 63 } 64 65 Console.WriteLine(); 66 67 Console.WriteLine("==============================================="); 68 69 Console.WriteLine("地址:" + e.Uri.ToString()); 70 71 Console.WriteLine("耗时:" + e.Milliseconds + "毫秒"); 72 73 }
可以看到我们使用了PlantomJS+Selenium来解析DOM数据最终得到相应的结果数据。
其实,用C#虽然功能方便,调试清楚,可是还是有一些不足的,比如代码冗长,实现抓取网页需要大量的工作量,下次我们将使用天生的抓包工具Python来模拟抓包。