第三次作业:Scrapy 框架数据序列化与存储实践
第三次作业:Scrapy 框架数据序列化与存储实践
本次作业通过 Scrapy 框架结合 XPath、MySQL 数据库,完成了网页图片的多线程爬取以及结构化数据的提取与持久化存储,深入实践了 Item 与 Pipeline 的序列化处理机制。
作业①
1)、爬取气象网图片实验
实验说明:
本次实验指定中国气象网为目标网站,爬取网页中的所有图片。根据要求,我在爬虫中设置了 max_pages(限制总页数为13页)和 max_images(限制总图片数为113张)的计数器。程序在控制台输出下载的图片 URL,并将图片以二进制流的形式保存到本地的 images 子文件夹中。
同时,结合课堂知识,我对比了单线程与多线程的差异,最终采用了 threading.Thread 多线程的方式,将每个图片的下载任务放入子线程中执行,避免了因网络波动导致单张图片下载阻塞主线程的问题。
相应代码:
imageSpider.py
import scrapy
import os
import urllib.request
import threading
class ImagespiderSpider(scrapy.Spider):
name = "imageSpider"
allowed_domains = ["weather.com.cn"]
start_urls = ["http://www.weather.com.cn/weather/101280601.shtml"]
max_pages = 13
max_images = 113
count = 0
current_page = 1
headers = {
"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 6.0 x64; en-US; rv:1.9pre) Gecko/2008072421 Minefield/3.0.2pre"
}
def parse(self, response):
if not os.path.exists("images"):
os.makedirs("images")
img_srcs = response.xpath("//img/@src").extract()
for src in img_srcs:
if self.count >= self.max_images:
break
url = response.urljoin(src)
print("下载URL:", url)
self.count += 1
T = threading.Thread(target=self.download, args=(url, self.count))
T.setDaemon(False)
T.start()
if self.current_page < self.max_pages and self.count < self.max_images:
self.current_page += 1
print(f"准备爬取第 {self.current_page} 页")
yield scrapy.Request(url=self.start_urls[0], callback=self.parse, dont_filter=True)
def download(self, url, count):
try:
ext = url[len(url)-4:] if url[len(url)-4] == "." else ".jpg"
req = urllib.request.Request(url, headers=self.headers)
data = urllib.request.urlopen(req, timeout=100).read()
fobj = open("images\\" + str(count) + ext, "wb")
fobj.write(data)
fobj.close()
print("downloaded " + str(count) + ext)
except Exception as err:
print("下载失败:", err)
settings.py
BOT_NAME = "weather_spider"
SPIDER_MODULES = ["weather_spider.spiders"]
NEWSPIDER_MODULE = "weather_spider.spiders"
ROBOTSTXT_OBEY = False
运行结果截图:
控制台输出 URL 与下载提示:


下载的图片存储在 images 文件夹:


Gitee文件夹链接:https://gitee.com/lilyluna/2026shujucaiji/tree/master/3/1
2)、心得体会
通过本次实验,我深刻理解了多线程在实际爬虫应用中的重要性。引入多线程后,下载任务互不干扰,大大提高了爬取的效率和程序的可靠性。同时,通过计数器限制爬取数量,也让我掌握了如何给爬虫设置合理的“刹车”,避免对目标网站造成过大压力。
作业②
1)、爬取股票信息实验
实验说明:
本实验采用 Scrapy + Selenium + XPath + MySQL 技术路线。首先在数据库中创建了 StockDB 数据库及包含各项股票指标(股票代码、名称、最新报价、涨跌幅等)的 stocks 表。
由于东方财富网的数据是动态渲染的,我编写了 SeleniumDownloaderMiddleware 中间件,让 Scrapy 能够获取到渲染后的完整 HTML。随后在 Spider 中通过精准的 XPath 提取数据,最后利用 StockPipeline 实现了数据的异步入库。
F12调试:

数据库创建截图:
DBeaver建库

DBeaver建表

