一个流水账式的nodejs爬虫项目介绍(下)

好像不管怎么写都没法摆脱它流水账的本质,所以,我摊牌了。

照例目录:

  • 介绍
  • 实现过程
  1. MySQL数据库
    1.1 MySQL结构
    1.2 插入信息(nodejs接入MySQL)
    1.3 操作方法(筛选、排序、统计)
  2. 网站搭建
    2.1 express框架一点简单的认识
    2.2 关于html内嵌js代码
    2.3 使用express框架
  • 一些坑和扩展
    • MySQL中文乱码
    • 可选搜索范围
    • 日期排序
  • 最终效果、代码
  • 总结

介绍

接着上篇讲,下篇要介绍的是把爬取到的数据存入MySQL,利用MySQL本身的存储、查询、筛选、排序等功能实现搜索,以及搭建网站,将MySQL的功能通过网页对话框输入——网页输出的形式显式表现出来。MySQL和express框架的知识点从信息量上讲比request、cheerio等模块大得多,比如说在使用express框架时就需要同时对html和js代码都比较熟练(html中嵌入js),然而我对此基本上没啥经验。而且感觉之前的内容还是一环套一环,结果一到express框架这里就信息爆炸了orz,所以就很难像上篇一样讲得那么细致。

过程

1. MySQL数据库

1.1 MySQL结构在这里插入图片描述

MySQL下可以有好几个数据库,每个数据库可以有好几个表格,表格在创建时规定了表头。这里我们可以把所有数据都存在一个表格里。

贴一下新闻table的创建方式。把id作为自增量和唯一量,根据需求避免重复爬取,将url也设置为唯一量。

CREATE TABLE `news`(
    `id` INT NOT NULL AUTO_INCREMENT,
    `title` VARCHAR(200) NOT NULL,
    `resource` VARCHAR(200) NOT NULL,
    `author` VARCHAR(200) NOT NULL,
    `content` LONGTEXT,
    `keywords` VARCHAR(200) NOT NULL,
    `pubtime` VARCHAR(200) NOT NULL,
    `fetchtime` VARCHAR(200) NOT NULL,
    `url` VARCHAR(200) NOT NULL,
    PRIMARY KEY(`id`),
    UNIQUE KEY `id_UNIQUE` (`id`),
    UNIQUE KEY `url_UNIQUE` (`url`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

1.2 插入信息

首先在安装好MySQL模块的前提下,自己做一个模块’mysql.js’便于之后的操作。

var mysql = require("mysql");
var pool = mysql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: 'root',
    database: 'mycrawler1'
});
var query = function(sql, sqlparam, callback) {
    pool.getConnection(function(err, conn) {
        if (err) {
            callback(err, null, null);
        } else {
            conn.query(sql, sqlparam, function(qerr, vals, fields) {
                conn.release(); //释放连接 
                callback(qerr, vals, fields); //事件驱动回调 
            });
        }
    });
};
var query_noparam = function(sql, callback) {
    pool.getConnection(function(err, conn) {
        if (err) {
            callback(err, null, null);
        } else {
            conn.query(sql, function(qerr, vals, fields) {
                conn.release(); //释放连接 
                callback(qerr, vals, fields); //事件驱动回调 
            });
        }
    });
};
exports.query = query;
exports.query_noparam = query_noparam;

这里规定了MySQL数据库信息,并把对MySQL的连接、释放一系列操作模块化。

然后在主程序里还是使用一个函数完成插入操作,将新闻内容存在info里作为参数。

function Insert_into_sql(info){
    var add_sql="INSERT INTO news(id,title,resource,author,content,keywords,pubtime,fetchtime,url)VALUES(0,?,?,?,?,?,?,?,?)"
    var add_sql_params=[info.title,info.resource,info.author,info.content,info.keywords,info.pubtime,info.fetchtime,info.url];
    mysql.query(add_sql,add_sql_params,function(qerr,vals,fields){
        if(qerr){
            console.log("新闻插入失败~");
            console.log(qerr.message,info);
            return;
        }
        else
            console.log("新闻插入成功~");
    });
}

这里我理解为通过js代码模拟MySQL的命令行操作。

1.3 操作方法(筛选、排序、统计)

这部分其实和js无关,完全是MySQL的语法。

查询table中的元素,取出table中所有数据:

SELECT * FROM [table];

利用like筛选,筛选标题中含有“新冠”的。

SELECT * FROM [table] where title like="%新冠%";

