地理可视化(一):地址清洗

R
Python
tidyverse
爬虫
Author

Rui

Published

April 1, 2023

写在前面

3月31日中午,老板给我布置了一个任务:将绍兴市所有高企、研究院、孵化器在地图上展现出来。 具体要求如下:

  • 可交互:地图可缩放而不是一张静态图片,地图上的点可点击、可展示信息,不同企业/机构的颜色不同

  • 易于展示:完成后不能以代码形式展示,毕竟上级不懂代码更不会运行。最好是提交一个平台或者一个链接,要可以随时随地打开并使用(比如说在 ppt 中附上超链接)

  • 免费:使用的所有工具必须免费

我的脑回路一下子接通了:可以使用 R 语言进行可视化,再以 Rmarkdown 或者 Quarto 展现出来。如此,提交给上级的可以是一个 html 文件,也可以是一个可以打开相应网站的链接,二者都可以非常方便地展示可视化成果。

听到“可交互”一词,我又立刻想到 R 中十分强大的两个包 leaflet 和 shiny。前者可用于绘制可交互的地图,后者可以快速构建一个可交互的应用平台,二者结合一定妙不可言。于是一条路线自然而然在我脑海中浮现:tidyverse 数据清洗 -> leaflet 地理可视化 -> shiny 整合。

我当时只是对 leaflet 和 shiny 早有耳闻,但却没有实际使用过,从未料想之后的过程是如此困难。好在最后克服重重困难,收获满满,特意制作这一系列教程/笔记供自己反思总结。

数据的提取与问题的发现

老板提供的数据包含多张表格(xlsx),经过另一位组员的整合后汇总成一张表格(csv)。

当我拿到数据之后,还是很谨慎地转换了以下文件格式。因为数据中包含大量中文,如果不能保证文件以 UTF-8 编码,那么导入 R 中很有可能乱码。

导入到 R 中,数据如下:

Code
library(tidyverse)
library(stringr)

df <- read.csv("越城区科创企业名单.csv", encoding = 'UTF-8')
df <- df %>% rename(type = 类型, name = 名称, address = 地址)

df %>% head(15) %>% knitr::kable()
type name address
共建研究院 浙江大学绍兴研究院 浙江省绍兴市越城区迪荡湖隧道科学园3号楼26-27层
共建研究院 天津大学浙江国际创新设计与智造研究院 浙江省绍兴市越城区洋泾湖科创园1号楼
共建研究院 上海大学绍兴研究院 浙江省绍兴市越城区三江路78号
共建研究院 浙江工业大学绍兴研究院 浙江省绍兴市越城区洋泾湖科创园2号楼
共建研究院 江南大学(绍兴)产业技术研究院 浙江省绍兴市越城区洋江东路19号
共建研究院 中国纺织科学研究院江南分院 浙江省绍兴市越城区双堰路30号
共建研究院 浙江数字内容研究院 浙江省绍兴市越城区马欢路398号科创园综合楼1-3楼
共建研究院 浙江理工大学绍兴生物医药研究院 浙江省绍兴市越城区马欢路398号科创园C幢5楼
共建研究院 上海交通大学绍兴新能源与分子工程研究院 浙江省绍兴市越城区马欢路398号科创园B幢
共建研究院 北京大学信息技术科创中心 浙江省绍兴市越城区银桥路326号
共建研究院 绍兴市通越宽禁带半导体研究院 浙江省绍兴市越城区平江路中关村绍兴水木湾区科学园平江路2号
省级重点企业研究院 铭众生物医用材料与器械研究院 绍兴平江路328号
省级重点企业研究院 浙江航天长峰科技重点企业研究院 浙江省绍兴市阳明北路683号科创大厦22楼
省级重点企业研究院 浙江医药企业研究院 绍兴滨海新城致远中大道168号
省级重点企业研究院 浙江省喜临门家具研究院 浙江省绍兴市越城区灵芝镇二环北路1号

然后进行一次简单的清洗,主要是去除 address 一列中多余的引文空格和中文空格(半角、全角)。

