Python爬虫零基础入门【第三章:Requests 静态爬取入门·第4节】列表页→详情页:两段式采集(90%项目都这样)!

🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》

订阅后更新会优先推送,按目录学习更高效~

📌 上期回顾

上期内容《稳定性第一课:超时、重试、退避(指数退避)!》,我们重点学习了爬取的核心技能:编写了生产级的 HttpClient、实现了智能重试和限速机制。现在,你已经掌握了稳定、礼貌的请求能力

从本节开始,我们将这些技能组合起来,解决真实的业务场景。而最经典、最常见的场景就是:列表页→详情页的两段式采集

这个模式适用于 90% 的爬虫项目:新闻网站、电商平台、招聘信息、房产数据...几乎所有结构化数据的采集都是这个套路!

🎯 本节目标

通过本节学习,你将能够:

  1. 理解"列表→详情"模式的设计思想
  2. 从列表页提取详情页 URL
  3. 设计数据流转管道(采集→解析→存储)
  4. 处理翻页逻辑(页码、offset、cursor)
  5. 实现去重机制(避免重复采集)
  6. 交付验收:完成一个新闻采集器(列表+详情完整流程)

一、理解"列表→详情"模式

1.1 为什么需要两段式?

典型网站结构

json 复制代码
首页 / 分类页(列表页)
├─ 新闻标点击] → 详情页1(完整内容)
├─ 新闻标题2  → [点击] → 详情页2
├─ 新闻标题3  → [点击] → 详情页3
...
└─ [下一页]

为什么不能只采集列表页?

❌ 列表页通常只有:

  • 标题
  • 摘要(截断的)
  • 发布时间
  • 缩略图

✅ 详情页才有完整数据:

  • 完整正文
  • 作者信息
  • 标签/分类
  • 评论数/阅读量
  • 相关推荐
1.2 数据流程图
json 复制代码
┌─────────────┐
│  列表页URL   │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  请求列表页  │  ← HttpClient + 限速
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ 解析列表页  │  ← BeautifulSoup/lxml
│ 提取URL列表 │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  遍历URL    │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ 请求详情页  │  ← HttpClient + 限速
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ 解析详情页  │  ← 提取完整字段
│ 提取数据    │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  清洗数据   │  ← DataCleaner
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  保存数据   │  ← CSV/JSON/数据库
└─────────────┘

二、第一步:采集列表页

2.1 分析列表页结构

示例:新闻列表页

html 复制代码
<div class="news-list">
    <div class="news-item">
        <h3><a href="/news/12345">新闻标题A</a></h3>
        <p class="summary">这是摘要...</p>
        <span class="time">2025-01-21</span>
    </div>
    <div class="news-item">
        <h3><a href="/news/12346">新闻标题B</a></h3>
        <p class="summary">这是摘要...</p>
        <span class="time">2025-01-20</span>
    </div>
    ...
</div>

需要提取的信息

  • ✅ 详情页 URL(必需)
  • ✅ 标题(可选,详情页会重复采集)
  • ✅ 发布时间(可选,用于判断是否需要采集)
2.2 提取 URL 列表
python 复制代码
from bs4 import BeautifulSoup
from urllib.parse import urljoin

def extract_detail_urls(html, base_url, selector='div.news-item h3 a'):
    """
    从列表页提取详情页 URL
    
    Args:
        html: 列表页 HTML
        base_url: 基础 URL(用于拼接相对路径)
        selector: CSS 选择器
        
    Returns:
        list: URL 列表
    """
    soup = BeautifulSoup(html, 'lxml')
    urls = []
    
    # 提取所有链接
    links = soup.select(selector)
    
    for link in links:
        href = link.get('href')
        if href:
            # 拼接完整 URL(处理相对路径)
            full_url = urljoin(base_url, href)
            urls.append(full_url)
    
    print(f"📋 从列表页提取到 {len(urls)} 个URL")
    return urls

# 使用示例
base_url = "https://example.com/news/"
html = fetch_page(base_url + "?page=1")

