导言
正如上一篇文章所说的,后来想了想,还是跟大家简单地分享一下其中的具体实现原理吧。该程序主要是由爬虫、数据库、flask端口三部分组成,下面我们一步一步来解决。
正文
爬虫
我们的行政区划数据主要来源于官方的民政部公开的数据:2020年11月中华人民共和国县以上行政区划代码
先说一下总的。我们知道,中国的行政区划码分为三级:省、市、县, 如何更详细、更简单地存储相关的数据呢?无疑字典是个不错的选择,我们可以用一个大的字典囊括所有的数据,然后用独一无二的行政区划代码(例如北京市东城区的110101)作为字典的key值,把相关的详细省市县数据再用一个字典存储起来,我们就可以得到一个二层字典,形如:
{'110000': {'province': '北京市', 'city': None, 'county': None}, '110101': {'province': '北京市', 'city': '北京市', 'county': '东城区'}}
这里还需要解释一下,省的行政区划代码末尾四个零(例如广东省440000),市的末尾是两个零(例如广州市440100),这是一个规律,对于我们后面数据的提取很有帮助。
通过前期的分析,我们知道,通过re进行提取可能会有些小麻烦,需要解决一些小问题,所以我们今天选择更加方便快捷的 pyquery第三方库帮助我们提取(这可是个好东西,帮了我不少忙。这家伙可以直接使用pip命令安装)。
PyQuery类似于bs4、lxml一类,但它又区别于后二者,PyQuery更加灵活,提供增加节点的class信息,移除某个节点,提取文本信息等功能。当然,这个的前提是你得熟悉一些基本的html知识。关于pyquery的系统知识,请参见其官网or其他教程,这里不再累赘。
通过前期的分析我们发现,省、县市三级名称、代码都对应着各自的html class属性,而pyquery的一大优点正是可以通过属性提取相应的全部数据。至于对应的属性值,我们可以通过re进行自动提取,这样,当下一次我们提取数据时,就又可以省下一些功夫。
province_res = requests.get(self.region_code_url, headers=self.headers)
province_css = re.search(r'<td class=(.+?)>110000</td>', province_res.text).group(1)
county_css = re.search(r'<td class=(.+?)>110101</td>', province_res.text).group(1)
在使用pyquery方法查询之前,我们需要先把所有的span元素删除,否则会出现无法提取地区名称的情况。
document = query(province_res.text)
document("span").remove()
region_data_raw = document(f".{province_css}, .{county_css}")
最后,我们只需要使用一个for循环迭代数据,判断地区类型,然后存入字典就大功告成啦。这部分的整体代码如下:
class RegionCodeSpider(object):
def __init__(self):
self.region_code_url = "http://preview.www.mca.gov.cn/article/sj/xzqh/2020/2020/202101041104.html"
self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"}
def region_code_spider(self):
region_data = {}
province_res = requests.get(self.region_code_url, headers=self.headers)
try:
province_css = re.search(r'<td class=(.+?)>110000</td>', province_res.text).group(1)
county_css = re.search(r'<td class=(.+?)>110101</td>', province_res.text).group(1)
except TypeError:
print("Spider Failed")
else:
document = query(province_res.text)
document("span").remove()
region_data_raw = document(f".{province_css}, .{county_css}")
index = 0
city_name = None
province_name = None
for data in region_data_raw:
try:
if data.text[-4:] == "0000":
province_code = data.text
province_name = region_data_raw[index + 1].text.strip()
region_data[province_code] = {"province": province_name, "city": None, "county": None}
elif data.text[-2:] == "00" and data.text[-4:] != "0000":
city_code = data.text
city_name = region_data_raw[index + 1].text.strip()
region_data[city_code] = {"province": province_name, "city": city_name, "county": None}
else:
try:
temp = int(data.text)
except ValueError:
pass
else:
if str(temp)[-2:] != "00":
county_code = data.text
county_name = region_data_raw[index + 1].text.strip()
if city_name is None:
city_name = province_name
region_data[county_code] = {"province": province_name, "city": city_name,
"county": county_name}
except TypeError:
pass
index += 1
self.put_data(region_data=region_data) # 这一部分在下面的数据库部分会解释。
数据库
要想把数存入mysql数据库,就必须得先掌握一些数据库的知识,以及相应的操作mysql的python第三方库。你可以选择使用pymysql,但今天,再给大家推荐一个ORM(Object Relational Mapping, 对象关系映射)操作库: sqlalchemy
同样的,你也可以直接使用pip安装。
根据前面的分析,我们需要创建五个字段: id, code, province, city, county, 分别存储着主键,地区代码,省份名称,市级名称,县级名称。你可以通过创建sqlalchemy模型的方式来创建表,也可以通过直接操作mysql数据库来创建。对前者有兴趣的朋友,可以自行百度学习一下。我们这里已经创建好了表格,就不再多说。
在使用sqlalchemy数据之前,我们需要建立一个ORM模型,根据上面的分析我们可以得到这样的一个Object类型。需要解释一下的是,由于待会我们需要使用flask制作端口,为了方便,我们选择继承 flask_sqlalchemy中的模型。如果第一行中的db.Model
你还不清楚是啥意思的话,待会看完端口部分你就会明白的。
class RegionCode(db.Model):
__tablename__ = "region_code"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
code = db.Column(db.Integer, index=True, nullable=False, comment="城市行政区码")
province = db.Column(db.VARCHAR(64), index=True, nullable=False, comment="所在省")
city = db.Column(db.VARCHAR(128), index=True, nullable=True, comment="所在市")
county = db.Column(db.VARCHAR(128), index=True, nullable=True, comment="所在县")
def __repr__(self):
return f"Region: {self.code} -- {self.province}-{self.city}-{self.county}"
在将数据存入数据库之前,必然需要连接到数据库。在原生的sqlalchemy中,与mysql数据库建立连接十分简单,只需要提供一个数据库uri链接,再调用相应的方法创建session会话即可。例如这样
sqlalchemy_database_uri = "mysql+pymysql://test:123456@localhost:3306/test?charset=utf8"
engine = create_engine(sqlalchemy_database_uri)
database_session = sessionmaker(bind=engine)
return {"code": 200, "data": database_session()}
对于uri需要说明一下,它的格式为
sqlalchemy_database_uri = "mysql+pymysql://username:password@host:port/database?charset=utf-8"
完成了上面的工作,mysql的增删改查操作就变得非常简单了, 我们只需要操作这个模型的实例即可。于是,增便是实例化一个模型,删便删除这个实例,改便是更改实例属性,查便是通过该属性独一的属性来查找,最后提交就可以啦。
以帐号操作为例,
sqlalchemy_database_uri = "mysql+pymysql://test:123456@localhost:3306/test?charset=utf8"
engine = create_engine(sqlalchemy_database_uri)
database_session = sessionmaker(bind=engine)
session = database_session()
# 查
user = session.query(User).filter_by(username="123456").first()
# 增
new_user= User(username="123456", password="123456")
session.add(new_user)
session.commit()
# 删
user = session.query(User).filter_by(username="123456").first()
session.delete(user)
session.commit()
# 改
user = session.query(User).filter_by(username="123456").first()
user.password = "12345678"
session.add(user)
session.commit()
以我们这次的任务为例,
session_res = self.create_session()["data"]
new_region = RegionCode(code=region_code,
province=province,
city=city,
county=county)
session.add(new_region)
session.commit()
这部分的代码整体如下(爬虫和数据库同在RegionCodeSpider类之中, 这里我们把这个文件命名为spider.py):
@staticmethod
def create_session():
sqlalchemy_database_uri = "mysql+pymysql://test:896944660@localhost:3306/test?charset=utf8"
engine = create_engine(sqlalchemy_database_uri)
database_session = sessionmaker(bind=engine)
return {"code": 200, "data": database_session()}
def put_data(self, region_data):
session_res = self.create_session()
if session_res["code"]:
session = session_res["data"]
for region_code in region_data:
province = region_data[region_code]["province"].encode("utf-8")
city = region_data[region_code]["city"]
if city:
city = city.encode("utf-8")
county = region_data[region_code]["county"]
if county:
county = county.encode("utf-8")
new_region = RegionCode(code=region_code,
province=province,
city=city,
county=county)
session.add(new_region)
session.commit()
print("Succeed")
else:
print("Database Failed")
端口
端口的制作,我们使用非常灵活强大的Flask来完成(Flask也是一个宝贝), 它只需要简单的几行代码便可以完成一个端口的制作,如果感兴趣,可以参阅Miguel Grinberg的: Flask Web开发 基于Python的Web应用开发实战 第2版(图灵出品), 或者直接通过Flask官网学习
这里需要引进一个概念叫做视图函数:说明白点就是一个处理网页请求的函数方法罢了。在这里面,我们通过编写代码来决定当一个地址被请求后需要执行哪些操作。
在创建视图函数之前,我们实例一个应用,像这样
from flask import Flask
app = Flask(__name__)
视图函数有个特点就是,它每次都会被app.route()装饰器修饰, 括号里面传递的是路由地址,最前面始终是/, 在末尾我们可以使用/<>来传递参数, 比如这样子
@app.route("/region/search/<region>")
def region_code_search(region):
pass
这样,这样待会我们就可以 http://localhost:5000/region/search/string
来访问我们的端口了, string即为我们要查询的数据,可以是地区名称中文,也可以是地区行政代码。另外,需要再提醒一下的是,Flask默认开放端口是5000。
按照我的计划,端口提供两种搜索的模式,一种是通过地区名称搜索,另一种是通过行政区划代码来搜索。后一种没什么需要特别讲得,无非就是通过行政区划代码来检索数据库,取出相应的数据就可以了。由于二者是通过同一个视图函数,同一个参数传递的,我们可以通过try语句来分别二者,像这样,
try:
region = int(region)
except ValueError:
pass # 执行地区名称搜索逻辑
else:
pass # 执行地区代码搜多逻辑
需要说的是通过地区名称搜索。这里我们需要再介绍一个特别强大的中文分词器jieba(一样可以通过pip安装)。同样的,我们只讲我们需要用到的,至于其他的功能请自行百度学习。
通过jieba.lcut(string, cut_all=True)方法(string是需要分词的字符串, cut_all=True表示启用全模式),我们可以获取一个list列表,列表里面是已经分好的数据,它可以帮助我们分出一些我们想要的数据,比如说,县市省的名称。另外需要搞清一点的是:我们默认的查询顺序是: 县>市>省,即存在县的数据,直接返回县的数据,以此类推,这样可以保证查询结果的准确性。然后,拿到结果以后,我们通过模糊搜索查询结果,再将其打包成一个字典,最后调用flask自带的jsonify方法并将返回即可。
最后需要说明一下的是,原生的sqlalchemy和flask_sqlalchemy二者大同小异,在某些如数据库连接,检索方式等方面略有不同。flask sqlalchemy教程可以参考Flask官网的。
最后我们可以得到这样的代码(通常情况下,我们习惯把存放flask app应用的文件命名为run.py)
import re
import jieba
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app) # 前面数据库部分提到
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://test:123456@localhost:3306/test?charset=utf8" # 指定flask应用的数据库链接
app.config["JSON_AS_ASCII"] = False # 将json格式的ASCII模式设置为False,否则无法显示中文。
@app.route("/region/search/<region>")
def region_code_search(region):
try:
region = int(region)
except ValueError:
region_list = jieba.lcut(region, cut_all=True)[::-1]
for region_str in region_list:
region_str = region_str.strip()
if region_str in ["省", "自治区", "区", "省", "市", "县", "自治县", "特别行政区"]:
continue
elif re.search(r'[0-9a-zA-Z!\\@#$%^&*()_+{}|:"<>?/*-+.\[\];\', ]', region_str):
continue
else:
county_res = RegionCode.query.filter(RegionCode.county.like(f"%{region_str}%")).first()
city_res = RegionCode.query.filter(RegionCode.city.like(f"%{region_str}%")).first()
province_res = RegionCode.query.filter(RegionCode.province.like(f"%{region_str}%")).first()
if not county_res and not city_res and not province_res:
continue
elif county_res:
data = {"province": county_res.province,
"city": county_res.city,
"county": county_res.county,
"code": county_res.code}
return jsonify({"code": 200, "msg": "获取成功", "data": data})
elif city_res:
data = {"province": city_res.province,
"city": city_res.city,
"county": city_res.county,
"code": city_res.code}
return jsonify({"code": 200, "msg": "获取成功", "data": data})
elif province_res:
data = {"province": province_res.province,
"city": province_res.city,
"county": province_res.county,
"code": province_res.code}
return jsonify({"code": 200, "msg": "获取成功", "data": data})
return jsonify({"code": 0, "msg": "未查询到相关数据"})
else:
region_data = RegionCode.query.filter_by(code=region).first()
if region_data:
data = {"province": region_data.province,
"city": region_data.city,
"county": region_data.county}
return jsonify({"code": 200, "msg": "获取成功", "data": data})
else:
return jsonify({"code": 200, "msg": "未找到相应数据"})
调试
你可以通过在cmd命令行环境下,切换到flask应用app文件所在的目录后,输入flask run(run是你flask app所在应用的名称,不用带py后缀)来调试你的代码,当出现 Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
之后,你即可访问http://0.0.0.0:5000/region/search/
or http://localhost:5000/region/search/
来查询数据。如flask提示所言,你可以同时按下ctrl + c退出代码程序。
你也可以在pycharm开发环境中调试,添加flask app 代码末尾添加如下代码后,运行即可,
if __name__ == "__main__":
app.run(debug=True)
当然的,真正的应用部署,比这个要麻烦许多,这里也不在累赘。前文提到的Miguel Grinberg的书中以及网上都有许多教程,请自行百度学习吧。
结语
作者已经做好了成品,欢迎大家调用 http://www.apkey.cn/api/v1/region/search/region_string
此外,如果pip安装第三方库失败,可以参考作者的另一篇pip换源文章:Python - pip 换源
如果觉得有用,也请留个赞吧,毕竟写这篇文章还是着实费了些力气。
如果你有什么建议,欢迎留言评论,作者看到后,会尽快回复。