如果使用 View(df) 在 RStudio 中检查数据,细心的你可能会发现:有个别单元格为空格但不显示 NA。如果使用 df[i, j] 对数据进行索引则返回 ""

需要注意的是:在 R 中 NA 指的是缺失值,是 Not Available 的缩写,是不存在的值;而 "" 指的是空值,空值存在,而不是缺失值。后面一种情况通常是统计人员在操作 Excel 时所致,特别是在写入字符型数据并进行删改时产生。同样需要使用代码将空值转换为 NA。

Code
df <- df %>% 
  mutate(address = str_remove_all(address, "\\s+")) %>%
  mutate(address = str_remove_all(address, "[\u3000\u0020]+")) %>%
  mutate(address = if_else(address == "", NA, address)) # 将空白单元格转换为NA

仔细观察 address 一列,发现有不少企业填写的地址是十分模糊的,比如:仅仅只填写了街道和门牌号、“XXX工业区”或者“XX路口往东50米”。这些模糊的地址一定会影响之后在地图上的标点。现在我们需要将这些企业抽取出来,以人工或者代码的方式核实这些企业的地址。

如何识别地址模糊的企业呢?一个比较傻瓜但又好用的方法是:将所有地址中不含有“绍兴”二字的地址提取出来。因为老板给我的数据全部是绍兴高企数据。

Code
blur_companies <- df %>%
  filter(is.na(address)|!str_detect(address, "绍兴"))
blur_companies %>% write_excel_csv("地址有问题的企业.csv")
Tip

推荐使用 tidyverse 中的 write_excel_csv() 函数来保存数据,不容易乱码。

使用 Python 进行爬虫

将地址模糊的企业数据提取出来后发现数据量不小,绝对不可能以一己之力通过百度搜索的方式人工审核。所以我打算使用网络爬虫,在企查查网站上爬取这些企业的地址。

Note
  • 建议使用 Chrome 浏览器

  • 建议在爬取前注册登录

关于爬虫的工具和方法有很多,R 中有一个 rvest 包就可以实现网络爬虫。但我选择使用 Python 来实现,因为在爬虫这一领域还是 python 用起来比较顺手,更何况我还有现成的代码,只需要一点简单的修改即可。

我使用的是平台是 Jupyter Lab,导入相关库:

Code
import pandas as pd
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote
import time
import csv
import random
Code
data = pd.read_csv("地址有问题的企业.csv")
data.head(10)
##              type                     name                address
## 0       省级重点企业研究院             明峰核医学影像系统研究院                    NaN
## 1         省级企业研究院             明峰核医学影像系统研究院                    NaN
## 2         省级企业研究院               浙江省贝得医药研究院         浙江省越城区袍江工业区三江路
## 3  省级高新技术企业研究开发中心    明峰医学影像系统省高新技术企业研究开发中心                    NaN
## 4  省级高新技术企业研究开发中心        贝得医药省高新技术企业研究开发中心         浙江省越城区袍江工业区三江路
## 5  省级高新技术企业研究开发中心    富陵新型塑料包装材料省高新技术研究开发中心                    NaN
## 6  省级高新技术企业研究开发中心        浙江向日葵光能科技技术研究开发中心                    NaN
## 7  省级高新技术企业研究开发中心       浙江越韩科技透气材料技术研究开发中心                    NaN
## 8  省级高新技术企业研究开发中心          绍兴金道齿轮箱技术研究开发中心                    NaN
## 9  省级高新技术企业研究开发中心  芯谷科技集成电路设计省高新技术企业研究开发中心  越城区城南街道中兴南路851号3号厂房3楼

只取前 10 行数据来演示爬虫过程:

Code
companies = data["name"].tolist()
companies = companies[:10] # 只取前10个作为展示

作为展示,爬取的数据被写进 company_info_demo.csv 文件中,实际操作时保存在 company_info.csv 中。