urls = extract_detail_urls(html, base_url)
print(urls)
# ['https://example.com/news/12345', 'https://example.com/news/12346', ...]
2.3 提取额外的列表字段(可选)
python 复制代码
def extract_list_items(html):
    """
    提取列表页的完整信息
    
    Returns:
        list: 列表项信息字典
    """
    soup = BeautifulSoup(html, 'lxml')
    items = []
    
    news_items = soup.select('div.news-item')
    
    for item in news_items:
        # 提取标题和链接
        title_elem = item.select_one('h3 a')
        if not title_elem:
            continue
        
        # 提取各字段
        item_data = {
            'url': urljoin(base_url, title_elem.get('href')),
            'title': title_elem.text.strip(),
            'summary': item.select_one('p.summary').text.strip() if item.select_one('p.summary') else '',
            'publish_time': item.select_one('span.time').text.strip() if item.select_one('span.time') else '',
        }
        
        items.append(item_data)
    
    print(f"📋 提取到 {len(items)} 条列表信息")
    return items

三、第二步:采集详情页

3.1 设计详情页解析器
python 复制代码
class NewsDetailParser:
    """新闻详情页解析器"""
    
    def __init__(self):
        """初始化选择器配置"""
        self.selectors = {
            'title': 'h1.article-title',
            'author': 'span.author',
            'publish_time': 'span.publish-time',
            'content': 'div.article-content',
            'tags': 'div.tags a',
        }
    
    def parse(self, html, url):
        """
        解析详情页
        
        Args:
            html: 详情页 HTML
            url: 详情页 URL
            
        Returns:
            dict: 解析后的数据
        """
        soup = BeautifulSoup(html, 'lxml')
        
        data = {
            'url': url,
            'crawl_time': datetime.now().isoformat(),
        }
        
        # 提取标题
        title_elem = soup.select_one(self.selectors['title'])
        data['title'] = title_elem.text.strip() if title_elem else ''
        
        # 提取作者
        author_elem = soup.select_one(self.selectors['author'])
        data['author'] = author_elem.text.strip() if author_elem else ''
        
        # 提取发布时间
        time_elem = soup.select_one(self.selectors['publish_time'])
        data['publish_time'] = time_elem.text.strip() if time_elem else ''
        
        # 提取正文
        content_elem = soup.select_one(self.selectors['content'])
        if content_elem:
            # 移除脚本和样式
            for script in content_elem.select('script, style'):
                script.decompose()
            data['content'] = content_elem.get_text(separator='\n', strip=True)
        else:
            data['content'] = ''
        
        # 提取标签
        tag_elems = soup.select(self.selectors['tags'])
        data['tags'] = [tag.text.strip() for tag in tag_elems]
        
        return data

# 使用示例
parser = NewsDetailParser()
html = fetch_page("https://example.com/news/12345")
data = parser.parse(html, "https://example.com/news/12345")

print(data)

四、完整的两段式采集流程

4.1 流程编排器
python 复制代码
"""
两段式采集器
文件名: two_stage_spider.py
"""

import time
from typing import List, Dict, Optional
from datetime import datetime
import json


