Python爬虫入门实战:requests+BeautifulSoup批量下载壁纸高清原图

发布时间:2026/10/11 9:37:54

Python爬虫入门实战:requests+BeautifulSoup批量下载壁纸高清原图 1. 为什么拿手机壁纸下载当入门案例选题逻辑与技术栈拆解学 Python 的人基本都会经历一个阶段语法书翻完前几章循环、函数、列表这些基础刚上手就开始手痒想写点真正能干活的东西。而爬虫几乎是所有人绕不开的第一个目标——毕竟能自动把网页上的数据抓到本地这种即时反馈太有成就感了。可爬虫案例那么多为什么我偏偏推荐爬取手机壁纸批量下载作为入门的第一站这个选题其实藏着不少刻意为之的考量。先说它解决了什么问题。爬虫的本质是用程序替代人工的重复性浏览操作。壁纸站这类网站信息结构非常规整一个列表页挂着密密麻麻的展示图点进详情页或点开原图链接才能拿到高清大图。人工操作要挨个点开、另存为碰上几十张壁纸会让人崩溃。而这个场景几乎完美覆盖了爬虫入门必须面对的三大核心操作构造 HTTP 请求、解析 HTML 内容、保存二进制数据到本地。更妙的是壁纸站的结构通常不需要处理复杂的登录态、加密参数和 JavaScript 动态渲染——这对新手极度友好。再说它的技术栈为什么是标配。我用的是requestsBeautifulSoup4这一对组合。绝大多数入门教程都会告诉你这俩是爬虫的基础工具但很多人只记住了用法没记住为什么是它们。requests负责跟服务器对话把网页内容拉下来BeautifulSoup4负责从拉下来的 HTML 里挑出你要的那部分数据——比如图片的真实链接。整个流程非常像你打开浏览器、看到页面、然后另存为图片的过程只是把每个动作都变成了一行 Python 代码。这个案例特别适合入门者的另一个原因是它的失败成本很低。爬壁纸站不像爬某些平台那样需要处理复杂的反爬机制即使暂时没抓到数据你损失的只是一两分钟调试时间不会触发什么严重问题。但它又能真实地逼你把一些爬虫通用知识学扎实请求头怎么设置、url 规律怎么找、大文件下载时怎么用流式写入这些能力放到后面的任何爬虫项目里都能直接复用。当然我也得提前打个预防针案例里的目标站点和代码我全部用示例域名和模拟结构来展开但技术思路和代码逻辑是真实可跑的。你拿到代码后把它当作一个爬虫骨架套用到任何结构类似的图片站上都不违和——这才是这个项目更值钱的地方。2. 目标站点分析与 URL 结构拆解先看懂数据从哪来新手写爬虫最容易犯的一个错误是拿到目标网站就急着写代码连页面上有哪些元素、图片地址长什么样都没看清楚。实际上爬虫项目启动前的人工侦查环节往往决定了整个项目的成败。这一节我不讲代码专门掰开揉碎讲讲怎么审一个壁纸站——这部分能力比代码本身更能区分你是真会爬虫还是只会复制粘贴。2.1 壁纸网站的页面结构认知把一个典型的壁纸站拆开看通常就三层结构首页/栏目页展示分类入口比如风景动漫美女极简等。列表页某个分类下的缩略图墙通常一页有 2040 张图底部有页码或加载更多。原图/详情页点击缩略图后进入的页面展示可下载的高清原图地址。我们的需求是批量下载高清原图所以最理想的抓取路径其实是跳过详情页直接从列表页的缩略图地址推算出原图地址。很多壁纸站在 HTML 里就有规律缩略图 URL 和原图 URL 只是路径中的尺寸参数不同。举个典型的示例缩略图: https://cdn.example-wallpaper.com/uploads/thumbnail/20240101_001_800x1200.jpg 原图: https://cdn.example-wallpaper.com/uploads/wallpaper/20240101_001_1920x1080.jpg注意看两者只有中间目录和尺寸参数不一样。这种规律性就是我们能写代码批量替换 URL 的基础。2.2 真正需要返回给程序的是什么很多新手拿到 HTML 后第一反应是用 BeautifulSoup 去找图片但到底找什么标签、取什么属性心里没数。这里必须先把三个概念理清img标签的src属性通常是缩略图地址直接下载可能拿到糊图。a标签的href属性可能是详情页链接需要再跟进一步解析。页面中内嵌的>mkdir wallpaper_spider cd wallpaper_spider python -m venv venv创建后需要激活。MacOS/Linux 执行source venv/bin/activateWindows 执行venv\Scripts\activate。激活成功后终端前面会多一个(venv)前缀这意味着你后续装的包都进这个隔离间了不会污染系统全局也不会被别的项目污染。3.2 依赖库最小集合requests 与 BeautifulSoup4这个项目只需要两个第三方库安装命令如下pip install requests beautifulsoup4可能有新手会问解析 HTML 不是还有 lxml、parsel 吗图片下载不是还有 aiohttp 吗你说得都对但入门阶段切忌追求全家桶。requests是同步 HTTP 请求库虽然性能不如 aiohttp 的异步方案但它出错信息直观、调试简单语法也特别好记requests.get(url).text就完事了。BeautifulSoup4常简称为 bs4的 API 设计对新手极其友好soup.find_all()这种写法几乎接近自然语言。我遇到过很多一上来就上 Scrapy 框架的新手结果被框架的项目结构、中间件、Settings 配置劝退了。Scrapy 本身很好但不该是初学者的第一站。先把 requests 和 bs4 玩明白你才能真正理解爬虫的底层逻辑之后学 Scrapy 的时候能省一半时间。安装完成后可以跑一个快速验证import requests from bs4 import BeautifulSoup print(requests:, requests.__version__) print(bs4:, BeautifulSoup)如果这两行没有报错环境就齐活了进入下一节开始写正式代码。4. 核心代码逐段拆解从发送请求到图片落盘现在到了这篇文章最硬核的部分——代码。我不打算直接甩一大段代码让你复制了事而是把它拆成几个模块逐一讲清楚每一行在干什么、为什么这么写。这样即便目标站点的结构跟代码里不完全一致你也能根据原理自行修改而不是被代码困住。4.1 请求头与 Session 维持第一个模块是请求逻辑。很多网站会用请求头里的 User-Agent 是否来自真实浏览器作为最基础的门卫。如果你不带任何请求头发请求服务器返回的 User-Agent 会被识别为一些写死的库标识墙头草一点儿的站点直接丢给你一个 403 或一封服务器繁忙。import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://www.example-wallpaper.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, } session requests.Session() session.headers.update(HEADERS)这里我使用了requests.Session()而不是每次都直接requests.get()。Session 的底层会维护一个连接池并自动保存服务端返回的 Cookie。如果目标站点后期加了先访问首页再访问列表页的校验逻辑Session 能让你始终处于同一个浏览器会话里不至于每次请求都像是新访客。Referer请求头经常被忽略但很多图片防盗链就是靠它。图片服务器看到Referer不是本站域名直接拒绝返回图片。所以批量下载图片时这个头必须带上而且值要指向你正在爬的站点地址。4.2 解析列表页提取原图链接第二步是解析列表页拿到当前页所有图片的原图链接。这里的关键是理解 HTML 树形结构你要先找到承载图片的容器标签再往下去找img或a。def get_image_urls_from_page(page_url): resp session.get(page_url, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding # 防止乱码 soup BeautifulSoup(resp.text, html.parser) image_urls [] # 找到所有 img 标签提取 src 或>from pathlib import Path def download_image(img_url, save_dir): save_dir Path(save_dir) save_dir.mkdir(parentsTrue, exist_okTrue) # 从 URL 中提取合理的文件名 filename img_url.split(/)[-1] # 去除非法字符避免 Windows 文件名报错 filename filename.replace(|, _).replace(:, _).strip() file_path save_dir / filename with session.get(img_url, streamTrue) as r: r.raise_for_status() # 常见图片扩展名过滤防止下载到 html 页面 content_type r.headers.get(content-type, ) if image not in content_type: print(f跳过非图片内容: {img_url} - {content_type}) return with open(file_path, wb) as f: for chunk in r.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f已下载: {filename})这里值得说透两件事。第一with session.get(img_url, streamTrue) as r:这个上下文管理器会在退出时自动关闭响应对象避免连接泄漏。入门阶段养成的这个习惯到后面写大规模抓取时会帮你躲开大量连接异常。第二r.headers.get(content-type)检查的是内容类型。有些情况下网站防盗链会返回一个 HTML 错误页面但 HTTP 状态码却是 200。如果我们不做检查直接写入文件得到的图片实际上是一堆乱码的 HTML 代码放到图片浏览器里还报错。加上这一步检查能有效滤掉这种假图片。iter_content(chunk_size8192)是流式下载的核心。8KB 是个不过不失的块大小太小的块会增加 IO 次数太大又会在内存里堆积较多数据。你不必纠结这个数字保持默认即可但要知道为什么是一块一块写而不是一次性写完。4.4 完整代码组装起来把上面几个模块组装起来加上翻页逻辑和主入口就是一个可以直接运行的完整脚本了。import requests from bs4 import BeautifulSoup from pathlib import Path import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://www.example-wallpaper.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, } session requests.Session() session.headers.update(HEADERS) BASE_PAGE_URL_TEMPLATE https://www.example-wallpaper.com/wallpaper/page/{} def get_image_urls_from_page(page_url): resp session.get(page_url, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) image_urls [] for img in soup.find_all(img): url img.get(data-original) or img.get(src) if not url: continue if not url.startswith(http): url requests.compat.urljoin(page_url, url) if /thumbnail/ in url: url url.replace(/thumbnail/, /wallpaper/) image_urls.append(url) return image_urls def download_image(img_url, save_dir): save_dir Path(save_dir) save_dir.mkdir(parentsTrue, exist_okTrue) filename img_url.split(/)[-1] filename filename.replace(|, _).replace(:, _).strip() file_path save_dir / filename try: with session.get(img_url, streamTrue, timeout15) as r: r.raise_for_status() content_type r.headers.get(content-type, ) if image not in content_type: print(f跳过非图片内容: {img_url} - {content_type}) return with open(file_path, wb) as f: for chunk in r.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f已下载: {filename}) except Exception as e: print(f下载失败 {img_url}: {e}) def main(): save_dir wallpapers start_page 1 max_pages 5 # 这里控制要爬多少页 for page_num in range(start_page, max_pages 1): page_url BASE_PAGE_URL_TEMPLATE.format(page_num) print(f正在抓取列表页: {page_url}) image_urls get_image_urls_from_page(page_url) print(f本页发现 {len(image_urls)} 张图片) for url in image_urls: download_image(url, save_dir) time.sleep(1) # 礼貌性间隔避免对服务器造成压力 time.sleep(2) # 翻页间隔 if __name__ __main__: main()运行它你会在项目目录下看到wallpapers文件夹里面按列表页顺序躺着下载好的原图。整个流程跑通之后你就已经亲手走完了一个爬虫项目的完整生命周期——但到这一步还只是能跑的水平离好用还有距离。5. 下载过程中的常见报错与排查思路把坑提前踩一遍代码顺利跑完当然是理想状态但真实项目里出状况才是常态。我把自己在实际调试这类图片爬虫时最容易碰到的几个问题列出来每一条后面都附上排查路径这样你遇到时不至于抓瞎。5.1 请求超时与连接重置现象requests.exceptions.ConnectTimeout或ConnectionError程序跑着跑着就断了。原因基本有两个。一是目标站点检测到高频访问对当前 IP 做了临时限制二是你本地网络到目标服务器的链路不稳定。排查思路先检查单次请求是否正常比如用浏览器直接打开这个 URL看能不能秒开。如果能正常打开那大概率是请求频率问题。代码里我已经预留了time.sleep(1)如果还是频繁超时就把间隔调大到 3 秒、5 秒不要觉得慢——稳定性永远比速度重要。如果你只是单纯追求更快也要克制住把间隔调到 0 的冲动。爬虫是你在借用自己的网络身份跟陌生服务器打交道礼貌阈值要守住。这不仅是技术问题也是基本的网络礼仪。5.2 页面解析结果为空现象get_image_urls_from_page返回空列表或者抓到的图片 URL 数量忽多忽少。原因大概率是选择器写死了。很多图片站在改版后>import re def ext_from_content_type(content_type): match re.search(rimage/(\w), content_type) if match: fmt match.group(1).lower() if fmt jpeg: return .jpg if fmt jpg: return .jpg if fmt png: return .png if fmt webp: return .webp return .bin这个小函数解决了URL 后缀不可信的问题我在实际项目中几乎都会带上。5.4 Windows 下的文件名非法字符现象Windows 系统报[Errno 22] Invalid argument明明 URL 和路径都看起来没问题。原因URL 里的某些字符——比如问号、冒号、竖线——到了 Windows 文件系统里属于非法字符不能出现在文件名中。代码里我已经用.replace(|, _).replace(:, _)做了一次粗清洗但如果文件名里还有问号、星号、引号等Windows 依然不认。通用的做法是正则替换import re def sanitize_filename(name): return re.sub(r[\\/:*?|], _, name).strip()把所有非法字符统一替换成下划线一劳永逸。这一步看着不起眼但在批量下载场景里只要有一张图报错整个进程就可能中断损失的可不只是一张图的问题。6. 批量下载的工程化进阶从能跑到好用代码跑通了常见坑也踩平了接下来的内容属于加分项。作为一个曾经拿爬虫接单度日的老手我深知一个爬虫脚本从自己偶尔跑跑进化到能交给别人稳定用需要过哪些关。这一节全是我个人的经验沉淀新手可以先把这些技巧存起来等第一个版本跑顺了之后再逐步加上去。6.1 断点续传与去重当你的图片量从几十张变成几千张时网络波动导致的下载中断是必然的。中断后重新跑一遍脚本等于重新下载了所有已成功的图片又慢又浪费。解决办法有二。最简单的方案是下载前检查文件是否已存在if file_path.exists() and file_path.stat().st_size 0: print(f已存在跳过: {filename}) return这就天然实现了去重——重跑时跳过已下载的文件只补下载失败的。代价是如果你爬的目标站图片内容更新了同名的新图去重会把新图也漏掉。不过壁纸站这种场景文件名一般带时间戳或 ID基本不会一样所以这个方法非常契合本案例。更稳妥的方案是维护一个已下载清单文件比如 JSON 或纯文本每次下载成功后记录 URL 和文件路径。重跑时先读清单再决定要不要下载。这个方案灵活度高适合后续要扩展的场景但代码量会多一些。入门阶段我推荐先做简单的文件存在性检查等逻辑熟练了再上清单方案。6.2 进度显示与日志输出print(f已下载: {filename})在数量少的时候没问题到了几百张时就显得太吵而且屏幕拉得特别长。更专业的做法是为下载模块加上进度条和结构化日志。Python 内置的logging模块足以满足需求import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, datefmt%Y-%m-%d %H:%M:%S, ) logger logging.getLogger(wallpaper_spider)这样日志里自带时间戳方便事后统计下载速度、定位失败时间点。如果想让控制台好看一点可以引入tqdm库给循环加进度条pip install tqdm后几行代码就能加上from tqdm import tqdm for url in tqdm(image_urls, desc正在下载壁纸): download_image(url, save_dir) time.sleep(1)进度条带来的直观反馈会让你在爬大量数据时安心很多。但要注意 tqdm 跟普通 print 混用时会刷屏建议整个脚本统一用日志或进度条不要两种混着来。6.3 异常重试与退避策略网络请求失败是个概率事件写进爬虫里的网络请求迟早会遇到一次超时或 5xx。你的代码不能一碰到异常就崩也不能无脑重试几百次把服务器惹毛。这里我常用的是一个简单的指数退避策略def fetch_with_retry(url, retries3): for attempt in range(retries): try: resp session.get(url, timeout10) resp.raise_for_status() return resp except Exception as e: wait_time 2 ** attempt # 1s, 2s, 4s logger.warning(f第 {attempt 1} 次请求失败: {e}, {wait_time}秒后重试) time.sleep(wait_time) logger.error(f请求最终失败: {url}) return None指数退避的原理是每次重试都翻倍等待时间给服务器留出恢复窗口也避免了自己被判定为恶意请求。把它用在请求函数外沿整个爬虫的健壮性立刻上一个台阶。6.4 合规与边界爬虫的正确打开方式最后这部分不是技术但可能是整篇博文里最重要的一段。爬虫本质上是自动访问服务器资源。它本身并不违法但使用方式有严格的边界。以壁纸站为例有几条红线你必须守住遵守站点版权声明下载的壁纸仅限个人使用不要二次传播或商用。很多壁纸站的图片来自画师授权你用爬虫批量抓取并打包出售是很明确的侵权行为。查看并遵守robots.txt站点根目录下通常有这个文件它用标准化的格式声明了哪些路径允许爬取。比如Disallow: /admin/你就不该尝试访问这些路径。虽然robots.txt不是硬性的法律约束但对独立站点来说遵守它是基本的尊重。控制请求频率爬虫跑起来你能直观感受到快但这种快是建立在服务器资源消耗上的。单线程加time.sleep(1)是我对入门项目的默认建议。后面你如果学了多线程并发也要记住并发越大被限制的概率越高对目标站点的压力也越大。设置爬虫标识如果你在做一个长期项目建议在请求头里加入X-Spider-Name之类的头信息让网站管理员在日志里能认出你。这不是必须的但能体现一个爬虫开发者的专业度和诚意。不要突破反爬机制中的认证边界比如用技术手段绕过登录、批量注册账号、破解验证码这些行为在法律和道德层面都有更大风险也远远超出入门爬虫的范畴。入门阶段就把这些红线立清楚以后无论如何拓展都不会跑偏。我个人对这些边界的态度是把爬虫当作降低重复劳动的工具来用而不是薅服务器羊毛的手段。同一个站点你按 1 秒一张的节奏下 50 张图和按 0.1 秒一张的节奏下 5000 张图前者大概率安然无恙后者分分钟被限流甚至被拉黑而且拉黑的往往是你所在的 NAT 出口——如果你在公司或学校网络还会连累一屋子人。这种连带责任真正吃过亏才懂。回到这个案例本身壁纸下载爬虫最大的意义是让你用最低的试错成本把请求—解析—保存—健壮性处理这条爬虫主线完整走了一遍。之后你无论是回去补前端知识、学异步编程、还是转向 Scrapy 框架都会带着真正的需求去学而不是为了学而学。我个人的体会是爬虫入门最大的分水岭不是能不能把代码跑通而是能不能在代码跑通之后自主地拆解一个新网站的结构、定位数据来源、归纳地址规律。做到这一步你才算真正迈过了从抄代码到写代码的那道坎。希望这篇完整代码加思路拆解能帮你稳稳跨过这道坎。
延伸阅读