Code
with open('company_info_demo.csv', mode='w', newline='') as csv_file:
    writer = csv.writer(csv_file)
    writer.writerow(['Company Name', 'Address'])
    
    for i in companies:
        
        headers = {
            'authority': 'www.qcc.com',
            'method': 'GET',
            'path': r'/',
            'scheme': 'https',
            'accept': r'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
            'accept-encoding': 'gzip, deflate, br',
            'accept-language': 'zh-CN,zh;q=0.9',
            'cache-control': 'max-age=0',
            'cookie': '我的cookie',
            'sec-ch-ua': '"Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"',
            'sec-ch-ua-mobile': '?0',
            'sec-fetch-dest': 'document',
            'sec-fetch-mode': 'navigate',
            'sec-fetch-site': 'same-origin',
            'sec-fetch-user': '?1',
            'upgrade-insecure-requests': '1',
            'user-agent': '我的user-agent'
        }
    
        url = 'https://www.qcc.com/web/search?key={}'.format(quote(i))
        html = requests.get(url, headers = headers)
        soup = BeautifulSoup(html.text, 'lxml')
        info = soup.select('span.copy-value.address-map')
        
        time.sleep(random.uniform(0.05, 1)) # 设置随机的停止时长
        
        # 异常捕获,当爬取出现异常时记录“未找到地址”,同时程序也不会中断
        try:
            address = info[0].text
            print(i, ":", address)
        except:
            info.append("未找到地址")
            address = info[0]
            print(i, ":", address)
    
        writer.writerow([i, address])
  • headers 中的参数很多,但一般而言只需要 cookieuser-agent 就够了。具体方法是在网页主页按下 F12 调出开发者工具,点击 Network,选择相应内容点击 Headers,在 Request Headers 一栏中即可找到。

  • 在搜索框输入“浙江大学绍兴研究院”并回车,即可在地址栏获得 url(建议使用 Chrome 浏览器)

  • 要注意的是 url 中显示不了中文,中文会被 URL ecode 为一串神秘的代码。在 Python 中可以通过 urllib.parse 中的 quote 函数进行转换。你可以试一试: quote("浙江大学绍兴研究院")

  • 设置 time.sleep 避免访问网页过于频繁被封 ip

  • soup.select() 如何找到自己想要的内容?按 F12,点击 Elements,再点击左侧的小箭头,将鼠标移到网页上我们想要提取信息的地方,自动显示该元素的标签值

  • 要注意的是在搜索结果页面展示的不只有我们搜索的企业信息,还有返回的其他企业的信息。可以先写一个单个的爬虫,通过爬取的结果 info 来选择我们需要的信息。一般来说,搜索最匹配的结果会放在最前面,那么 info[0] 就是目标信息,通过 info[0].text 将目标信息转换为文本

数据整合与清洗

爬虫过程中时不时会被提示“一段时间内访问次数过多”或者要求验证,甚至是被封 ip 地址,从而导致信息爬取失败。一般而言解决的办法是:使用 time.sleep 强制程序停止几秒钟,构建代理 ip 池以及使用代理,分布式爬虫以及其他一些高级爬虫方法。

实际过程中 time.sleep 方法并没有很好的解决问题,网络上的免费 ip 基本没法用,又不想花钱买 ip,分布式爬虫等一些高级方法要花相当一段时间学习。

最后我采用一种笨办法:将原本整个 csv 文件拆分成若干子文件来进行爬虫。于是在爬虫结束后,得到了数个存储企业地址的 csv 文件。如何将这些 csv 文件中的数据纵向拼接呢?方法如下:

  • 将所有 csv 转移到一个新的空文件夹中

  • 获取文件夹路径,通过匹配 .csv 从而获取该文件夹中的所有后缀为 .csv 文件的名称

  • 通过 do.call 函数对 csv 文件批量执行 rbind 操作

Code
folder_path <- "爬虫获取的企业地址"
file_names <- list.files(folder_path, pattern = ".csv")
data_list <- lapply(
  file_names, 
  function(file) {read.csv(paste(folder_path, file, sep = "/"), encoding = 'utf-8')}
)
company_info <- do.call(rbind, data_list)

