完整工具链:从爬取、解析到可视化12306城市数据的全流程实现

简介: 完整工具链:从爬取、解析到可视化12306城市数据的全流程实现

在数据驱动的时代,获取并理解公共数据已成为技术决策和商业分析的关键环节。12306作为中国铁路客运服务的核心系统,其背后庞大的城市站点数据不仅对旅行规划至关重要,更是观察中国城市化进程和交通网络布局的独特窗口。本文将带领您构建一个完整的技术工具链,从数据爬取、解析处理到最终的可视化呈现,全方位挖掘12306城市数据的价值。
技术架构与工具选型
在开始实现之前,我们先规划整个技术栈:
● 数据获取层:使用 requests 模块处理HTTP请求
● 数据处理层:利用 json 和 pandas 进行数据清洗与结构化
● 数据存储层:采用轻量级的SQLite数据库
● 可视化层:基于 pyecharts 创建交互式地理图表
这种分层架构确保了各模块的职责单一,同时保持了整个流程的连贯性和可维护性。
第一阶段:数据爬取 - 精准获取城市JSON
12306的城市数据通过一个特定的接口提供,我们需要模拟浏览器请求来获取这些数据。
import requests
import json
import re
from typing import Dict, Any

class CityDataCrawler:
def init(self):
self.session = requests.Session()

    # 设置请求头,模拟浏览器行为
    self.headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
        'Referer': 'https://www.12306.cn/index/'
    }

    # 代理配置
    self.proxyHost = "www.16yun.cn"
    self.proxyPort = "5445"
    self.proxyUser = "16QMSOML"
    self.proxyPass = "280651"

    # 设置代理
    self._setup_proxy()

def _setup_proxy(self):
    """设置代理配置"""
    proxy_url = f"http://{self.proxyUser}:{self.proxyPass}@{self.proxyHost}:{self.proxyPort}"
    self.proxies = {
        'http': proxy_url,
        'https': proxy_url
    }

    # 为session设置代理
    self.session.proxies.update(self.proxies)

def get_city_data(self) -> Dict[str, Any]:
    """
    获取12306城市数据
    返回字典格式的城市信息
    """
    try:
        # 12306城市数据接口
        url = "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9270"

        # 使用代理发送请求
        response = self.session.get(
            url, 
            headers=self.headers, 
            timeout=10,
            verify=False  # 如果代理有SSL证书问题,可以暂时关闭验证
        )
        response.encoding = 'utf-8'

        if response.status_code == 200:
            return self._parse_raw_data(response.text)
        else:
            print(f"请求失败,状态码:{response.status_code}")
            return {}

    except requests.exceptions.ProxyError as e:
        print(f"代理连接错误:{e}")
        return {}
    except requests.exceptions.ConnectTimeout as e:
        print(f"连接超时:{e}")
        return {}
    except requests.exceptions.RequestException as e:
        print(f"请求异常:{e}")
        return {}
    except Exception as e:
        print(f"获取数据时发生错误:{e}")
        return {}

def _parse_raw_data(self, raw_text: str) -> Dict[str, Any]:
    """
    解析原始数据,提取城市信息
    """
    # 使用正则表达式提取车站数据
    pattern = r"@[a-z]+\|([^\|]+)\|([a-z]+)\|([a-z]+)\|([a-z]+)\|([a-z]+)\|([0-9]+)\|([a-z]+)"
    matches = re.findall(pattern, raw_text)

    cities = {}
    for match in matches:
        city_data = {
            'name': match[0],  # 中文站名
            'code': match[1],  # 车站代码
            'pinyin': match[2],  # 拼音
            'abbr': match[3],   # 缩写
            'number': match[6]  # 编号
        }
        cities[city_data['code']] = city_data

    print(f"成功解析 {len(cities)} 个车站数据")
    return cities

def test_proxy_connection(self):
    """测试代理连接是否正常"""
    test_url = "http://httpbin.org/ip"
    try:
        response = self.session.get(test_url, timeout=5)
        if response.status_code == 200:
            print("代理连接测试成功")
            print(f"当前IP信息:{response.text}")
            return True
        else:
            print("代理连接测试失败")
            return False
    except Exception as e:
        print(f"代理测试异常:{e}")
        return False

执行爬取

if name == "main":
crawler = CityDataCrawler()

# 测试代理连接
print("正在测试代理连接...")
if crawler.test_proxy_connection():
    print("代理连接正常,开始爬取数据...")
    city_data = crawler.get_city_data()
    if city_data:
        print(f"数据爬取完成!共获取 {len(city_data)} 个城市数据")
        # 打印前5个城市作为示例
        for i, (code, city) in enumerate(list(city_data.items())[:5]):
            print(f"{i+1}. {city['name']} - 代码: {code}")
    else:
        print("数据爬取失败!")
else:
    print("代理连接失败,请检查代理配置")

