Linux下网络爬虫程序

<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"><span style="white-space:pre">	</span>网络爬虫是搜索引擎最主要的组成部分,网络爬虫通过互联网获取网页,并将其存储在本地。然后通过对下载的网页的进一步分析,获取网页中的URL从而下载更多的资源。本文通过借助于curl库以及C++中的STL库来编写一个简单的网络爬虫程序。程序需要输入一个索引网页(例如www.baidu.com),然后程序会下载解析该网页,并将其中的url按照优先级进行保存。</span>

1、网页下载:

网页下载主要是借助于现有的curl库进行,curl的具体使用方法可以参考man手册,在这里我们仅用到其-o(保存下载的网页)和-m(设置下载时间)选项。下载文件我们通过执行脚本程序来完成。

</pre><pre name="code" class="cpp"><span style="font-size:18px;"># !/bin/bash
# Progran:
# 	This program is going to down url from web by curl
# Para:
#	$1:the url will be download	$2: the new name of the url
# History:
#	2014.11.06	By Gao Fan, SEU		first release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/local/bin:/local/sbin:~/bin
export PATH

curl -m 30 $1 -o $2 && exit 0

exit 1</span>
脚本程序传入两个参数,其中$1为要下载网页的URL,$2表示保存的文件名。为了方便,我们将url中的‘/’替换为‘_’来作为该url的本地保存文件名。其中我们设置连接时间最长为30s以防止一直试图下载一个存在但不能下载的网页。
以上是采用curl进行网页下载的,该方法只能将网页下载完后对其解析。当然也可以自己写TCP程序请求服务器获得网页内容,这样就可以边下载边解析。

然后我们定义一个URL类,类名为URLclass,该类主要是解析URL,将其主机名和所请求的资源名分开,本程序由于采用curl下载网页,所以用不到该类,但如果想要通过自己写TCP来从主机直接获得网页资源,运用该类会非常方便。

#ifndef URL_H__
#define URL_H__

#include "webspider.h"
#include<string>

using namespace std;

class URLclass
{
    private:
        string url;            //提供的URL
        string hostURL;    //主机名
        string file;         //文件名(包含参数)
		int paraseURL();
    public:
        URLclass()
        {}
        int setURL(string murl);
        string getHostURL();
        string getFile();
};

#endif // URL_H__
string URLclass::getHostURL()
{
    return hostURL;
}

string URLclass::getFile()
{
    return file;
}

int URLclass::paraseURL()
{
    string s1;
    if(url.find("//") == string::npos)
    {
        return 0;
    }
    s1 = url.substr(url.find("//") + 2, url.size());
    if(s1.find('/', 0) == string::npos)
    {
        hostURL = s1;
        file = "/";
    }
    else
    {
        hostURL = s1.substr(0, s1.find("/"));
        file = s1.substr(s1.find("/"), s1.size());
    }
    return 1;
}

int URLclass::setURL(string murl)
{
   url = murl;
   int flag = paraseURL();
   return flag;
}
我们可以通过声明URLclass对象,并使用setURL来对其赋予url,通过getHostUrl和getFIle方法获得url中的主机名和请求资源。
当我们下载完一个资源后,将该资源的本地文件名放到消息队列里,供解析文件程序解析。我们可以定义消息队列消息为如下结构:

struct mymesg
{
    long mtype;
    int len;
    char pathName[512];
};
下载URL的完整程序如下:

int Download(const char *url, char *pathName)
{
    char *cmd;
    cmd = (char*)malloc(1024);
    memset(cmd, '\0', 1024);
    sprintf(cmd, "/home/web/Spiderscript/downURL %s %s", url, pathName);  //downURL是上述脚本程序
    int flag = system(cmd);
    if(flag != 0)
    {
        return 0;
    }

    mymesg *MyMsg;
    MyMsg = (mymesg*)malloc(sizeof(mymesg));
    memset(MyMsg, '\0', sizeof(mymesg));
    MyMsg->mtype = 1;
    memcpy(MyMsg->pathName, pathName, strlen(pathName));
    MyMsg->len = sizeof(int) + strlen(MyMsg->pathName);
    msgsnd(msgID,(char *)MyMsg, MyMsg->len, IPC_NOWAIT);
    free(MyMsg);
    free(cmd);
    return 1;
}

为了使程序执行较快,我们将定义一个线程专门下载网页。

int tranName(const char *str, char *name)
{
    memcpy(name, str, strlen(str));
    int i;
    for(i = 0; i < strlen(name); i++)
    {
        if(name[i] == '/')
        {
            name[i] = '_';
        }
    }
    return 1;
}

