---
title: "地理可视化(一):地址清洗"
author: "Rui"
date: "2023-04-01"
categories: [R, Python, tidyverse, 爬虫]
image: "seashore.jpg"
format:
html:
code-fold: true
code-tools: true
css: css/custom.css
---
```{r setup, include = FALSE}
# 设置默认参数
knitr::opts_chunk$set(
echo = TRUE,
fig.align = "center",
message = FALSE,
warning = FALSE,
collapse = TRUE
)
```
![](seashore.jpg)
## 写在前面
3月31日中午,老板给我布置了一个任务:将绍兴市所有高企、研究院、孵化器在地图上展现出来。
具体要求如下:
- 可交互:地图可缩放而不是一张静态图片,地图上的点可点击、可展示信息,不同企业/机构的颜色不同
- 易于展示:完成后不能以代码形式展示,毕竟上级不懂代码更不会运行。最好是提交一个平台或者一个链接,要可以随时随地打开并使用(比如说在 ppt 中附上超链接)
- 免费:使用的所有工具必须免费
我的脑回路一下子接通了:可以使用 R 语言进行可视化,再以 Rmarkdown 或者 Quarto 展现出来。如此,提交给上级的可以是一个 html 文件,也可以是一个可以打开相应网站的链接,二者都可以非常方便地展示可视化成果。
听到“可交互”一词,我又立刻想到 R 中十分强大的两个包 leaflet 和 shiny。前者可用于绘制可交互的地图,后者可以快速构建一个可交互的应用平台,二者结合一定妙不可言。于是一条路线自然而然在我脑海中浮现:tidyverse 数据清洗 -> leaflet 地理可视化 -> shiny 整合。
我当时只是对 leaflet 和 shiny 早有耳闻,但却没有实际使用过,从未料想之后的过程是如此困难。好在最后克服重重困难,收获满满,特意制作这一系列教程/笔记供自己反思总结。
## 数据的提取与问题的发现
老板提供的数据包含多张表格(xlsx),经过另一位组员的整合后汇总成一张表格(csv)。
当我拿到数据之后,还是很谨慎地转换了以下文件格式。因为数据中包含大量中文,如果不能保证文件以 UTF-8 编码,那么导入 R 中很有可能乱码。
::: {layout-ncol=2}
![](保存为csv1.png)
![](保存为csv2.png)
:::
导入到 R 中,数据如下:
```{r}
library(tidyverse)
library(stringr)
df <- read.csv("data/越城区科创企业名单.csv", encoding = 'UTF-8')
df <- df %>% rename(type = 类型, name = 名称, address = 地址)
df %>% head(15) %>% knitr::kable()
```
然后进行一次简单的清洗,主要是去除 address 一列中多余的英文空格和中文空格(半角、全角)。
如果使用 `View(df)` 在 RStudio 中检查数据,细心的你可能会发现:有个别单元格为空格但不显示 NA。如果使用 `df[i, j]` 对数据进行索引则返回 `""`。
需要注意的是:在 R 中 NA 指的是缺失值,是 Not Available 的缩写,是不存在的值;而 `""` 指的是空值,空值存在,而不是缺失值。后面一种情况通常是统计人员在操作 Excel 时所致,特别是在写入字符型数据并进行删改时产生。同样需要使用代码将空值转换为 NA。
```{r}
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米”。这些模糊的地址一定会影响之后在地图上的标点。现在我们需要将这些企业抽取出来,以人工或者代码的方式核实这些企业的地址。
如何识别地址模糊的企业呢?一个比较傻瓜但又好用的方法是:将所有地址中不含有“绍兴”二字的地址提取出来。因为老板给我的数据全部是绍兴高企数据。
```{r}
blur_companies <- df %>%
filter(is.na(address)|!str_detect(address, "绍兴"))
blur_companies %>% write_excel_csv("data/地址有问题的企业.csv")
```
:::{.callout-tip}
推荐使用 tidyverse 中的 `write_excel_csv()` 函数来保存数据,不容易乱码。
:::
## 使用 Python 进行爬虫
将地址模糊的企业数据提取出来后发现数据量不小,绝对不可能以一己之力通过百度搜索的方式人工审核。所以我打算使用网络爬虫,在企查查网站上爬取这些企业的地址。
:::{.callout-note}
- 建议使用 Chrome 浏览器
- 建议在爬取前注册登录
:::
关于爬虫的工具和方法有很多,R 中有一个 rvest 包就可以实现网络爬虫。但我选择使用 Python 来实现,因为在爬虫这一领域还是 python 用起来比较顺手,更何况我还有现成的代码,只需要一点简单的修改即可。
我使用的是平台是 Jupyter Lab,导入相关库:
```{python}
import pandas as pd
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote
import time
import csv
import random
```
```{python}
data = pd.read_csv("data/地址有问题的企业.csv")
data.head(10)
```
只取前 10 行数据来演示爬虫过程:
```{python}
companies = data["name"].tolist()
companies = companies[:10] # 只取前10个作为展示
```
作为展示,爬取的数据被写进 company_info_demo.csv 文件中,实际操作时保存在 company_info.csv 中。
```{python, eval = FALSE}
with open('data/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` 中的参数很多,但一般而言只需要 `cookie` 和 `user-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` 将目标信息转换为文本
![](单击查看完整URL.png)
![](设置headers.png)
![](查找标签.png)
![](查看info.png)
## 数据整合与清洗
爬虫过程中时不时会被提示“一段时间内访问次数过多”或者要求验证,甚至是被封 ip 地址,从而导致信息爬取失败。一般而言解决的办法是:使用 `time.sleep` 强制程序停止几秒钟,构建代理 ip 池以及使用代理,分布式爬虫以及其他一些高级爬虫方法。
实际过程中 `time.sleep` 方法并没有很好的解决问题,网络上的免费 ip 基本没法用,又不想花钱买 ip,分布式爬虫等一些高级方法要花相当一段时间学习。
最后我采用一种笨办法:将原本整个 csv 文件拆分成若干子文件来进行爬虫。于是在爬虫结束后,得到了数个存储企业地址的 csv 文件。如何将这些 csv 文件中的数据纵向拼接呢?方法如下:
- 将所有 csv 转移到一个新的空文件夹中
- 获取文件夹路径,通过匹配 `.csv` 从而获取该文件夹中的所有后缀为 `.csv` 文件的名称
- 通过 `do.call` 函数对 csv 文件批量执行 `rbind` 操作
```{r}
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
```{r}
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 中。
```{r}
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("data/清洗后的数据.csv")
```
如此,得到了相对干净的企业地址数据:
```{r}
clean_df %>% head(20) %>% knitr::kable()
```