Click here to Skip to main content
15,881,709 members
Articles / Programming Languages / C#
Article

How To Parse Hyperlinks in an HTML Document and Follow Them Recursively

Rate me:
Please Sign up or sign in to vote.
4.85/5 (12 votes)
5 Jul 2007CPOL1 min read 61.6K   1.3K   87   12
This article shows you how to parse hyperlinks in an HTML document using regular expressions and follow them recursively
Screenshot - HyperlinkParserScreenshot.png

Introduction

This code parses hyperlinks from an HTML document and follows them recursively, like a Web spider. The sample is embedded into a Windows form containing a list view which shows the parsed hyperlinks.

It doesn't really parse only hyperlinks ("<a href..."). Iframes, Frames and Image Maps are also supported.

This is NOT an introduction to regular expressions!

Background

I needed this code to create a "Google Sitemap" for my own private homepage.

Using the Code

The code supports a maximum depth parameter so that you will not follow too many hyperlinks. It also follows only URLs which are "below" the URL you have specified for the start.

Here's the nearly complete code of this sample, including a lot of comments explaining what it does.

C#
protected Regex m_rxBaseHref;

protected Regex m_rxHref;
protected Regex m_rxFrame;
protected Regex m_rxIframe;
protected Regex m_rxArea;

protected List<string> m_strListUrlsAdded;
protected List<string> m_strListUrlsFollowed;

public Form1()
{
    InitializeComponent();

    RegexOptions rxOpt = RegexOptions.Singleline | 
                            RegexOptions.Compiled | 
                            RegexOptions.IgnoreCase;

    //We have to create our regular expression for parsing hyperlinks, 
    //base hrefs and so on
    m_rxHref = new Regex("<a[^>]*href=(\"|')(.*?)\\1[^>]*>(.*?)</a>", rxOpt);
    m_rxFrame = new Regex("<frame[^>]*src=(\"|')(.*?)\\1[^>]*>", rxOpt);
    m_rxIframe = new Regex("<iframe[^>]*src=(\"|')(.*?)\\1[^>]*>", rxOpt);
    m_rxArea = new Regex("<area[^>]*href=(\"|')(.*?)\\1[^>]*>", rxOpt);
    m_rxBaseHref = new Regex("<base[^>]* href=(\"|')(.*?)\\1[^>]*>", rxOpt);
}  
private void btnReadFromUrl_Click(object sender, EventArgs e)
{
    Cursor = Cursors.WaitCursor;

    //Clear existing URLs
    lvUrls.Items.Clear();

    m_strListUrlsAdded = new List<string>();
    m_strListUrlsFollowed = new List<string>();

    ReadUrls(tbUrl.Text, tbUrl.Text, ref m_strListUrlsAdded, 
        ref m_strListUrlsFollowed, (int)numMaxDepth.Value, 0);

    Cursor = Cursors.Default;
}

