.NET中如何使用PhantomJS与Selenium模拟采集数据

在现代 web 开发中,数据采集是一个非常常见的需求。无论是从网站中提取信息,还是进行自动化测试,模拟用户行为都变得尤为重要。在 .NET 环境中,常见的两种模拟采集的工具是 PhantomJS 和 Selenium。这两者各具特点,适用于不同的场景。本文将介绍这两款工具及其如何在 .NET 中使用。

1. PhantomJS——无头浏览器的利器

什么是 PhantomJS?

PhantomJS 是一个无头浏览器(headless browser),意味着它没有图形界面,完全通过命令行操作。它可以模拟浏览器行为,执行网页中的 JavaScript 代码,获取渲染后的网页内容,并支持截图、页面交互等操作。由于它不需要图形界面,运行效率比普通浏览器更高,尤其适用于大规模的网页抓取和自动化测试。

PhantomJS 在数据采集中的应用

PhantomJS 非常适合用来进行 网页抓取,特别是当页面内容通过 JavaScript 动态生成时。很多现代网站的数据展示依赖于 JavaScript 渲染,传统的 HTML 解析工具(如 HtmlAgilityPack)在这种情况下无法直接提取数据,而 PhantomJS 可以通过模拟浏览器,渲染页面并抓取其中的数据。

在 .NET 中使用 PhantomJS

虽然 PhantomJS 自身是用 JavaScript 编写的,但我们可以通过使用外部库来在 .NET 中调用 PhantomJS。一个常用的库是 PhantomJS.Net,它提供了 .NET 与 PhantomJS 的接口,使得在 .NET 中模拟浏览器变得更加方便。

使用 PhantomJS.NET 的示例代码:

// language: csharp
using System;
using PhantomJS.Net;

class Program
{
    static void Main()
    {
        var driver = new PhantomJSDriver();
        driver.Navigate().GoToUrl("https://example.com");

        // 获取渲染后的网页内容
        var pageSource = driver.PageSource;
        Console.WriteLine(pageSource);

        driver.Quit();
    }
}

在这个示例中,我们首先初始化了一个 PhantomJS 驱动器,加载了目标网页,然后获取了渲染后的 HTML 内容。

2. Selenium——强大的自动化测试工具

什么是 Selenium?

