一只简单的网络爬虫(基于linux C/C++)————利用正则表达式解析页面
我们向一个HTTP的服务器发送HTTP的请求后,服务器会返回可能一个HTML页面(当然也可以是其他的资源),我们可以利用返回的HTML页面,在其中寻找其他的Url,例如我们可以这样在浏览器上查看一下HTML页面:
右键——>查看源代码
出现的页面大致如下:
我们可以看到,一个HTML的页面内容是想当多的,如果我们使用之前查找字符串的方法一行一行查找的话,效率是想当低下的。同时我们可以看到,大多数的Url例如
href=http://news.baidu.com
是以href=开头的,以及例如
src="https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/logo_white_81d09391.png"
以src开头。因此我们可以使用另一种手段达到比较高效的查找——正则表达式
C语言处理正则表达式常用的函数有regcomp()、regexec()、regfree()和regerror(),一般分为三个步骤,如下所示:
C语言中使用正则表达式一般分为三步:
编译正则表达式 regcomp()
匹配正则表达式 regexec()
释放正则表达式 regfree()
下边是对三个函数的详细解释
1、int regcomp (regex_t *compiled, const char *pattern, int cflags)
为了提高效率,在将一个字符串与正则表达式进行比较之前,首先要用regcomp()函数对它进行编译,将其转化为regex_t结构,这个函数把指定的正则表达式pattern编译成一种特定的数据格式compiled,这样可以使匹配更有效。函数regexec 会使用这个数据在目标文本串中进行模式匹配。执行成功返回0。
参数说明:
①regex_t 是一个结构体数据类型,用来存放编译后的正则表达式,它的成员re_nsub 用来存储正则表达式中的子正则表达式的个数,子正则表达式就是用圆括号包起来的部分表达式。
②pattern 是指向我们写好的正则表达式的指针。
③cflags 有如下4个值或者是它们或运算(|)后的值:
REG_EXTENDED 以功能更加强大的扩展正则表达式的方式进行匹配。
REG_ICASE 匹配字母时忽略大小写。
REG_NOSUB 不用存储匹配后的结果。
REG_NEWLINE 识别换行符,这样’$’就可以从行尾开始匹配,’^’就可以从行的开头开始匹配。
2. int regexec (regex_t *compiled, char *string, size_t nmatch, regmatch_t matchptr [], int eflags)
当我们编译好正则表达式后,就可以用regexec 匹配我们的目标文本串了,如果在编译正则表达式的时候没有指定cflags的参数为REG_NEWLINE,则默认情况下是忽略换行符的,也就是把整个文本串当作一个字符串处理。执行成功返回0。
regmatch_t 是一个结构体数据类型,在regex.h中定义:
typedef struct
{
regoff_t rm_so;
regoff_t rm_eo;
} regmatch_t;
成员rm_so 存放匹配文本串在目标串中的开始位置,rm_eo 存放结束位置。通常我们以数组的形式定义一组这样的结构。因为往往我们的正则表达式中还包含子正则表达式。数组0单元存放主正则表达式位置,后边的单元依次存放子正则表达式位置。
参数说明:
①compiled 是已经用regcomp函数编译好的正则表达式。
②string 是目标文本串。
③nmatch 是regmatch_t结构体数组的长度。
④matchptr regmatch_t类型的结构体数组,存放匹配文本串的位置信息。
⑤eflags 有两个值
REG_NOTBOL 按我的理解是如果指定了这个值,那么’^’就不会从我们的目标串开始匹配。总之我到现在还不是很明白这个参数的意义;
REG_NOTEOL 和上边那个作用差不多,不过这个指定结束end of line。
3. void regfree (regex_t *compiled)
当我们使用完编译好的正则表达式后,或者要重新编译其他正则表达式的时候,我们可以用这个函数清空compiled指向的regex_t结构体的内容,请记住,如果是重新编译的话,一定要先清空regex_t结构体。
4. size_t regerror (int errcode, regex_t *compiled, char *buffer, size_t length)
当执行regcomp 或者regexec 产生错误的时候,就可以调用这个函数而返回一个包含错误信息的字符串。
参数说明:
①errcode 是由regcomp 和 regexec 函数返回的错误代号。
②compiled 是已经用regcomp函数编译好的正则表达式,这个值可以为NULL。
③buffer 指向用来存放错误信息的字符串的内存空间。
④length 指明buffer的长度,如果这个错误信息的长度大于这个值,则regerror 函数会自动截断超出的字符串,但他仍然会返回完整的字符串的长度。所以我们可以用如下的方法先得到错误字符串的长度。
size_t length = regerror (errcode, compiled, NULL, 0);
该爬虫里所使用正则表达式字符串如下:
static const char * HREF_PATTERN = "href=\"\\s*\\([^ >\"]*\\)\\s*\"";
在一个模块中还有一个是下面这样的:
static const char * IMG_PATTERN = "<img [^>]*src=\"\\s*\\([^ >\"]*\\)\\s*\"";
说说第一个,第二个差不多。
href=\"\\s*\\([^ >\"]*\\)\\s*\"
里面很多的‘\’是用来转义的,因为本身字符串在双括号里面,\s表示一个空格,*表示1个或者多个匹配,当前的意思是一个或者多个空格是正常的字符,而且这里前后都有,\\(前面一个表示转义,这个的意思是\后面带一个(括号,子正则表达式就是用圆括号包起来的部分表达式,[^ >\”],在方括号中的^,应该是不包含的意思,就是不包含>和引号,因为我们所使用的这句字符所匹配的字符串查找出来的主正则表达式应该是下面这样的:
而我们要的url其实不含href=“”这样的字符,所以应该去掉,借用子正则表达式来处理
该爬虫里面使用了下面的处理来获取url
while (regexec(re, p, nmatch, matchptr, 0) != REG_NOMATCH)//匹配所有能够匹配的
{//这样操作可以去掉href,matchptr[1]是子正则表达式,matchptr[0]是主正则表达式,我们要的是子正则表达式的部分
len = (matchptr[1].rm_eo - matchptr[1].rm_so);//长度
p = p + matchptr[1].rm_so;
char *tmp = (char *)calloc(len+1, 1);
strncpy(tmp, p, len);
tmp[len] = '\0';
// printf("正则表达式匹配结果%s\n", tmp);
p = p + len + (matchptr[0].rm_eo - matchptr[1].rm_eo);//更新下要匹配的字符串
……
……
……
我们要的是不含引号的子正则表达式,所以应该是matchptr[1],matchptr[1].rm_eo 是开头, matchptr[1].rm_so结尾
其实这样的操作不能获取HTML页面的所有Url,因为还存在很多其他的情况,例如:
不是所有的href后面都有双引号,还有其他很多的情况。所以说这个爬虫的功能是相当单一的。
保存页面只实现了保存HTML和图片,分别用两个动态模块实现。
在使用正则表达式获取了原始Url后,就使用下面的语句,调用动态模块实现保存:
for (i = 0; i < (int)modules_post_html.size(); i++)
{
SPIDER_LOG(SPIDER_LEVEL_WARN, "保存文件");
modules_post_html[i]->handle(resp);//此模块就是保存文件的
}
下面看看两个模块
savehtml.cpp
#include "dso.h"
#include "socket.h"
#include <fcntl.h>
#include <stdlib.h>
#include "spider.h"
static int handler(void * data)
{
SPIDER_LOG(SPIDER_LEVEL_WARN, "保存HTML文件");
Response *r = (Response *)data;
//确认类型
if (strstr(r->header->content_type, "text/html") == NULL)
return MODULE_ERR;
char *fn = url2fn(r->url);
int fd = -1;
if ((fd = open(fn, O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0) //以fn为文件名
{
return MODULE_ERR;
}
int left = r->body_len;
int n = -1;
while (left) //一直写直到写完
{
if ((n = write(fd, r->body, left)) < 0)
{
// error
close(fd);
unlink(fn);//unlink()会删除参数pathname指定的文件
free(fn);
return MODULE_ERR;
}
else
{
left -= n;
}
}
close(fd);
free(fn);
SPIDER_LOG(SPIDER_LEVEL_WARN, "保存HTML文件完成");
return MODULE_OK;
}
static void init(Module *mod)
{
SPIDER_ADD_MODULE_POST_HTML(mod);
}
//这里的模块名称和最后生成的.so的名称,已经在加载的时候使用的那个名称一定要相同
Module savehtml =
{
STANDARD_MODULE_STUFF,
init,
handler
};
在确认类型之后便是使用write函数写入文件
saveimage.cpp
#include "dso.h"
#include "socket.h"
#include "url.h"
#include <fcntl.h>
#include <stdlib.h>
#include "spider.h"
// 解析src的正则表达式
static const char * IMG_PATTERN = "<img [^>]*src=\"\\s*\\([^ >\"]*\\)\\s*\"";
static int handler(void * data)
{
Response *r = (Response *)data;
const size_t nmatch = 2;
regmatch_t matchptr[nmatch];
int len;
regex_t re;
if (strstr(r->header->content_type, "text/html") != NULL)//找到text/html这个类型
{
SPIDER_LOG(SPIDER_LEVEL_WARN, "saveimage正则匹配");
if (regcomp(&re, IMG_PATTERN, 0) != 0)
{//正则表达式编译错误
return MODULE_ERR;
}
char *p = r->body;
while (regexec(&re, p, nmatch, matchptr, 0) != REG_NOMATCH)
{
len = (matchptr[1].rm_eo - matchptr[1].rm_so);
p = p + matchptr[1].rm_so;
char *tmp = (char *)calloc(len+1, 1);
strncpy(tmp, p, len);
tmp[len] = '\0';
p = p + len + (matchptr[0].rm_eo - matchptr[1].rm_eo);
char *url = attach_domain(tmp, r->url->domain);//attach_domain在url.cpp
//printf("加上域名后的Url%s\n", url);
if (url != NULL)
{
Surl * surl = (Surl *)malloc(sizeof(Surl));
surl->level = r->url->level;
surl->type = TYPE_IMAGE;
// normalize url
if ((surl->url = url_normalized(url)) == NULL)
{
free(surl);
continue;
}
if (iscrawled(surl->url))
{ // if is crawled 已经抓取过
free(surl->url);
free(surl);
continue;
}
else
{
SPIDER_LOG(SPIDER_LEVEL_WARN, "%s加入原始队列",surl);
push_surlqueue(surl);
}
}
}
}
else if (strstr(r->header->content_type, "image") != NULL)
{
SPIDER_LOG(SPIDER_LEVEL_WARN, "保存二进制文件");
char *fn = url2fn(r->url);
int fd = -1;
if ((fd = open(fn, O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0)
{
return MODULE_ERR;
}
// save image
int left = r->body_len;
int n = -1;
while (left)
{
if ((n = write(fd, r->body, left)) < 0)
{
// error
close(fd);
unlink(fn);
free(fn);
return MODULE_ERR;
}
else
{
left -= n;
}
}
close(fd);
free(fn);
SPIDER_LOG(SPIDER_LEVEL_WARN, "保存二进制文件成功");
return MODULE_OK;
}
}
static void init(Module *mod)
{
SPIDER_ADD_MODULE_POST_HTML(mod);
}
Module saveimage = {
STANDARD_MODULE_STUFF,
init,
handler
};
这里还处理了另一种情况,以src开头的Url,然后匹配类型,写入文件
上面两个模块便是所谓的持久化器,因为将资源用文件的形式保存了下来。
下面还有另一函数
//加上域名
//参数为:正则表达式匹配到的结果,原来url的域名
char * attach_domain(char *url, const char *domain)
{
if (url == NULL)
return NULL;
//这里情况其实是比较复杂的
if (strncmp(url, "http", 4) == 0)//以http开头,表示已经有完整的域名了
{
return url;
}
else if(strncmp(url, "www", 3) == 0)
{
return url;
}
else if(strncmp(url, "//www", 5) == 0)
{
int j=0;
int len = strlen(url);
char *tmp1 = (char *)malloc(len+1);
for(j=2;j < len;j++)
{
tmp1[j-2] = url[j];
}
free(url);
SPIDER_LOG(SPIDER_LEVEL_DEBUG, "新组装的url:%s",tmp1);
return tmp1;
}
else if(*url == '/')//不是完整的url,补充域名,这里可能会出现一些错误的情况,可能多一个'/',或者原来是以www开头
{
int i;
int ulen = strlen(url);
int dlen = strlen(domain);
char *tmp = (char *)malloc(ulen+dlen+1);
for (i = 0; i < dlen; i++)
tmp[i] = domain[i];
for (i = 0; i < ulen; i++)
tmp[i+dlen] = url[i];
tmp[ulen+dlen] = '\0';
free(url);
return tmp;
}
else//其他情况
{
//do nothing
free(url);
return NULL;
}
}
因为HTML页面中的url情况是多样的,有些并没有完整的域名,这时我们需要帮其加上,有些还存在这种情况
这种出现两斜杠的情况我们这里也要处理一下,还有其他的情况,这里就没有实现了,可以自己扩展。
因为文件名不支持‘/’,所以不能直接使用url作为文件名,把斜杠替换成其他的字符即可,可以参考下面的函数
//生成url写到文件的文件名
char * url2fn(const Url * url)
{
int i = 0;
int l1 = strlen(url->domain);
int l2 = strlen(url->path);
char *fn = (char *)malloc(l1+l2+2);
for (i = 0; i < l1; i++)
fn[i] = url->domain[i];//写域名
fn[l1++] = '_';//在域名后面加上'_',所以下载的时候域名末尾都有个'_'
for (i = 0; i < l2; i++)
fn[l1+i] = (url->path[i] == '/' ? '_' : url->path[i]);//路径的'/'号换为'_'
fn[l1+l2] = '\0';
return fn;
}
一只简单的网络爬虫(基于linux C/C++)————利用正则表达式解析页面的更多相关文章
- 一只简单的网络爬虫(基于linux C/C++)————socket相关及HTTP
socket相关 建立连接 网络通信中少不了socket,该爬虫没有使用现成的一些库,而是自己封装了socket的相关操作,因为爬虫属于客户端,建立套接字和发起连接都封装在build_connect中 ...
- 一只简单的网络爬虫(基于linux C/C++)————开篇
最近学习开发linux下的爬虫,主要是参考了该博客及其他一些网上的资料.网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息 ...
- 一只简单的网络爬虫(基于linux C/C++)————主事件流程
该爬虫的主事件流程大致如下: 1.获取命令行参数,执行相应操作 2.读取配置文件,解析得到各种设置 3.载入各种模块 4.种子入队,开启DNS解析线程(原始队列不为空时解析) 5.创建epoll,开启 ...
- 一只简单的网络爬虫(基于linux C/C++)————浅谈并发(IO复用)模型
Linux常用的并发模型 Linux 下设计并发网络程序,有典型的 Apache 模型( Process Per Connection ,简称 PPC ), TPC ( Thread Per Conn ...
- 一只简单的网络爬虫(基于linux C/C++)————支持动态模块加载
插件在软件设计中有很大的好处,可以方便地扩展各种功能,使用插件技术能够在分析.设计.开发.项目计划.协作生产和产品扩展等很多方面带来好处: (1)结构清晰.易于理解.由于借鉴了硬件总线的结构,而且各个 ...
- 一只简单的网络爬虫(基于linux C/C++)————守护进程
守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程.它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.守护进程常常在系统引导装入时启动, ...
- 一只简单的网络爬虫(基于linux C/C++)————读取命令行参数及日志宏设计
linux上面的程序刚开始启动的时候一般会从命令行获取某些参数,比如以守护进程运行啊什么的,典型的例子就是linux下的man,如下图所示 实现该功能可以使用getopt函数实现,该函数在头文件uni ...
- 一只简单的网络爬虫(基于linux C/C++)————线程相关
爬虫里面采用了多线程的方式处理多个任务,以便支持并发的处理,把主函数那边算一个线程的话,加上一个DNS解析的线程,以及我们可以设置的max_job_num值,最多使用了1+1+max_job_num个 ...
- 一只简单的网络爬虫(基于linux C/C++)————Url处理以及使用libevent进行DNS解析
Url处理 爬虫里使用了两个数据结构来管理Url 下面的这个数据结构用来维护原始的Url,同时有一个原始Url的队列 //维护url原始字符串 typedef struct Surl { char * ...
随机推荐
- MySQL学习之路4-数据的导入导出
数据的导入 通过数据库管理工具,先建表,然后导入表记录. 通过sql语句导入: load data local infile '表路径' into table stuscore fields term ...
- 28.5 Integer-- int的包装类
* 由于基本数据类型只能做一些简单的操作和运算,所以Java为我们封装了基本数据类型,为每种基本数据类型提供了包装类 * 包装类就是封装了基本数据类型的类,为我们提供了更多复杂的方法和一些变量 * * ...
- python字符串列表元组序列操作
Table of Contents generated with DocToc python系列-字符串.列表.元组的操作 序列的访问及运算符 序列通用操作 访问单个元素 切片访问一部分元素 序列的复 ...
- three.js - 一个javascript 3D代码库
这个项目的目的是用最简单的开发模式创建一个轻量级的3 d代码库,这个js库提供了canvas,svg,css3d和webgl这四种渲染方式. 下载地址: 下载地址:https://github.com ...
- JAVA—HashMap
一些关于hashmap的学习笔记 1.HashMap底层实现原理 在JDK1.7中HashMap是以数组加链表的形式组成的,在JDK1.8之后新增了红黑树的组成结构,当链表大于8并且容量大于64时,链 ...
- day7作业
# day7作业 # 1. 使用while循环输出1 2 3 4 5 6 8 9 10 count = 1 while count < 11: if count == 7: count += 1 ...
- Daily Scrum 12/17/2015
Process: Zhaoyang:完成了相册图片的异步加载. Yandong&Dong: 对Azure的体系架构进行学习和相应的编程. Fuchen: 对Oxford计划中的NLP接 ...
- 并发工具——CyclicBarrier
本博客系列是学习并发编程过程中的记录总结.由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅. 并发编程系列博客传送门 CyclicBarrier简介 CyclicBarrie ...
- Laravel 分页 数据丢失问题解决
问题: to do list 中有32条数据,每页10条,共3页. 做完了一个事项之后,准备打卡,发现找不到这个事项. 数据库查询正常,有这一条数据. 原因: 发现是分页出了问题,第1页的数据和第2页 ...
- anaconda 使用conda命令创建虚拟环境
1.首先在所在系统中安装Anaconda.可以打开命令行输入conda -V检验是否安装以及当前conda的版本. 2.conda常用的命令. 1)conda list 查看安装了哪些包. 2)con ...