数据采集实践第三次作业—102302131陈宇新
数据采集实践第三次作业—102302131陈宇新
代码路径:https://gitee.com/chenyuxin0328/data-collection/tree/master/作业3
作业1
要求:指定一个网站,爬取这个网站中的所有的所有图片,例如中国气象网(http://www.weather.com.cn)。实现单线程和多线程的方式爬取。
完整代码
import requests
from bs4 import BeautifulSoup
import os
import time
from urllib.parse import urljoin, urlparse
from concurrent.futures import ThreadPoolExecutor
# --- 配置信息:这里是目标和参数 ---
TARGET_URL = 'http://www.weather.com.cn' # 我们的目标网站!
SAVE_DIR = 'my_crawler_images' # 图片要保存在这里
MAX_WORKERS = 10 # 设定最大并发线程数,看看能多快!
HEADERS = {
# 伪装成浏览器,防止被网站拒绝
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
# -----------------
def create_directory():
"""开始前,先创建图片保存目录,防止找不到家!"""
if not os.path.exists(SAVE_DIR):
os.makedirs(SAVE_DIR)
print(f"【准备工作】创建了图片存放目录: {SAVE_DIR}")
def get_image_urls(target_url):
"""
第一步:访问目标网页,找到所有图片的地址!
"""
print(f"\n--- 1. 抓取网页内容,解析图片链接 ({target_url}) ---")
urls = set()
try:
# 请求网页内容
response = requests.get(target_url, headers=HEADERS, timeout=10)
response.raise_for_status()
response.encoding = response.apparent_encoding # 尝试解决中文乱码问题
soup = BeautifulSoup(response.text, 'html.parser')
# 遍历所有 <img> 标签,寻找 src 或 data-src
for img in soup.find_all('img'):
img_src = img.get('src') or img.get('data-src')
if img_src:
# 相对路径转成完整的绝对路径,Scrapy里是urljoin
absolute_url = urljoin(target_url, img_src)
# 简单检查,确保它是图片格式
if absolute_url.startswith('http') and any(
ext in absolute_url.lower() for ext in ['.jpg', '.jpeg', '.png', '.gif']):
urls.add(absolute_url)
except requests.exceptions.RequestException as e:
print(f"【错误】访问 {target_url} 失败了: {e}")
print(f"【结果】找到了 {len(urls)} 个独特的图片链接,不错!")
return list(urls)
def download_image(img_url):
"""
单个下载任务:把图片从网上取下来,存到本地。
"""
# 告诉大家我们正在下载哪个文件
print(f"→ 正在下载: {img_url}")
try:
response = requests.get(img_url, headers=HEADERS, stream=True, timeout=10)
response.raise_for_status()
# 聪明地从 URL 里提取文件名
url_path = urlparse(img_url).path
filename = os.path.basename(url_path)
# 万一URL里没有文件名,就自己生成一个独一无二的名字!
if not filename or '.' not in filename:
filename = f"image_{time.time()}_{os.urandom(4).hex()}.png"
save_path = os.path.join(SAVE_DIR, filename)
# 写入文件,使用 stream 模式边下载边写入,节省内存
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
except requests.exceptions.RequestException:
# 如果下载失败,就跳过这个,继续下一个
# print(f"【跳过】下载 {img_url} 失败...")
pass
# --- 3. 单线程实现 (串行):一步一个脚印,慢慢爬 ---
def single_thread_crawler(urls):
print("\n--- 2. [单线程模式] 开始串行下载任务 ---")
start_time = time.time()
for url in urls:
download_image(url)
end_time = time.time()
# 重点:记录耗时,方便跟多线程做比较!
print(f"\n【单线程总结】爬取耗时: {end_time - start_time:.2f} 秒 (好慢呀...)")
# --- 4. 多线程实现 (并行):大家一起上,效率翻倍! ---
def multi_thread_crawler(urls):
print(f"\n--- 3. [多线程模式] 开始并行下载任务 (并发数: {MAX_WORKERS}) ---")
start_time = time.time()
# ThreadPoolExecutor:这就是实现并发的关键!
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
# 使用 map 函数,让线程池里的工人同时去下载图片
executor.map(download_image, urls)
end_time = time.time()
# 重点:记录耗时,证明多线程的优越性!
print(f"\n【多线程总结】爬取耗时: {end_time - start_time:.2f} 秒 (哇!快多了!)")
# --- 主程序执行:开始我们的实验! ---
if __name__ == '__main__':
create_directory()
# 步骤 1: 获取所有图片 URL
all_urls = get_image_urls(TARGET_URL)
if not all_urls:
print("【失败】没有找到任何链接,退出程序。")
else:
# 步骤 2: 单线程性能测试 (只测试前20个,节约时间)
print("\n--- [性能对比实验开始] ---")
single_thread_crawler(all_urls[:20])
# 步骤 3: 多线程性能测试 (这次爬全部,展示最终效率)
multi_thread_crawler(all_urls)
print("\n--- [性能对比实验结束] ---")
关键代码解释
1)from concurrent.futures import ThreadPoolExecutor:导入 ThreadPoolExecutor,这是实现“多线程并发”的关键,让我们能同时下载多个图片,大大加快速度!
2)TARGET_URL = 'http://www.weather.com.cn':定义了我们的“爬取目标”,程序会先访问这个地址去寻找图片链接。
3)HEADERS:模拟浏览器的身份信息,这是“反爬虫的第一道防线”。加上它,网站就不容易发现我们是爬虫,避免被拒绝访问。
4)create_directory():在下载图片前,先运行这个函数“创建本地文件夹”(images 目录),确保下载的图片有地方存放。
5)requests.get(target_url, headers=HEADERS, ...):发送主要的 GET 请求去获取网页的 HTML 内容。
6)soup = BeautifulSoup(response.text, 'html.parser'):用内置解析器把获取到的 HTML 文本整理成 BeautifulSoup 对象,方便我们用 Python 语句来“定位标签”。
7)for img in soup.find_all('img'):这是寻找图片链接的语句,它会“找出网页上所有的 标签”。
8)img_src = img.get('src') or img.get('data-src'):从 标签中提取图片的 URL。因为有些网站会把图片地址放在 src,有些放在 data-src,所以我们都得检查一下。
9)absolute_url = urljoin(target_url, img_src):将图片链接从相对路径(比如 /logo.png)“转换成完整的绝对 URL”,确保我们可以直接访问。
10)with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:启动线程池,设定“最大工人数量(线程数)”,开始准备并行工作。
11)executor.map(download_image, all_urls):这是加速的核心!将 download_image 这个下载任务,“同时分配给所有线程”去执行,实现多线程下载,效率远高于排队(单线程)。
12)requests.get(img_url, ..., stream=True):下载单个图片时开启 stream=True,让程序可以“边下载边写入文件”,避免将大文件一次性加载到内存中。
13)for chunk in response.iter_content(...):实现流式写入,将下载的数据“分成小块(Chunk)”,然后逐块写入本地文件,既省内存又高效。
14)os.path.basename(url_path):从复杂的图片 URL 中“提取出简洁的文件名”,方便我们保存。
运行结果
【准备工作】创建了图片存放目录: my_crawler_images
--- 1. 抓取网页内容,解析图片链接 (http://www.weather.com.cn) ---
【结果】找到了 43 个独特的图片链接,不错!
--- [性能对比实验开始] ---
--- 2. [单线程模式] 开始串行下载任务 ---
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/10/24/535D7D0CD6C8B8E511C272C2EA656387.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/18/A3BCF14CFDAD5F2E97D403F458416BFE.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/20/8D6BE3579BEFFAAEA187B1068D28978F.jpg
→ 正在下载: https://i.i8tq.com/index/search.png
→ 正在下载: http://i.weather.com.cn/images/cn/index/2025/11/21/78445BF216F9C4AB622AB9074BE4FF37.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/26/20210326150416454FB344B92EC8BD897FA50DF6AD15E8.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2019/qxfw_01.png
→ 正在下载: https://i.i8tq.com/mysky/sksd.png
→ 正在下载: https://i.i8tq.com/video/index_spring.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/06/1FAD9AA63152B70E568FC36914F8966E.jpg
→ 正在下载: http://www.weather.com.cn/2025/11/09/CA392075C79C24AE996ED79B15B2B729.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2017/selectCityBtnCur.png
→ 正在下载: https://i.i8tq.com/adImg/zgtqsc.png
→ 正在下载: https://i.i8tq.com/mysky/skwind.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/l/sevp_nsmc_wxbl_fy4b_etcc_achn_lno_py_20251125084500000.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/19/20210319155915527B7F1E763A8D9C65581C8A4873D2B0.png
→ 正在下载: https://i.weather.com.cn/images/cn/video/lssj/2025/11/25/6E0C1D56F571B91B55F43C95AC250E98_m.png
→ 正在下载: https://i.i8tq.com/jieri/fbh_2021.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_nmc_stfc_sfer_er24_achn_l88_p9_20251125010002400.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/D66A4D956BC744F4AC95C0AC199C0F20.jpg
【单线程总结】爬取耗时: 47.60 秒 (好慢呀...)
--- 3. [多线程模式] 开始并行下载任务 (并发数: 10) ---
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/10/24/535D7D0CD6C8B8E511C272C2EA656387.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/18/A3BCF14CFDAD5F2E97D403F458416BFE.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/20/8D6BE3579BEFFAAEA187B1068D28978F.jpg→ 正在下载: https://i.i8tq.com/index/search.png
→ 正在下载: http://i.weather.com.cn/images/cn/index/2025/11/21/78445BF216F9C4AB622AB9074BE4FF37.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/26/20210326150416454FB344B92EC8BD897FA50DF6AD15E8.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2019/qxfw_01.png
→ 正在下载: https://i.i8tq.com/mysky/sksd.png
→ 正在下载: https://i.i8tq.com/video/index_spring.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/06/1FAD9AA63152B70E568FC36914F8966E.jpg
→ 正在下载: http://www.weather.com.cn/2025/11/09/CA392075C79C24AE996ED79B15B2B729.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2017/selectCityBtnCur.png
→ 正在下载: https://i.i8tq.com/adImg/zgtqsc.png
→ 正在下载: https://i.i8tq.com/mysky/skwind.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/l/sevp_nsmc_wxbl_fy4b_etcc_achn_lno_py_20251125084500000.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/19/20210319155915527B7F1E763A8D9C65581C8A4873D2B0.png
→ 正在下载: https://i.weather.com.cn/images/cn/video/lssj/2025/11/25/6E0C1D56F571B91B55F43C95AC250E98_m.png
→ 正在下载: https://i.i8tq.com/jieri/fbh_2021.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_nmc_stfc_sfer_er24_achn_l88_p9_20251125010002400.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/D66A4D956BC744F4AC95C0AC199C0F20.jpg
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/z_rada_c_babj_20251125092451_p_dor_achn_cref_20251125_091800.png
→ 正在下载: https://i.i8tq.com/adImg/pc_index.png
→ 正在下载: https://i.i8tq.com/shengtai/tzh202501.jpg
→ 正在下载: https://i.i8tq.com/weather2020/search/rbAd.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/sjztj/2020/07/20/20200720142523B5F07D41B4AC4336613DA93425B35B5E_xm.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/19FB810CD191A8478360C8E3F2485BF2.jpg
→ 正在下载: https://i.i8tq.com/index/img-qrcode-3.png
→ 正在下载: https://i.tq121.com.cn/i/weather2015/index/loading.gif
→ 正在下载: http://i.tq121.com.cn/i/weather2017/cx_new.png
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2019/10/28/20191028144048D58023A73C43EC6EEB61610B0AB0AD74_xm.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/24/CC78B7E96DCAA9F7E5AC32105CD1ADD7.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/BF57450309CE9F67263B9E8DC23FCF9E.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/05/14/20210514192548638D53A47159C5D97689A921C03B6546.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/index/2024/03/18/202403181336470A7F03D1DA389C5AC74DDB7AC0745065.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/14/C19340667E7B19F42AD27995DE6E1DFB.jpg
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_ncc_zki_shlsk_eci_achn_l88_pb_20251125000000000.png
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/05/FF1551323C2D0F90FD2D3BB88E7186D0.jpg
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_nsmc_wxbl_fy4b_etcc_achn_lno_py_20251125084500000.jpg
→ 正在下载: https://i.weather.com.cn/images/cn/video/lssj/2025/11/25/FE0476C4C7D5E1F14E8D08400DB3B04E_m.png
→ 正在下载: https://i.i8tq.com/index/logo2.png
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/18/602BF174707604395A015608FF740F08.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/14/BAF29F277AA2CAA1AF7FF12F9D92128B.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/science/2020/07/28/202007281001285C97A5D6CAD3BC4DD74F98B5EA5187BF.jpg

实验心得
本次实验成功对比了图片下载中单线程和多线程的效率。
核心心得是:图片下载是典型的 I/O 密集型任务。通过引入 ThreadPoolExecutor,程序能够高效利用等待网络响应的时间,实现任务并发,总耗时比串行模式显著缩短。
主要挑战在于确保 URL 路径的准确转换,以及在多线程环境下,需要更健壮地处理因网络超时导致的请求失败和异常
作业2

完整代码
项目结构
stock_scraper/ <-- Scrapy 项目根目录 (旧项目)
├── stock_scraper/
│ ├── __init__.py
│ ├── items.py <-- 包含 StockItem 和 CurrencyItem (如果为作业三新增)
│ ├── middlewares.py
│ ├── pipelines.py <-- 包含 MysqlPipeline 逻辑 (已修改)
│ ├── settings.py <-- 包含 MySQL 配置 (已修改)
│ └── spiders/
│ ├── __init__.py
│ ├── boc_currency.py <-- 外汇爬虫文件
│ └── sina_stock.py <-- 股票爬虫文件 (假设原始爬虫名为这个)
│
└── scrapy.cfg
代码
items.py
定义了 StockItem 类,明确了爬虫需要抓取的 所有股票字段(如:股票代码、名称、最新价、涨跌幅等),是数据的容器和规范。
# 文件: currency_scraper/currency_scraper/items.py
import scrapy
class CurrencyItem(scrapy.Item):
# 货币名称,用作唯一标识
currency = scrapy.Field() # Currency
# 现汇买入价 (TBP)
tbp = scrapy.Field() # TBP (Telegraphic Transfer Buying Price)
# 现钞买入价 (CBP)
cbp = scrapy.Field() # CBP (Cash Buying Price)
# 现汇卖出价 (TSP)
tsp = scrapy.Field() # TSP (Telegraphic Transfer Selling Price)
# 现钞卖出价 (CSP)
csp = scrapy.Field() # CSP (Cash Selling Price)
# 发布时间
time = scrapy.Field() # Time
pipelines.py
接收 StockItem,进行最终的数据清洗和格式转换(如将 None 转化为 NULL),并执行 MySQL 数据库的写入(包括建表和 INSERT/UPDATE 操作)
# Scrapy settings for currency_scraper project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# https://docs.scrapy.org/en/latest/topics/settings.html
# https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
BOT_NAME = "currency_scraper"
SPIDER_MODULES = ["currency_scraper.spiders"]
NEWSPIDER_MODULE = "currency_scraper.spiders"
ADDONS = {}
# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = "currency_scraper (+http://www.yourdomain.com)"
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
# Concurrency and throttling settings
#CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 1
DOWNLOAD_DELAY = 1
# Disable cookies (enabled by default)
#COOKIES_ENABLED = False
# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False
# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
# "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
# "Accept-Language": "en",
#}
# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
# "currency_scraper.middlewares.CurrencyScraperSpiderMiddleware": 543,
#}
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
# "currency_scraper.middlewares.CurrencyScraperDownloaderMiddleware": 543,
#}
# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
# "scrapy.extensions.telnet.TelnetConsole": None,
#}
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
#ITEM_PIPELINES = {
# "currency_scraper.pipelines.CurrencyScraperPipeline": 300,
#}
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False
# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = "httpcache"
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"
# Set settings whose default value is deprecated to a future-proof value
FEED_EXPORT_ENCODING = "utf-8"
# 文件: currency_scraper/settings.py
# ... (原始代码不变) ...
# --- 1. MySQL 数据库配置 (!!! 替换占位符 !!!) ---
# 推荐使用 127.0.0.1 避免 Socket 连接问题
MYSQL_HOST = '127.0.0.1'
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'mysql123' # <<< 替换成你的 MySQL 密码!
MYSQL_DB = 'boc_currency_db' # 建议使用新的数据库名
# --------------------------------------------------
# --- 2. 启用 ITEM PIPELINES ---
ITEM_PIPELINES = {
# 启用我们自定义的 Pipeline 类
"currency_scraper.pipelines.MysqlPipeline": 300,
}
# --- 3. 增加请求头 (反爬虫优化) ---
# 取消 USER_AGENT 的注释,并使用一个常见的浏览器User-Agent
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'
# --- 4. 增加下载延迟 (反爬虫优化) ---
DOWNLOAD_DELAY = 1.0
ROBOTSTXT_OBEY = False # 目标网站可能禁止爬虫,暂时关闭 robots.txt 检查
stock_spider.py
负责发起网络请求(通过 API 接口)、使用 JSON 解析来获取原始数据,并根据字段映射规则生成 StockItem。
import scrapy
from stock_scraper.items import StockItem
import re
import json
class SinaStockApiSpider(scrapy.Spider):
name = 'sina_stock'
allowed_domains = ['finance.sina.com.cn']
# 基础 API URL(不包含 biz_type, page 和 callback)
BASE_API_URL = "https://stock.finance.sina.com.cn/stock/api/openapi.php/LhbService.getList?list_type=1&extra=symbol,plate,tip,symbol_num&pagesize=50&no_chg_type_3=1"
# 定义要爬取的业务类型和初始页
# biz_type=1: 机构专用; biz_type=3: 沪股通/深股通; biz_type=4: 游资/其他
BIZ_TYPES = [1, 3, 4]
# 定义每页大小和最大页数 (请根据实际情况调整最大页数)
PAGESIZE = 50
MAX_PAGES = 5 # 假设爬取前5页数据
def start_requests(self):
"""生成初始请求"""
for biz_type in self.BIZ_TYPES:
# 初始请求从 page=1 开始
url = f"{self.BASE_API_URL}&biz_type={biz_type}&page=1&callback=hqccall_biz_{biz_type}_page_1"
yield scrapy.Request(
url=url,
callback=self.parse_api,
# meta 用于传递状态信息,以便在 parse_api 中使用
meta={'biz_type': biz_type, 'page': 1}
)
def parse_api(self, response):
"""解析 JSONP 格式的 API 响应并生成后续页请求"""
self.logger.info(f"Processing API data from: {response.url}")
# 1. 清理 JSONP 格式,提取 JSON 字符串
# JSONP 格式: hqccall_biz_1_page_1({...JSON DATA...})
jsonp_data = response.text.strip()
try:
start_index = jsonp_data.index('{')
end_index = jsonp_data.rindex('}') + 1
json_str = jsonp_data[start_index:end_index]
data_dict = json.loads(json_str)
except Exception as e:
self.logger.error(f"Failed to parse JSONP at {response.url}: {e}")
return # 无法解析,退出
# 2. 提取股票数据列表
stock_list = data_dict.get('result', {}).get('data', {}).get('data', [])
current_page = response.meta.get('page')
biz_type = response.meta.get('biz_type')
# 3. 遍历数据并 yield Item
for stock_data in stock_list:
item = StockItem()
# --- 字段映射 ---
# 注意:API 中的 symbol_name 是 Unicode 编码 (\u开头),Python 的 json.loads 会自动解码
item['stock_code'] = stock_data.get('code', '')
item['stock_name'] = stock_data.get('symbol_name', '')
item['latest_price'] = stock_data.get('actprice', '')
item['price_change_rate'] = stock_data.get('changeratio', '')
# 以下字段在龙虎榜 API 中可能缺失或名称不同,需要根据你的 Item 调整
item['price_change_amount'] = None
item['volume'] = stock_data.get('amount', '') # 交易额
item['turnover'] = stock_data.get('turnover', '') # 换手率
item['opening_price'] = None
item['prev_close'] = None
item['high_price'] = None
item['low_price'] = None
item['amplitude'] = None
# 数据清洗 (主要针对百分号和单位)
for key, value in item.items():
if value is not None and isinstance(value, str):
cleaned_value = re.sub(r'[\s,%]', '', value)
item[key] = cleaned_value.replace('万', '').replace('亿', '')
yield item
# 4. 生成下一页请求 (分页逻辑)
if stock_list and current_page < self.MAX_PAGES:
next_page = current_page + 1
next_url = f"{self.BASE_API_URL}&biz_type={biz_type}&page={next_page}&callback=hqccall_biz_{biz_type}_page_{next_page}"
self.logger.info(f"Generating request for page {next_page}, biz_type {biz_type}")
yield scrapy.Request(
url=next_url,
callback=self.parse_api,
meta={'biz_type': biz_type, 'page': next_page}
)
关键代码解释
1)class SinaStockApiSpider(scrapy.Spider):定义了继承自 scrapy.Spider 的主爬虫类,包含了爬虫的名称、域限制以及所有数据抓取和流程控制方法。
2)name = 'sina_stock':定义了爬虫的唯一名称,用于在命令行中(scrapy crawl sina_stock)启动它。
3)allowed_domains = [...]:定义了爬虫允许访问的域名列表,限制了爬虫的活动范围,保障了爬虫的聚焦性。
4)BASE_API_URL = "...":定义了新浪股票 API 的基础接口地址,是构造所有分页和多类型请求的起点。
5)def start_requests(self):重写了 Scrapy 的起始请求方法。这是关键,它不再依赖一个静态的 start_urls 列表,而是主动生成了多个带参数的 scrapy.Request 对象,用于并发抓取不同业务类型和页码的数据。
5)url = f"{self.BASE_API_URL}&biz_type={biz_type}&page={page}...":利用 f-string 动态拼接 URL,将业务类型(biz_type)和页码(page)等请求参数嵌入到 API 接口中,实现数据的筛选和分页获取。
6)def parse_api(self, response):定义了专门用于处理 API 响应的回调函数。当一个 API 请求完成后,Scrapy 会将响应结果传递给这个函数进行处理。
7)jsonp_data = response.text.strip():获取 API 返回的原始文本内容。由于 API 采用 JSONP 格式,返回内容带有函数包裹,需要进行预处理。
8)json_str = jsonp_data[start_index:end_index]:通过查找 { 和 } 的索引,手动去除 JSONP 的函数包裹(例如 hqccall123({...})),得到纯净的 JSON 字符串。
9)data_dict = json.loads(json_str):使用 json 库将清洗后的 JSON 字符串解析为 Python 字典,这样我们就可以像操作普通字典一样提取股票数据了。
10)for stock_data in stock_list:遍历解析字典中包含的实际股票数据列表,对每一条记录进行处理。
11)item = StockItem():实例化一个新的 StockItem 对象,准备将数据字段准确地映射和填充进去。
12)item['stock_code'] = stock_data.get('code', ''):执行字段映射。将 API 返回字典中的 'code' 字段的值,赋值给 Item 中的 'stock_code' 字段,确保数据格式和名称统一。
13)cleaned_value = re.sub(r'[\s,%]', '', value):在数据交付给 Pipeline 前,使用正则表达式对价格、涨跌幅等字段进行基础清洗,去除百分号或空格,确保数据是纯净的数字字符串。
14)yield item:将处理完毕的 StockItem 提交给 Scrapy 引擎,进入 Pipeline 流程,最终存储到 MySQL 数据库中。
15)if stock_list and current_page < self.MAX_PAGES:实现分页逻辑。如果当前页有数据且未达到设定的最大页数,则构造并 yield 下一页的请求,实现自动连续爬取。
运行结果

