一个博客爬取网站的设计思路与流程

作者:日期:2017-07-22 16:28:45 点击:26

 前后花了两个月的时间,终于把数据库课设做完了。。。考虑到wooyun关闭后各种wooyun资源dump的流行,加上正好最近在通过各个博客学习安全知识,就想着做一个博客爬取网站吧,首先是写出一个爬虫来爬取整个博客的文章,然后再展示出来。

这是网站的登陆界面,在左侧的文字概述了我做整个网站的初衷,希望做一个blog的收集平台,能够方便的爬取博客,然后展示这些dump下来的文章,以备wooyun那样的不测2333 
对了,细心的同学应该能够发现,这个页面使用的是github的css文件

本篇文章我将从以下几方面对开发过程进行描述:

  • 爬虫的编写 
    • 爬取逻辑
    • 解析html以获取页面样式
    • 入口参数检测以提高爬虫健壮性
  • 后端逻辑的实现 
    • php后台处理的实现
    • 轮询来获取后台程序的运行进度
    • 后台文件结构与设计
  • 网站安全性的考虑
  • 样式设计(这方面我第一次接触,非常菜没什么好借鉴的。。。)
  • 其他的一些问题 
    • 整个网站的编码
    • 页面标签事件触发后传this
    • 使用中文智能分词实现模糊搜索

爬虫的编写

首先是爬取逻辑

我采用的是正则表达式匹配的方式,逻辑是这样的:维护一个目录列表,把博客初始链接放进去作为列表第一个成员,之后对目录列表进行遍历,解析链接中所有符合目录正则表达式的新的目录 url,并将新的目录 url增加到目录列表中,这样在循环结束后,就相当于把所有的目录页面都走了一遍。

同时在这个过程中,我们对每个链接的html文件可不只是扫描出符合目录正则表达式的链接,我们同时还会通过article正则表达式扫描出所有文章链接,并添加到文章列表。所以在对目录列表的整个遍历结束之后,我们不仅仅是遍历解析了所有的page页面,还获得了网站中所有的文章链接

然后是对单个页面的样式,js代码,图片等的解析下载

拿到所有的文章链接后,就是对文章的解析下载了,但是使用python下载链接只能拿到html文件,不包括css文件,js文件,图片这些其他的资源,如果想在本地离线使用的话,就需要解析下载下来,这里我首先使用python的BeautifulSoup模块对html进行了解析,使HTML文件变成可以操作的DOM形式。

之后我找了几个常见的博客文章样本进行分析,可以看到基本上需要下载的链接在 

关于提高爬虫的健壮性

首先要确定由用户指定的对象,也就是程序输入点

  • 爬取的链接可能无法访问
  • 用户提供的路径可能不存在或没有权限读写

除此之外还会存在问题的对象

  • 本地存放是基于windows文件系统的,在命名上会有要求,在写完整个爬虫后,我最终正则表达式需要去过滤的字符有这些:[\/:*?\”<>|\x20\n\t\r\f\a]
  • 在爬取中涉及到对html文件解析之后再下载,这些从页面中解析出来的链接可能存在问题

还有一个之前没太注意过的问题: 
因为爬取中需要对各种书写方式做出解析,所以用到了很多切片,分割等操作,就可能会存在溢出问题,比如说我想去掉http://,用str[7:]实现,在遇到不是http://格式的时候就会出现溢出问题。这些错误在进行各种程序输入统筹标准化的时候经常出现

在进行参数检测的时候,需要遵循的原则如下:

  • 尽量在参数入口进行检测,这样以后用的时候就不用再检测了
  • 要仔细分析那些是来源不可靠的参数,尽量在他们出现的时候就进行参数检测

后端逻辑的实现

php后台处理的实现

php调用爬虫的方式采用命令行的形式,在php中使用shell_exec()函数来对python爬虫进行调用,但是php程序不可能等待爬虫运行结束,处理的逻辑应该是:在爬虫开始前断开http链接,之后让爬虫自己去运行。具体的实现分析我在php输出缓冲与http的联系中已经做出了详细解释

