我的第一次Web编程实验

实验任务

在这里插入图片描述

实验结果展示

在这里插入图片描述
在这里插入图片描述

搜索功能

准备工作

  1. 安装Node.js
  2. 安装request, cheerio, mysql
    在控制台输入
    npm install -g request
    npm install -g cheerio
    npm install -g mysql
  3. 安装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语法以及简单的前端写法。从一开始的畏手畏脚,到后来可以帮助周围的同学,我也明白了,学习编程最怕的是因为“不会”然后不去写。真正当敲下代码的那一刻,才是真正学习的开始。

あの鳥はまだうまく飛べないけど
いつかは風を切って知る

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值