作业3

由于整体的项目结构和框架代码与作业2类似,这里注重将核心的爬虫代码
完整代码
# 文件: currency_scraper/spiders/boc_currency.py
import scrapy
from currency_scraper.items import CurrencyItem
class BocCurrencySpider(scrapy.Spider):
name = 'boc_currency'
# 确保 allowed_domains 是正确的
allowed_domains = ['boc.cn', 'www.boc.cn']
start_urls = ['https://www.boc.cn/sourcedb/whpj/']
def parse(self, response):
self.logger.info(f"Start parsing URL: {response.url}")
# XPath 定位:数据表格通常在中行页面位于 div[@class="publish"]/table/tbody/tr
# 我们寻找类名为 publish 的 div 下面的 table 中的所有行 (<tr>)
# 注意:中行网站的表格结构比较特殊,经常在 <body> 下直接是 <table> 或嵌套在多个 <div> 中
# 使用更具鲁棒性的 XPath 定位到包含数据的表格行
rows = response.xpath('//div[@class="publish"]//table//tr')
# 遍历所有行,跳过表头 (rows[0] 是表头)
for row in rows[1:]:
# 提取单元格文本内容 (每个 td 的直接文本内容)
cells = row.xpath('./td/text()').getall()
# 确保获取到的列数足够 (外汇牌价表通常有 8 列,索引从 0 开始)
# 0: 货币名称, 1: 现汇买入价, 2: 现钞买入价, 3: 现汇卖出价, 4: 现钞卖出价, 6: 发布时间
if len(cells) < 7:
continue
item = CurrencyItem()
# 字段映射
item['currency'] = cells[0].strip()
item['tbp'] = cells[1].strip()
item['cbp'] = cells[2].strip()
item['tsp'] = cells[3].strip()
item['csp'] = cells[4].strip()
item['time'] = cells[6].strip()
# 传递 Item 给 Pipeline
yield item
核心代码解释
1)name = 'boc_currency':定义爬虫的唯一启动名称,用于命令行启动(scrapy crawl boc_currency)。
2)start_urls = ['https://www.boc.cn/sourcedb/whpj/']:定义爬虫的起始目标 URL,即中国银行外汇牌价页面。
3)def parse(self, response):Scrapy 接收到网页响应后,默认执行的解析函数,是数据提取的起点。
4)rows = response.xpath('//div[@class="publish"]//table//tr'):使用 XPath 定位到包含外汇数据的核心表格行()。这是精准提取数据的关键步骤。
5)for row in rows[1:]:遍历所有表格行,从索引 [1] 开始,目的是跳过表格的标题行(表头)。
6)cells = row.xpath('./td/text()').getall():定位到当前行中的所有表格单元格(),并用 getall() 提取所有单元格的文本内容,获取原始数据列表。
7)if len(cells) < 7:进行数据有效性检查,确保当前行的数据完整。
8)item = CurrencyItem():实例化一个新的 CurrencyItem 对象,准备进行字段映射和赋值。
9)item['currency'] = cells[0].strip():执行字段映射,将提取到的原始数据列表中的第一项(cells[0])赋值给 Item 的 'currency' 字段。其他价格字段同理。
10)yield item:将封装完整的外汇数据对象 Item 交付给 Scrapy 引擎,使其流转到下一步的 pipelines.py 进行存储。
运行结果

心得体会
实验二和实验三的核心在于 API 和数据流的实践。 我们确认了爬取结构化数据时,调用 API 接口(实验二)比解析复杂 HTML 表格(实验三)更高效可靠。整个项目的成功取决于 Scrapy 完整数据流的打通,特别是 Pipeline 中必须严格处理字段名称匹配和数据类型兼容性,这是确保数据能准确无误写入 MySQL 数据库的关键。

浙公网安备 33010602011771号