数据采集与融合技术第三次作业
数据采集与融合技术第三次作业
学号: 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 与下载统计


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


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

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

单线程与多线程最终都成功下载了 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 接口分析


(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 爬取成功的日志


MySQL 终端返回的数据表格

eastmoney_stock.jsonl 文件内容

eastmoney_stock.csv 文件内容

本次共抓取 约 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 分析


(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 成功的日志


MySQL 终端执行 SELECT * FROM boc_forex;

本次共抓取 44 条外汇牌价数据,覆盖美元、欧元、英镑、日元、港币等主要货币,字段包括货币名称、现汇买入价、现钞买入价、现汇卖出价、现钞卖出价和发布时间。
2. 实验心得体会
相比于东方财富,中国银行的网页是纯静态的,数据直接写在 HTML 源码里。通过分析 DOM 树,我使用 XPath 的轴定位与过滤(如 position()>1 跳过表头、contains(text(), "货币名称") 精确定位表格)精准提取了数据,感受到了 XPath 在结构清晰的 HTML 中的强大威力。
同时,通过这两个 Scrapy 作业自定义 Pipeline,我实现了同一份 Item 数据同时落库 MySQL 并导出为 CSV/JSONL,真正体会到了 Scrapy 框架解耦设计的强大——同一个爬虫可以通过配置不同的管道完成多种任务,采集、清洗、存储各司其职,扩展性非常好。

浙公网安备 33010602011771号