轮询读取后台程序进度

后台的程序在断开http链接之后就无法返回给前端任何信息了,这时用户对后台运行的程序无法掌控,对于这种情况,常见的解决方案是轮询或websocket,websocket一般在软件中使用,网站中经常使用轮询的方式。 
所谓轮询,就是后台的程序把运行的实时进度都写到数据库中,然后在前端使用ajax来不断的去拿取数据库的信息来实现对后台运行程序的实时掌控,因为前端要不断的发起访问请求,所以叫轮询

为了减轻带宽与机子压力而采取的措施:

  • 在js获取更新资源的api函数中,将传输的更新资源在数据库中使用mysqli_fetch_row()获取而不使用mysqli_fetch_arrow(),这两个取出的结果,一个是列表格式,一个是字典格式的,自然列表格式的要更小
  • js获取更新资源后,在没有打开进度详情页面的时候,只更新主页面的通知图标,在打开进度详情页面的时候再对详情页面进行实时更新。可以避免对js对页面大量不必要的重写

可能的更新: 
关于cgi,fast-cgi这些关于php数据传输方式的名词 
redist是个后台处理队列的扩展,可能经常在后台大规模计算的时候使用

php后台文件结构与设计

因为是第一次写网站,参考火日的xss平台的文件目录,把网站的文件结构设计如下:

  • 首先是存放静态文件的static文件夹,里面分为css,js,images,font四个文件夹,所有的引用都来自它们
  • 两个php扩展文件,也放在同级目录下,在调用的时候直接require到对应的路径
  • 将所有允许直接访问的php文件放到外层目录下
  • 所有提供辅助功能,也就是让外层php调用的php文件,放到内层目录下,像config.php,function.php,sql.php,auth.php等,这些辅助文件有些像functioin.php,sql.php这样提供各种需要的函数,有些像auth.php,waf.php这样提供一段必要的执行代码

对于js发出请求的相响应设计

这些响应逻辑都在api.php页面中实现,以api接口的形式实现,不做登陆检测,同时为了保证有些api的管理员才能调用特性,需要建立admin_api.php,做管理员登陆检测。

基本的api.php书写结构如下:

1.if ( isset( $_GET['cmd']) && $_GET['cmd'] != "" ){
2. switch( $_GET['cmd'] ){
3. case 'deleteblog':
4. if(isset($_GET['blog']) && $_GET['blog'] != ""){
5. }
6. break;
7. case 'deletecrawlerstate':
8. if(isset($_GET['blog']) && $_GET['blog'] != "") {
9. }
10. break;
11. case 'deletecrawler':
12. if(isset($_GET['blog']) && $_GET['blog'] != "") {
13. }
14. break;
15. }
16.}

除此之外,这些页面返回的数据建议使用json格式,也就是echo(json_encode(变量)),并且在程序开头指明header('Content-Type: application/json');

这样在js接收中$.get(url,function(data, textstates, fn){ })中,data就是可以直接使用的变量格式 
对了,要注意json只能接受utf编码格式的变量,如果php使用gb2312编码的话,需要转化为utf编码的

访问控制如下设计:

所有内层的辅助函数都禁止直接访问,这样实现

1.if(!defined('IN_BLOG_PLATFORM')){
2. exit('Access Denied');
3.}
4.如果是管理员页面需要调用的话,还需要加上
5.if(!defined('IS_ADMIN')){
6. exit('Access Denied');
7.}

这样的话,在直接访问的时候,就会因为没有定义宏变量而禁止访问

外层的函数在所有入口点需要进行session检查,将所有未登录的访问都重定向到login.php