class TwoStageSpider:
    """两段式爬虫(列表→详情)"""
    
    def __init__(self, 
                 http_client,
                 list_parser,
                 detail_parser,
                 save_dir='data/output'):
        """
        初始化
        
        Args:
            http_client: HTTP 客户端(带限速)
            list_parser: 列表页解析器
            detail_parser: 详情页解析器
            save_dir: 数据保存目录
        """
        self.client = http_client
        self.list_parser = list_parser
        self.detail_parser = detail_parser
        self.save_dir = save_dir
        
        # 去重集合
        self.crawled_urls = set()
        
        # 统计
        self.stats = {
            'list_pages': 0,
            'detail_pages': 0,
            'success': 0,
            'failed': 0,
            'duplicates': 0,
        }
        
        os.makedirs(save_dir, exist_ok=True)
    
    def crawl_list_page(self, list_url: str) -> List[str]:
        """
        采集列表页,提取详情页 URL
        
        Args:
            list_url: 列表页 URL
            
        Returns:
            list: 详情页 URL 列表
        """
        print(f"\n{'='*60}")
        print(f"📋 采集列表页: {list_url}")
        print(f"{'='*60}")
        
        response = self.client.get(list_url)
        if not response:
            print(f"❌ 列表页请求失败")
            return []
        
        self.stats['list_pages'] += 1
        
        # 解析列表页
        detail_urls = self.list_parser.parse(response.text, list_url)
        
        print(f"✅ 提取到 {len(detail_urls)} 个详情页 URL")
        return detail_urls
    
    def crawl_detail_page(self, detail_url: str) -> Optional[Dict]:
        """
        采集详情页
        
        Args:
            detail_url: 详情页 URL
            
        Returns:
            dict: 解析后的数据,失败返回 None
        """
        # 去重检查
        if detail_url in self.crawled_urls:
            print(f"⏭️  跳过重复: {detail_url}")
            self.stats['duplicates'] += 1
            return None
        
        print(f"\n📄 采集详情页: {detail_url}")
        
        response = self.client.get(detail_url)
        if not response:
            print(f"❌ 详情页请求失败")
            self.stats['failed'] += 1
            return None
        
        self.stats['detail_pages'] += 1
        self.crawled_urls.add(detail_url)
        
        # 解析详情页
        try:
            data = self.detail_parser.parse(response.text, detail_url)
            print(f"✅ 解析成功: {data.get('title', 'N/A')[:30]}")
            self.stats['success'] += 1
            return data
        except Exception as e:
            print(f"❌ 解析失败: {e}")
            self.stats['failed'] += 1
            return None
    
    def crawl_list_and_details(self, list_url: str) -> List[Dict]:
        """
        完整流程:采集列表页 → 采集所有详情页
        
        Args:
            list_url: 列表页 URL
            
        Returns:
            list: 所有详情页的数据
        """
        # 第一步:采集列表页
        detail_urls = self.crawl_list_page(list_url)
        
        if not detail_urls:
            return []
        
        # 第二步:采集所有详情页
        all_data = []
        
        for i, url in enumerate(detail_urls, 1):
            print(f"\n[{i}/{len(detail_urls)}]")
            
            data = self.crawl_detail_page(url)
            if data:
                all_data.append(data)
            
            # 礼貌延迟(可选,HttpClient 已有限速)
            time.sleep(0.5)
        
        return all_data
    
    def crawl_multiple_pages(self, 
                            list_url_template: str,
                            page_range: range) -> List[Dict]:
        """
        采集多页列表
        
        Args:
            list_url_template: 列表页 URL 模板
                              例如:"https://example.com/news?page={page}"
            page_range: 页码范围
                       例如:range(1, 6) 表示第 1-5 页
            
        Returns:
            list: 所有数据
        """
        all_data = []
        
        for page in page_range:
            list_url = list_url_template.format(page=page)
            
            page_data = self.crawl_list_and_details(list_url)
            all_data.extend(page_data)
            
            print(f"\n📊 第 {page} 页完成,累计 {len(all_data)} 条数据")
            
            # 页间延迟
            time.sleep(2)
        
        return all_data
    
    def save_data(self, data: List[Dict], filename: str = None):
        """
        保存数据到 JSON 文件
        
        Args:
            data: 数据列表
            filename: 文件名(可选)
        """
        if not filename:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            filename = f"news_{timestamp}.json"
        
        filepath = os.path.join(self.save_dir, filename)
        
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        
        print(f"\n💾 数据已保存: {filepath}")
        print(f"📊 共 {len(data)} 条记录")
    
    def print_stats(self):
        """打印统计信息"""
        print("\n" + "=" * 60)
        print("📊 采集统计")
        print("=" * 60)
        print(f"列表页数: {self.stats['list_pages']}")
        print(f"详情页数: {self.stats['detail_pages']}")
        print(f"成功: {self.stats['success']}")
        print(f"失败: {self.stats['failed']}")
        print(f"重复跳过: {self.stats['duplicates']}")
        
        total = self.stats['success'] + self.stats['failed']
        if total > 0:
            success_rate = self.stats['success'] / total * 100
            print(f"成功率: {success_rate:.2f}%")
        
        print("=" * 60 + "\n")
4.2 列表页解析器示例
python 复制代码
class NewsListParser:
    """新闻列表页解析器"""
    
    def parse(self, html: str, base_url: str) -> List[str]:
        """
        解析列表页,提取详情页 URL
        
        Args:
            html: 列表页 HTML
            base_url: 基础 URL
            
        Returns:
            list: 详情页 URL 列表
        """
        from bs4 import BeautifulSoup
        from urllib.parse import urljoin
        
        soup = BeautifulSoup(html, 'lxml')
        urls = []
        
        # 根据实际网站调整选择器
        links = soup.select('div.news-item h3 a')
        
        for link in links:
            href = link.get('href')
            if href:
                full_url = urljoin(base_url, href)
                urls.append(full_url)
        
        return urls

五、处理分页逻辑

5.1 三种常见分页方式

方式1:基于页码

python 复制代码
# URL 模板
list_url_template = "https://example.com/news?page={page}"

# 采集第 1-10 页
spider.crawl_multiple_pages(list_url_template, range(1, 11))