利用order by排序,根据时间升序排序。

SELECT * FROM [table] order by pubtime ASC;//升序

利用count(*)统计,统计标题含有“新冠”的新闻条数。

SELECT count(*) FROM [table] where title like="%新冠%";

具体可看:https://www.runoob.com/mysql/mysql-tutorial.html

这个也是网站实现查询等功能的核心部分,可以说接下来所有的操作都是围绕MySQL来进行的。而构建网站的目的就是在MySQL命令行操作和网站的图形化界面间构建一个桥梁。

2. 网站搭建

2.1 express框架一点简单的认识

一开始觉得网页前端和后端的交互挺难理解的。后来读了几段搭建网站的代码,感觉也可以简单理解成一个从输入到输出的过程,所有的输入包含在request里,输出则体现在response里。

ar express = require('express');
var mysql = require('./mysql.js')
var app = express();
//app.use(express.static('public'));
app.get('/7.03.html', function(req, res) {
    console.log(__dirname);
    res.sendFile(__dirname + "/" + "7.03.html");//__dirname就是代码所在文件夹的位置
})
app.get('/7.04.html', function(req, res) {
    console.log(__dirname + "/" + "7.04.html");
    res.sendFile(__dirname + "/" + "7.04.html");
})
app.get('/process_get', function(req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
    var fetchSql = "select url,source_name,title,author,publish_date from news where title like '%" +
        req.query.title + "%'";
    mysql.query(fetchSql, function(err, result, fields) {
        console.log(result);
        res.write(JSON.stringify(result));
        //res.end(JSON.stringify(result));
    });
})
var server = app.listen(8080, function() {
    console.log("访问地址为 http://127.0.0.1:8080/7.03.html")
})

最基本的输入方式是直接访问网站所在的ip,比如在浏览器中输入 “http://127.0.0.1:8080”,也可以是网页提交表单之后跳转到的指定ip,也可以是对按钮点击信息的捕获之类的。

大多数输入本身并不能直接产生一个结果(比如显示数据或者跳转到某个页面)。输入的意义是由后端定义的。后端根据输入,决定发送一张网页或者一些数据给前端,最终呈现在浏览器上,即输出。

2.2 html嵌入js代码

纯html页是静态的。如果想做出一些动态的功能比如搜索啊排序啊,要么在后端通过切换网页的方式实现动态,要么就是嵌入js代码,动态改变同一个页面内的html的元素(这个好像就是js最基本的用途)。

首先在header里面引入jQuery(对就是那个跟cheerio用法很像的)

<header>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
</header>

引入之后就可以用$来选择html元素了,比如给某个元素加上一句话。(需要的话,在append的时候可以直接把一串html代码填进去,并将其作为html代码正常显示,看来插入之后是会重新生成页面的?)

<body>
    <div>一段文字。</div>
    <script>
        $(document).ready(function(){
            $('div').append("两段文字。<input type='text'>");
        })
    </script>
</body>

也可以获取一个input框内的信息,以按钮点击为触发(搜索功能的基本组件)

<body>
    <input type="text">
    <button>就是一个按钮</button>
    <script>
        $('button').click(function(){
            alert($('input').val());
        })
    </script>
</body>

所以具体说来cheerio相当于jQuery的一个子集,用来获取html元素(输入),而jQuery同时具备了获取、修改等等功能(输入+输出)。

好在jQuery起码在选择器的用法上和cheerio就是一样的,所以还蛮亲切的。当然有很多产生输出的方法就没怎么见过,但是查查手册也还算好理解。

2.3 使用express框架

通过

express -e search_site

来搭建一个express脚手架。

然后看依赖项需要的模块,用npm install全部安装

"dependencies": {
    "cookie-parser": "^1.4.5",
    "data-utils": "0.0.2",
    "date-utils": "^1.2.21",
    "debug": "^2.6.9",
    "ejs": "^2.7.4",
    "express": "^4.17.1",
    "http-errors": "^1.7.3",
    "morgan": "^1.10.0",
    "mysql": "^2.18.1",
    "parser": "^0.1.4"
  }

然后就生成了一个好像很高级但是看不懂的文件夹结构。。

应该是整个项目最不理解的地方吧。

不过既然都说是框架了也没必要搞那么清楚了。把mysql.js放在根目录,后端index.js放在routes下,前端页面search.html放在public里面,然后需要自己做的也就是一个js和一个html。其他的统统当作黑箱操作

具体代码就放在最后吧。

一些坑和扩展