1.这个函数在页面入口除了检查了$_SESSION['isLogin']外还检查了user_agent值
2.if (!( isset($_SESSION['isLogin']) && $_SESSION['isLogin'] === true &&
3. isset($_SESSION['user_agent']) && $_SESSION['user_agent'] === $_SERVER['HTTP_USER_AGENT'] )){
4. $_SESSION['isLogin'] = false;
5. $_SESSION['user_IP'] = "";
6. $_SESSION['user_agent'] = "";
7. //清空session会话,清除session变量,结束session会话
8. session_unset();
9. session_destroy();
10. header("Location: login.php");
11. exit();
12.}
13.管理员页面需要在入口进行管理员检查,我是这样设计的
14.if(isset($_SESSION['username']) && $_SESSION['username'] != ""){
15. if(!checkAdmin($mysqli, $_SESSION['username'])){
16. exit('Access Denied');
17. }
18.}

综上所述,一个页面做出完整的访问控制需要添加如下功能代码:

  • 定义相应的宏变量,以引入需要的辅助函数 
    define("IN_BLOG_PLATFORM", true);
  • 引入需要的辅助函数 
    require_once ("./libs/waf.php");
  • 设置http_only并开启session 
    ini_set("session.cookie_httponly", 1); session_start();
  • 基于session中的值进行个各种检查,像是否登陆,user-agent是否符合等,只有在检查通过的时候才允许访问页面
  • 链接数据库

表单提交中token的作用分析

之前一直没有注意到过,见到火日师傅的xss平台中使用了,自己也就在表单提交中加上了,所谓表单提交中的token,指的是在表单中隐藏一行input,其值是随机生成的,并存放到session中,然后随着表单数据提交到服务端,服务端会对token值与存储在session的值进行比对,只有在相同的情况下才会继续往下执行 
一开始使用这项功能的时候没能体会到什么作用,现在多少能理解一点了

  • 填写表单,提交表单这个流程必须人为完整的走下来才行,如果想直接提交表单参数的话,会因为没有token而被拒绝,这应该能够把很多自动化爬虫,暴力破解都拒之门外。
  • 我在设计login.php, registe.php页面的时候,这些页面的处理逻辑基本上分成两种情况:一种是没有提交表单参数的,这是展示登陆界面,另一种是提交了表单参数的,这时执行登陆验证逻辑。而区分这两种情况的判断条件,我们可以采用检查token的方式:如果token存在并与session中的存储值相同,就判定为登陆验证情况,反之则为登陆页面展示情况

网站安全性的考虑

首先是关于数据库的

  1. 对所有的数据库入口参数做过滤,使用的过滤函数如下:
1.    function d_addslashes(array $array){
2. foreach($array as $key=>$value){
3. if(!is_array($value)){
4. //查看php的魔术引号是否开启了,如果没有开启则使用addslashes函数自行改变所有的引号
5. !get_magic_quotes_gpc() && $value=addslashes($value);
6. $array[$key]=$value;
7. }else{
8. $array[$key] = d_addslashes($array[$key]);
9. }
10. }
11. return $array;
12. }
13.使用方式如下,比如要过滤$blog参数
14.list($blog) = d_addslashes([$blog]);
  1. 对数据库返回的错误代码替换为自定义语句

  2. 尽量保证编码的一致性,因为数据处理流程中编码的转换可能导致某些字符的消失,详见ph师傅的文章,这个我的网站真的很难做到,php使用的gbk编码,数据库使用utf8编码。。

然后是关于访问控制的

页面是怎么实现访问控制这点在之前已经叙述过了,除此之外,用户表中不存放明文密码防止拖库,存储密码hash的时候要加盐防止彩虹表攻击,然后应我们密码学老师的强烈要求,把所有的hash函数由md5换成了SHA3

登陆机制防护

  • 登陆表单中使用token应该可以在一定程度上防止暴力破解攻击,因为这时候你的暴力遍历速度最高只能和网络速度等同,一次尝试流程需要先接收html页面,从中拿到token值后才能提交尝试的表单

  • 当然,防止暴力破解攻击,最有效的方式还是使用错误次数限制。在数据库中建立forbidden_ip表,登陆错误的时候将会记录下此ip,在错误次数达上线的时候,ip封禁

