数据采集技术 - 第三次作业:Scrapy框架与数据库存储
作业①
1)、图片爬取实验
1. 实验描述
指定一个网站(以中国气象网为例),爬取该网站下的所有图片。
难点:需分别实现单线程和多线程两种方式,并控制总下载数量不超过学号后3位(130张)。
2. 核心代码
(1) 环境适配与SSL修复
在 MacOS 环境下,Python 的 SSL 模块与系统底层的 OpenSSL 库版本不兼容,会导致 HTTPS 请求失败。必须在代码头部注入 pyopenssl 库。
import urllib3
try:
import urllib3.contrib.pyopenssl
urllib3.contrib.pyopenssl.inject_into_urllib3()
except ImportError:
print("Warning: pyOpenSSL not installed")
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
(2) 学号限制与单线程下载
import os
import time
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
# ... (导入SSL修复代码同上)
STUDENT_ID = '102302130'
MAX_IMAGES = int(STUDENT_ID[-3:]) # 限制 130 张
TARGET_URL = '[http://www.weather.com.cn/](http://www.weather.com.cn/)'
SAVE_DIR = 'images'
def main():
if not os.path.exists(SAVE_DIR):
os.makedirs(SAVE_DIR)
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
})
try:
response = session.get(TARGET_URL, verify=False)
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
img_tags = soup.find_all('img')
except Exception as e:
print(f"请求失败: {e}")
return
current_count = 0
for index, img in enumerate(img_tags):
# 实时检查是否达到限制
if current_count >= MAX_IMAGES:
print(f"【限制触发】已达到学号限制的 {MAX_IMAGES} 张图片,停止下载。")
break
src = img.get('src')
if src:
full_url = urljoin(TARGET_URL, src)
if not full_url.startswith('http'): continue
ext = os.path.splitext(full_url)[-1]
if not ext or len(ext) > 5: ext = '.jpg'
file_name = f"image_{index}{ext}"
# 下载逻辑
try:
res = session.get(full_url, timeout=10, verify=False)
if res.status_code == 200:
with open(os.path.join(SAVE_DIR, file_name), 'wb') as f:
f.write(res.content)
print(f"[下载成功] {file_name}")
current_count += 1
except Exception as e:
print(f"[下载出错] {e}")
(3) 多线程并发逻辑
from concurrent.futures import ThreadPoolExecutor
# ... (前面的解析代码与单线程类似) ...
# 构建下载任务列表
download_list = []
for index, img in enumerate(img_tags):
src = img.get('src')
if src:
full_url = urljoin(TARGET_URL, src)
if not full_url.startswith('http'): continue
ext = os.path.splitext(full_url)[-1]
if not ext or len(ext) > 5: ext = '.jpg'
file_name = f"multi_img_{index}{ext}"
download_list.append((full_url, file_name))
# 关键步骤:根据学号限制截取任务列表
if len(download_list) > MAX_IMAGES:
print(f"发现图片数量超过限制,仅下载前 {MAX_IMAGES} 张")
download_list = download_list[:MAX_IMAGES]
# 开启 10 个线程并发下载
MAX_WORKERS = 10
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
executor.map(download_task, download_list)
3. 运行结果