MySQL中文乱码

“众所周知,有中文的地方。。它就有乱码。”
在这里插入图片描述

不过MySQL出现乱码,一般不是爬取的数据出错,只是因为cmd编码和MySQL编码方式不匹配,这里只要统一设定为一种编码就可以了,比如utf8。

右键->属性->查看cmd编码,可以看到编码类型。

在这里插入图片描述

图里已经是UTF-8就不用设置了,如果是GBK或者其他类型的可以通过在命令行里输入

chcp 65001

接着设置MySQL的编码,在MySQL中输入"show variables like “%char%”;"

mysql> show variables like "%char%";
+--------------------------+--------------------------+
| Variable_name            | Value                    |
+--------------------------+--------------------------+
| character_set_client     | utf8mb4                  |
| character_set_connection | utf8mb4                  |
| character_set_database   | utf8mb4                  |
| character_set_filesystem | binary                   |
| character_set_results    | gbk                      |
| character_set_server     | utf8mb4                  |
| character_set_system     | utf8                     |
| character_sets_dir       | c:\mysql\share\charsets\ |
+--------------------------+--------------------------+

把character_set_results也设置成UTF-8;

set character_set_results=utf8;

编码统一之后就可以正常显示了。

可选搜索范围

根据MySQL功能进行的拓展,本质上就是扩展where子句之后判断方法。然而实现起来还是挺麻烦的。
在这里插入图片描述

首先用checkbox做出三个可选项,分别对应在标题、关键字、内容里面搜索。在用户点下搜索按钮时,将三个checkbox的值作为参数传给后端,并生成相应的MySQL查询命令。需要特判一下三个都没有选的情况,否则会崩溃。

创建三个checkbox,先判断是否都为false,再传参。