protected void ReadUrls(string strURL, string strStartBase,
                ref List<string> strUrlsAdded, 
                ref List<string> strUrlsFollowed,
                int iMaximumDepth,
                int iCurrentDepth)
{
    //Increase the depth. If we reach the maximum depth: return
    if (++iCurrentDepth == iMaximumDepth)
    {
        return;
    }

    //Now we create the WebRequest and get the response. If something fails
    //we return
    HttpWebRequest req = null;

    try
    {
        req = HttpWebRequest.Create(strURL) as HttpWebRequest;
    }
    catch (Exception) { }

    if(req == null)
    {
        return;
    }

    req.Method = "GET";

    HttpWebResponse res = null;
    try
    {
        res = req.GetResponse() as HttpWebResponse;
    }
    catch (Exception){}

    if(res == null || res.StatusCode != HttpStatusCode.OK)
    {
        return;
    }

    Stream s = res.GetResponseStream();

    StreamReader sr = new StreamReader(s);

    //Read the whole content of the response stream into a string
    string strHTML = sr.ReadToEnd();

    sr.Close();
    sr.Dispose();
    sr = null;

    s.Close();
    s.Dispose();
    s = null;

    int iPos, iPos2;

    /*After getting a response the Address property of the
    Web request contains the real URL from the served document.
    This is automatically done by reading the HTTP header
    "Content-Location"
    For example, you request the URL "http://www.test.abc/test
    But "test" is just a directory, so the server returns 
    the index document of this directory. In this case the
    address property is "http://www.test.abc/test/index.htm"
    */
    //We need the base to follow relative URLs
    string strBase = req.Address.AbsoluteUri;            

    //If the base contains a query string, we remove that string
    //because we don't need it.
    iPos = strBase.IndexOf('?');
    if(iPos > -1)
    {
        strBase = strBase.Substring(0, iPos);
    }

    //Assure that the base ends with a slash
    if(strBase[strBase.Length - 1] != '/')
    {
        iPos = strBase.LastIndexOf('/');
        if(iPos < 0)
        {
            return;
        }

        strBase = strBase.Substring(0, iPos + 1);
    }

    iPos = strBase.IndexOf("://");
    if(iPos < 0)
    {
        return;
    }

    iPos = strBase.IndexOf('/', iPos + 3);
    if(iPos < 0)
    {
        return;
    }

    //We need the base host URL for hyperlinks that start with a slash
    string strBaseHostUrl = strBase.Substring(0, iPos + 1);         

    //Test if the HTML contains a base href
    Match matchBaseHref = m_rxBaseHref.Match(strHTML);
    if (matchBaseHref.Success)
    {
        string strHtmlBase = matchBaseHref.Groups[2].Value.Trim();
        if(strHtmlBase.StartsWith("/"))
        {
            strBase = strBaseHostUrl + strHtmlBase.Substring(1);
        }
        else
        {
            strBase = strHtmlBase;
        }
    }

    //This dictionary contains all hyperlinks and their
    //associated "texts" (anything between <a> and </a>)
    Dictionary<string, string> dictHrefs = new Dictionary<string, string>();

    MatchCollection matchesHref = m_rxHref.Matches(strHTML);
    AddHrefMatches(matchesHref, ref dictHrefs);

    MatchCollection matchesFrame = m_rxFrame.Matches(strHTML);
    AddHrefMatches(matchesFrame, ref dictHrefs);

    MatchCollection matchesIframe = m_rxIframe.Matches(strHTML);
    AddHrefMatches(matchesIframe, ref dictHrefs);

    MatchCollection matchesArea = m_rxArea.Matches(strHTML);
    AddHrefMatches(matchesArea, ref dictHrefs);

    //Now we iterate through all Hyperlinks we found
    foreach (string strUrlFound in dictHrefs.Keys)
    {
        string strUrlNew = strUrlFound;

        //Skip this links if it starts with ftp://, news://, mailto:,
        //JavaScript:
        if (IsAbsoluteUrl(strUrlNew) && !IsHttpUrl(strUrlNew))
        {
            continue;
        }

        //if this isn't an absolute URL
        if (!IsHttpUrl(strUrlNew))
        {
            if (strUrlNew.StartsWith("/"))
            {
                strUrlNew = strBaseHostUrl + strUrlNew.Substring(1);
            }
            else
            {
                strUrlNew = strBase + strUrlNew;
            }
        }

        //Now we remove all parent paths
        while ((iPos = strUrlNew.IndexOf("../")) > -1)
        {
            iPos2 = strUrlNew.Substring(0, iPos).LastIndexOf('/');
            iPos2 = strUrlNew.Substring(0, iPos2).LastIndexOf('/');

            strUrlNew = strUrlNew.Substring(0, iPos2) +
                "/" + strUrlNew.Substring(iPos + 3);
        }

        //if the URL doesn't start with our starting base
        //(the address we entered into our textbox) then
        //skip this
        if (!strUrlNew.StartsWith(strStartBase))
        {
            continue;
        }

        //If we haven't added the URL yet to our listview do it now
        if (!strUrlsAdded.Contains(strUrlNew))
        {
            ListViewItem lvi = new ListViewItem(new string[]{
                strUrlNew,
                dictHrefs[strUrlFound]
            });
            lvUrls.Items.Add(lvi);

            strUrlsAdded.Add(strUrlNew);
        }

        //Follow this URL if not already done
        if (!strUrlsFollowed.Contains(strUrlNew))
        {
            strUrlsFollowed.Add(strUrlNew);
            ReadUrls(strUrlNew, strStartBase,
                ref strUrlsAdded, ref strUrlsFollowed,
                iMaximumDepth, iCurrentDepth);
        }
    }
} 

Points of Interest

It was a little bit tricky trying to discover how to handle parent links ("../"), base hrefs, Content-Locations and so on. But now I know a little bit more about this.

History

  • 06.07.2007
    • Added debug output in a textbox to solve problems
    • The reading of the URLs is now done in a separate thread
  • 03.06.2007
    • Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Austria Austria
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Amir Mehrabi-Jorshari10-Dec-10 10:13
Amir Mehrabi-Jorshari10-Dec-10 10:13 
GeneralGood Job, but... Pin
cheepau7-Oct-07 23:45
cheepau7-Oct-07 23:45 
GeneralBackgroundWorker Pin
Arnie100020-Sep-07 4:33
Arnie100020-Sep-07 4:33 
GeneralHTML Page is complex! Pin
Oleksandr Kucherenko9-Jul-07 21:44
Oleksandr Kucherenko9-Jul-07 21:44 
GeneralRe: HTML Page is complex! Pin
Christoph Schick10-Jul-07 10:36
Christoph Schick10-Jul-07 10:36 
GeneralRe: HTML Page is complex! Pin
Oleksandr Kucherenko10-Jul-07 21:51
Oleksandr Kucherenko10-Jul-07 21:51 
GeneralUri Pin
Steve Hansen5-Jul-07 22:18
Steve Hansen5-Jul-07 22:18 
GeneralRe: Uri Pin
Christoph Schick6-Jul-07 2:50
Christoph Schick6-Jul-07 2:50 
GeneralNothing Happening Pin
tbaseflug5-Jul-07 5:51
tbaseflug5-Jul-07 5:51 
AnswerRe: Nothing Happening Pin
Christoph Schick5-Jul-07 9:24
Christoph Schick5-Jul-07 9:24 
I have no idea whats going wrong. But I'll add some debug output to the sample so that we can analyze this problem. In a few hours, I have to do some other things before Wink | ;-) , an updated version will be available.
GeneralRe: Nothing Happening Pin
tbaseflug5-Jul-07 9:28
tbaseflug5-Jul-07 9:28 
AnswerRe: Nothing Happening Pin
Christoph Schick5-Jul-07 12:21
Christoph Schick5-Jul-07 12:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.