资讯动态

Python爬虫练手项目:requests+bs4/lxml解析书籍列表实战

发布时间:2026/10/15 19:24:53 来源:尧图企业网站定制
1. 从“学完语法就迷茫”说起这个练手项目到底在练什么刚把 Python 的基础语法啃完的人几乎都会撞上同一堵墙变量、循环、函数、类都懂了可打开编辑器却不知道敲什么。这个阶段最难受因为语法书上的例子都太“干净”了——算个斐波那契、写个学生成绩管理跑完就扔跟真实世界隔着一层。我自己当年也是这样直到有人跟我说了一句去抓点真数据回来你就知道该写什么了。这个项目的定位非常明确就是给“语法过关但没实战”的人一个能立刻上手、又能看到真实产出的练手场景抓取一个书籍列表页面把书名、评分、评价人数、简介这些字段结构化地存下来。它不追求工程化也不碰反爬对抗核心目的是让你第一次完整体验“发请求 → 拿响应 → 解析 HTML → 提取字段 → 落盘保存”这条链路。而这条链路恰恰是后面所有数据类工作的最小闭环。为什么选“书籍列表”这种页面因为它结构规整、字段清晰、没有登录门槛、没有复杂异步加载对新手极其友好。你不需要处理验证码、不需要逆向接口、不需要维护会话打开页面就能看到一页一页的条目每个条目长得几乎一样。这种“重复结构”正是解析库最擅长处理的场景也是你理解HTML 树结构和选择器的最佳教材。这个内容适合谁三类人最合适。第一类是刚学完 Python 基础、急需一个“有反馈”的项目来巩固语法的人第二类是做数据分析、运营、内容整理平时需要手动复制粘贴表格想用脚本替代重复劳动的人第三类是想入门爬虫但被各种框架吓退的人——这个项目只用两个解析库不引入任何重型框架跑起来就是几十行代码。读完你能得到什么一套可复用的解析模板、一份能直接改字段就换目标的代码骨架以及最重要的——遇到新页面时知道从哪下手的判断力。我先把话说在前面这个项目里真正值钱的不是“抓到数据”而是解析思路。请求谁都会发但面对一坨 HTML能快速定位到你要的字段、能判断用哪种选择器更稳、能在页面改版后快速修好这才是区分“会写”和“会干活”的地方。下面我就按实际动手的顺序把每个环节拆开讲透。2. 动手前的整体设计为什么是 requests bs4/lxml 这套组合2.1 方案选型的底层逻辑新手最容易犯的错是一上来就纠结“用哪个框架”。我的建议很直接这个阶段不要碰框架。框架帮你封装了调度、去重、并发、中间件但代价是你不知道底层发生了什么一旦出问题就完全懵。而这个项目的价值恰恰在于让你亲手摸到每一层。请求层用requests理由很朴素它的 API 设计几乎就是“人话”requests.get(url)一行拿到响应.text拿文本.status_code看状态.headers看头信息。对新手来说可读性比性能重要得多。解析层有两个选择BeautifulSoup简称 bs4和lxml。很多人问到底用哪个我的答案是两个都摸一遍因为它们代表两种不同的解析哲学。BeautifulSoup是纯 Python 实现的解析器容错性极强。哪怕你的 HTML 有标签没闭合、属性没引号它也能尽量帮你补全成一棵合理的树。它的选择器语法友好soup.find()、soup.select()写起来像自然语言。缺点是慢处理大文档时明显吃力。lxml则是基于 C 语言的解析库速度快、内存占用低支持完整的 XPath 语法定位精准。缺点是容错性差一些遇到畸形 HTML 可能直接报错而且 XPath 的学习曲线比 CSS 选择器陡。那为什么标题里写的是“bs4/lxml 简单使用”因为在实际项目里这两者经常是配合使用的用lxml作为BeautifulSoup的底层解析引擎既拿到 bs4 的友好 API又享受 lxml 的解析速度。写法就是BeautifulSoup(html, lxml)。这个细节很多人不知道以为两者是二选一其实它们可以是上下游关系。2.2 数据字段的规划在写代码之前先想清楚你要抓什么。这一步比写代码重要。打开目标页面你会看到每个书籍条目大致包含这些信息书名、作者/译者、出版社、出版日期、价格、评分、评价人数、一句话简介可能还有封面图链接。新手常犯的错是“看到什么抓什么”结果代码越写越乱。我的做法是先列一张字段表明确每个字段的来源位置和数据类型再决定解析策略。比如书名通常在条目的标题链接里评分在某个 class 固定的 span 里评价人数往往和评分挨着。把这张表列出来代码结构自然就清晰了。字段名页面位置数据类型解析方式书名条目标题链接文本字符串CSS 选择器取文本评分评分 span 的 class浮点数取文本后转 float评价人数评分旁的文本整数正则提取数字简介条目描述段落字符串取文本并去空白详情链接标题链接的 href字符串取属性值这张表看起来简单但它决定了你后面代码的骨架。字段一旦定下来解析函数就可以按字段逐个写写完一个测一个不会乱。2.3 请求头的必要性很多人第一次跑requests.get()就吃闭门羹返回 403 或者一堆乱码。原因通常有两个一是没带User-Agent服务器一看就知道你不是浏览器二是没处理编码中文页面经常返回乱码。所以请求头里至少要放一个正常的User-Agent这是最基本的礼貌也是让服务器愿意把内容给你的前提。提示请求头不是“作弊”而是模拟正常浏览器行为。真实浏览器每次请求都会带一堆头信息你只补一个 User-Agent 已经算很克制了。编码问题也顺手解决掉requests有时候猜不准编码导致中文变乱码。稳妥的做法是拿到响应后手动指定response.encoding utf-8或者用response.apparent_encoding让它自己判断。我一般直接看页面源码里的 charset 声明写死成对应的编码最省心。3. 核心细节拆解HTML 树、选择器与解析思路3.1 把 HTML 想象成一棵家谱树新手看 HTML 源码容易晕满屏尖括号。但只要你把它想象成一棵家谱树一切就清楚了。最外层的html是老祖宗往下分head和body两个分支body里又有div、ul、li这些子孙。每个标签都是树上的一个节点节点之间是父子、兄弟关系。解析库干的事就是把这棵树的每个节点变成一个 Python 对象让你能通过“找某个标签”“找某个 class”“找某个属性”来定位节点。BeautifulSoup把整棵树装进一个soup对象你调soup.find(div)就是找第一个 div 节点soup.find_all(li)就是找所有 li 节点。理解了这个模型选择器就不再是死记硬背的语法而是“从树根往下找路”的过程。3.2 CSS 选择器与 XPath 的分工BeautifulSoup的select()方法支持 CSS 选择器这是前端开发天天用的东西语法直观。比如.item选所有 class 为 item 的元素#main选 id 为 main 的元素div a选 div 的直接子元素 aa[href]选所有带 href 属性的 a 标签。这些组合起来定位能力已经很强了。lxml的强项是 XPath。XPath 用路径表达式描述节点位置比如//div[classitem]表示“文档中任意位置、class 为 item 的 div”。它的优势在于轴axis概念可以表达“某节点的父节点”“某节点的后续兄弟节点”这类关系CSS 选择器做不到。举个实际例子你想找“评分元素的下一个兄弟节点里的评价人数”用 XPath 写//span[classrating]/following-sibling::span[1]非常直接用 CSS 就得绕。那到底怎么选我的经验是能用 CSS 就用 CSSCSS 表达不了再上 XPath。因为 CSS 选择器更短、更易读、维护成本低。只有当你要表达“父级”“兄弟”“位置索引”这类关系时才切到 XPath。这个项目里大部分字段用 CSS 就够了个别字段用 XPath 补刀。3.3 定位策略从“唯一锚点”出发解析页面最怕的是选择器写得太“脆”页面稍微改一下就失效。我的原则是找唯一锚点。什么叫唯一锚点就是页面上只有一个元素符合这个特征用它作为定位起点再往下找子节点。比如每个书籍条目通常有一个固定的 class这个 class 就是锚点先find_all拿到所有条目再在每个条目内部找字段。这样做的好处是隔离性字段解析只在条目内部进行不会误抓到页面其他地方的同类元素。我见过太多新手直接全局find_all(span)结果把导航栏、页脚的 span 全抓进来了数据一团糟。正确的姿势是“先框定范围再在范围内精确定位”这跟数据库查询先缩小表范围再筛字段是一个道理。注意不要依赖那些看起来像随机字符串的 class 名比如css-1x2y3z这类通常是构建工具生成的改版必变。优先选语义化的 class比如book-item、rating、author这种。3.4 文本清洗抓下来只是第一步抓到文本不等于拿到干净数据。页面上取下来的字符串经常带着换行、多余空格、全角符号、甚至隐藏的空白字符。比如书名后面跟一个换行加若干空格评分文本里混着“分”字评价人数里带着“人评价”这种后缀。这些都得在解析阶段处理掉。我的处理习惯是取到文本后统一.strip()去首尾空白再用正则把数字部分抠出来。评分转 float人数转 int转换失败就记成 None 而不是让程序崩掉。这一步看似琐碎但它决定了你后面能不能直接拿数据做分析。脏数据留在那里后面清洗的成本会翻好几倍。4. 完整实操流程从发请求到数据落盘4.1 环境准备与依赖安装先把环境搭起来。Python 3.7 以上都行我建议用虚拟环境避免污染全局包。依赖就三个requests负责请求beautifulsoup4负责解析lxml作为解析引擎。安装命令很直接pip install requests beautifulsoup4 lxml装完之后验证一下进 Python 交互环境敲import requests, bs4, lxml不报错就说明齐了。这里有个小坑lxml在某些系统上需要编译如果 pip 装不上可以找对应的预编译包。不过现在主流平台基本都有现成轮子直接装问题不大。4.2 发送请求并保存响应第一步先把页面拿下来并且存一份到本地。为什么要存因为开发阶段你会反复调试解析代码如果每次都重新请求既慢又给对方服务器添麻烦。存成 HTML 文件后解析代码直接读本地文件调试效率高很多等解析逻辑稳定了再切回实时请求。import requests url 目标页面地址 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 with open(page.html, w, encodingutf-8) as f: f.write(resp.text) print(resp.status_code, len(resp.text))这段代码里有几个细节值得说。timeout10是必须的不设超时的话网络一卡程序就永远挂在那里。resp.encoding手动指定避免中文乱码。存文件时用encodingutf-8保证写入和读取一致。打印状态码和长度是快速判断“有没有拿到东西”的土办法——如果长度只有几百多半是被拦了或者页面是空的。4.3 用 bs4 解析并提取字段拿到 HTML 后进入解析环节。先构造 soup 对象注意这里用lxml作为解析器from bs4 import BeautifulSoup with open(page.html, r, encodingutf-8) as f: html f.read() soup BeautifulSoup(html, lxml) items soup.select(.book-item) # 假设条目 class 为 book-item print(条目数量:, len(items))先打印条目数量这是验证锚点是否正确的关键一步。如果数量是 0说明选择器写错了如果数量远超预期说明锚点太宽泛。确认数量合理后再逐个条目提取字段results [] for item in items: title_tag item.select_one(.title a) rating_tag item.select_one(.rating) count_tag item.select_one(.rating-count) title title_tag.get_text(stripTrue) if title_tag else None link title_tag[href] if title_tag and title_tag.has_attr(href) else None rating None if rating_tag: try: rating float(rating_tag.get_text(stripTrue)) except ValueError: rating None count None if count_tag: digits re.findall(r\d, count_tag.get_text()) count int(digits[0]) if digits else None results.append({ title: title, link: link, rating: rating, count: count, })这段代码体现了几个我反复强调的原则。第一每个字段都做空值判断if xxx_tag else None避免某个条目缺字段导致整个循环崩掉。第二类型转换包在 try 里评分转 float 失败就记 None不让异常中断流程。第三用正则抠数字因为评价人数文本里往往混着中文直接转 int 会失败。这些细节看着啰嗦但正是它们让脚本从“能跑一次”变成“能稳定跑”。4.4 用 lxml XPath 处理复杂定位有些字段用 CSS 不好定位这时候切到 XPath。比如你想抓“评分元素后面紧跟的那个兄弟节点里的评价人数”用 lxml 的 XPath 写起来很顺from lxml import etree tree etree.HTML(html) counts tree.xpath(//span[classrating]/following-sibling::span[1]/text())following-sibling::span[1]的意思是“当前节点之后的第一个 span 兄弟节点”。这种关系表达是 CSS 的短板。实际用的时候我通常先用 bs4 把条目范围框出来遇到个别难啃的字段再对那段 HTML 片段用 lxml 处理两者混用各取所长。4.5 数据落盘CSV 还是 JSON抓完数据总得存下来。新手最常用的是 CSV因为能直接用表格软件打开直观。用 Python 内置的csv模块几行就搞定import csv with open(books.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, link, rating, count]) writer.writeheader() writer.writerows(results)注意encodingutf-8-sig这个-sig是为了让表格软件正确识别中文不然打开可能乱码。newline是 csv 模块的官方建议避免 Windows 下多出空行。如果数据后面还要给程序用JSON 更合适json.dump(results, f, ensure_asciiFalse, indent2)一行搞定ensure_asciiFalse保证中文正常显示。存储格式适用场景优点注意点CSV人工查看、表格分析直观、通用注意编码 utf-8-sigJSON程序消费、嵌套结构保留类型、易扩展ensure_asciiFalse数据库大量数据、频繁查询查询强、可增量新手阶段不必上4.6 加一点节奏控制即使是练手项目也不建议把请求发得太密。加个time.sleep(1)在每次请求之间既是对目标服务器的尊重也是养成好习惯。真实项目里还会有重试机制、失败记录、断点续爬但这个阶段先把“慢一点、稳一点”的意识建立起来就够了。我见过太多人一上来就写多线程狂发请求结果被封了还不知道为什么。5. 常见问题与排查技巧实录5.1 抓下来是空列表怎么办这是最高频的问题。items长度为 0通常有三个原因。第一选择器写错了class 名拼错或者层级不对。排查方法是把 HTML 存下来用浏览器打开按 F12 检查目标元素的真实 class。第二页面内容是 JavaScript 动态渲染的requests拿到的 HTML 里根本没有这些数据。判断方法是搜索响应文本里有没有你要的字段没有就是动态加载这个项目不涉及但要知道这个坑。第三请求被拦了返回的是验证页面而不是真实内容看resp.status_code和文本长度就能判断。5.2 中文乱码的三种表现与解法乱码分三种情况。第一种是全部变成\uXXXX这种转义那是显示问题数据本身没错。第二种是变成“锟斤拷”这种经典乱码是编码声明和实际编码不一致手动设resp.encoding解决。第三种是部分字符乱码通常是页面混用了多种编码这种最麻烦一般靠apparent_encoding兜底。我的习惯是拿到响应先打印前 200 个字符肉眼确认中文正常再往下走。5.3 选择器今天能用明天失效页面改版是常态。降低维护成本的办法是把选择器集中管理不要散落在代码各处。我通常把选择器写成常量字典改版时只改这一处SELECTORS { item: .book-item, title: .title a, rating: .rating, count: .rating-count, }这样页面一变你只需要对着字典逐个核对不用满代码找。另外优先选语义化 class、优先选稳定的属性比如>

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