<input type="checkbox" class="option1">标题
<input type="checkbox" class="option2">关键字
<input type="checkbox" class="option3">内容
<script>
    $(document).ready(function(){
        $('input.search_title').click(function(){
            var title=$('input.option1').is(":checked");
            var keywords=$('input.option2').is(":checked");
            var content=$('input.option3').is(":checked");
            if (title.valueOf()==false && keywords.valueOf()==false && content.valueOf()==false){
                alert("请至少选择一项作为搜索范围~");
                return;
            }
        $.get('/process_get?title='+title+'&keywords='+keywords+'&content='+content+'&sort='+sort+'&words='+words,function(){...}
    }

相对应的非常朴素的后端代码

var fetchSql = "select id,title,keywords,url,resource,pubtime " + "from news where";
    var conc=false;
    if (request.query.title=="true"){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" title like '%"+request.query.words + "%'";
    }
    if (request.query.keywords=="true"){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" keywords like '%"+request.query.words + "%'";
    }
    if (request.query.content=="true"){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" content like '%"+request.query.words + "%'";
    }

日期排序

跟上面那个就是一个原理。贴一下后端代码。

if (request.query.sort=="true"){
        fetchSql+=" order by pubtime ASC";
    }
    fetchSql+=";";

最终效果、代码

在这里插入图片描述

就是能在标题、关键字、内容里面选择搜索范围,并选择是否要按日期排序,差不多上面都提到了。再加一点简陋的css样式就完事了。

后续可以做一下统计关键词热度的功能。其实本来就想做的,发现不会,就回头研究一下吧。(如果只是手动给几个关键词,统计一下出现次数还是可以的,但是没啥意义)

总结

总结一下就是要用的东西太多但是会的太少所以就虎头蛇尾了。后半部分基本也不是自己完整写下来的,都是照着示例和网上的代码调调改改然后弄出来一个能不崩溃的东西而已。说实话,按照以前写代码的习惯,我不太喜欢调用一些自己实现不了的模块和功能,或者起码要清楚知道它的原理才能放心用。但是这个项目做下来就感觉,如果真的要把方方面面都讲清楚,哪怕只是一个模块,都是很复杂的,像MySQL这种可能都可以专门开一节课去讲了。这也就是我在下半篇其实几乎没有对代码的解释的原因,一是确实不够了解,二是心态已经变成“无所谓,能用就行”了。当然收获也是很大的,对很多网络相关的知识以前是完全没概念,现在起码有了一点思路,同时体现在对前后端交互方式的感性认识上。

可能得以后自己真的要做一个网页的时候才能有更深的理解吧。但是作为作业做到这应该也差不多了,因为我高数真的要挂科了orz。

代码的话就贴一个爬虫的(一共有五个,对应五个新闻网)一个前端的一个后端的,比较长,后面也没东西了,所以看到这就可以结束了。

爬虫:

var myRequest = require('request');
var myIconv = require('iconv-lite');
var myCheerio = require('cheerio');
var fs = require('fs');
var mysql=require('./mysql.js');
require('date-utils');

var myURL="http://www.chinanews.com/";
var myResource="中国新闻网"
var myEncoding='UTF-8';
var newsExg=/\d{4}\/\d{2}-\d{2}\/\d{7}.shtml/;
var options={
    url:myURL,
    encoding:null,
    timeout:10000
};
myRequest(options,function(err,res,body){
    if (err || res.statusCode!==200){
        console.log("种子页面爬取失败~");
        return;
    }
    var html=myIconv.decode(body,myEncoding);
    var $=myCheerio.load(html,{decodeEntities:false});
    var newsDiv=$('a');
    newsDiv.each(function(i,e){
        news_url=get_news_url(e);
        if (newsExg.test(news_url)){
            check(news_url);
        }
    })
})
function get_news_url(e){
    var $_=myCheerio.load(e,{decodeEntities:false});
    var news_url=$_('a').attr('href');
    if (news_url==="javascript:void(0)"||news_url===undefined) return;
    if (news_url.startsWith("http://"))
        news_url=news_url;
    else
        if (news_url.startsWith("//"))
            news_url="http:"+news_url;
        else
            if (news_url.startsWith("/"))
                news_url=myURL.slice(0,-1)+news_url;
    return news_url;
}
function check(news_url){
    var sel_sql="SELECT url from news where url=? ";
    var sel_sql_params=[news_url];
    mysql.query(sel_sql,sel_sql_params,function(err,vals,fields){
        if (err){
            console.log("检索url错误~");
            return;
        }
        if (vals.length>0){
            console.log("url重复访问~");
            ret=false;
        }
        else get_news(news_url);
    });
};

function get_news(news_url){
    var options={
        url:news_url,
        encoding:null,
        timeout:10000
    }
    var info={
        title:'',resource:'',author:'',content:'',
        keywords:'',pubtime:'',fetchtime:'',url:''
    }
    myRequest(options,function(err,res,body){
        if (err || res.statusCode!==200){
            console.log("新闻页面爬取失败~");
            return;
        }    
        var html=myIconv.decode(body,myEncoding);
        var $=myCheerio.load(html,{decodeEntities:false});
        info.title=get_title($);
        info.resource=get_resource();
        info.author=get_author($);
        info.content=get_content($);
        info.keywords=get_keywords($);
        info.pubtime=get_pubtime($);
        info.fetchtime=get_fetchtime();
        info.url=get_url(news_url);
        console.log("信息整理完毕~");
        Insert_into_sql(info);
    });
}
function get_title($){
    var title='';
    title=$('title').text();
    var tmp=title.substring(title.indexOf('-中新网'));
    title=title.replace(tmp,'');
    return title;
}
function get_resource(){
    var resource=myResource;
    return resource;
}
function get_author($){
    var author='';
    author=$("#author_baidu").text();
    author=author.substring(3);
    return author;
}
function get_content($){
    var content='';
    content=$('div.left_zw').children('p').text();
    content=content.replace(/[\n\t\s\t]/g,'');
    return content;
}
function get_keywords($){
    var key='';
    key=$('meta[name=keywords]').attr('content');
    return key;
}
function get_pubtime($){
    var pubtime='';
    pubtime=$('div.left-t').text();
    var tmp=pubtime.substring(pubtime.indexOf("来源"));
    pubtime=pubtime.replace(tmp,'');
    pubtime=pubtime.replace(/\s/g,'');
    pubtime=pubtime.replace(/[年月日:]/g,'-');
    return pubtime;
}
function get_fetchtime(){
    var fetchtime='';
    fetchtime=new Date().toFormat("YYYY-MM-DD-HH-MM-SS");
    return fetchtime;
}
function get_url(news_url){
    var url='';
    url=news_url;
    return url;
}
function Insert_into_sql(info){
    var add_sql="INSERT INTO news(id,title,resource,author,content,keywords,pubtime,fetchtime,url)VALUES(0,?,?,?,?,?,?,?,?)"
    var add_sql_params=[info.title,info.resource,info.author,info.content,info.keywords,info.pubtime,info.fetchtime,info.url];
    mysql.query(add_sql,add_sql_params,function(qerr,vals,fields){
        if(qerr){
            console.log("新闻插入失败~");
            console.log(qerr.message,info);
            return;
        }
        else
            console.log("新闻插入成功~");
    });
}

前端:

<head>
    <style>
        body{
            background-color: cornflowerblue;
            margin-left:30px;
            margin-right:30px;
            border:3px white solid;
        }
        view.title{
            color: white;
            font-size: 25px;
            display: flex;
            justify-content: center;
        }
        form{
            display: flex;
            justify-content: center;
            color:white;
        }
        form input.search{
            width: 400px;
        }
        div.cardLayout{
            color: white;
        }
        table{
            padding:80px;
            border:2px solid white;
        }
        th{
            font-size: 20px;
            color:white;
            border:2px solid white;
            text-align: center;
        }
        td{
            color:white;
            width: 10%;
            height: 80px;
            border:2px solid white;
            text-align: center;
        }
    </style>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
</head>
<body>
    <view class='title'>
        <h1>近期新闻内容整合搜索</h1>
    </view>
    <form>
        请输入关键词:<input type="text" class="search">
        <input type="button" class="search_title" value="搜索">
        搜索范围:
        <input type="checkbox" class="option1">标题
        <input type="checkbox" class="option2">关键字
        <input type="checkbox" class="option3">内容
        <input type="checkbox" class="option4">根据日期排序
    </form>
    <div class="cardLayout"></div>
    <table width="100%" id="record2"></table>
</div>
    <script>
        $(document).ready(function(){
            $('input.search_title').click(function(){
              var title=$('input.option1').is(":checked");
              var keywords=$('input.option2').is(":checked");
              var content=$('input.option3').is(":checked");
              if (title.valueOf()==false && keywords.valueOf()==false && content.valueOf()==false)
              {
                alert("请至少选择一项作为搜索范围~");
                return;
              }
              var sort=$('input.option4').is(":checked");
              var words=$('input.search').val();
              $.get('/process_get?title='+title+'&keywords='+keywords+'&content='+content+'&sort='+sort+'&words='+words, function(data) {
                    $("#record2").empty();
                    $("#record2").append('<tr class="cardLayout"><th>新闻编号</th><th>标题</th>' +
                        '<th>关键字</th><th>链接</th><th>来源</th><th>日期</th></tr>');
                    for (let list of data) {
                        let table = '<tr class="cardLayout">';
                        Object.values(list).forEach(element => {
                            table += ('<td>'+element + '</td>');
                        });
                        $("#record2").append(table + '</tr>');
                    }
                });
            })
        })
        
    </script>
</body>

后端:

var express = require('express');
var router = express.Router();
var mysql = require('../mysql.js');

/* GET home page. */
router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express' });
});

router.get('/process_get', function(request, response) {
    //sql字符串和参数
    var fetchSql = "select id,title,keywords,url,resource,pubtime " + "from news where";
    var conc=false;
    if (request.query.title=="true"){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" title like '%"+request.query.words + "%'";
    }
    if (request.query.keywords=="true"){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" keywords like '%"+request.query.words + "%'";
    }
    if (request.query.content=="true"){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" content like '%"+request.query.words + "%'";
    }
    if (request.query.sort=="true"){
        fetchSql+=" order by pubtime ASC";
    }
    fetchSql+=";";
    mysql.query(fetchSql, function(err, result, fields) {
        response.writeHead(200, {
            "Content-Type": "application/json"
        });
        response.write(JSON.stringify(result));
        response.end();
    });
});
router.get('/process_get_inorder', function(request, response) {
    //sql字符串和参数
    var fetchSql = "select id,title,keywords,url,resource " + "from news where";
    var conc=false;
    if (request.query.title){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" title like '%"+request.query.words + "%'";
    }
    if (request.query.keywords){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" keywords like '%"+request.query.words + "%'";
    }
    if (request.query.content){
        if (!conc){
            conc=true;
        }
        else fetchSql+=" or"
        fetchSql+=" content like '%"+request.query.words + "%'";
    }
    fetchSql+=";";
    console.log(fetchSql);
    fetchSql="select id,title,keywords,url,resource from news where title like '%新冠%' or keywords like '%新冠%' or content like '%新冠%';";
    mysql.query(fetchSql, function(err, result, fields) {
        response.writeHead(200, {
            "Content-Type": "application/json"
        });
        response.write(JSON.stringify(result));
        response.end();
    });
});
module.exports = router;
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值