代码截图:
items.py
import scrapy
class StockItem(scrapy.Item):
bStockNo = scrapy.Field()
bStockName = scrapy.Field()
bLatestPrice = scrapy.Field()
bChangePercent = scrapy.Field()
bChangeAmount = scrapy.Field()
bVolume = scrapy.Field()
bAmplitude = scrapy.Field()
bHigh = scrapy.Field()
bLow = scrapy.Field()
bOpen = scrapy.Field()
bPreClose = scrapy.Field()
pipelines.py
import pymysql
class StockPipeline(object):
def open_spider(self, spider):
try:
self.con = pymysql.connect(
host="127.0.0.1",
port=3306,
user="root",
passwd="......",
db="StockDB",
charset="utf8"
)
self.cursor = self.con.cursor()
self.opened = True
print("数据库连接成功")
except Exception as err:
print("数据库连接失败:", err)
self.opened = False
def close_spider(self, spider):
if self.opened:
self.con.commit()
self.con.close()
print("数据库连接已关闭")
def process_item(self, item, spider):
try:
if self.opened:
sql = """
INSERT INTO stocks
(bStockNo, bStockName, bLatestPrice, bChangePercent,
bChangeAmount, bVolume, bAmplitude, bHigh, bLow, bOpen, bPreClose)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
self.cursor.execute(sql, (
item["bStockNo"], item["bStockName"], item["bLatestPrice"],
item["bChangePercent"], item["bChangeAmount"], item["bVolume"],
item["bAmplitude"], item["bHigh"], item["bLow"], item["bOpen"], item["bPreClose"]
))
print(f"成功插入数据: {item['bStockName']} ({item['bStockNo']})")
except Exception as err:
print("插入数据失败:", err)
return item
middlewares.py
import logging
logging.getLogger('selenium').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
from scrapy import signals
from itemadapter import ItemAdapter
from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
class SeleniumDownloaderMiddleware:
def __init__(self):
print("====== 正在初始化 Selenium 浏览器 ======")
chrome_options = Options()
chrome_options.add_argument('--headless=new')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
self.driver = webdriver.Chrome(options=chrome_options)
def process_request(self, request, spider):
print(f"====== Selenium 正在访问: {request.url} ======")
self.driver.get(request.url)
time.sleep(5)
body = self.driver.page_source
print(f"====== 获取到页面源码,长度: {len(body)} ======")
return HtmlResponse(
url=self.driver.current_url,
body=body,
encoding='utf-8',
request=request
)
stockSpider.py
import scrapy
from stock_spider.items import StockItem
class StockspiderSpider(scrapy.Spider):
name = "stockSpider"
allowed_domains = ["eastmoney.com"]
start_urls = ["https://quote.eastmoney.com/center/gridlist.html#hs_a_board"]
def parse(self, response):
rows = response.xpath("//table[contains(@class, 'default-table')]//tbody/tr")
if not rows:
rows = response.xpath("//tbody/tr")
print(f"\n====== [数据提取] 本次共找到 {len(rows)} 行数据 ======\n")
for row in rows:
item = StockItem()
stock_no = row.xpath("string(./td[2])").get()
if not stock_no:
continue
stock_no = stock_no.strip()
if len(stock_no) != 6 or not stock_no.isdigit():
continue
item["bStockNo"] = stock_no
name = row.xpath("./td[3]//a/text()").get()
if not name:
name = row.xpath("string(./td[3])").get()
item["bStockName"] = name.strip() if name else "-"
item["bLatestPrice"] = row.xpath("string(./td[7])").get().strip()
item["bChangePercent"] = row.xpath("string(./td[8])").get().strip()
item["bChangeAmount"] = row.xpath("string(./td[9])").get().strip()
item["bVolume"] = row.xpath("string(./td[10])").get().strip()
item["bAmplitude"] = row.xpath("string(./td[12])").get().strip()
item["bHigh"] = row.xpath("string(./td[13])").get().strip()
item["bLow"] = row.xpath("string(./td[14])").get().strip()
item["bOpen"] = row.xpath("string(./td[15])").get().strip()
item["bPreClose"] = row.xpath("string(./td[16])").get().strip()
if item["bLatestPrice"] and item["bLatestPrice"] != "-":
yield item
```
`settings.py`
```python
BOT_NAME = "stock_spider"
SPIDER_MODULES = ["stock_spider.spiders"]
NEWSPIDER_MODULE = "stock_spider.spiders"
ADDONS = {}
ITEM_PIPELINES = {
'stock_spider.pipelines.StockPipeline': 300,
}
ROBOTSTXT_OBEY = False
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
}
DOWNLOADER_MIDDLEWARES = {
'stock_spider.middlewares.SeleniumDownloaderMiddleware': 543,
}
运行结果截图:
控制台输出“成功插入数据”:



DBeaver 中查询 SELECT * FROM stocks; 的数据:

Gitee文件夹链接:https://gitee.com/lilyluna/2026shujucaiji/tree/master/3/2
2)、心得体会
本次实验让我真正体会到了 Scrapy “5+2” 结构中 Pipeline 的作用。Scrapy 将数据爬取与数据存储完全解耦,Spider 爬取到数据后直接 yield 抛出;而 Pipeline 则像一个流水线工人,专门负责异步接收 Item 并写入数据库,非常高效。
同时,面对 JS 动态渲染的网页,我结合了学习的 Selenium 知识。在编写 XPath 时,我发现页面中存在大量的隐藏干扰列(如“股吧”、“资金流”等),导致了数据错位。我通过 F12 仔细分析 DOM 树,精准定位 td 索引,并结合 isDigit() 过滤脏数据,最终提取到了完美的数据。
作业③
1)、爬取外汇数据实验
实验说明:
本实验目标为中国银行外汇牌价页面。与作业②不同,该页面是纯静态的 HTML 表格。为了防止中文乱码,我运用了学习中提到的 UnicodeDammit 库对 response.body 进行编码识别与转换。
利用 scrapy.Selector 构建选择器后,直接使用 response.xpath('//table//tr') 并利用 position()>1 过滤掉表头,精确提取了表格每一行的 6 个字段(货币、现汇买入价、现钞买入价、现汇卖出价、现钞卖出价、时间),并通过 Pipeline 顺利写入 MySQL 的 ForexDB 数据库中。
F12调试:

DBeaver建库建表

代码截图:
items.py
import scrapy
class ForexItem(scrapy.Item):
Currency = scrapy.Field()
TBP = scrapy.Field()
CBP = scrapy.Field()
TSP = scrapy.Field()
CSP = scrapy.Field()
Time = scrapy.Field()
pipelines.py
import pymysql
class ForexPipeline(object):
def open_spider(self, spider):
try:
self.con = pymysql.connect(host="127.0.0.1", port=3306, user="root",
passwd="......", db="ForexDB", charset="utf8")
self.cursor = self.con.cursor()
self.opened = True
except Exception as err:
print("数据库连接失败:", err)
self.opened = False
def close_spider(self, spider):
if self.opened:
self.con.commit()
self.con.close()
def process_item(self, item, spider):
try:
if self.opened:
sql = """INSERT INTO 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"]
))
print(f"成功插入: {item['Currency']} | 现汇买入价: {item['TBP']} | 时间: {item['Time']}")
except Exception as err:
print("插入失败:", err)
return item
forexSpider.py
import scrapy
from forex_spider.items import ForexItem
from bs4 import UnicodeDammit
class ForexpiderSpider(scrapy.Spider):
name = "forexSpider"
allowed_domains = ["boc.cn"]
start_urls = ["https://www.boc.cn/sourcedb/whpj/"]
def parse(self, response):
try:
dammit = UnicodeDammit(response.body, ["utf-8", "gbk"])
data = dammit.unicode_markup
selector = scrapy.Selector(text=data)
rows = selector.xpath('//div[contains(@class,"BOC_main")]//table//tr[position()>1]')
for row in rows:
item = ForexItem()
item["Currency"] = row.xpath("./td[1]/text()").get(default="").strip()
item["TBP"] = row.xpath("./td[2]/text()").get(default="").strip()
item["CBP"] = row.xpath("./td[3]/text()").get(default="").strip()
item["TSP"] = row.xpath("./td[4]/text()").get(default="").strip()
item["CSP"] = row.xpath("./td[5]/text()").get(default="").strip()
item["Time"] = row.xpath("./td[7]/text()").get(default="").strip()
if item["Currency"]:
yield item
except Exception as err:
print("解析出错:", err)
settings.py
BOT_NAME = "forex_spider"
SPIDER_MODULES = ["forex_spider.spiders"]
NEWSPIDER_MODULE = "forex_spider.spiders"
ITEM_PIPELINES = {
'forex_spider.pipelines.ForexPipeline': 300,
}
ROBOTSTXT_OBEY = False
DEFAULT_REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
}
运行结果截图:
控制台输出“成功插入”的日志:



DBeaver 中查询 SELECT * FROM forex; 的数据:

Gitee文件夹链接:https://gitee.com/lilyluna/2026shujucaiji/tree/master/3/3
2)、心得体会
本次作业是对整个 Scrapy 框架使用的综合检验。在处理外汇数据时,XPath 的灵活性得到了充分体现,通过 position()>1 可以轻松跳过表头。通过对比作业②和作业③,我明白了在爬虫开发中,根据目标网站的类型灵活选择解析策略是十分重要的。
同时,我也注意到部分小众币种因银行未公布现汇买入价,导致数据库中对应字段为空,而这其实是符合实际网页情况的,让我明白了爬虫不仅要能抓取数据,更要保证数据是真实与正确的。整个作业流程从建库建表,到定义 Item,再到编写 Spider 和 Pipeline,让我建立了一套完整的数据爬取思维。

浙公网安备 33010602011771号