更多相关文章

2026/10/11 9:32:54

素材自动变大纲一键出片——一次做 PPT 流程的工程化尝试

## 背景做 PPT 找模板排版到半夜是很多团队都遇到过的老问题。## 核心能力- 文本素材自动梳理大纲- 大纲支持二次编辑- 多套配色模板可选- 渲染16:9标准PPTX文件## 落地场景日常办公等场景都能直接搬进工作流,输入是散乱的原始材料,输出是可直接交付的成…

2026/10/11 9:32:54

城市生命线无人机智能巡检:选型、技术路线与落地实践

城市生命线这个词,听起来有点宏大,其实就是我们每天都离不开的燃气管道、供水管网、供电线路、桥梁隧道,还有地下排水系统。前几年做这类基础设施巡检,主要靠人跑、靠眼看、靠笔记,效率低不说,很多隐蔽缺陷…

2026/10/11 10:37:59

AI代码编辑器规则配置指南:从默认踩坑到高效生成

1. 为什么默认配置的AI编辑器总差点意思刚上手AI代码编辑器那会儿,我跟大多数人一样,装完就开干,觉得这玩意儿自带智能,写代码应该像开了挂。结果用了两周,效率不升反降——生成的代码风格跟项目里现有的完全对不上&am…

2026/10/11 10:37:59

