导言

正如上一篇文章所说的,后来想了想,还是跟大家简单地分享一下其中的具体实现原理吧。该程序主要是由爬虫、数据库、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 换源
如果觉得有用,也请留个赞吧,毕竟写这篇文章还是着实费了些力气。
如果你有什么建议,欢迎留言评论,作者看到后,会尽快回复。

Last modification:January 12, 2022
如果觉得我的文章对你有用,请随意赞赏