方式2:基于偏移量

python 复制代码
# URL 模板
list_url_template = "https://example.com/news?offset={offset}&limit=20"

# 生成 offset
page_size = 20
total = 200  # 总记录数

for offset in range(0, total, page_size):
    url = list_url_template.format(offset=offset)
    spider.crawl_list_and_details(url)

方式3:下一页链接

python 复制代码
def crawl_with_next_link(spider, start_url):
    """跟随"下一页"链接采集"""
    current_url = start_url
    
    while current_url:
        # 采集当前页
        data = spider.crawl_list_and_details(current_url)
        
        # 查找"下一页"链接
        response = spider.client.get(current_url)
        soup = BeautifulSoup(response.text, 'lxml')
        
        next_link = soup.select_one('a.next-page')
        if next_link:
            current_url = urljoin(current_url, next_link.get('href'))
        else:
            print("✅ 已到最后一页")
            break
5.2 自动识别总页数
python 复制代码
def get_total_pages(html):
    """
    从列表页提取总页数
    
    常见位置:
    - 分页组件中的最后一页链接
    - "共 XX 页" 的文本
    """
    soup = BeautifulSoup(html, 'lxml')
    
    # 方法1:从分页链接提取
    page_links = soup.select('div.pagination a')
    if page_links:
        last_link = page_links[-2]  # 倒数第二个(最后一个通常是"下一页")
        try:
            return int(last_link.text)
        except ValueError:
            pass
    
    # 方法2:从文本提取
    import re
    pagination_text = soup.select_one('div.pagination').text
    match = re.search(r'共\s*(\d+)\s*页', pagination_text)
    if match:
        return int(match.group(1))
    
    return None

# 使用
first_page_html = fetch_page("https://example.com/news?page=1")
total_pages = get_total_pages(first_page_html)
print(f"总共 {total_pages} 页")

if total_pages:
    spider.crawl_multiple_pages(list_url_template, range(1, total_pages + 1))

六、去重机制

6.1 基于 URL 的去重
python 复制代码
class URLDeduplicator:
    """URL 去重器"""
    
    def __init__(self, persistence_file=None):
        """
        初始化
        
        Args:
            persistence_file: 持久化文件路径(可选)
        """
        self.seen_urls = set()
        self.persistence_file = persistence_file
        
        # 加载已采集的 URL
        if persistence_file and os.path.exists(persistence_file):
            self.load()
    
    def is_duplicate(self, url: str) -> bool:
        """检查 URL 是否重复"""
        return url in self.seen_urls
    
    def add(self, url: str):
        """添加 URL 到已见集合"""
        self.seen_urls.add(url)
    
    def save(self):
        """保存到文件"""
        if self.persistence_file:
            with open(self.persistence_file, 'w') as f:
                json.dump(list(self.seen_urls), f, indent=2)
    
    def load(self):
        """从文件加载"""
        with open(self.persistence_file, 'r') as f:
            urls = json.load(f)
            self.seen_urls = set(urls)
        
        print(f"📂 加载了 {len(self.seen_urls)} 个已采集 URL")

# 使用
deduplicator = URLDeduplicator(persistence_file='crawled_urls.json')

for url in detail_urls:
    if not deduplicator.is_duplicate(url):
        data = spider.crawl_detail_page(url)
        deduplicator.add(url)

# 保存去重记录
deduplicator.save()
6.2 基于内容的去重
python 复制代码
import hashlib

def get_content_hash(content: str) -> str:
    """计算内容的 MD5 哈希"""
    return hashlib.md5(content.encode()).hexdigest()

class ContentDeduplicator:
    """内容去重器"""
    
    def __init__(self):
        self.seen_hashes = set()
    
    def is_duplicate(self, content: str) -> bool:
        """检查内容是否重复"""
        content_hash = get_content_hash(content)
        
        if content_hash in self.seen_hashes:
            return True
        
        self.seen_hashes.add(content_hash)
        return False

# 使用
content_dedup = ContentDeduplicator()

for url in detail_urls:
    data = spider.crawl_detail_page(url)
    
    if not content_dedup.is_duplicate(data['content']):
        # 保存数据...
        pass
    else:
        print(f"⏭️  内容重复,跳过: {data['title']}")

七、完整示例:新闻采集器

python 复制代码
"""
示例:新闻网站采集器
"""