彻底卸载VSPD 6.9:虚拟串口驱动残留清理实战指南

简介:针对VSPD6.9虚拟串口卸载后残留的问题,这份PDF操作指南面向需要在Windows环境下彻底清理虚拟串口信息的开发调试人员与普通用户。文档围绕“软件已卸载但设备管理器中虚拟串口仍存在”的典型故障,梳理出一套从重置端口到正常卸载&#x…

2026/10/11 10:37:59

洛谷 P1223 排队接水:贪心策略与代码逐行详解

1. 题目回顾 排队接水是洛谷上一道经典的贪心入门题(P1223)。题目大意是:有 n 个人在一个水龙头前排队接水,第 i 个人接水需要 w[i] 秒。每个人接水时,后面的人都要等待。问:如何安排接水顺序,使…

2026/10/11 10:37:59

DMD实战指南:从流场快照到动态模态分解的完整实现

简介:这份资源是面向动力系统数据分析学习者与科研人员的MATLAB版动态模式分解(DMD)实现包,适合具备一定线性代数与MATLAB基础、希望将高维时间序列降维并提取低维动态模式的中高级用户。包内共3个文件,包含1个m脚本、…

2026/10/11 10:32:59

旧款手表数据同步:中文绿色版ZIP工具的完整使用指南

简介:松拓Moveslink2中文绿色版是一款针对松拓Ambit系列运动手表开发的免安装同步工具,主要帮助用户在电脑端完成运动数据上传、设备设置更新以及Movescount账户授权等操作,适合需要频繁在不同电脑间管理手表的运动爱好者或入门用户。压缩包共…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