还有个比较有意思的点——记录user-agent

在用户登录的时候记录user-agent的值到session中,然后所有的页面在登陆的时候都会与将访问者的user-agent字段与session中记录的值相比较,如果不相同的话就注销用户。

具体有什么作用,目前我发现的用处就是增加xss攻击获取cookie后冒充管理员登陆的难度,因为这时你必须要保证user-agent要和管理员所用的一样才行

样式设计

好吧,界面样式的设计才是我花费时间最多的地方,虽然之前大一时学习过css但现在基本都忘了。 
网站中的样式主要有以下来源

  • 去其他网站寻找符合页面结构的网页,之后F12分析下主要使用的css文件,然后把这些css dump下来,之后仿照着源网页的结构进行编排就好了,我的网站中,登陆与注册界面使用github登陆界面的css文件,admin页面使用的是腾讯云主页的css文件
  • 对于某些元素,像按钮格式,表格格式这些,可以去网上直接下载模板css文件,书写的时候直接在class中加上对应的标志就好
  • 还可能在其他页面看到好看的元素样式,可以F12分析后,把此元素的的css都复制下来,添加到我们的标签中。不过经常出现的情况是,你需要仔细分析到底是哪个标签藏着关键的css样式,毕竟一个标签可能有很多层div嵌套,常用的方法是在F12中依次把样式去掉看界面变化效果
  • 最后实在不行我们可以自己手写嘛,使用bootstrap书写出来的页面还是说的过去的

在页面中应该尽量使用百分比布局,这样在不同分辨率,不同窗口大小下都能灵活展示。我花费了很多时间,就是在调整页面的布局问题。直接使用其他网站的css文件并仿照div结构编写一般就不会出现这种问题

网站编写中其他的一些问题

网站整体编码

php使用gb2312编码,所以在所有php页面要指定输出页面的编码方式 
header("Content-type: text/html; charset=gb2312"); 
或者在html代码里加上<meta charset="gbk">。 
当然我建议大家两种方式都写上以防万一

数据库存储使用utf-8编码,在调用数据库的时候需要指定连接编码方式set names gbk;

api传输数据使用json编码,但是json只接受utf编码的数据,所以php编码要先转化为utf8编码方式

1.function list_gbk_to_utf_8($data) {
2. if( is_array($data) ) {
3. foreach ($data as $k => $v) {
4. if ( is_array($v) ) {
5. $data[$k] = list_gbk_to_utf_8($v);
6. } else {
7. $data[$k] = iconv('gb2312', 'utf-8//IGNORE', ($data[$k]));
8. }
9. }
10. return $data;
11. } else {
12. $data = iconv('gb2312', 'utf-8//IGNORE',$data);
13. return $data;
14. }
15.}

页面标签事件触发后传this

页面中书写如下:

1.<xxxx onclick="test(this)">

script代码如下:

1.function test(yiruma){
2. $(yiruma) 这就是触发响应的那个标签了,使用$()的方法是为了将yiruma标签从js格式转到jq格式
3.}

使用中文智能分词实现模糊搜索

百度上可以查到,搜索引擎的模糊搜索是要先进行智能分词的,比如 恶意广告,会分为恶意 与 广告两个关键词,然后还要进行各种过滤匹配,这里我自己实现时暂且先不管那么多,毕竟不是专门做这个方向的,我只使用了中文智能分词的方式。

首先在github上找到了php端很流行的scws,使用这些代码提供的接口对搜索框输入的关键字进行分词,之后在每个分词间加上%,在数据库中做like搜索。比如恶意广告就是 where name like ‘%恶意%广告%’然后把搜索结果返回。

当然还有更多的算法,时间紧迫就先做这个最简单的,以后有时间再添加新的匹配规则。

上一篇: JS构造函数及new运算符

下一篇: 19种JavaScript常用简写方法