第二阶段:数据解析与存储 - 构建结构化数据体系
获得原始数据后,我们需要进行数据清洗、结构化,并存储到数据库中以便后续分析。
import pandas as pd
import sqlite3
from datetime import datetime

class DataProcessor:
def init(self, city_data: Dict[str, Any]):
self.city_data = city_data
self.df = None

def create_dataframe(self):
    """将城市数据转换为DataFrame"""
    records = []
    for code, info in self.city_data.items():
        records.append({
            'station_code': code,
            'station_name': info['name'],
            'pinyin': info['pinyin'],
            'abbreviation': info['abbr'],
            'station_number': info['number']
        })

    self.df = pd.DataFrame(records)
    print(f"创建DataFrame成功,共 {len(self.df)} 条记录")

    # 数据质量检查
    self._data_quality_check()

def _data_quality_check(self):
    """执行数据质量检查"""
    print("\n=== 数据质量报告 ===")
    print(f"总记录数: {len(self.df)}")
    print(f"空值统计:")
    print(self.df.isnull().sum())
    print(f"重复车站数: {self.df.duplicated('station_code').sum()}")
    print("====================\n")

def save_to_sqlite(self, db_path: str = "12306_cities.db"):
    """保存数据到SQLite数据库"""
    try:
        conn = sqlite3.connect(db_path)

        # 创建数据表
        create_table_sql = """
        CREATE TABLE IF NOT EXISTS railway_stations (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            station_code TEXT UNIQUE NOT NULL,
            station_name TEXT NOT NULL,
            pinyin TEXT,
            abbreviation TEXT,
            station_number TEXT,
            created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        """
        conn.execute(create_table_sql)

        # 保存数据
        self.df.to_sql('railway_stations', conn, if_exists='replace', index=False)

        conn.commit()
        conn.close()
        print(f"数据已保存到数据库: {db_path}")

    except Exception as e:
        print(f"数据库操作失败: {e}")

def get_regional_statistics(self):
    """生成区域统计信息"""
    if self.df is not None:
        # 简单的统计分析
        stats = {
            'total_stations': len(self.df),
            'stations_per_province': self._count_stations_by_province()
        }
        return stats
    return {}

def _count_stations_by_province(self):
    """按省份统计车站数量(简化版本)"""
    # 在实际应用中,这里需要更复杂的地理编码逻辑
    province_keywords = ['北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都']
    province_count = {}

    for keyword in province_keywords:
        count = self.df[self.df['station_name'].str.contains(keyword)].shape[0]
        if count > 0:
            province_count[keyword] = count

    return province_count

数据处理流程

processor = DataProcessor(city_data)
processor.create_dataframe()
processor.save_to_sqlite()

输出统计信息

stats = processor.get_regional_statistics()
print("区域统计信息:", stats)
第三阶段:数据可视化 - 让数据说话
数据只有通过可视化才能真正展现其价值。我们将使用pyecharts创建交互式的地理分布图。
from pyecharts import options as opts
from pyecharts.charts import Map, Bar, Page
from pyecharts.globals import ThemeType
import random

class DataVisualizer:
def init(self, df):
self.df = df

def create_station_distribution_map(self):
    """创建车站分布地图"""
    # 模拟各省份车站数量(实际项目中应从准确的地理编码获取)
    province_data = [
        ("北京市", 15), ("天津市", 12), ("河北省", 45),
        ("山西省", 38), ("内蒙古自治区", 28), ("辽宁省", 42),
        ("吉林省", 35), ("黑龙江省", 40), ("上海市", 18),
        ("江苏省", 55), ("浙江省", 48), ("安徽省", 42),
        ("福建省", 38), ("江西省", 36), ("山东省", 52),
        ("河南省", 58), ("湖北省", 45), ("湖南省", 44),
        ("广东省", 62), ("广西壮族自治区", 40), ("海南省", 12),
        ("重庆市", 25), ("四川省", 65), ("贵州省", 35),
        ("云南省", 42), ("西藏自治区", 8), ("陕西省", 38),
        ("甘肃省", 32), ("青海省", 18), ("宁夏回族自治区", 12),
        ("新疆维吾尔自治区", 28), ("台湾省", 15)
    ]

    distribution_map = (
        Map(init_opts=opts.InitOpts(
            theme=ThemeType.ROMA,
            width="1200px",
            height="600px"
        ))
        .add(
            series_name="车站数量",
            data_pair=province_data,
            maptype="china",
            is_map_symbol_show=False,
        )
        .set_global_opts(
            title_opts=opts.TitleOpts(
                title="12306全国铁路车站分布图",
                subtitle="数据来源:12306官方",
                pos_left="center"
            ),
            visualmap_opts=opts.VisualMapOpts(
                min_=0,
                max_=70,
                is_calculable=True,
                orient="horizontal",
                pos_left="center",
                pos_bottom="10%",
                range_color=["#B0E0E6", "#1E90FF", "#0000CD"]
            ),
            legend_opts=opts.LegendOpts(is_show=False)
        )
        .set_series_opts(
            label_opts=opts.LabelOpts(is_show=True)
        )
    )

    return distribution_map