if __name__ == "__main__":
    # 1. 创建 HTTP 客户端
    from http_client import HttpClientV2
    client = HttpClientV2(rate_limit=5)  # 每秒 5 个请求
    
    # 2. 创建解析器
    list_parser = NewsListParser()
    detail_parser = NewsDetailParser()
    
    # 3. 创建爬虫
    spider = TwoStageSpider(
        http_client=client,
        list_parser=list_parser,
        detail_parser=detail_parser,
        save_dir='data/news'
    )
    
    # 4. 采集数据
    print("🕷️  开始采集...")
    
    # 方式1:单页测试
    data = spider.crawl_list_and_details("https://example.com/news?page=1")
    
    # 方式2:多页采集
    # list_url_template = "https://example.com/news?page={page}"
    # data = spider.crawl_multiple_pages(list_url_template, range(1, 6))
    
    # 5. 保存数据
    if data:
        spider.save_data(data)
    
    # 6. 打印统计
    spider.print_stats()
    
    # 7. 关闭客户端
    client.close()

八、本节小结

本节我们学习了两段式采集的完整流程:

模式理解 :列表页→详情页是最经典的爬虫模式

列表解析 :提取详情页 URL 和基础信息

详情解析 :提取完整的结构化数据

流程编排 :设计可复用的 TwoStageSpider 类

分页处理 :支持页码、偏移量、下一页链接

去重机制:基于 URL 和内容的去重策略

核心原则

  • 职责分离:列表解析、详情解析、数据保存各司其职
  • 可复用性:编写通用类,适配不同网站
  • 鲁棒性:异常处理、去重、统计贯穿始终
  • 可维护性:选择器配置化,便于网站改版后调整

九、课后作业(必做,验收进入下一节)

任务1:分析目标网站

选择一个新闻或资讯网站,完成:

  1. 找到列表页和详情页的 URL 规律
  2. 分析列表页结构,写出提取 URL 的选择器
  3. 分析详情页结构,列出所有可提取字段
  4. 设计数据结构(
任务2:编写完整采集器

使用本节的 TwoStageSpider 模板,完成:

  1. 实现 ListParser 和 DetailParser
  2. 采集至少 3 页列表(约 50 条数据)
  3. 保存为 JSON 文件
  4. 生成采集统计报告
任务3:优化去重策略

为你的采集器添加:

  1. URL 去重(持久化到文件)
  2. 内容去重(基于标题或正文)
  3. 增量采集(只采集新数据)
  4. 测试并统计去重效果

验收方式:在留言区提交:

  • 目标网站分析报告(含截图和选择器)
  • 完整采集器代码
  • 采集结果的 JSON 文件(前 5 条)
  • 统计报告和学习心得

🔮 下期预告

下一节《数据存储:CSV、JSON、SQLite 三剑客》,我们将学习:

  • 结构化数据的三种存储方式
  • pandas 读写 CSV 的最佳实践
  • JSON Lines 格式(适合增量追加)
  • SQLite 数据库的使用和查询
  • 选择存储方案的决策树

预习建议

复习 pandas 的基本用法,了解 SQLite 是什么。思考:什么场景用 CSV,什么场景用数据库?


💬 两段式采集是爬虫的核心模式!掌握它,通吃 90% 场景!

记住:好的架构让代码自己说话。职责分离、可复用、易维护------这是专业工程师的标准。

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
zzZ··*2 小时前
自动登录上海大学校园
python·网络协议·selenium
weisian1512 小时前
进阶篇-4-数学篇-3--深度解析AI中的向量概念:从生活到代码,一文吃透核心逻辑
人工智能·python·生活·向量
写代码的【黑咖啡】2 小时前
Python中的Msgpack:高效二进制序列化库
开发语言·python
MistaCloud2 小时前
Pytorch进阶训练技巧(二)之梯度层面的优化策略
人工智能·pytorch·python·深度学习
AIFQuant2 小时前
2026 全球股市实时行情数据 API 对比指南
python·websocket·金融·数据分析·restful
爱吃肉的鹏2 小时前
使用Flask在本地调用树莓派摄像头
人工智能·后端·python·flask·树莓派
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 主题切换实现
android·开发语言·javascript·python·flutter·游戏·django
深蓝电商API2 小时前
Selenium处理iframe嵌套页面实战
爬虫·python·selenium
一晌小贪欢2 小时前
Python 健壮性进阶:精通 TCP/IP 网络编程与 requirements.txt 的最佳实践
开发语言·网络·python·网络协议·tcp/ip·python基础·python小白