鱼C论坛

 找回密码
 立即注册
查看: 1438|回复: 1

[技术交流] 教你轻松爬取全国行政区代码并制作Flask端口

[复制链接]
发表于 2021-6-20 15:04:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 896944660 于 2021-6-20 15:06 编辑

导言
正如上一篇文章所说的,后来想了想,还是跟大家简单地分享一下其中的具体实现原理吧。该程序主要是由爬虫、数据库、flask端口三部分组成,下面我们一步一步来解决。


正文
爬虫
我们的行政区划数据主要来源于官方的民政部公开的数据:[2020年11月中华人民共和国县以上行政区划代码        ][1]

先说一下总的。我们知道,中国的行政区划码分为三级:省、市、县, 如何更详细、更简单地存储相关的数据呢?无疑字典是个不错的选择,我们可以用一个大的字典囊括所有的数据,然后用独一无二的行政区划代码(例如北京市东城区的110101)作为字典的key值,把相关的详细省市县数据再用一个字典存储起来,我们就可以得到一个二层字典,形如:
  1. {'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进行自动提取,这样,当下一次我们提取数据时,就又可以省下一些功夫。
  1. province_res = requests.get(self.region_code_url, headers=self.headers)
  2. province_css = re.search(r'<td class=(.+?)>110000</td>', province_res.text).group(1)
  3. county_css = re.search(r'<td class=(.+?)>110101</td>', province_res.text).group(1)
复制代码


在使用pyquery方法查询之前,我们需要先把所有的span元素删除,否则会出现无法提取地区名称的情况。

  1. document = query(province_res.text)
  2. document("span").remove()
  3. region_data_raw = document(f".{province_css}, .{county_css}")
复制代码


最后,我们只需要使用一个for循环迭代数据,判断地区类型,然后存入字典就大功告成啦。这部分的整体代码如下:
  1. class RegionCodeSpider(object):
  2.     def __init__(self):
  3.         self.region_code_url = "http://preview.[url]www.mca.gov.cn/article/sj/xzqh/2020/2020/202101041104.html[/url]"
  4.         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"}

  5.     def region_code_spider(self):
  6.         region_data = {}

  7.         province_res = requests.get(self.region_code_url, headers=self.headers)
  8.         try:
  9.             province_css = re.search(r'<td class=(.+?)>110000</td>', province_res.text).group(1)
  10.             county_css = re.search(r'<td class=(.+?)>110101</td>', province_res.text).group(1)
  11.         except TypeError:
  12.             print("Spider Failed")
  13.         else:
  14.             document = query(province_res.text)
  15.             document("span").remove()
  16.             region_data_raw = document(f".{province_css}, .{county_css}")

  17.             index = 0
  18.             city_name = None
  19.             province_name = None

  20.             for data in region_data_raw:
  21.                 try:
  22.                     if data.text[-4:] == "0000":
  23.                         province_code = data.text
  24.                         province_name = region_data_raw[index + 1].text.strip()
  25.                         region_data[province_code] = {"province": province_name, "city": None, "county": None}

  26.                     elif data.text[-2:] == "00" and data.text[-4:] != "0000":
  27.                         city_code = data.text
  28.                         city_name = region_data_raw[index + 1].text.strip()
  29.                         region_data[city_code] = {"province": province_name, "city": city_name, "county": None}

  30.                     else:
  31.                         try:
  32.                             temp = int(data.text)
  33.                         except ValueError:
  34.                             pass
  35.                         else:
  36.                             if str(temp)[-2:] != "00":
  37.                                 county_code = data.text
  38.                                 county_name = region_data_raw[index + 1].text.strip()
  39.                                 if city_name is None:
  40.                                     city_name = province_name
  41.                                 region_data[county_code] = {"province": province_name, "city": city_name,
  42.                                                             "county": county_name}

  43.                 except TypeError:
  44.                     pass
  45.                 index += 1

  46.             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` 你还不清楚是啥意思的话,待会看完端口部分你就会明白的。


  1. class RegionCode(db.Model):
  2.     __tablename__ = "region_code"

  3.     id = db.Column(db.Integer, primary_key=True, autoincrement=True)
  4.     code = db.Column(db.Integer, index=True, nullable=False, comment="城市行政区码")
  5.     province = db.Column(db.VARCHAR(64), index=True, nullable=False, comment="所在省")
  6.     city = db.Column(db.VARCHAR(128), index=True, nullable=True, comment="所在市")
  7.     county = db.Column(db.VARCHAR(128), index=True, nullable=True, comment="所在县")

  8.     def __repr__(self):
  9.         return f"Region: {self.code} -- {self.province}-{self.city}-{self.county}"
复制代码


在将数据存入数据库之前,必然需要连接到数据库。在原生的sqlalchemy中,与mysql数据库建立连接十分简单,只需要提供一个数据库uri链接,再调用相应的方法创建session会话即可。例如这样
  1. sqlalchemy_database_uri = "mysql+pymysql://test:123456@localhost:3306/test?charset=utf8"

  2. engine = create_engine(sqlalchemy_database_uri)
  3. database_session = sessionmaker(bind=engine)
  4. return {"code": 200, "data": database_session()}
复制代码


对于uri需要说明一下,它的格式为
  1. sqlalchemy_database_uri = "mysql+pymysql://username:password@host:port/database?charset=utf-8"
复制代码


完成了上面的工作,mysql的增删改查操作就变得非常简单了, 我们只需要操作这个模型的实例即可。于是,增便是实例化一个模型,删便删除这个实例,改便是更改实例属性,查便是通过该属性独一的属性来查找,最后提交就可以啦。

以帐号操作为例,
  1. sqlalchemy_database_uri = "mysql+pymysql://test:123456@localhost:3306/test?charset=utf8"
  2. engine = create_engine(sqlalchemy_database_uri)
  3. database_session = sessionmaker(bind=engine)
  4. session = database_session()

  5. # 查
  6. user = session.query(User).filter_by(username="123456").first()

  7. # 增
  8. new_user= User(username="123456", password="123456")
  9. session.add(new_user)
  10. session.commit()

  11. # 删
  12. user = session.query(User).filter_by(username="123456").first()
  13. session.delete(user)
  14. session.commit()

  15. # 改
  16. user = session.query(User).filter_by(username="123456").first()
  17. user.password = "12345678"
  18. session.add(user)
  19. session.commit()
复制代码


以我们这次的任务为例,

  1. session_res = self.create_session()["data"]
  2. new_region = RegionCode(code=region_code,
  3.                         province=province,
  4.                         city=city,
  5.                         county=county)

  6. session.add(new_region)
  7. session.commit()
复制代码


这部分的代码整体如下(爬虫和数据库同在RegionCodeSpider类之中, 这里我们把这个文件命名为spider.py):
  1. @staticmethod
  2.     def create_session():
  3.         sqlalchemy_database_uri = "mysql+pymysql://test:896944660@localhost:3306/test?charset=utf8"
  4.    
  5.         engine = create_engine(sqlalchemy_database_uri)
  6.         database_session = sessionmaker(bind=engine)
  7.         return {"code": 200, "data": database_session()}

  8.     def put_data(self, region_data):
  9.         session_res = self.create_session()

  10.         if session_res["code"]:
  11.             session = session_res["data"]
  12.             for region_code in region_data:

  13.                 province = region_data[region_code]["province"].encode("utf-8")

  14.                 city = region_data[region_code]["city"]
  15.                 if city:
  16.                     city = city.encode("utf-8")

  17.                 county = region_data[region_code]["county"]
  18.                 if county:
  19.                     county = county.encode("utf-8")

  20.                 new_region = RegionCode(code=region_code,
  21.                                         province=province,
  22.                                         city=city,
  23.                                         county=county)

  24.                 session.add(new_region)
  25.                 session.commit()
  26.             print("Succeed")
  27.         else:
  28.             print("Database Failed")
复制代码


端口
端口的制作,我们使用非常灵活强大的Flask来完成(Flask也是一个宝贝), 它只需要简单的几行代码便可以完成一个端口的制作,如果感兴趣,可以参阅Miguel Grinberg的: [Flask Web开发 基于Python的Web应用开发实战 第2版(图灵出品)][2], 或者直接通过[Flask官网][3]学习

这里需要引进一个概念叫做视图函数:说明白点就是一个处理网页请求的函数方法罢了。在这里面,我们通过编写代码来决定当一个地址被请求后需要执行哪些操作。

在创建视图函数之前,我们实例一个应用,像这样
  1. from flask import Flask
  2. app = Flask(__name__)
复制代码


视图函数有个特点就是,它每次都会被app.route()装饰器修饰, 括号里面传递的是路由地址,最前面始终是/, 在末尾我们可以使用/<>来传递参数, 比如这样子
  1. @app.route("/region/search/<region>")
  2. def region_code_search(region):
  3.     pass
复制代码


这样,这样待会我们就可以 `http://localhost:5000/region/search/string` 来访问我们的端口了, string即为我们要查询的数据,可以是地区名称中文,也可以是地区行政代码。另外,需要再提醒一下的是,Flask默认开放端口是5000。

按照我的计划,端口提供两种搜索的模式,一种是通过地区名称搜索,另一种是通过行政区划代码来搜索。后一种没什么需要特别讲得,无非就是通过行政区划代码来检索数据库,取出相应的数据就可以了。由于二者是通过同一个视图函数,同一个参数传递的,我们可以通过try语句来分别二者,像这样,
  1. try:
  2.         region = int(region)
  3.     except ValueError:
  4.         pass #  执行地区名称搜索逻辑
  5.     else:
  6.         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)

  1. import re
  2. import jieba
  3. from flask import Flask, jsonify
  4. from flask_sqlalchemy import SQLAlchemy

  5. app = Flask(__name__)
  6. db = SQLAlchemy(app)  # 前面数据库部分提到
  7. app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://test:123456@localhost:3306/test?charset=utf8"  # 指定flask应用的数据库链接
  8. app.config["JSON_AS_ASCII"] = False  # 将json格式的ASCII模式设置为False,否则无法显示中文。


  9. @app.route("/region/search/<region>")
  10. def region_code_search(region):
  11.     try:
  12.         region = int(region)
  13.     except ValueError:
  14.         region_list = jieba.lcut(region, cut_all=True)[::-1]
  15.         for region_str in region_list:
  16.             region_str = region_str.strip()
  17.             if region_str in ["省", "自治区", "区", "省", "市", "县", "自治县", "特别行政区"]:
  18.                 continue
  19.             elif re.search(r'[0-9a-zA-Z!\\@#$%^&*()_+{}|:"<>?/*-+.\[\];\', ]', region_str):
  20.                 continue
  21.             else:
  22.                 county_res = RegionCode.query.filter(RegionCode.county.like(f"%{region_str}%")).first()
  23.                 city_res = RegionCode.query.filter(RegionCode.city.like(f"%{region_str}%")).first()
  24.                 province_res = RegionCode.query.filter(RegionCode.province.like(f"%{region_str}%")).first()

  25.                 if not county_res and not city_res and not province_res:
  26.                     continue
  27.                 elif county_res:
  28.                     data = {"province": county_res.province,
  29.                             "city": county_res.city,
  30.                             "county": county_res.county,
  31.                             "code": county_res.code}
  32.                     return jsonify({"code": 200, "msg": "获取成功", "data": data})
  33.                 elif city_res:
  34.                     data = {"province": city_res.province,
  35.                             "city": city_res.city,
  36.                             "county": city_res.county,
  37.                             "code": city_res.code}
  38.                     return jsonify({"code": 200, "msg": "获取成功", "data": data})
  39.                 elif province_res:
  40.                     data = {"province": province_res.province,
  41.                             "city": province_res.city,
  42.                             "county": province_res.county,
  43.                             "code": province_res.code}
  44.                     return jsonify({"code": 200, "msg": "获取成功", "data": data})

  45.         return jsonify({"code": 0, "msg": "未查询到相关数据"})

  46.     else:
  47.         region_data = RegionCode.query.filter_by(code=region).first()
  48.         if region_data:
  49.             data = {"province": region_data.province,
  50.                     "city": region_data.city,
  51.                     "county": region_data.county}

  52.             return jsonify({"code": 200, "msg": "获取成功", "data": data})

  53.         else:
  54.             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 代码末尾添加如下代码后,运行即可,

  1. if __name__ == "__main__":
  2.     app.run(debug=True)