即便是爬虫获取到的地址信息,也不能保证全部干净、正确,还是需要进一步清洗:

  • 再次清洗,去除英文、中文(半角全角)空格,将“未找到地址”转换为 NA

  • 通过观察发现一些查询到的企业的地址在深圳,显然有问题。将所有地址中不包含“绍兴”的数据转换为 NA

Code
company_info <- company_info %>%
  distinct() %>% # 以防重复记录
  rename(name = Company.Name, address = Address) %>%
  mutate(address = str_remove_all(address, "\\s+")) %>% # 去除英文空格
  mutate(address = str_remove_all(address, "[\u3000\u0020]+")) %>% # 去除中文全角半角空格
  mutate(address = if_else(address == "未找到地址", NA, address)) %>% # 将“未找到地址”转换为NA
  mutate(address = if_else(str_detect(address, "绍兴"), address, NA)) # 爬取的数据未必正确,地址不包含“绍兴”的也转换为NA
  

将原来的 df 与爬虫得到的 company_info 进行 join,创建新列 clean_address,若 company_info 的 address 缺失,则使用 df 中的 address,否则使用 company_info 中的 address。最后,删除 clean_address 为 NA 的记录,再写入 csv 中。

Code
clean_df <- df %>% 
  left_join(company_info, by = "name") %>%
  mutate(clean_address = if_else(is.na(address.y), address.x, address.y)) %>%
  select(-starts_with("address")) %>%
  filter(!is.na(clean_address)) %>%
  mutate(clean_address = str_remove_all(clean_address, "[\uFF08].*?[\uFF09]")) # 删除中文圆括号以及括号内的文字

clean_df %>% write_excel_csv("清洗后的数据.csv")

如此,得到了相对干净的企业地址数据:

Code
clean_df %>% head(20) %>% knitr::kable()
type name clean_address
共建研究院 浙江大学绍兴研究院 浙江省绍兴市越城区迪荡湖隧道科学园3号楼26-27层
共建研究院 天津大学浙江国际创新设计与智造研究院 浙江省绍兴市越城区洋泾湖科创园1号楼
共建研究院 上海大学绍兴研究院 浙江省绍兴市越城区三江路78号
共建研究院 浙江工业大学绍兴研究院 浙江省绍兴市越城区洋泾湖科创园2号楼
共建研究院 江南大学(绍兴)产业技术研究院 浙江省绍兴市越城区洋江东路19号
共建研究院 中国纺织科学研究院江南分院 浙江省绍兴市越城区双堰路30号
共建研究院 浙江数字内容研究院 浙江省绍兴市越城区马欢路398号科创园综合楼1-3楼
共建研究院 浙江理工大学绍兴生物医药研究院 浙江省绍兴市越城区马欢路398号科创园C幢5楼
共建研究院 上海交通大学绍兴新能源与分子工程研究院 浙江省绍兴市越城区马欢路398号科创园B幢
共建研究院 北京大学信息技术科创中心 浙江省绍兴市越城区银桥路326号
共建研究院 绍兴市通越宽禁带半导体研究院 浙江省绍兴市越城区平江路中关村绍兴水木湾区科学园平江路2号
省级重点企业研究院 铭众生物医用材料与器械研究院 绍兴平江路328号
省级重点企业研究院 浙江航天长峰科技重点企业研究院 浙江省绍兴市阳明北路683号科创大厦22楼
省级重点企业研究院 浙江医药企业研究院 绍兴滨海新城致远中大道168号
省级重点企业研究院 浙江省喜临门家具研究院 浙江省绍兴市越城区灵芝镇二环北路1号
省级重点企业研究院 明峰核医学影像系统研究院 绍兴市越城区稽山街道东山路6号2-3办公楼
省级重点企业研究院 浙江省创伤修复材料重点企业研究院 绍兴市越城区皋埠镇皋北工业区
省级企业研究院 浙江省浙江医药药物研究院 绍兴滨海新城致远中大道168号
省级企业研究院 明峰核医学影像系统研究院 绍兴市越城区稽山街道东山路6号2-3办公楼
省级企业研究院 浙江省喜临门家具研究院 浙江省绍兴市越城区灵芝镇二环北路1号