数据采集与融合技术第三次作业

数据采集与融合技术第三次作业

学号: 102402114
姓名: 谢慧彬
Gitee 仓库链接

本次作业共三个任务:① 中国气象网图片的单线程/多线程爬取;② 东方财富股票数据爬取(Scrapy + MySQL);③ 中国银行外汇牌价爬取(Scrapy + XPath + MySQL)。三个任务均使用 Python,数据最终落库 MySQL 并导出 CSV/JSONL。


一、作业①:气象网图片单线程与多线程爬取

本项目使用 Python 的 requests 库请求中国气象网,通过正则表达式提取网页源码中的图片 URL。严格遵照作业要求,通过学号动态计算限制——总页数 = 学号尾数 2 位(14),总下载图片数 = 学号尾数后 3 位(114),并分别实现单线程与多线程下载。

1. 作业代码与结果截图

(1)学号限制与图片 URL 采集(核心)

import re, os, time, requests
from concurrent.futures import ThreadPoolExecutor, as_completed

# 学号控制的数量限制(102402114)
STUDENT_ID = "102402114"
MAX_PAGES  = max(1, int(STUDENT_ID[-2:]))   # 尾数 2 位 = 14  → 总页数上限
MAX_IMAGES = max(1, int(STUDENT_ID[-3:]))   # 尾数后 3 位 = 114 → 总图片数上限
MAX_WORKERS = 8                              # 多线程并发数

# 用正则从 HTML 里提取图片地址(兼容 src / data-src / data-original / data-lazy-src)
IMG_PATTERN = re.compile(
    r'<img[^>]+?(?:src|data-src|data-original|data-lazy-src)\s*=\s*["\']([^"\']+)["\']',
    re.I
)

(2)单线程下载

def download_all_single(urls):
    """单线程:一张接一张阻塞下载"""
    ok = 0
    for i, url in enumerate(urls, 1):
        print(f"[{i}/{len(urls)}] {url}")
        try:
            resp = requests.get(url, headers=HEADERS, timeout=15, stream=True)
            if resp.status_code == 200:
                with open(os.path.join(SAVE_DIR, safe_filename(url)), "wb") as f:
                    for chunk in resp.iter_content(8192):   # 分块写入,防止大图占满内存
                        if chunk:
                            f.write(chunk)
                ok += 1
        except Exception as e:
            print(f"    下载失败:{e}")
    return ok

(3)多线程下载

def download_one(url):
    """线程池任务:下载单张图片,返回 (url, 是否成功, 失败原因)"""
    try:
        resp = requests.get(url, headers=HEADERS, timeout=15, stream=True)
        if resp.status_code != 200:
            return url, False, f"HTTP {resp.status_code}"
        with open(os.path.join(SAVE_DIR, safe_filename(url)), "wb") as f:
            for chunk in resp.iter_content(8192):
                if chunk:
                    f.write(chunk)
        return url, True, ""
    except Exception as e:
        return url, False, str(e)

def download_all_multi(urls):
    """多线程:线程池并发下载"""
    ok = 0
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
        futures = [pool.submit(download_one, url) for url in urls]
        for future in as_completed(futures):   # 谁先下完谁先返回,输出顺序被打乱
            url, success, err = future.result()
            if success:
                ok += 1
    return ok

(4)运行结果截图

单线程控制台输出 URL 与下载统计

wps_doc_2

wps_doc_3

多线程控制台输出 URL 与下载统计
wps_doc_0

wps_doc_1

images_single文件夹内容(单线程结果)

wps_doc_4

images 文件夹内容(多线程结果)
wps_doc_5

单线程与多线程最终都成功下载了 114 张图片(上限由学号尾数后 3 位决定),分别保存在 images_single/ 与 images/ 目录下。

2. 实验心得体会

在本次图片爬取实验中,我深刻体会到了多线程在 I/O 密集型任务中的优势。单线程是阻塞式的,必须等上一张图片存盘后才能请求下一张,网络等待时间被白白浪费;而使用 ThreadPoolExecutor 线程池后,程序并发发起多个网络请求,充分利用了带宽,总耗时大幅缩短。

同时我也观察到两个现象:一是多线程的打印输出是乱序的(谁先下完谁先打印),这正体现了并发任务"谁先完成谁返回"的特性;二是加速比并不会随线程数线性增长,它受限于目标网站的反爬策略与本地带宽,并非线程越多越好,合理设置 max_workers 才是关键。