Selenium 是一款广泛使用的自动化测试工具,支持多种编程语言(包括 C#、Java、Python 等)。它不仅可以用来模拟浏览器行为,还支持模拟用户操作,如点击、输入、滚动等。Selenium 支持所有现代浏览器,包括 Chrome、Firefox 和 Safari 等,除了无头浏览器模式(headless mode)外,它还支持在有图形界面的环境下操作浏览器。

Selenium 在数据采集中的应用

Selenium 可以模拟真实的用户行为,因此非常适合 交互式数据采集。例如,你需要登录网站、填写表单、点击按钮来获取数据时,Selenium 是一个理想的选择。而且它支持 JavaScript 执行,因此可以抓取动态加载的网页内容。

在 .NET 中使用 Selenium

在 .NET 环境中使用 Selenium 需要安装相应的 NuGet 包。常见的库有 Selenium.WebDriver 和 Selenium.WebDriver.ChromeDriver,这些包可以通过 NuGet 安装。

使用 Selenium 的示例代码:

// language: csharp
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using System;

class Program
{
    static void Main()
    {
        // 设置 Chrome 驱动器
        var options = new ChromeOptions();
        options.AddArgument("headless"); // 无头模式

        var driver = new ChromeDriver(options);
        driver.Navigate().GoToUrl("https://example.com");

        // 获取渲染后的网页内容
        var pageSource = driver.PageSource;
        Console.WriteLine(pageSource);

        driver.Quit();
    }
}

这个示例展示了如何通过 Selenium 在无头模式下启动 Chrome 浏览器,加载目标网页,并获取渲染后的 HTML 内容。

3. PhantomJS 与 Selenium 的对比

虽然 PhantomJS 和 Selenium 都可以用来模拟采集,但它们有一些显著的区别:

特性PhantomJSSelenium
浏览器类型无头浏览器 (Headless Browser)可以支持有头和无头浏览器
性能高效,启动速度快,适合大规模抓取启动速度稍慢,但更强大、灵活
JavaScript 支持完全支持 JavaScript完全支持 JavaScript
支持的浏览器仅 PhantomJS支持所有主要浏览器(Chrome, Firefox, Safari)
适用场景爬虫和抓取静态页面自动化测试和交互式爬虫

从对比表格中可以看出,PhantomJS 更适合于快速的网页抓取,尤其是当你只需要获取页面内容而不需要模拟用户交互时。而 Selenium 更适合需要复杂交互的场景,如登录、填写表单、点击按钮等。

4. 总结

PhantomJS 和 Selenium 都是强大的工具,能够帮助开发者在 .NET 中模拟浏览器行为进行数据采集。选择哪个工具取决于你的需求:

  • 如果你需要快速抓取页面并且不需要复杂的用户交互,PhantomJS 是一个不错的选择。
  • 如果你需要模拟更复杂的用户行为(如填写表单、点击按钮等),Selenium 会更为适合。

无论你选择哪个工具,正确使用它们将能帮助你高效地进行数据采集工作。

C#使用phantomjs采集页面HTML

C#使用phantomjs采集页面HTML。

code.js

phantom.outputEncoding="gb2312"
system = require('system')   
address = system.args[1];//获得命令行第二个参数 接下来会用到   
//console.log('Loading a web page');   
var page = require('webpage').create();   
var url = address;   
//console.log(url);   
page.open(url, function (status) {   
    //Page is loaded!   
    if (status !== 'success') {   
        console.log('Unable to post!');   
    } else {   
        //console.log(page.content);   
        //var title = page.evaluate(function() {   
        //  return document.title;//示范下如何使用页面的jsapi去操作页面的  
        //  });   
        //console.log(title);   
        //console.log(encodeURIComponent(page.content));   
        console.log(page.content);   
    }     
    phantom.exit();   
});

C#代码

public string getAjaxCotnent(String url)
{
    var b = System.Reflection.Assembly.GetEntryAssembly().Location;
    var path = Path.GetDirectoryName(b); // 末尾不带斜杠

    ProcessStartInfo start = new ProcessStartInfo(path + "//phantomjs.exe");

    start.Arguments = "code.js" + " " + url;//设置命令参数
    StringBuilder sb = new StringBuilder();
    start.CreateNoWindow = false;//不显示dos命令行窗口
    start.RedirectStandardOutput = true;//
    start.RedirectStandardInput = true;//
    start.UseShellExecute = false;//是否指定操作系统外壳进程启动程序
    Process p = Process.Start(start);

   // string encoding = p.StandardOutput.CurrentEncoding.ToString();
    StreamReader reader = p.StandardOutput;//截取输出流
    string line = reader.ReadLine();//每次读取一行
    sb.AppendLine(line);
    while (!reader.EndOfStream)
    {
        line = reader.ReadLine();
        sb.AppendLine(line);
    }
    p.WaitForExit();//等待程序执行完退出进程
    p.Close();//关闭进程
    reader.Close();//关闭流
    string strRet = System.Web.HttpUtility.UrlDecode(sb.ToString());
    return strRet;
}

调用

var url = "http://po.baidu.com/feed/share?context=%7B%22nid%22%3A%22news_3493960622142783493%22%2C%22sourceFrom%22%3A%22bjh%22%7D";
var detailHtml = getAjaxCotnent(url);

模拟网页登录的一种全能思路

原理就是从webbrowser拿到Cookie

一般做网页登录,比如登录QQ空间,则要破解登录的相关的一系列加密算法。

在给客户维护以前登录QQ空间采集好友访客的一个软件时,发现2015年的空间密码加密方式变了,由以前的MD5改为了RSA,找到了加密的JS文件,但懒的转为C#代码。本来想用Interop.MSScriptControl.dll在C#中调用JS,但是直接调用这个JS文件会报错。实在不想继续找下去,一直在想着有什么简单方便的方法。终于功夫不负有心人,还真让我找到了。

原来的方式是,是从UI界面上获取用户要登录的用户名和密码,C#代码经过系统的加密,转换为TX需要的格式,并发包的服务器,服务器反回一些cookie和一些有用的值(比如g_tk),然后后续的一些操作要带上这些cookie和g_tk。现在既然这些过程解不了密,那么是否可以用Webbrowser控件来登录,登录完成后从webbrowser中拿到cookie,后续的操作带上这些cooke即可。

经过动手尝试,证明是可行的。对于QQ空间的登录,过程是这样的:

webbrowser中进行登录,登录完成后,在恰当的时机,拿到webbrowser的cookie,然后在cookieContainer中找到skey

至此,所有cookie和skey都拿到了。然后还有一个重要的参数g_tk, 这个参数是根据skey计算出来的。算法如下:

/// <summary>
/// 获取空间登录的g_tk
/// </summary>
/// <param name="skey"></param>
/// <returns></returns>
public long g_tk(string skey)
{
        long hash = 5381;
        for ( int i = 0; i < skey.Length; i++)
       {
              hash += (hash << 5) + skey[i];
       }
        return hash & 0x7fffffff;
}

试了下,能用,还好,g_tk的算法没有改动。

这样,项目维护完成,600块钱到手。

下面附上webbrowser登录过程以及拿到cookie和相cookie中取skey的所有代码:

public partial class QZoneWebLogin : Form
{
    private bool flag = false;
    private string _skey = string.Empty;
    private CookieContainer _cookie = new CookieContainer();

    public string skey
    {
        get { return _skey; }
    }

    public CookieContainer cookie
    {
        get { return _cookie; }
    }

    public QZoneWebLogin()
    {
        InitializeComponent();
    }

    private void webBrowser1_DocumentCompleted( object sender, WebBrowserDocumentCompletedEventArgs e)
    {
        if (webBrowser1.Url.ToString().Contains( "http://user.qzone.qq.com/") && webBrowser1.Url.ToString().Contains("?ptsig=" ))
        {
            if (!flag)
            {
                flag = true;
                _skey = GetSkey();
                this.DialogResult = System.Windows.Forms. DialogResult.OK;
            }

        }

    }


    private string GetSkey()
    {
        string skey = string.Empty;

        GetCookie();
        List< Cookie> listCookie = GetAllCookies(_cookie);
        foreach ( Cookie c in listCookie)
        {
            if (c.Name == "skey")
            {
                skey = c.Value;
                break;
            }
        }
        return skey;
    }

    private void GetCookie()
    {
        if (webBrowser1.Document.Cookie != null)
        {
            string cookieStr = webBrowser1.Document.Cookie;
            string[] cookstr = cookieStr.Split( ';');
            foreach ( string str in cookstr)
            {
                string[] cookieNameValue = str.Split( '=');
                Cookie ck = new Cookie(cookieNameValue[0].Trim().ToString(), cookieNameValue[1].Trim().ToString());
                ck.Domain = ".qq.com";
                _cookie.Add(ck);
            }
        }
    }


    /// <summary>
    /// 把CookieContainer所有的Cookie读出来
    /// </summary>
    /// <param name="cc"></param>
    /// <returns></returns>
    private List<Cookie> GetAllCookies( CookieContainer cc)
    {
        List< Cookie> lstCookies = new List< Cookie>();
        Hashtable table = ( Hashtable)cc.GetType().InvokeMember( "m_domainTable",
            System.Reflection. BindingFlags.NonPublic | System.Reflection.BindingFlags .GetField |
            System.Reflection. BindingFlags.Instance, null , cc, new object [] { });

        foreach ( object pathList in table.Values)
        {
            SortedList lstCookieCol = (SortedList)pathList.GetType().InvokeMember("m_list" ,
                System.Reflection. BindingFlags.NonPublic | System.Reflection.BindingFlags .GetField
                | System.Reflection. BindingFlags.Instance, null , pathList, new object[] { });
            foreach ( CookieCollection colCookies in lstCookieCol.Values)
                foreach ( Cookie c in colCookies)
                    lstCookies.Add(c);
        }

        return lstCookies;
    }
}

.NET采集用到的包

ScrapySharp + HtmlAgilityPack

这两个库基本可以完成98%的采集需求,剩下的2%可以再加一个库: phantomjs

目前接到的需求中,还没有使用到 phantomjs这个库。

补充(2015-3-9):

今天又发现了几个和采集网页相关的库(以下4个都没用过,先记下来)

  1. CsQuery。CsQuery可以算是.net中实现的Jquery, 可以使用类似Jquery中的方法来处理html页面。CsQuery的项目地址是https://github.com/afeiship/CsQuery
  2. AngleSharp (据说有内存泄漏,不知道现在还有没有)
  3. fizzler   fizzler是HtmlAgilityPack的一个扩展,支持jQuery Selector
  4. NSoup   NSoup是JSoup的Net移植版本。

ScrapySharp + HtmlAgilityPack 基本用法举例:

string htmlstr = HttpHelper.HttpPost(“http://www.tzgsj.gov.cn/baweb/show/shiju/queryByName.jsp” , “spellcondition=%E9%BE%99” );

HtmlDocument doc = new HtmlDocument ();

doc.LoadHtml(htmlstr);

HtmlNode docNode = doc.DocumentNode;

var nodes = docNode.CssSelect(“#content” );

这样就拿到了所有ID为content的DOM元素。

这段HTML如下:

为了进一步拿到里面的td的文字,可以接着这样写:

foreach(var node in nodes)
{
    var tdNodes = node.CssSelect( "td");
    foreach(var td in tdNodes)
    {
        string text = td.InnerText;
    }
}

经过这个双重循环后,所有的td里的内容就都被采集出来了。

总结,ScrapySharp 主要是这个CssSelect好用。它是HtmlAgilityPack的一个扩展