2)、心得体会
效率对比:通过对比实验,我深刻体会到了多线程技术的优势。在处理图片下载这种 IO 密集型任务时,单线程的大部分时间都浪费在等待网络响应上;而多线程可以“以数量换时间”,显著提升了爬取效率。
环境适配:在 MacOS环境下开发时,遇到了严重的 SSL: TLSV1_ALERT_PROTOCOL_VERSION 报错。通过查阅资料,我学会了使用 pyopenssl 库进行底层 SSL 注入的方法,成功解决了跨平台开发中的环境冲突问题。
逻辑控制:将学号逻辑融入代码控制中,增强了程序的定制化能力,也培养了我在编程中对边界条件(Boundary Condition)的控制意识。
作业②
1)、股票数据爬取实验
1. 实验描述
使用 Scrapy + MySQL 技术路线,爬取东方财富网的股票相关信息(代码、名称、最新价、涨跌幅等),并存储至本地数据库。
2. 核心代码
(1) Item 定义 (items.py)
import scrapy
class StockItem(scrapy.Item):
stock_code = scrapy.Field() # 股票代码
stock_name = scrapy.Field() # 股票名称
latest_price = scrapy.Field() # 最新报价
change_percent = scrapy.Field() # 涨跌幅
change_amount = scrapy.Field() # 涨跌额
volume = scrapy.Field() # 成交量
turnover = scrapy.Field() # 成交额
amplitude = scrapy.Field() # 振幅
high = scrapy.Field() # 最高
low = scrapy.Field() # 最低
open_price = scrapy.Field() # 今开
prev_close = scrapy.Field() # 昨收
(2) 爬虫逻辑 (spiders/stock.py)
采用 API 逆向分析法,直接请求 JSON 接口并清洗数据。
import scrapy
import json
from homework2.items import StockItem
class StockSpider(scrapy.Spider):
name = 'stock'
allowed_domains = ['eastmoney.com']
# 真实的 JSON 数据接口
start_urls = [
'[http://82.push2.eastmoney.com/api/qt/clist/get?cb=jQuery1124&pn=1&pz=50&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&wbp2u=](http://82.push2.eastmoney.com/api/qt/clist/get?cb=jQuery1124&pn=1&pz=50&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&wbp2u=)|0|0|0|web&fid=f3&fs=m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048&fields=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18,f20,f21,f23,f24,f25,f22,f11,f62,f128,f136,f115,f152'
]
def parse(self, response):
# 清洗 jQuery 包裹字符,提取 JSON
data_text = response.text
start_idx = data_text.find('(')
end_idx = data_text.rfind(')')
if start_idx != -1 and end_idx != -1:
json_str = data_text[start_idx+1 : end_idx]
data_json = json.loads(json_str)
if 'data' in data_json and 'diff' in data_json['data']:
for stock in data_json['data']['diff']:
item = StockItem()
item['stock_code'] = stock.get('f12')
item['stock_name'] = stock.get('f14')
item['latest_price'] = self.safe_float(stock.get('f2'))
item['change_percent'] = str(stock.get('f3')) + '%'
item['change_amount'] = self.safe_float(stock.get('f4'))
item['volume'] = str(stock.get('f5'))
item['turnover'] = str(stock.get('f6'))
item['amplitude'] = str(stock.get('f7')) + '%'
item['high'] = self.safe_float(stock.get('f15'))
item['low'] = self.safe_float(stock.get('f16'))
item['open_price'] = self.safe_float(stock.get('f17'))
item['prev_close'] = self.safe_float(stock.get('f18'))
yield item
def safe_float(self, value):
try:
return float(value)
except (ValueError, TypeError):
return 0.0
(3) 数据库存储 (pipelines.py)
import pymysql
class MySQLPipeline:
def __init__(self):
self.host = 'localhost'
self.port = 3306
self.user = 'root'
self.password = 'LJ100328'
self.db = 'spider_homework'
def open_spider(self, spider):
self.conn = pymysql.connect(
host=self.host, port=self.port, user=self.user,
password=self.password, database=self.db, charset='utf8mb4'
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = """
INSERT INTO stock_data (
stock_code, stock_name, latest_price, change_percent,
change_amount, volume, turnover, amplitude,
high, low, open_price, prev_close
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
params = (
item.get('stock_code'), item.get('stock_name'), item.get('latest_price'),
item.get('change_percent'), item.get('change_amount'), item.get('volume'),
item.get('turnover'), item.get('amplitude'), item.get('high'),
item.get('low'), item.get('open_price'), item.get('prev_close')
)
try:
self.cursor.execute(sql, params)
self.conn.commit()
except Exception as e:
self.conn.rollback()
return item
3. 运行结果



2)、心得体会
动态网页处理:东方财富网的数据是通过 Ajax 动态加载的,直接使用 XPath 无法提取。通过分析浏览器 Network 请求,我找到了真实的 JSON 数据接口。这让我明白了爬虫不仅是解析 HTML,更重要的是分析数据源(Source Analysis)。
Scrapy 架构:深入理解了 Scrapy 的模块化设计:Spider 负责抓取,Item 定义结构,Pipeline 负责存储。这种分层架构使得代码逻辑清晰,维护性远超单脚本爬虫。
数据库连接:在使用 DBeaver 连接 MySQL 8.0 时,遇到了 Public Key Retrieval is not allowed 错误。通过修改驱动属性 allowPublicKeyRetrieval=true,我成功解决了这一安全策略限制,积累了数据库配置经验。
作业③
1)、外汇牌价爬取实验
1. 实验描述
爬取中国银行外汇牌价(货币名称、现汇/现钞买入卖出价、发布时间),使用 XPath 技术解析数据并存储至 MySQL。
2. 核心代码
(1) 数据库建表 (SQL)
CREATE TABLE IF NOT EXISTS forex_data (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '序号',
currency VARCHAR(50) COMMENT '货币名称',
tbp DECIMAL(10, 2) COMMENT '现汇买入价',
cbp DECIMAL(10, 2) COMMENT '现钞买入价',
tsp DECIMAL(10, 2) COMMENT '现汇卖出价',
csp DECIMAL(10, 2) COMMENT '现钞卖出价',
pub_time DATETIME COMMENT '发布时间',
crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '爬取时间'
) COMMENT '外汇牌价表';
(2) XPath 解析逻辑 (spiders/boc.py)
import scrapy
from homework3.items import ForexItem
class BocSpider(scrapy.Spider):
name = 'boc'
allowed_domains = ['boc.cn']
start_urls = ['[https://www.boc.cn/sourcedb/whpj/](https://www.boc.cn/sourcedb/whpj/)']
def parse(self, response):
# 使用 XPath 定位表格中的所有行 (排除表头)
rows = response.xpath('//div[@class="publish"]/div/table/tr')
for row in rows[1:]:
item = ForexItem()
# 相对路径提取单元格数据
# 货币名称 (第1列)
item['currency'] = row.xpath('./td[1]/text()').get()
# 现汇买入价 (第2列)
item['tbp'] = self.safe_float(row.xpath('./td[2]/text()').get())
# 现钞买入价 (第3列)
item['cbp'] = self.safe_float(row.xpath('./td[3]/text()').get())
# 现汇卖出价 (第4列)
item['tsp'] = self.safe_float(row.xpath('./td[4]/text()').get())
# 现钞卖出价 (第5列)
item['csp'] = self.safe_float(row.xpath('./td[5]/text()').get())
# 发布时间 (第7列)
item['pub_time'] = row.xpath('./td[7]/text()').get()
if item['currency']:
yield item
def safe_float(self, value):
try:
return float(value)
except (ValueError, TypeError):
return 0.0
3. 运行结果


2)、心得体会
XPath 实践:相比于作业②的 JSON 解析,作业③回归了经典的静态网页爬取。通过使用 //tr 定位行容器,再结合 ./td 相对路径提取单元格数据,我熟练掌握了 DOM 树的遍历与节点定位技巧。
数据清洗:网页上的外汇数据可能存在空值或特殊字符(如“-”),直接存入数据库会报错。我在代码中编写了 safe_float 辅助函数进行预处理,这让我体会到了数据清洗在数据采集流程中的重要性。
全流程贯通:本次作业完整贯穿了“数据库设计 -> 爬虫项目构建 -> 数据采集 -> 持久化存储”的全流程,极大地提升了我的数据采集工程能力。

浙公网安备 33010602011771号