二、作业②:东方财富股票数据爬取(Scrapy + MySQL)

1. 作业代码与结果截图

通过浏览器 F12 抓包分析发现,东方财富网页的 HTML 源码中并无直接数据,股票数据是通过 Ajax 请求底层 JSON 接口动态加载的。如 Response 面板所示,在返回的 JSON 中,data.diff 数组包含了核心行情,其中:

  • f12 对应股票代码
  • f14 对应股票名称
  • f2 对应最新报价

因此本项目使用 Scrapy 直接请求 push2.eastmoney.com 接口获取数据,并通过 Pipeline 序列化存入 MySQL,同时导出 CSV 与 JSONL。

(1)F12 接口分析

wps_doc_9

wps_doc_10

(2)核心代码:Spider 解析

# spiders/eastmoney.py —— 核心解析
import scrapy, json
from urllib.parse import urlencode
from stock_spider.items import EastmoneyStockItem

class EastmoneySpider(scrapy.Spider):
    name = "eastmoney"
    allowed_domains = ["push2.eastmoney.com"]

    def start_requests(self):
        base = {
            "po": "1", "np": "1", "fltt": "2", "invt": "2", "fid": "f3",
            "fs": "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23",   # 沪深A股
            "fields": "f2,f3,f4,f5,f7,f12,f14,f15,f16,f17,f18",
        }
        for pn in range(1, 4):           # 翻页爬取前 3 页
            params = dict(base)
            params["pn"] = str(pn)       # 页码
            params["pz"] = "100"         # 每页数量
            url = "https://push2.eastmoney.com/api/qt/clist/get?" + urlencode(params)
            yield scrapy.Request(url, callback=self.parse,
                                 headers={"Referer": "https://quote.eastmoney.com/"},
                                 dont_filter=True)

    def parse(self, response):
        data = json.loads(response.text)
        diff = (data.get("data") or {}).get("diff") or []
        for row in diff:
            item = EastmoneyStockItem()
            item["bStockNo"]    = row.get("f12")   # 股票代码
            item["bStockName"]  = row.get("f14")   # 股票名称
            item["latestPrice"] = row.get("f2")    # 最新价
            item["changePct"]   = row.get("f3")    # 涨跌幅
            item["changeAmt"]   = row.get("f4")    # 涨跌额
            item["volume"]      = row.get("f5")    # 成交量
            item["amplitude"]   = row.get("f7")    # 振幅
            item["highPrice"]   = row.get("f15")   # 最高价
            item["lowPrice"]    = row.get("f16")   # 最低价
            item["openPrice"]   = row.get("f17")   # 开盘价
            item["preClose"]    = row.get("f18")   # 昨收价
            yield item

(3)核心代码:Pipeline 入库 MySQL 并导出 CSV/JSONL

# pipelines.py —— 一条数据同时落库 MySQL 并导出 CSV / JSONL
import pymysql

class MySQLPipeline:
    def open_spider(self, spider):
        self.conn = pymysql.connect(host="localhost", port=3306, user="root",
                                    password="123456", database="homework3",
                                    charset="utf8mb4")
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        sql = """INSERT IGNORE INTO eastmoney_stock
        (bStockNo, bStockName, latestPrice, changePct, changeAmt,
         volume, amplitude, highPrice, lowPrice, openPrice, preClose)
        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"""
        self.cursor.execute(sql, (item["bStockNo"], item["bStockName"],
                                  item["latestPrice"], item["changePct"],
                                  item["changeAmt"], item["volume"],
                                  item["amplitude"], item["highPrice"],
                                  item["lowPrice"], item["openPrice"], item["preClose"]))
        self.conn.commit()
        return item
# CSVPipeline 与 JSONLinesPipeline 分别把同一条 Item 写成 eastmoney_stock.csv / .jsonl

(4)运行与存储结果截图

终端运行 python run.py 爬取成功的日志

wps_doc_6

wps_doc_4

MySQL 终端返回的数据表格

wps_doc_8

eastmoney_stock.jsonl 文件内容
wps_doc_5

eastmoney_stock.csv 文件内容
wps_doc_7

本次共抓取 约 300 条股票数据(3 页 × 每页 100 条),字段包含代码、名称、最新价、涨跌幅、涨跌额、成交量、振幅、最高/最低价、开盘价、昨收价。

2. 实验心得体会