复制代码


当然的,真正的应用部署,比这个要麻烦许多,这里也不在累赘。前文提到的Miguel Grinberg的书中以及网上都有许多教程,请自行百度学习吧。

结语
作者已经做好了成品,欢迎大家调用 http://www.moyo1.cn:8080/api/v1/region/search/region_string
此外,如果pip安装第三方库失败,可以参考作者的另一篇pip换源文章:[Python - pip 换源][4]
如果觉得有用,也请留个赞吧,毕竟写这篇文章还是着实费了些力气。
如果你有什么建议,欢迎留言评论,作者看到后,会尽快回复。


附注:
这篇文章我已经发表在其他论坛,我是直接复制过来的,由于鱼C论坛不支持markdown语法,所以有些地方看起来可能有些别扭。
然后,有些内容的话,对初入门者会有些难度,但是知识点还是值得研究一下的。

  [1]: http://preview.[url=http://www.mca.gov.cn/article/sj/xzqh/2020/2020/202101041104.html]www.mca.gov.cn/article/sj/xzqh/2020/2020/202101041104.html[/url]
  [2]: https://item.jd.com/12418677.html
  [3]: https://flask.palletsprojects.com/en/2.0.x/
  [4]: http://www.moyo1.cn/index.php/archives/7/
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2021-6-20 16:06:05 | 显示全部楼层
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-20 18:41

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表