void* Down(void* ptr)
{
     char name[200];
     while(1)
     {
        if(URLMap.empty())
        {
            sleep(10);
        }
        else
        {
            if(URLDeque.empty())
            {
                pthread_mutex_lock(&DequeMutex);          //实现线程同步
                mapSortByValue(URLMap, URLDeque);
                pthread_mutex_unlock(&DequeMutex);
            }
            string url = URLDeque.front();        //获取队列中第一个URL
            VisitedURL.insert(url);               //存入已访问set中。
            URLDeque.pop_front();
            cout << "visit:" << url << endl;
            memset(name, '\0', 200);
            tranName(url.c_str(), name);
            Download(url.c_str(), name);
        }
        sleep(1);
     }
}
其中tranName函数为将url中的'/'替换为'_'的函数


2、网页解析

网页解析主要是解析网页内容并提取其中的URL,然后对URL进行优先级分类。我们利用pcre库提供的正则表达式来解析网页并提取其中的url。pcre的安装参考http://blog.csdn.net/lanxinglan/article/details/40798933。正则表达式的使用参考http://blog.chinaunix.net/uid-26575352-id-3517146.html。

为了解析出rul,我们定义如下的正则表达式:const char pattern[] = "<a href=\"([^\"]*)\"";此表达式可以提取出

<a href="http://blog.csdn.net/microzone/article/details/6684436">
类似的URL,其中提取到的URL为
http://blog.csdn.net/microzone/article/details/6684436
这样我们就可以根据此URL来下载相应的网页了。

接下来的问题就是如何对URL进行优先级分级,对网页解析一般分为广度优先遍历算法,PageRank算法和OPIC算法。

其中广度优先遍历算法最简单,其将每一个网页中的url按照解析的顺序插入到一个队列的尾部,然后从队列头部取url进行下载解析。离root(index)越近的url会优先得到解析。

PageRank算法是将所有网页抓到本地,然后对其url解析,此算法认为当所有网页中某个url数量最多时其最重要,优先级也就最高。然而在现实中无法获取所有网页后再对其解析,因此我们采用非完全的PageRank算法。解析一定数量网页后取出数量最多的url再进行下载。

OPIC是将一个网页的权值平均分给该网页中所有URL的一种算法。

我们采用非完全的PageRank算法,改程序我们声明一个map<string, int>URLMap、一个deque<string>URLDeque、一个set<string>VisitedURL来完成此算法。其中URLMap中存放解析出来的url和该url目前为止出现的次数。URLDeque中存放URLMap中value值最大的前DequeSize个url。Visit而URL中存放已经访问过的url。

由于map结构无法使其按照value值进行排序,所以我们需要定义一个函数来获得map中value值最大的url。该函数定义如下:

bool cmp(const PAIR &val1, const PAIR &val2)
{
    return val1.second > val2.second;
}
void mapSortByValue(map<string, int> &myMap, deque<string> &myDeque)
{
    vector<PAIR> tmpVec(myMap.begin(), myMap.end());       //利用vector作为中介对其进行排序
    sort(tmpVec.begin(), tmpVec.end(), cmp);

    vector<PAIR> :: iterator iter = tmpVec.begin();
    int i = 0;

    while(i != DequeSize && iter != tmpVec.end())
    {
        myDeque.push_back(iter->first);
        cout << "\n" ;
        cout << iter->first << " " << iter->second << "\n";
        myMap.erase(iter->first);
        ++i;
        ++iter;
    }
}
我们通过读取本地网页来对网页进行解析,每次读入一行,然后调用正则表达式获取此行中的url并保存起来。具体函数如下:

/*
 * 函数名:GetURL
 * 函数描述:读取消息队列中第一条消息,得到要解析URL的文件名,对其进行解析,将解析到的
 * URL放到优先队列URLDeququ中,此函数只得到带.html的URL
 * 参数:*ptr(由于在用g++编译时必须指定参数,所以在此指定参数,该参数没有什么实际运用)
 */