def create_top_cities_chart(self):
    """创建主要城市车站数量柱状图"""
    # 基于实际数据生成主要城市统计
    major_cities = ['北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', '重庆']
    city_counts = []

    for city in major_cities:
        count = len(self.df[self.df['station_name'].str.contains(city)])
        city_counts.append(count)

    bar_chart = (
        Bar(init_opts=opts.InitOpts(
            theme=ThemeType.ROMA,
            width="1000px",
            height="500px"
        ))
        .add_xaxis(major_cities)
        .add_yaxis(
            "车站数量",
            city_counts,
            itemstyle_opts=opts.ItemStyleOpts(color="#5470C6")
        )
        .set_global_opts(
            title_opts=opts.TitleOpts(
                title="主要城市铁路车站数量统计",
                pos_left="center"
            ),
            xaxis_opts=opts.AxisOpts(
                axislabel_opts=opts.LabelOpts(rotate=45)
            ),
            yaxis_opts=opts.AxisOpts(
                name="车站数量"
            )
        )
    )

    return bar_chart

def generate_dashboard(self):
    """生成完整的数据仪表板"""
    page = Page(layout=Page.SimplePageLayout)

    # 添加各个图表
    page.add(
        self.create_station_distribution_map(),
        self.create_top_cities_chart()
    )

    # 渲染为HTML文件
    page.render("12306_cities_dashboard.html")
    print("可视化仪表板已生成: 12306_cities_dashboard.html")

执行可视化

visualizer = DataVisualizer(processor.df)
visualizer.generate_dashboard()
技术深度解析与业务价值

  1. 反爬虫策略应对
    在数据爬取阶段,我们采用了完整的请求头模拟,包括User-Agent和Referer,这对于绕过基础的反爬虫机制至关重要。在实际生产环境中,可能需要进一步处理IP轮换、验证码识别等复杂情况。
  2. 数据质量保障
    通过实现数据质量检查流程,我们能够及时发现数据缺失、重复等问题,确保后续分析的准确性。这是工业级数据管道不可或缺的环节。
  3. 可视化技术选型
    选择pyecharts而非matplotlib等传统库,是因为其出色的交互性和对中文的良好支持。用户可以通过悬停、缩放等操作与图表深度交互,获得更丰富的数据洞察。
    应用场景与扩展方向
    这个完整的工具链不仅限于技术演示,在实际业务中具有广泛的应用价值:
    ● 城市规划分析:通过车站分布密度了解区域交通发展水平
    ● 商业选址支持:为物流、零售等行业提供交通便利性参考
    ● 旅游产品开发:基于铁路网络优化旅行路线规划
    ● 投资决策辅助:分析交通基础设施建设的区域重点
    总结
    通过本文的完整实现,我们构建了一个从数据采集到价值呈现的端到端技术解决方案。这个工具链展示了现代数据工程的核心理念:自动化、结构化、可视化。每个技术环节都经过精心设计,既保证了功能的完整性,又为后续扩展留出了充足空间。
相关文章
|
6月前
|
JSON 安全 API
12306旅游产品数据抓取:Python+API逆向分析
12306旅游产品数据抓取:Python+API逆向分析
|
5月前
|
数据采集 Web App开发 自然语言处理
新闻热点一目了然:Python爬虫数据可视化
新闻热点一目了然:Python爬虫数据可视化
|
5月前
|
数据采集 存储 API
Scrapy框架实战:大规模爬取华为应用市场应用详情数据
Scrapy框架实战:大规模爬取华为应用市场应用详情数据
|
5月前
|
数据采集 运维 监控
构建企业级Selenium爬虫:基于隧道代理的IP管理架构
构建企业级Selenium爬虫:基于隧道代理的IP管理架构
|
7月前
|
存储 Web App开发 前端开发
Python + Requests库爬取动态Ajax分页数据
Python + Requests库爬取动态Ajax分页数据
|
6月前
|
数据采集 存储 前端开发
Java爬虫性能优化:多线程抓取JSP动态数据实践
Java爬虫性能优化:多线程抓取JSP动态数据实践
|
7月前
|
数据采集 存储 C++
Python异步爬虫(aiohttp)加速微信公众号图片下载
Python异步爬虫(aiohttp)加速微信公众号图片下载
|
7月前
|
Web App开发 存储 前端开发
Python+Selenium自动化爬取携程动态加载游记
Python+Selenium自动化爬取携程动态加载游记
|
5月前
|
数据采集 存储 Web App开发
处理Cookie和Session:让Python爬虫保持连贯的"身份"
处理Cookie和Session:让Python爬虫保持连贯的"身份"
|
6月前
|
数据采集 网络协议 大数据
如何用aiohttp实现每秒千次的网页抓取
如何用aiohttp实现每秒千次的网页抓取