项目介绍
为什么会有这么”奇葩“的项目呢?
这个想法的出现来源于,我之前想过要写一篇小说,但是苦于思考创造人名这一步,所以就在想能不能有一个网站,我只需要输入姓氏,就能得到一些人名?所以就有了这个项目。
哪里来的数据呢?
这个问题,其实也不难解决,百度搜一会就有答案啦。于是我找到了姓名大全这个网站,右击给这个网站做了一个“检查”,发现这个网站的构造挺简单的,不需要太高深的爬虫技巧就可以获取到数据。再把数据存到sqlite3数据库中,作为这个项目的源数据。
这个项目会很复杂吗?
越学越简单,越简单越难。
任何项目都可以很复杂的,所以以我的能力,尽可能的去实现较“复杂”的操作。
其实,乍一看,这个项目不复杂:
- 爬虫获取数据,并保存到数据库。
- 后端从数据获取数据,发送到前端。
- 前端一个搜索框,根据输入的姓氏,从后端得到姓名并展示。
我在这个项目中主要用到的是这些技术:requests,flask,vue。有些知识我也不是很懂,都是直接一边学一边用,我觉得学以致用,更能感受到学习的乐趣。
源代码获取
公众号“帅帅的Python”,回复“姓名”即可获取源码。
数据获取
网站分析
需要从姓名大全这个网站获取的数据有:
- 百家姓列表:[‘赵’,‘钱’,‘孙’,‘李’,…]
- 每个姓对应的男生名、女生名
获取一个百家姓列表,主要是为了判断前端输入的姓氏是否正确。
一、在谷歌浏览器中,右击“检查”,通过一番分析后,找到一个a标签,如下形式,可以通过这个标签,获取到该姓氏的具体的超链接(通过href获取),以及百家姓列表(通过文本拆分获取)。
<a class="btn btn2" href="//zhao.resgain.net/name_list.html" title="赵姓名字大全共有赵姓名字162998个">赵姓名字大全</a>
二、继续分析,点击超链接进入到姓氏的详情页,发现每个姓氏都有男生和女生的超链接,如下:
# 男生网站为:http://zhao.resgain.net/name/boys.html
# 女生网站为:http://zhao.resgain.net/name/girls.html
与之前分析的a标签中的href中的超链接有所不同,所以需要将**/name_list.html替换为/name/boys.html和/name/girls.html**才能得到对应姓氏的男生、女生姓名地址。
三、进入男生姓名网站中,每个姓名的html代码如下:
<a href="#" class="livemsg" data-name="赵竹林"><span class="glyphicon glyphicon-comment"></span></a>
通过class="livemsg"定位到该a标签,然后获取到data-name的属性值即可得到姓名。
数据库设计
这里选择sqlite3为数据库,主要是因为不需要复杂的配置,操作简单,轻便小巧。
这里只需要存姓名(NAME)、性别(SEX)两个字段即可,当然,再加一个自增的ID字段。
数据库设计代码如下:
def create_db():
conn = sqlite3.connect(r"tools_app.db")
cursor = conn.cursor()
cursor.execute('''CREATE TABLE RANDOM_NAME
(ID INTEGER PRIMARY KEY AUTOINCREMENT,
NAME TEXT NOT NULL,
SEX TEXT NOT NULL);''')
print("RANDOM_NAME created successfully")
conn.commit()
conn.close()
代码解释:
1、conn是一个sqlite3数据库对象,只需传入database参数,这里传的tools_app.db表示,连接的是同级目录下的名为tools_app.db的数据库(数据库不需要提前创建)
2、cursor为操作数据库的“游标”,可以把数据库比喻成一个仓库,游标就像是一个优秀的员工,cursor.execute()就是这个员工需要对这个仓库做的事情;conn.commit()就是员工操作完后,仓库将操作的结果告诉员工。conn.close()仓库关门,期待下一次的开启。
这个函数运行完之后,会在同级目录下创建一个名为tools_app.db的数据库,该数据库中有一个名为RANDON_NAME的表。
爬虫设计
通过之前的网站分析,爬虫的运行应该分为以下三步:
- 通过姓名大全地址获取到每个姓氏href和文本信息。
- 通过第一步获取的href,获取男生和女生的网站。
- 通过第二步获取的网站获取对应的姓名。
第一步,代码详解:
def get_name_link():
url = "http://www.resgain.net/xmdq.html"
res = requests.get(url)
soup = BeautifulSoup(res.text, 'lxml')
name_links = []
for s in soup.find_all(attrs={'class': 'btn btn2'}):
name_links.append("http:" + s.get('href'))
return name_links
代码解释:
1、通过request.get()方法,传入url就可以获取到对应的网页对象,使用res.text属性获取到网页源码
2、使用BeautifulSoup格式化网页,可以很方便,很快速的定位到需要的数据。
3、soup.find_all(attrs={‘class’: ‘btn btn2’}),通过class属性为btn btn2找到所有符合条件的元素,返回的是一个列表
4、通过s.get(‘href’)方法可以获取到标签元素的href属性的值
同理,可以获取到百家姓列表:
def get_name_list():
url = "http://www.resgain.net/xmdq.html"
res = requests.get(url)
soup = BeautifulSoup(res.text, 'lxml')
name_list = []
for s in soup.find_all(attrs={'class': 'btn btn2'}):
name_list.append(s.text.split("姓")[0])
return name_list
1、s.text可以获取该标签中的文本内容
2、split(“姓”)方法,按照"姓"分隔字符串,结果为一个列表,选择第一个即为百家姓的姓氏。例如:"赵姓名字大全"分隔后变成:[“赵”,“姓名字大全”]
第二步,构造男生和女生的详情链接:
name_link_list = get_name_link()
for name_link in name_link_list:
url_boys = name_link.replace("/name_list.html", "/name/boys.html")
url_girls = name_link.replace("/name_list.html", "/name/girls.html")
1、字符串替换用replace(需要替换的内容,替换的内容)
第三步,通过构造的链接获取姓名:
def get_data(url):
con = sqlite3.connect(r'tools_app.db')
cursor = con.cursor()
res = requests.get(url)
soup = BeautifulSoup(res.text, 'lxml')
if "boy" in url:
sex_ = "男"
else:
sex_ = "女"
for s in soup.find_all(attrs={'class': 'btn btn-default btn-lg namelist'}):
name = s.find(attrs={'class': 'livemsg'}).get('data-name')
sql = "insert into RANDOM_NAME (name,sex) values('{0}','{1}');".format(name,sex_)
cursor.execute(sql)
con.commit()
print(url, "完成")
con.close()
1、同样连接tools_app.db数据库,创建一个cursor对象,用来操作数据库
2、同样,使用soup.find_all()获取到所有的标签,通过get方法获取到姓名数据
3、构造sql语句,用format()方法格式化字符串
4、cursor.execute()执行sql插入语句,conn.commit()提交记录,最后for循环结束后,conn.close()关闭连接
开始运行:
if __name__ == '__main__':
# 创建数据库和RANDOM_NAME表
# print(get_name_list())
# create_db()
# get_data('http://zhang.resgain.net/name/boys.html')
name_link_list = get_name_link()
for name_link in name_link_list:
url_boys = name_link.replace("/name_list.html", "/name/boys.html")
url_girls = name_link.replace("/name_list.html", "/name/girls.html")
get_data(url_boys)
t = random.randint(1, 3)
time.sleep(t)
get_data(url_girls)
t = random.randint(1, 3)
time.sleep(t)
爬虫获取数据总计耗时2小时左右,获取到数据249298条。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RO6i5PpF-1617715189324)(./pictures/random_names/爬虫数据展示.png)]
前端设计
采用Bootstrap4前端框架、vue2.js框架、vue-ajax-axios异步加载框架。
页面构思
- 需要一个简单的搜索框,还需要两个列表组来展示搜索的男生和女生姓名。
- 第一次进入,会有默认的页面,默认加载郑姓的数据。
- 在前端这里判断输入的姓氏,是否在百家姓里面,如果不在,将确定按钮改为红色“禁用”。
- 输入正确的姓氏后,每次点击确定按钮,展示的姓名都是随机的。
确定页面展示:
禁用页面展示:
代码分析
卡片
将页面卡片化,会感觉更加的清晰明了,只需要将输入框放到card-header,列表组放到card-body中即可。
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
输入框
这里有许多漂亮的输入框,选择一个简单的即可。
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Recipient's username" aria-label="Recipient's username" aria-describedby="button-addon2">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-addon2">Button</button>
</div>
</div>
当然,直接拿来用是不行的,需要修改一番。使用vue框架的v-if来判断输入的姓氏是否正确,用v-on:click来触发点击事件。
1、判断一个元素在不在一个数组中,js的判断方法为:indexOf(),结果为-1表示不在数组中。
2、v-if和v-else所在的标签也紧挨着,才能生效。
修改代码如下:
<div class="input-group">
<input type="text" class="form-control" v-model="input_name" placeholder="您的姓氏">
<div class="input-group-append">
<button class="btn btn-primary" v-if="name_index >= 0" v-on:click="get_info">
确定
</button>
<button class="btn btn-danger" v-else>错误</button>
</div>
</div>
列表组
这里涉及到男生和女生,所以选择将两个列表组,并排放置在同一行中。
<div class="row">
<div class="col-sm-6">
<ul class="list-group">
<button type="button" class="list-group-item list-group-item-action active"
aria-current="true">
<b>男生姓名:</b>
</button>
<button class="list-group-item list-group-item-action text-center"
v-for="name in names['boy_names']">{[ name ]}
</button>
</ul>
</div>
<div class="col-sm-6">
<ul class="list-group">
<button type="button" class="list-group-item list-group-item-action active"
aria-current="true">
<b>女生姓名:</b>
</button>
<button class="list-group-item list-group-item-action text-center"
v-for="name in names['girl_names']">{[ name ]}
</button>
</ul>
</div>
</div>
JavaScript
使用vue的语法
1、由于vue的模板是{{}},与flask中内置的jinja2语法有冲突,所以需要用delimiters参数将vue的模板改为[{}]。
2、data:input_name为默认的姓氏;names中包含默认的男生和女生姓名;right_first_name为正确的百家姓氏。
3、name_index:默认为1,表示input_name在right_first_name中。
4、watch:监听input_name的值是否改变,函数的nval参数为新输入的姓氏。
5、methods:用ajax从后端的get_names路由获取对应的数据。
vm = new Vue({
el: "#rn_app",
delimiters: ['{[', ']}'],
data: function () {
return {
"input_name": "郑",
"names": {
"boy_names": ['郑宇坤', '郑鑫黎', '郑闰', '郑文锶', '郑小垚'],
"girl_names": ['郑琪', '郑雯欣', '郑丽萍', '郑伊春', '郑蘅'],
},
"right_first_name": ['赵', '钱', '孙', '李', '周', '吴', '郑', '王',......],
"name_index": 1
}
},
watch: {
input_name: function (nval) {
this.name_index = this.right_first_name.indexOf(nval);
}
},
methods: {
get_info: function () {
axios.get("/get_names", {params: {"input_name": this.input_name}})
.then(response => (this.names = response.data.names))
.catch(function (error) {
console.log(error)
})
}
}
})
后端
根据前端的分析,后端需要2个接口,index和get_names接口,index负责展示页面,get_names返回查询的姓名。
index接口
@app.route("/")
def index():
return render_template("random_name.html")
get_names接口
1、数据库配置为当前路径下的/spiders/tools_app.db。
2、使用SQL语句随机获取数据。
3、用jsonify将数据封装为json数返回。
@app.route("/get_names", methods=["GET", "POST"])
def get_names():
con = sqlite3.connect(r"./spiders/tools_app.db")
cursor = con.cursor()
input_name = request.args.get("input_name", "None")
# 根据input_name从数据库随机获取5个男生姓名,和5个女生姓名
boy_sql = f"select name from RANDOM_NAME where NAME like '{input_name}%' and SEX='男' order by random()limit 5"
girl_sql = f"select name from RANDOM_NAME where NAME like '{input_name}%' and SEX='女' order by random()limit 5"
cursor.execute(boy_sql)
con.commit()
boy_names_list = [i[0] for i in cursor.fetchall()]
cursor.execute(girl_sql)
con.commit()
girl_names_list = [i[0] for i in cursor.fetchall()]
con.close()
return jsonify({"names": {"boy_names": boy_names_list, "girl_names": girl_names_list}})
启动
本地启动,端口为5000端口,启动后访问http://localhost:5000/,即可看到页面。
if __name__ == '__main__':
app.run(host="localhost", port=5000, debug=True)
源代码获取
公众号“帅帅的Python”,回复“姓名”即可获取源码。