void* GetURL(void *ptr)
{
    mymesg *MyMsg;
    MyMsg = (mymesg*)malloc(sizeof(mymesg));
    memset(MyMsg, '\0', sizeof(mymesg));
    char buf[MAXLINE];
    int fd;
    rio_t rp;
    string str1, str2;

    pcre  *re;
    const char *error;
    int  erroffset;
    int  ovector[OVECCOUNT];
    int  rc, i;
    while(1)
    {
        memset(MyMsg, '\0', sizeof(mymesg));
        int flag = msgrcv(msgID, (char*)MyMsg, sizeof(mymesg) - sizeof(long), 1, 0);//获取消息队列里消息
        if(flag == -1)
        {
            continue;
        }
        else
        {
            fd = open(MyMsg->pathName, O_RDONLY);
            if(fd < 0)
            {
                continue;
            }
            rio_readinitb(&rp, fd);

            re = pcre_compile(pattern,       // pattern, 输入参数,将要被编译的字符串形式的正则表达式
                      0,            // options, 输入参数,用来指定编译时的一些选项
                      &error,       // errptr, 输出参数,用来输出错误信息
                      &erroffset,   // erroffset, 输出参数,pattern中出错位置的偏移量
                      NULL);
            if (re == NULL)
            {                 //如果编译失败,返回错误信息
                printf("PCRE compilation failed at offset %d: %s\n", erroffset, error);
                return (void*)0;
            }

            while(rio_readnb(&rp, buf, MAXLINE) > 0)
            {
                int t = 0;
                char *ptr = buf;
                do{
                    rc = pcre_exec(re,            // code, 输入参数,用pcre_compile编译好的正则表达结构的指针
                    NULL,          // extra, 输入参数,用来向pcre_exec传一些额外的数据信息的结构的指针
                    ptr,           // subject, 输入参数,要被用来匹配的字符串
                    strlen(ptr),  // length, 输入参数, 要被用来匹配的字符串的指针
                    0,             // startoffset, 输入参数,用来指定subject从什么位置开始被匹配的偏移量
                    0,             // options, 输入参数, 用来指定匹配过程中的一些选项
                    ovector,       // ovector, 输出参数,用来返回匹配位置偏移量的数组
                    OVECCOUNT);
                    if(rc <= 0)
                    {
                        break;
                    }
                    char str[1024];
                    memset(str, '\0', 1024);
                    char *substring_start = ptr + ovector[2];
                    int substring_length = ovector[3] - ovector[2];
                    memcpy(str, substring_start,substring_length);
                    string cstr = str;
                    if(cstr.find("http") != string::npos)    //某些URL中省略主机名,所以需要保存一个主机名。
                    {
                        memset(host, '\0', 50);
                        strcpy(host, "http://");
                        URLclass url;
                        url.setURL(cstr);
                        strcpy(host, url.getHostURL().c_str());
                    }
                    else if(cstr.find("http") == string::npos)<span style="white-space:pre">	</span>//一般省略的主机名为本网页中的主机名
                    {
                        char tmp[40];
                        strcpy(tmp, host);
                        strcat(tmp, str);
                        memset(str, '\0', strlen(str));
                        memcpy(str, tmp, strlen(tmp));
                    }
                    if(string(str).find(keyWord) == string::npos)  //keyWord为url限制
                    {
                    }
                    else if(VisitedURL.count(str) == 0)
                    {
                        pthread_mutex_lock(&MapMutex);      //多线程操作需对其同步
                        if(URLMap.count(str) == 0)
                        {
                            URLMap[str] = 1;
                        }
                        else
                        {
                            ++URLMap[str];
                        }
                        pthread_mutex_unlock(&MapMutex);
                    }

                    memset(str, '\0', 1024);

                    t = ovector[1];
                    ptr += t;
                    memset(ovector, '\0', sizeof(ovector));
                  }while(rc > 0);        //如果一行中包含多个url,需要进行循环获得
            }
            close(fd);
        }
        sleep(1);
    }

    free(MyMsg);
    return (void*)0;
}
我们运用到的读函数为rio_readnb,此函数鲁棒性较强,详细说明请参考《深入理解计算机操作系统》一书。另外此函数声明为void*,因为此函数为一个单独的线程函数。在C++中定义线程函数时必须包含参数。

3、main函数

我们的main函数主要是负责初始化和创建主线程。其中第二个参数argv[1]表示初始网页(index)。第三个参数argv[2]表示网页中需要包含的字符串。具体实现如下:

int main(int argc, char *argv[])
{


    if(argc < 2)
    {
        cout << "please input the index html for start" << endl;
        return 0;
    }

     int key = ftok(dirPath, 2);
     while((msgID = msgget(key,  IPC_CREAT | 0660)) < 0)
     {
        cout << "Create msgID failed..." << endl;
        sleep(1);
     }
	 
    Download(argv[1], "index.html");
    strcpy(host, argv[1]);
    strcpy(keyWord, argv[2]);
    pthread_t downhtml, geturl;
    pthread_create(&geturl, NULL, GetURL, NULL);
    pthread_create(&downhtml, NULL, Down, NULL);


    while(1)
    {
        sleep(10);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值