写爬虫这件事很多人的第一反应是抓网页HTML里的文本数据但图片爬虫其实才是最容易让新手建立信心、也最容易踩坑的项目。搜狗图片作为国内常用的图片搜索入口接口相对稳定返回结构也不复杂很适合用来做一次完整的爬虫实战练手。这篇文章我会把整个制作过程拆开讲清楚从接口定位、请求构造、图片下载到用数据库管理抓取状态完整走一遍适合有一定Python基础、想真正上手爬虫项目的人。做这个项目之前先想清楚三个问题抓什么、存哪里、合规怎么做。搜狗图片的搜索结果页就是一堆图片的URL我们要做的就是把这些URL批量抓下来然后下载到本地。这里说的重点不是怎么复制粘贴而是怎么从零开始分析一个动态加载的图片搜索页面找到真实的数据接口然后写代码去请求、解析、下载。整个过程会用到requests、BeautifulSoup、SQLAlchemy这几个常用库它们也是在真实工作中高频出现的工具。1. 项目定位搜狗图片爬虫到底要解决什么问题1.1 为什么选搜狗图片而不是其他图库我选搜狗图片作为练手目标主要图它两件事一是搜索结果页是典型的前后端分离结构图片数据通过AJAX异步加载这非常贴近现在绝大多数网页的真实形态二是它的请求参数和解码逻辑相对直白不像某些平台那样需要处理复杂的签名和加密参数适合作为理解动态网页抓取的入门案例。对比一下其他图库你就明白了。百度图片也有类似的异步接口但参数拼起来五花八门有的还需要额外的签名参数必应图片相对干净但它对爬虫的防护策略比较敏感连续请求很容易被临时限制。搜狗图片的接口就是一个简单的请求地址加关键词参数返回的是JSON格式的数据对新手来说是最友好的那一种。当然接口形态可能随时调整所以我会在第3章里专门讲怎么用开发者工具去定位当前的接口这个方法学会了以后换个目标平台也能照用。1.2 爬虫的合理边界与合规底线聊清楚了技术选型必须先说一句容易被忽略但非常重要的事爬虫不是什么东西都能爬的。做爬虫练手项目我给自己定了三条规矩也建议你照着做。第一只抓公开的、不需要登录就能看到的数据。搜狗图片的搜索结果页本身就是公开内容没有权限限制。第二严格遵守robots协议的约定目标站点如果明确禁止了某些路径的访问就别去碰。第三控制请求频率和总量不要用高并发把目标站点压垮也不要拿着抓到的图片去做商用或者任何可能侵权的用途。这几点不是场面话是真正影响你能否长期把爬虫技能用在正道上的关键。另外有一点实际操作中的体会抓下来的图片数据如果在人工筛选后发现包含敏感内容或者人物隐私一定要立刻删除不要存着研究也不要试图扩散。爬虫能力本身没有对错但用的人得心里有数。2. 技术选型Requests 加 BeautifulSoup 而非自动化浏览器2.1 为什么不选 Scrapy 和 Selenium不少新手一上来就问我为什么不用ScrapyScrapy确实是最流行的Python爬虫框架功能完善、性能强但这也意味着它的抽象层更多目录结构、中间件、Item Pipeline这些概念一开始会把人绕晕。对于搜狗图片这种小型项目用Scrapy等于杀鸡用牛刀而且调试起来要等整条链路跑通才能看到结果对培养信心不友好。Selenium和Playwright也不是首选。它们模拟真实浏览器渲染页面确实能解决很多JavaScript动态加载的问题但代价是启动一个浏览器实例会消耗大量内存速度比直接请求接口慢一个量级。搜狗图片虽然也是动态加载但它的数据本质上是走接口返回的JSON我们只要找到那个接口用requests直接请求就行完全不需要动用浏览器去渲染。这就是我做判断的思路先看数据到底从哪来再选最简单的工具。能用普通HTTP请求解决的事就不必上重型武器。2.2 环境准备与所需依赖动手写代码之前先把环境搭好。我习惯用虚拟环境隔离项目依赖避免多个项目冲突。这一步直接决定你后面提交代码到GitHub或者搬家到别的电脑时能不能快速恢复环境。mkdir sogou_image_spider cd sogou_image_spider python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate激活虚拟环境后安装下面几个库每个库干什么我都标注一下pip install requests beautifulsoup4 sqlalchemy lxmlrequests负责发HTTP请求这个是整个项目的地基beautifulsoup4在上面的版本里主要用来解析HTML我们这里其实主要用它的兄弟库lxml做某些HTML页面的解析不过为了以后扩展方便一起装上sqlalchemy是Python里非常主流的ORM工具用它把抓取记录写进数据库比直接用sqlite3手写SQL要省心很多尤其是后续想加字段、做查询的时候。为什么不装pandas我之前看到有人用pandas存抓取结果但这其实有点大材小用而且pandas的DataFrame做增量去重并不方便。爬虫项目里最顺手的数据存储方式要么是SQLite这种单文件数据库要么是MySQL、PostgreSQL。对个人练手来说SQLite已经足够而且它不需要额外启动服务文件就在项目目录下拷贝就能带走。3. 核心实现接口定位、解析、下载与数据库落库3.1 用开发者工具定位真实图片接口搜狗图片的页面长这样你输入一个关键词页面会先渲染出一些图片然后你往下滚它会继续加载更多。这个“继续加载更多”的过程就是前端发起了AJAX请求从后端获取图片数据。我们爬虫做的事情就是替前端把这个请求再模拟一遍。具体操作流程我用Chrome浏览器给你演示打开搜狗图片首页按F12打开开发者工具切到Network网络面板再点一下Fetch/XHR筛选把类型筛掉只留XMLHttpRequest。然后在搜索框里输入你想搜的关键词比如“日出”按回车。这时候你会看到Network面板里冒出好几个网络请求其中一个名字带pic或者search的请求就是我们要找的接口。点开这个请求看它的Headers面板里面有一个请求地址Request URL和一堆请求参数Query String Parameters。搜狗图片的接口地址大致是https://pic.sogou.com/pics/channel/pic.jsp参数包括keyword、start、len、reqType等等。其中keyword就是我们的搜索词start控制从第几张开始返回len控制返回多少条。看到这些参数你的爬虫思路马上就清晰了只要不断改变start的值就能一页一页地把所有图片的URL都取回来。我遇到很多新手会犯的一个错误不去看Network面板而是直接在Elements面板里找图片地址然后发现怎么都拿不全。原因很简单Elements面板显示的是渲染完成后的DOM里面很多图片是懒加载的滚动到哪加载到哪它不会把所有图片都放在初始HTML里。所以记住这条经验动态加载的页面数据基本都在XHR请求里别跟DOM死磕。3.2 请求构造与响应解析找到了接口看清楚了参数代码写起来就是一马平川。这里我直接把核心代码贴出来并用注释把每一段的关键逻辑讲明白。import requests import json import time def fetch_image_urls(keyword, start0, length30): url https://pic.sogou.com/pics/channel/pic.jsp params { keyword: keyword, start: start, len: length, reqType: ajax, pagesize: 20, query: keyword, } 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, Referer: https://pic.sogou.com/, } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.encoding utf-8 data resp.text # 搜索接口返回的往往不是干净JSON而是外层包了一层js变量需要剥出来 if data.startswith((): data data[1:-1] obj json.loads(data) items obj.get(items, []) return [item.get(picUrl) for item in items if item.get(picUrl)]这里有两个关键细节必须说明。第一请求头里的User-Agent一定要设置成浏览器模样。python-requests这个默认UA服务器一眼就能识别出是脚本在访问轻则返回空数据重则直接封IP。Referer字段也很关键搜狗图片会校验这个字段如果不带Referer或者Referer和图片域名不匹配可能会有防盗链问题。通常我访问的是搜狗图片的接口Referer就填https://pic.sogou.com/。第二接口返回的文本可能不是严格的JSON格式。搜狗这个接口有时会返回类似jsonp的东西最外层有括号包裹或者带一段JavaScript代码。所以我在代码里先判断text是不是以括号开头如果是就把它截掉然后再json.loads。这种“脏数据清洗”几乎是每一个真实爬虫项目都会遇到的问题提前处理掉能少踩很多坑。3.3 图片下载与文件命名策略拿到图片的URL列表之后下一步就是把它们下载下来。这一步看起来简单但有几个细节处理不好会下载出一堆损坏的小文件。先看基础下载代码import os import hashlib def download_image(url, save_dir, keyword, idx): os.makedirs(save_dir, exist_okTrue) 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, Referer: https://pic.sogou.com/, } resp requests.get(url, headersheaders, timeout15, streamTrue) if resp.status_code ! 200: return False ext url.split(?)[0].split(.)[-1] if ext not in (jpg, jpeg, png, gif, webp): ext jpg # 文件命名用url的md5避免重名覆盖 file_name hashlib.md5(url.encode(utf-8)).hexdigest()[:16] . ext file_path os.path.join(save_dir, f{keyword}_{idx}_{file_name}) with open(file_path, wb) as f: for chunk in resp.iter_content(chunk_size128): f.write(chunk) return True这里有三点经验是踩坑踩出来的。一是用streamTrue编码iter_content下载不要一次性resp.
