实验任务
实验结果展示
准备工作
- 安装Node.js
- 安装request, cheerio, mysql
在控制台输入
npm install -g request
npm install -g cheerio
npm install -g mysql
- 安装vs2019
开始实验
作为萌新,首先运行了老师给的代码,发现自己还是有些不明白,所以打算从先实践入手。毕竟胡老师说得好:“不是会了才去写,而是写了才会。”
1.调包
作为一个有丰富的 C语言编程经验的人,每次写程序时我都会先自信满满地敲入include <stdio.h>
然后陷入沉默,所以我明白第一步我要先引入一些包:
'use strict'; //使用严格模式
var cheerio = require('cheerio');
var request = require('request');
var fs = require('fs');
var database = require('./database.js');
2.通过request访问网页并储存
原神攻略网
因为最近在打游戏,所以打算先从爬取游戏攻略网站入手,初步熟悉爬虫的写法。
首先进入到游戏的wiki攻略,检查文章标题,发现二级网站的title和url在<a>
中,所以我需要想办法抓取含有url和title的<a>
。通过<a>
的子标签<font class="BOX-zt">
可以筛选出<a>
。
先开四个数组分别存放网页URL,标题,作者,内容等信息
var Url = [];
var Title = [];
var Content = [];
var Author = [];
然后在代码中写入:
request({ url: "https://wiki.biligame.com/ys/%E6%94%BB%E7%95%A5", encoding: null, headers: null }, function (err, res, body) {
if (err || res.statusCode != 200) {
console.error(err);
console.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
let cnt = 0;
let divEq = $("font.BOX-zt").eq(cnt); //定位<font class="BOX-zt">
let title = divEq.text(); //文章标题
let urlstr = divEq.parent("a").attr("href"); //文章url
});
当title与urlstr都非空时,说明抓取到了<a>
,之后便可通过urlstr进入到二级网站。分析其中的元素:
分析发现,文章标题都在<h1 id="firstHeading">
中,通过源代码找到了作者信息都在<div class="des-name">
中,故可通过通过这两个标签找到文章标题和作者。但当我想要找到文章内容时,一个障碍出现了:因为该网站由某一攻略组大佬们建立并编辑,内容非常复杂,并且都储存在了<div id="content">
里。但对比多个网站发现,所有的文章都有类似这样一句话:
在这句话之后便是正文部分,于是我想到爬取<div id="content">
中的内容后删除掉这句话之前的部分。故在request中敲入:
while (title && urlstr) {
let longURL = "https://wiki.biligame.com" + urlstr;
request({ url: longURL, encoding: null, headers: null }, function (err, res, body) {
if (err || res.statusCode != 200) {
console.error(err);
onsole.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
Url.push(res.request.uri.href);
Title.push($("h1#firstHeading").text());
Author.push($("div.des-name").text());
Content.push($("div#content").text().toString().split(",请注意时效性。")[1]);
cnt++;
divEq = $("font.BOX-zt").eq(cnt);
title = divEq.text();
urlstr = divEq.parent("a").attr("href");
}
这时将所有内容都储存在数组中,再将信息储存在了本地文件夹result中:
setTimeout(function () {
for (let i in Url) {
fs.writeFileSync("result/" + i + ".txt", `URL: ${Url[i]}\nTitle: ${Title[i]}\Author: ${Author[i]}\nContent: ${Content[i]}`);
}
}, 10000);
到这里第一个网站的爬虫已经写好了。
Danganronpa Wiki
虽然说老师建议爬取新闻网站,但因为我想在此基础上爬取一些个人喜好相关的东西,故找到了自己喜欢的游戏Danganronpa的wiki,想要爬取角色的个人信息,并且尝试将其储存在数据库中。
通过request请求网页
首先我将我想爬取的子网页放在数组中:
let URLs = [
"https://danganronpa.fandom.com/wiki/Danganronpa:_Trigger_Happy_Havoc",
"https://danganronpa.fandom.com/wiki/Danganronpa_2:_Goodbye_Despair",
"https://danganronpa.fandom.com/wiki/Danganronpa_V3:_Killing_Harmony"
];
进入到其中一个页面,分析元素
发现想要找到的人物信息都在这样的表格中(而且还是两张表)(所以说主角待遇就是不一样啊)。
进入到二级网页:
想爬取该人物的appearance和talent,分析之后发现了一件让我非常为难的事情:
它的文本不连续,而且不在同一个标签下。但我发现它们都在两个<h2>
中间,于是想通过提取出两个<h2>
中间的<p>
来实现。
但是我不知道该如何去实现这个功能,于是我打开了官方文档,发现了这样一个函数:
所以我想到了,通过<span id="Appearance">
定位到它的上一级标签,之后判断它的next()
是否为<h2>
即可
let appearance = $("span#Appearance").parent("h2").next();
let strAppearance = "";
while (!appearance.is("h2") && appearance.nextAll().text()) {
if (appearance.is("p")) {
strAppearance = strAppearance + appearance.text();
}
appearance = appearance.next();
}
之后便是找talent相关,分析发现不同人物之间的talent不尽相同。在写了许多层if之后,我想到了一个歪主意:所有人物的talent都包含“Ultimate”这样一个单词,所以我可以通过这个单词找到他们的talent:
let talentText = "";
let talentSpan = $("span.mw-headline");
talentSpan.each(function (index, element) {
let curText = $(this).text();
if (curText.search("Ultimate") == 0) {
talentText = curText;
}
}
完整代码如下:
for (let urlName of URLs) {
request({ url: urlName, encoding: null, headers: null }, function (err, res, body){
if (err || res.statusCode != 200) {
console.error(err);
console.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
let cnt = 0;
let tableEq = $("table.wikitable").eq(cnt);
while (tableEq.text()) {
let cnt1 = 0;
let trEq = tableEq.children("tbody").children("tr").eq(cnt1);
while (trEq.text()) {
let cnt2 = 0;
let thEq = trEq.children("th").eq(cnt2);
let name = thEq.children("a").text();
let urlstr = "https://danganronpa.fandom.com" + thEq.children("a").attr("href");
while (name && urlstr) {
console.log(`爬取人物${name}`);
request({ url: urlstr, encoding: null, headers: null }, function (err, res, body) {
if (err || res.statusCode != 200) {
console.error(err);
console.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
let appearance = $("span#Appearance").parent("h2").next();
let strAppearance = "";
while (!appearance.is("h2") && appearance.nextAll().text()) {
if (appearance.is("p")) {
strAppearance = strAppearance + appearance.text();
}
appearance = appearance.next();
}
let talentText = "";
let talentSpan = $("span.mw-headline");
talentSpan.each(function (index, element) {
let curText = $(this).text();
if (curText.search("Ultimate") == 0) {
talentText = curText;
}
});
database.query('INSERT INTO danganronpa(URL, Name, Appearance, Talent) VALUES(?, ?, ?, ?);', [
res.request.uri.href,
$("h1#firstHeading").text(),
strAppearance,
talentText
], function (err, vals, fields) {
if (err) {
console.error(`数据库错误:${err}`);
}
});
console.log(`完成爬取${$("h1#firstHeading").text()}`);
});
cnt2++;
thEq = trEq.children("th").eq(cnt2);
name = thEq.children("a").text();
urlstr = "https://danganronpa.fandom.com" + thEq.children("a").attr("href");
}
cnt1++;
trEq = tableEq.children("tbody").children("tr").eq(cnt1);
}
cnt++;
tableEq = $("table.wikitable").eq(cnt);
}
});
}
将数据储存至数据库
通过navicat这个软件我建好了数据库。并为数据建表:
当爬完二级网站时,将数据插入到数据库中:
database.query('INSERT INTO danganronpa(URL, Name, Appearance, Talent) VALUES(?, ?, ?, ?);', [
res.request.uri.href,
$("h1#firstHeading").text(),
strAppearance,
talentText
], function (err, vals, fields) {
if (err) {
console.error(`数据库错误:${err}`);
}
});
成功!
JOJO Wiki
作为一个JO厨,我找到了JOJO的Wiki,并找到了它的角色所在的页面
查看网页源代码后,将1-8部的页面储存在数组中:
let URLs = [
"https://jojo.fandom.com/wiki/Template:Part_1_Character_Table",
"https://jojo.fandom.com/wiki/Template:Part_2_Character_Table",
"https://jojo.fandom.com/wiki/Template:Part_3_Character_Table",
"https://jojo.fandom.com/wiki/Template:Part_4_Character_Table",
"https://jojo.fandom.com/wiki/Template:Part_5_Character_Table",
"https://jojo.fandom.com/wiki/Template:Part_6_Character_Table",
"https://jojo.fandom.com/wiki/Template:Part_7_Character_Table",
"https://jojo.fandom.com/wiki/Template:Part_8_Character_Table"
];
来到其中一个页面,发现想要爬取的二级网站还是存在于表格中,与之前的网页前端很像。
通过<span class="fadeout">
可以找到含有二级网站url的标签<a>
(同样需要手动补全),进入到个人页面之后同样想获得他们每个人的appearance和stand(替身)。但是分析发现情况远比我想得复杂得多,于是我写好之后运行,再逐个分析那些没有抓取到信息的人的页面,最后写出来一串代码:
let Stand = null; //将stand初始为null
let abilitySpan = $("span#Abilities").parent("h2"); //通过Abilities标签定位
if (!abilitySpan) { //特判,如果没有找到<span id="Abilities">,看是否有其他情况
abilitySpan = $("span#Abilities_and_Powers").parent("h2");
}
abilitySpan = abilitySpan.next();
while (!abilitySpan.is("h2") && abilitySpan.nextAll().text()) {
if (abilitySpan.is("dl")) {
Stand = abilitySpan.children("dd").children("i").children("a").text();
break;
}
abilitySpan = abilitySpan.next();
}
if (!Stand) { //特判,如果stand为null,看是否有其他情况
abilitySpan = $('span[id="Stand"]').parent("h3");
abilitySpan = abilitySpan.next();
while (!abilitySpan.is("h2") && abilitySpan.nextAll().text()) {
if (abilitySpan.is("p")) {
Stand = abilitySpan.children("b").children("a").text();
break;
}
abilitySpan = abilitySpan.next();
}
}
所以说编写网页的人能不能统一一下风格啊(╯‵□′)╯︵┻━┻
最终代码如下:
for (let urlName of URLs) {
request({ url: urlName, encoding: null, headers: null }, function (err, res, body){
if (err || res.statusCode != 200) {
console.error(err);
console.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
let cnt = 0;
let spanEq = $("span.fadeout").eq(cnt);
let name = spanEq.children("a").attr("title");
let urlstr = "https://jojo.fandom.com" + spanEq.children("a").attr("href");
while (name && urlstr) {
console.log(`爬取人物${name}`);
request({ url: urlstr, encoding: null, headers: null }, function (err, res, body) {
if (err || res.statusCode != 200) {
console.error(err);
console.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
let appearance = $('span[id="Appearance"]').parent("h2");
if (!appearance.text()) {
appearance = $('span[id="Appearance.2FPersonality"]').parent("h2");
if (!appearance.text()) {
appearance = $('span[id="_Appearance"]').parent("h2");
if (!appearance.text()) {
appearance = $('span[id="Appearance_and_Personality"]').parent("h2");
}
}
}
appearance = appearance.next();
let strAppearance = "";
while (!appearance.is("h2") && appearance.nextAll().text()) {
if (appearance.is("p")) {
strAppearance = strAppearance + appearance.text();
}
appearance = appearance.next();
}
let Stand = null;
let abilitySpan = $("span#Abilities").parent("h2");
if (!abilitySpan) {
abilitySpan = $("span#Abilities_and_Powers").parent("h2");
}
abilitySpan = abilitySpan.next();
while (!abilitySpan.is("h2") && abilitySpan.nextAll().text()) {
if (abilitySpan.is("dl")) {
Stand = abilitySpan.children("dd").children("i").children("a").text();
break;
}
abilitySpan = abilitySpan.next();
}
if (!Stand) {
abilitySpan = $('span[id="Stand"]').parent("h3");
abilitySpan = abilitySpan.next();
while (!abilitySpan.is("h2") && abilitySpan.nextAll().text()) {
if (abilitySpan.is("p")) {
Stand = abilitySpan.children("b").children("a").text();
break;
}
abilitySpan = abilitySpan.next();
}
}
database.query('INSERT INTO jojo(URL, Name, Appearance, Stand) VALUES(?, ?, ?, ?);', [
res.request.uri.href,
$("h1#firstHeading").text(),
strAppearance,
Stand
], function (err, vals, fields) {
if (err) {
console.error(`数据库错误:${err}`);
}
});
console.log(`完成爬取${$("h1#firstHeading").text()}`);
});
cnt++;
spanEq = $("span.fadeout").eq(cnt);
name = spanEq.children("a").attr("title");
urlstr = "https://jojo.fandom.com" + spanEq.children("a").attr("href");
}
});
}
写好程序第一天:我的代码只有我和上帝才能看懂!
第二天:我的代码只有上帝才能看懂!
第三天:谁都别想看懂我的代码!
Abyss Wiki
最后来到了著名的治愈漫画来自深渊的Wiki,和前两个网页结构差不多,风格也很统一,故相较于其他两个网站,这个网站的爬虫比较好写:
request({ url: "https://madeinabyss.fandom.com/wiki/Characters", encoding: null, headers: null }, function (err, res, body) {
if (err || res.statusCode != 200) {
console.error(err);
console.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
let cnt = 0;
let tbodyEq = $("tbody").eq(cnt);
while (tbodyEq.text()) {
let cnt1 = 0;
let tdEq = tbodyEq.children("tr").children("td").eq(cnt1);
let name = tdEq.children("div").children("div").children("p").children("a").attr("title");
let urlstr = "https://madeinabyss.fandom.com/" + tdEq.children("div").children("div").children("p").children("a").attr("href");
while (name && urlstr) {
console.log(`爬取人物${name}`);
request({ url: urlstr, encoding: null, headers: null }, function (err, res, body){
if (err || res.statusCode != 200) {
console.error(err);
console.error(res.statusCode);
return;
}
let $ = cheerio.load(body);
let Appearance = "";
let appearance = $('span[id="Appearance"]').parent("h2").next();
while (!appearance.is("h2") && appearance.nextAll().text()) {
if (appearance.is("p")) {
Appearance = Appearance + appearance.text();
}
appearance = appearance.next();
}
let Abilities = "";
let ability = $('span[id="Abilities"]').parent("h2").next();
while (!ability.is("h2") && ability.nextAll().text()) {
if (ability.is("h3")) {
if (Abilities) {
Abilities = Abilities + "/" + ability.children("span").children("b").text();
} else {
Abilities = ability.children("span").children("b").text();
}
}
ability = ability.next();
}
database.query('INSERT INTO abyss(URL, Name, Appearance, Abilities) VALUES(?, ?, ?, ?);', [
res.request.uri.href,
$("h1#firstHeading").text(),
Appearance,
Abilities
], function (err, vals, fields) {
if (err) {
console.error(`数据库错误:${err}`);
}
console.log(`完成爬取${$("h1#firstHeading").text()}`);
});
});
cnt1++;
tdEq = tbodyEq.children("tr").children("td").eq(cnt1);
name = tdEq.children("div").children("div").children("p").children("a").attr("title");
urlstr = "https://madeinabyss.fandom.com/" + tdEq.children("div").children("div").children("p").children("a").attr("href");
}
cnt++;
tbodyEq = $("tbody").eq(cnt);
}
});
这样,爬虫部分就写完了。
3.前端
爬虫部分已经差不多写好,接下来便是前端。首先我写了一个比较简单的前端,它有三张表,分别储存三个网站的信息,同时有一个搜索框,以便提供关键词搜索:
代码如下:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1900">
<title>二次元爬虫呈示页面</title>
</head>
<body>
<form action="/" method="GET">
<p>Please input your keywords: <input name="kw" type="text" style="width: 640px" /><input type="submit" value="Search" /></p>
</form>
<table border="1" id="danganronpa">
<caption style=" font-size: 24px;
">danganronpa</caption>
<tr>
<th>Name</th>
<th>Appearance</th>
<th>Talent</th>
</tr>
</table>
<table border="1" id="abyss">
<caption style="font-size: 24px;">abyss</caption>
<tr>
<th>Name</th>
<th>Appearance</th>
<th>Abilities</th>
</tr>
</table>
<table border="1" id="jojo">
<caption style="font-size: 24px;">jojo</caption>
<tr>
<th>Name</th>
<th>Appearance</th>
<th>Stand</th>
</tr>
</table>
</body>
</html>
4.数据库后端
之后便是完善后端,效果如下:
代码如下:
'use strict';
var express = require('express');
var cheerio = require('cheerio');
var fs = require('fs');
var database = require('./database.js');
var port = process.env.PORT || 1337;
var server = express();
server.use(express.static("public"));
server.get('/', function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/html' });
let AdditionSQL = "";
var html = fs.readFileSync("index.html");
let $ = cheerio.load(html);
if (req.query.kw) {
AdditionSQL = ` WHERE (Appearance LIKE '%${req.query.kw}%') OR (Name LIKE '%${req.query.kw}%')`;
} //关键词搜索
database.query_noparam('SELECT * FROM danganronpa' + AdditionSQL + ';', function(qerr, vals, fields) {
if (qerr) {
console.error(qerr);
return;
}
for (let i in vals) {
$('table#danganronpa').append(`<tr>
<td><a href=${vals[i].URL}>${vals[i].Name}</a></td>
<td>${vals[i].Appearance}</td>
<td>${vals[i].Talent}</td>
</tr>`);
}
database.query_noparam('SELECT * FROM abyss' + AdditionSQL + ';', function (qerr, vals, fields) {
if (qerr) {
console.error(qerr);
return;
}
for (let i in vals) {
$('table#abyss').append(`<tr>
<td><a href=${vals[i].URL}>${vals[i].Name}</a></td>
<td>${vals[i].Appearance}</td>
<td>${vals[i].Abilities}</td>
</tr>`);
}
database.query_noparam('SELECT * FROM jojo' + AdditionSQL + ';', function (qerr, vals, fields) {
if (qerr) {
console.error(qerr);
return;
}
for (let i in vals) {
$('table#jojo').append(`<tr>
<td><a href=${vals[i].URL}>${vals[i].Name}</a></td>
<td>${vals[i].Appearance}</td>
<td>${vals[i].Stand}</td>
</tr>`);
}
res.end($.html());
});
});
});
});
server.listen(port);
5.“装饰”前端
因为觉得这样的页面过于单调,想要美化它。之后我在Bilibili弹幕网中下载了大佬根瘤菌rkzj提供的可莉的live2d模型,想将其作为桌宠添加到前端。但是通过多种方式发现,网络上提供的方法只能添加model1文件,然而现在下载下来的live2d模型全是model3,无法直接添加至前端。事情到这里似乎没办法进行。
去网上查了许多资料,直到发现了一篇笔记。
于是我按照文中给的方法下载了live2d的软件以及官方SDK,并按照B站大佬仰望星空的sun的视频修改了源码。终于,一只会动会“说话”的可莉写成了。
点击可莉,会随机显示角色台词,并有可能触发动作。
眼球及头部可跟随鼠标转动。
到这里已经差不多了,但看着空白的背景,还是觉得空空荡荡。所以最后我给前端添加上了背景:在<body>
中输入以下代码:
<body background="background.jpg"
style="background-repeat:no-repeat;
background-size:100% 100%;
background-attachment: fixed;">
其中:
background-repeat:no-repeat; <!--设置图片不重复-->
background-size:100% 100%; <!--图片平铺-->
background-attachment: fixed; <!--图片固定,不随页面滚动-->
最终前端代码如下:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1900">
<title>二次元爬虫呈示页面</title>
<!--注意你复制到的位置,根据实际情况修改文件路径-->
<link rel="stylesheet" href="live2d/css/live2d.css" />
</head>
<body background="background.jpg"
style="background-repeat:no-repeat;
background-size:100% 100%;
background-attachment: fixed;">
<form action="/" method="GET">
<p>Please input your keywords: <input name="kw" type="text" style="width: 640px" /><input type="submit" value="Search" /></p>
</form>
<table border="1" id="danganronpa">
<caption style=" font-size: 24px;
">danganronpa</caption>
<tr>
<th>Name</th>
<th>Appearance</th>
<th>Talent</th>
</tr>
</table>
<table border="1" id="abyss">
<caption style="font-size: 24px;">abyss</caption>
<tr>
<th>Name</th>
<th>Appearance</th>
<th>Abilities</th>
</tr>
</table>
<table border="1" id="jojo">
<caption style="font-size: 24px;">jojo</caption>
<tr>
<th>Name</th>
<th>Appearance</th>
<th>Stand</th>
</tr>
</table>
<!-- Canvas -->
<div id="landlord">
<div class="message" style="opacity:0"></div>
<canvas id="live2d" width="280" height="250" class="live2d"></canvas>
<div class="hide-button">隐藏</div>
</div>
<!-- Pollyfill script -->
<script src="https://unpkg.com/core-js-bundle@3.6.1/minified.js"></script>
<!-- Live2DCubismCore script -->
<script src="Core/live2dcubismcore.js"></script>
<!-- Build script -->
<script src="dist/bundle.js"></script>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript">
//这个路径时message.json的存在目录
var message_Path = './live2d/'
</script>
<script type="text/javascript" src="live2d/js/message.js"></script>
</body>
</html>
大功告成!最终结果便是开头展示那样:
终于可以和可莉一起去炸鱼了!
6.词云
4月26日,为了更加符合老师的要求突然增加的需求令我措不及防,决定连夜删库跑路为了实现热度分析,我决定做一个词云。
在控制台输入:
pip install jieba
pip install wordcloud
安装好之后,借用了godweiyang的词云源码,在python环境下运行,生成如下图片:
为了将这张图片添加至前端,我在搜索框下添加了一个按钮,
<br />
<a href="/Klee_table.png"><button>Word Cloud</button></a>
<br />
点击即可查看。
一些小心得
作为一个大一学生,初次接触这些东西,感觉还是有些困难。首先是安装这样那样的包,然后是配置一些环境,对我来说过于抽象。从最开始写爬虫,到最后做成这样一个页面,离不开班里同学们的帮助,也非常感谢一些大佬在网上分享自己的经验。通过这次作业,我也初步了解到了一些Javascript语法以及简单的前端写法。从一开始的畏手畏脚,到后来可以帮助周围的同学,我也明白了,学习编程最怕的是因为“不会”然后不去写。真正当敲下代码的那一刻,才是真正学习的开始。
あの鳥はまだうまく飛べないけど
いつかは風を切って知る