在做东方财富爬虫时遇到了较大挑战。起初我试图用 XPath 直接解析网页,却怎么也抓不到数据。通过 F12 抓包排查后我才明白:现代网页大量使用 JavaScript 动态渲染,真正的数据来自后端 JSON API,而不是静态 HTML。转向直接请求 API 后,数据提取变得非常稳定。

这次踩坑让我明白了"先审查网页结构,再定技术路线"的重要性,不能盲目迷信某一种解析手段。此外,配置 Scrapy 环境时还踩了一个"Windows 中文路径导致找不到模块"的坑,最终通过编写 run.py 手动注入 sys.path 成功绕过,也提升了实际工程排错能力。


三、作业③:中国银行外汇牌价爬取(Scrapy + XPath + MySQL)

1. 作业代码与结果截图

本作业爬取中国银行外汇牌价网页。与东方财富不同,中行网页的数据是直接静态渲染在 HTML 中的。因此使用 Scrapy 结合 XPath 表达式直接解析 <table> 标签提取数据,并将结果存入 MySQL,同时导出 CSV 与 JSONL。

(1)网页结构 F12 分析

wps_doc_5

wps_doc_6

(2)核心代码:Spider XPath 解析

# spiders/boc.py —— 核心解析(XPath)
import scrapy
from forex_spider.items import ForexItem

class BocSpider(scrapy.Spider):
    name = "boc"
    allowed_domains = ["boc.cn"]
    start_urls = ["https://www.boc.cn/sourcedb/whpj/"]

    def parse(self, response):
        # 定位包含“货币名称”的表格,position()>1 跳过表头行
        rows = response.xpath(
            '//table[.//th[contains(text(), "货币名称")]]//tr[position()>1]')

        for tr in rows:
            tds = tr.xpath('./td')
            if len(tds) < 6:
                continue

            item = ForexItem()
            item["currency"] = tds[0].xpath('./text()').get(default="").strip()  # 货币名称
            item["TBP"]      = tds[1].xpath('./text()').get(default="").strip()  # 现汇买入价
            item["CBP"]      = tds[2].xpath('./text()').get(default="").strip()  # 现钞买入价
            item["TSP"]      = tds[3].xpath('./text()').get(default="").strip()  # 现汇卖出价
            item["CSP"]      = tds[4].xpath('./text()').get(default="").strip()  # 现钞卖出价
            item["Time"]     = (tds[6].xpath('./text()').get(default="").strip()
                                if len(tds) > 6 else "")                          # 发布时间
            yield item

(3)核心代码:Pipeline 入库 MySQL 并导出 CSV/JSONL

# pipelines.py —— 与作业②同理:MySQLForexPipeline 写入 boc_forex 表,
# CSVForexPipeline / JSONLinesForexPipeline 分别导出 boc_forex.csv / .jsonl
class MySQLForexPipeline:
    def process_item(self, item, spider):
        sql = """INSERT IGNORE INTO boc_forex
                 (currency, TBP, CBP, TSP, CSP, Time)
                 VALUES (%s, %s, %s, %s, %s, %s)"""
        self.cursor.execute(sql, (item["currency"], item["TBP"], item["CBP"],
                                  item["TSP"], item["CSP"], item["Time"]))
        self.conn.commit()
        return item

(4)运行与存储结果截图

终端运行 python run.py 成功的日志

wps_doc_2

wps_doc_3

MySQL 终端执行 SELECT * FROM boc_forex;

wps_doc_4

本次共抓取 44 条外汇牌价数据,覆盖美元、欧元、英镑、日元、港币等主要货币,字段包括货币名称、现汇买入价、现钞买入价、现汇卖出价、现钞卖出价和发布时间。

2. 实验心得体会

相比于东方财富,中国银行的网页是纯静态的,数据直接写在 HTML 源码里。通过分析 DOM 树,我使用 XPath 的轴定位与过滤(如 position()>1 跳过表头、contains(text(), "货币名称") 精确定位表格)精准提取了数据,感受到了 XPath 在结构清晰的 HTML 中的强大威力。

同时,通过这两个 Scrapy 作业自定义 Pipeline,我实现了同一份 Item 数据同时落库 MySQL 并导出为 CSV/JSONL,真正体会到了 Scrapy 框架解耦设计的强大——同一个爬虫可以通过配置不同的管道完成多种任务,采集、清洗、存储各司其职,扩展性非常好。

posted @ 2026-11-09 09:17  Xx__X  阅读(10)  评论(0)    收藏  举报