分享

用C#爬虫来抓取网页并解析

 长江黄鹤 2020-05-08

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方法执行完。    
View Code

    相当于代码:

View Code

第四:在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函数的定义如下:

View Code

 第八:计时函数

1
2
3
4
5
6
7
var watch = newStopwatch();
watch.Start();
。。。。
watch.Stop()
var milliseconds = watch.ElapsedMilliseconds;

 第九:伪造浏览器

View Code

   这是我们爬虫的主体部分,我们伪造浏览器,设置好一定的参数,进行访问服务器,得到结果然后解析结果,并显示。整个过程是非常恰当的。

 第十:对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;//爬虫出错事件
View Code

 第十一:代理服务器和测试服务器。

  //测试代理IP是否生效:http://1212.ip138.com/ic.asp
  //测试当前爬虫的User-Agent:http://www.

三、加强版的网络爬虫

    在简单版的基础上,这次我们不是直接伪造浏览器上网了,而是使用相应的工具来帮助我们进行网页解析。

首先我们需要四个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来模拟抓包。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多