Python数据抓取与质量校验:以杭州市公交线路为例

参考文章:Python爬取公交站点和线路数据(上下行双向) - 知乎 (zhihu.com)

之前也讲过数据抓取类型的文章很讲究时效性,该篇文章发于2020年,因为高德api策略的更新和网站抓取数据的机制变化等原因,如今脚本已经不能直接使用,我们这篇是对脚本原理的解释和流程的优化,本篇文章讲二个重点:1、解释清楚逻辑并略作优化让脚本可以重新跑起来了;2、讲一下数据质量,本文以杭州市公交线路为例,那么杭州市到底有多少条公交线路呢?

先讲一下方法思路,一共三个步骤;

方法思路

  1. 获取公交信息网站------8684网站
  2. 获取经纬度------通过调用高德地图API
  3. 坐标转换------高德坐标系(GCJ-02) to WGS84

PS:运行前把代码中的url里面的key和安全密钥换成自己,另外把下面的城市参数改成自己需要获取数据的城市;

python 复制代码
# 设定参数
citys = ['hangzhou']  # 城市总列表
chinese_city_names = ['杭州']

完整代码#运行环境Python 3.11

python 复制代码
# -*- coding: utf-8 -*-

import requests
import pandas as pd
import json
import re
import time
from bs4 import BeautifulSoup
import math

# 获取城市公交线路的首字母列表
def get_initial_letters(city_name, headers):
    # 构造请求URL
    url = f'https://{city_name}.8684.cn/list1'
    # 设置请求头
    headers = {'User-Agent': headers}
    # 发送GET请求
    response = requests.get(url, headers=headers)
    # 解析HTML
    soup = BeautifulSoup(response.text, 'lxml')
    # 查找包含首字母的标签
    initial_letters = soup.find_all('div', {'class': 'tooltip-inner'})[3].find_all('a')
    # 提取每个首字母
    return [letter.get_text() for letter in initial_letters]

# 根据首字母获取公交线路名称
def get_bus_lines_by_letter(city_name, letter, headers, lines):
    # 构造请求URL
    url = f'https://{city_name}.8684.cn/list{letter}'
    # 设置请求头
    headers = {'User-Agent': headers}
    # 发送GET请求
    response = requests.get(url, headers=headers)
    # 解析HTML
    soup = BeautifulSoup(response.text, 'lxml')
    # 查找包含线路名称的标签
    bus_lines = soup.find('div', {'class': 'list clearfix'}).find_all('a')
    # 提取每个线路名称
    lines.extend([line.get_text() for line in bus_lines])

# 公交坐标信息转化(GCJ-02到WGS84)
def gcj02_to_wgs84(lng, lat):
    if out_of_china(lng, lat):  # 如果不在中国境内,则直接返回原坐标
        return lng, lat
    # 计算偏移量
    dlat = transformlat(lng - 105.0, lat - 35.0)
    dlng = transformlng(lng - 105.0, lat - 35.0)
    radlat = lat / 180.0 * math.pi
    magic = math.sin(radlat)
    magic = 1 - 0.00669342162296594323 * magic * magic
    sqrtmagic = math.sqrt(magic)
    dlat = (dlat * 180.0) / ((6378245.0 * (1 - 0.00669342162296594323)) / (magic * sqrtmagic) * math.pi)
    dlng = (dlng * 180.0) / (6378245.0 / sqrtmagic * math.cos(radlat) * math.pi)
    mglat = lat + dlat
    mglng = lng + dlng
    return [lng * 2 - mglng, lat * 2 - mglat]

# 辅助函数,计算纬度的偏移量
def transformlat(lng, lat):
    ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * math.sqrt(abs(lng))
    ret += (20.0 * math.sin(6.0 * lng * math.pi) + 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0
    ret += (20.0 * math.sin(lat * math.pi) + 40.0 * math.sin(lat / 3.0 * math.pi)) * 2.0 / 3.0
    ret += (160.0 * math.sin(lat / 12.0 * math.pi) + 320 * math.sin(lat * math.pi / 30.0)) * 2.0 / 3.0
    return ret

# 辅助函数,计算经度的偏移量
def transformlng(lng, lat):
    ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * math.sqrt(abs(lng))
    ret += (20.0 * math.sin(6.0 * lng * math.pi) + 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0
    ret += (20.0 * math.sin(lng * math.pi) + 40.0 * math.sin(lng / 3.0 * math.pi)) * 2.0 / 3.0
    ret += (150.0 * math.sin(lng / 12.0 * math.pi) + 300.0 * math.sin(lng / 30.0 * math.pi)) * 2.0 / 3.0
    return ret

# 检查坐标是否在中国境内
def out_of_china(lng, lat):
    if lng < 72.004 or lng > 137.8347:
        return True
    if lat < 0.8293 or lat > 55.8271:
        return True
    return False

# 将坐标字符串转换为坐标元组
def parse_coordinates(coord_str):
    lng, lat = coord_str.split(',')
    return float(lng), float(lat)

# 获取公交站点信息
def get_station_info(city, line_name):
    # 构造请求URL
    url = f'https://restapi.amap.com/v3/bus/linename?s=rsv3&extensions=all&key=你自己的key&jscode=你自己的安全密钥&output=json&city={city}&offset=2&keywords={line_name}&platform=JS'
    # 发送GET请求
    response = requests.get(url)
    # 解析JSON响应
    data = json.loads(response.text)
    if data.get('status') == '1' and data.get('buslines'):  # 检查状态码是否为1,并且有数据
        buslines = data['buslines']
        if len(buslines) > 0:
            results = []
            for busline in buslines:
                line_name = busline['name']
                stations = [{'name': station['name'], 'location': station['location'], 'sequence': station['sequence']}
                            for station in busline['busstops']]
                results.append({'line_name': line_name, 'stations': stations})
            return pd.DataFrame(results)
    return None

# 获取公交线路数据信息
def get_line_info(city, line_name):
    # 构造请求URL
    url = f'https://restapi.amap.com/v3/bus/linename?s=rsv3&extensions=all&key=你自己的key&jscode=你自己的安全密钥&output=json&city={city}&offset=2&keywords={line_name}&platform=JS'
    # 发送GET请求
    response = requests.get(url)
    # 解析JSON响应
    data = json.loads(response.text)
    if data.get('status') == '1' and data.get('buslines'):  # 检查状态码是否为1,并且有数据
        buslines = data['buslines']
        if len(buslines) > 0:
            results = []
            for busline in buslines:
                line_name = busline['name']
                polyline = busline['polyline']
                results.append({'line_name': line_name, 'polyline': polyline})
            return pd.DataFrame(results)
    return None

# 爬取公交数据主程序
def get_station_and_line_info(citys, chinese_city_names, headers, file_path):
    for city, city_name in zip(citys, chinese_city_names):
        # 创建公交线路空列表
        lines = []
        # 获取首字母列表
        initial_letters = get_initial_letters(city, headers)
        # 根据首字母列表获取线路名称
        for letter in initial_letters:
            get_bus_lines_by_letter(city, letter, headers, lines)
        print(f'正在爬取{city_name}市的公交线路名称...')

        # 保存路径
        station_file = f'{file_path}{city}_station.csv'
        line_file = f'{file_path}{city}_line.csv'

        # 爬取公交站点数据
        print(f'正在爬取{city_name}市的公交站点数据...')
        station_data = pd.DataFrame()
        for line in lines:
            data = get_station_info(city_name, line)
            if data is not None:
                station_data = pd.concat([station_data, data], ignore_index=True)

        # 处理站点坐标
        processed_stations = []
        for _, row in station_data.iterrows():
            line_name = row['line_name']
            stations = row['stations']
            coord_x, coord_y = [], []
            for station in stations:
                lng, lat = parse_coordinates(station['location'])
                wlng, wlat = gcj02_to_wgs84(lng, lat)
                coord_x.append(wlng)
                coord_y.append(wlat)
            processed_stations.append({'line_name': [line_name] * len(stations), 'coord_x': coord_x, 'coord_y': coord_y,
                                       'station_name': [s['name'] for s in stations],
                                       'sequence': [s['sequence'] for s in stations]})

        # 合并站点数据
        stations_df = pd.concat([pd.DataFrame(s) for s in processed_stations], ignore_index=True)
        stations_df.to_csv(station_file, index=False, encoding='utf_8_sig')
        print(f'已保存{city_name}公交站点数据至 {station_file}')

        # 爬取公交线路数据
        print(f'正在爬取{city_name}市的公交线路数据...')
        line_data = pd.DataFrame()
        for line in lines:
            data = get_line_info(city_name, line)
            if data is not None:
                line_data = pd.concat([line_data, data], ignore_index=True)

        # 处理线路坐标
        processed_lines = []
        for _, row in line_data.iterrows():
            line_name = row['line_name']
            polyline = row['polyline'].split(';')
            for order, coord in enumerate(polyline):
                wlng, wlat = gcj02_to_wgs84(*parse_coordinates(coord))
                processed_lines.append({'line_name': line_name, 'order': order, 'lon': wlng, 'lat': wlat})

        # 保存线路数据
        lines_df = pd.DataFrame(processed_lines)
        lines_df.to_csv(line_file, index=False, encoding='utf_8_sig')
        print(f'已保存{city_name}公交线路数据至 {line_file}')

# 设定参数
citys = ['hangzhou']  # 城市总列表
chinese_city_names = ['杭州']
headers = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'  # 浏览器user-agent
file_path = 'D://data//bus_data//'  # 数据存储路径
get_station_and_line_info(citys, chinese_city_names, headers, file_path)

脚本运行完成后会在D://data//bus_data这个路径下生成二个csv,这个有需要可以改成自己的路径,不改也行,会自己生成路径;

hangzhou_line.csv线路表包括线路名称、站点顺序、x坐标、y坐标的标签;

hangzhou_station.csv线路表包括线路名称、站点名称、站点顺序、x坐标、y坐标的标签;

把hangzhou_station.csv导入arcgis/arcgispro中,直接检索【点集转线】,且线字段选择line_name即可连成直线,方法在历史多篇文章里详细说明过,这里不再赘述;

接下来就是校对数据质量环节,我们打开hangzhou_line.csv这张表,把line_name单独复制出来,另存一个sheet ,在Excel中删除重复值,有2128条线路,另外因为是双向的原因,我们除于2,也就是1064条线路,我们再来打开杭州市公交集团官方来一探究竟,看看有多少条线路:集团介绍 | 杭州市公共交通集团有限公司 (hzbus.com.cn)

介绍里提及,截止2024年6月营运线路1147条,差值83条线路,另外数据包含12条杭州地铁线路。偏差率大约8.28%,但这个偏差率大体上可以接受,这个差值大概是因为8684是第三方网站,数据更新有一定滞后性等原因,但作为学术论文分析等还是有一定的分析价值,可以作为基础数据分析出杭州市的公共交通分布情况、公交线路重复率、公交非直线系数等一些指标。

另外这里可以看一下我这篇文章:利用高德API获取整个城市的公交路线并可视化(一)_实时公交api-CSDN博客

我同样获取了杭州市的所有公交线路,且获取方式基本一致,但是这个数据获取的公交线路去除12条地铁线路,是1122条公交线路,差值仅25条,偏差率大约2.17%,这里姑且留个伏笔,待我研究研究。

文章仅用于分享个人学习成果与个人存档之用,分享知识,如有侵权,请联系作者进行删除。所有信息均基于作者的个人理解和经验,不代表任何官方立场或权威解读。

相关推荐
yuanbenshidiaos几秒前
c++------------------函数
开发语言·c++
程序员_三木12 分钟前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
是小崔啊22 分钟前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
tianmu_sama28 分钟前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
黄公子学安全31 分钟前
Java的基础概念(一)
java·开发语言·python
liwulin050632 分钟前
【JAVA】Tesseract-OCR截图屏幕指定区域识别0.4.2
java·开发语言·ocr
jackiendsc37 分钟前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
Oneforlove_twoforjob41 分钟前
【Java基础面试题027】Java的StringBuilder是怎么实现的?
java·开发语言
羚羊角uou43 分钟前
【C++】优先级队列以及仿函数
开发语言·c++
云云3211 小时前
怎么通过亚矩阵云手机实现营销?
大数据·服务器·安全·智能手机·矩阵