资讯动态

爬虫基础实战:requests与XPath的text()用法解析

发布时间:2026/10/5 16:09:29 来源:尧图企业网站定制
我把这个案例从最基础的思路讲起。坦白讲爬虫学习最忌讳的就是上来就撸重型框架你会发现一整天都在跟环境配置较劲真正学到的东西反而没多少。案例3我用了一个非常经典的静态站点——Books to Scrape它天生就是给爬虫初学者准备的陪练场页面结构规整、反爬极低可以让我把注意力全部集中在requests、XPath和text()函数这些真正的基础功上。1. 案例场景与学习目的为什么静态页面反而是最好的教材这个网站全称叫Books to Scrape是一个专门模拟真实电商结构的练习站。它的首页是一张150本书的网格列表每一本书都有标题、评分、价格、库存信息点击进去还有更完整的描述页这种结构跟真实的大型电商平台几乎一模一样。但关键区别在于它没有那些恶意反爬手段不会因为你频繁请求就封IProbots.txt也明确允许爬虫抓取。这意味着我可以大胆尝试各种写法而不必担心被拉黑。我做这个案例的核心目的有三个。第一是彻底打通“发起HTTP请求”到“拿到HTML文档”再到“提取目标字段”这条主链路这是所有爬虫的地基。第二是专门把XPath里的text()函数挖透因为我在多个技术群里看到新手总在text()和string()之间纠结还经常遇到明明选对了节点却提取出空白的情况。第三是顺手补上翻页循环和CSV落盘这是把一个小demo扩展成真正能用的工具的必经之路。在技术选型上我坚持只用requests和lxml不碰Scrapy也不碰Selenium。原因很直接Scrapy自带了一套完整的异步引擎和Item Pipeline新手被它封装掉的细节太多一旦出了问题很难定位到底哪一步错了Selenium则是浏览器驱动对静态页面来说完全没必要。requests加lxml是最短路径Windows用户装lxml可能会踩个小坑用pip install lxml如果编译失败直接去下载对应Python版本的.whl文件安装就行macOS和Linux通常一条命令就过。2. requests基础请求从一行代码到完整请求头requests库的入门非常友好核心就是一个get方法import requests url https://books.toscrape.com/ response requests.get(url) print(response.status_code)这是最基础的用法但如果你真拿它去爬真实站点大概率会撞上403或者被重定向到验证码页。原因就一条裸奔的requests默认User-Agent是Python-requests/x.x服务器一眼就能认出你是爬虫。所以从案例3开始我养成了一个习惯所有请求都带上明确的headers。import requests url https://books.toscrape.com/ headers { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 } response requests.get(url, headersheaders, timeout5) print(response.status_code)这里我顺便把timeout参数加上。很多初学者会忽视它但network请求如果一直不返回你的脚本就永远卡在那里。我见过有人写完爬虫后放在服务器上跑半夜莫名其妙就停了一查日志是某个请求挂起到了超时时间上限根本没触发异常处理。加了timeout之后直接配合try-except就能实现断线重试。拿到response之后下一个容易踩坑的地方是编码。response.text默认根据响应头里的charset去解码但有些网站不写charset或者写错了就会出现乱码。Books to Scrape其实没问题但为了养成好习惯我建议用response.content配合apparent_encoding来解码response.encoding response.apparent_encoding html response.textapparent_encoding是requests通过分析字节内容推断出的编码在国内的一些GBK老站上尤其好用。你要是直接用response.text遇到中文乱码十有八九都是编码推断出了问题。请求这块还有一个细节容易被忽略Session会话保持。如果你只是单页抓取直接用requests.get就够了但一旦涉及多个页面、需要传递Cookie的时候Session就非常关键。我用它来维持一种“同一个人持续浏览”的感觉降低被识别为机器人的概率session requests.Session() session.headers.update(headers) response session.get(url, timeout5)3. XPath解析核心text()函数究竟是干什么的XPath在整个流程里扮演的是“定位器”的角色。lxml会把你拿到的HTML字符串解析成一棵节点树而XPath的任务就是在这棵树里找到你想要的节点。我一开始用的方式是from lxml import html doc html.fromstring(response.text) books doc.xpath(//article[classproduct_pod])这行代码的意思是在整个文档里找所有article标签并且这个标签的class属性值是product_pod取回一个节点列表。接下来我要从每本书的节点里提取书名这就是text()函数登场的时刻。text()函数在XPath中的作用是取当前节点的直接文本内容。有点绕我直接用例子说明。一本书的HTML结构大概是h3a href... titleA Light in the AtticA Light in the Attic/a/h3我想拿到“A Light in the Attic”这个书名最常见的写法是name book.xpath(.//h3/a/text())[0]这里.text是取当前节点下的a标签的文本结果是所有直接文本节点组成的列表。所以必须加上[0]来取第一个值。问题来了如果我想取h3这个节点本身的文本呢不少人会写name book.xpath(.//h3/text())[0]结果返回一个空列表然后报IndexError。原因非常清楚h3标签里并没有直接文本它的文本内容是嵌在a标签里的。text()只认直接子文本节点不递归深入子标签去取嵌套文本。这个坑我在群里帮别人排查过很多次几乎每个新手都踩过一回。另一种常见情况是标签内部有文本也有子标签比如pPrice: span classprice_color£51.77/span/p如果我用price book.xpath(.//p/text())[0]拿到的可能是“Price: ”或者空白甚至包括换行符。文本节点在这种混排情况下会被拆成多个单靠text()很难拿到拼接后的完整内容。这时候需要用到string()函数lxml里还有一种更直接的方式是获取节点的text_content()属性它会把所有后代文本拼在一起。虽然lxml的xpath对string()的支持有点特殊但更常见的做法是用xpath()拿到p节点后再调用.text_content()price_node book.xpath(.//p[classprice_color])[0] price price_node.text_content().strip()这里我干脆绕开混排问题直接定位到价格所在的class节点它的text_content()会把“£51.77”干净地拿回来。到目前为止我要传达的核心观点是text()适合处理“纯文本叶子节点”一旦遇到嵌套文本或者需要拼接的场景text()就不是最优解。你需要建立一种意识——看到HTML先问自己我要的文本是直接子文本还是嵌套文本这个判断比背一百条XPath语法都管用。4. 翻页循环、数据清洗与CSV结构化输出Books to Scrape的翻页结构一目了然它底部有个“next”按钮对应的链接是这样a hrefcatalogue/page-2.htmlnext/a我在抓取第一页之后用XPath拿到这个链接的href再拼接完整URL循环往下走import requests from lxml import html import csv base_url https://books.toscrape.com/ page_url base_url books_data [] for page in range(50): response requests.get(page_url, headersheaders, timeout5) doc html.fromstring(response.text) books doc.xpath(//article[classproduct_pod]) for book in books: title book.xpath(.//h3/a/text())[0].strip() price_text book.xpath(.//p[classprice_color]/text())[0] price price_text.replace(£, ).replace(\u00a3, ).strip() stock_text book.xpath(.//p[classinstock availability]/text()) availability .join(stock_text).strip() if stock_text else Unknown rating_class book.xpath(.//p[contains(class, star-rating)]/class)[0] rating rating_class.split(star-rating)[-1].strip() books_data.append({ title: title, price: price, availability: availability, rating: rating }) next_page doc.xpath(//a[contains(text(), next)]/href) if next_page: if catalogue/ in page_url: page_url base_url next_page[0] else: page_url base_url catalogue/ next_page[0] else: break这段代码里有几个值得展开的细节。第一是股票库存那段我用了//p[classinstock availability]/text()但这类标签通常文本里夹着很多换行和空格所以我用.join把所有文本节点拼起来再用strip去掉两端空白最终拿到的就是“In stock”之类的内容。第二是价格教材页面上价格是英磅符号直接用replace去掉即可但如果是从复杂页面抓的价格里可能还有逗号分隔符需要一并处理例如price_num float(price.replace(,, ))第三是评分它的class是star-rating Three所以用contains来匹配到class属性再按空格拆分拿第二个值。这种做法在真实页面里很常用尤其是当一个节点同时挂多门课的状态时我习惯用contains配合class来精确定位。数据都收集好之后我直接用csv模块落盘。这里注意一点CSV的写入格式要指定newline不然在Windows下每一行后面会多一个空行网上很多人因为这个小细节反复折腾with open(books.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, price, availability, rating]) writer.writeheader() writer.writerows(books_data)编码用utf-8-sig而不是utf-8是因为Excel打开CSV时如果是纯utf-8会中文乱码加了个BOM头之后就完美兼容了。5. 反爬机制初探与分布式爬虫扩展思路案例3完成以后你可能觉得爬虫也就这样了——发请求、解析、存库一气呵成。但真实世界的网站远没有这么配合它们可能会用频率限制、验证码、动态加载等手段来阻挠爬虫。所以我建议你在跑通案例3之后给自己加两道练习题。第一道是给请求加一个简单的延迟控制比如用time.sleep(random.uniform(1, 3))。别小看这行代码它模拟的是人浏览时的真实间隔。我在实际项目里见过不少新手用for循环瞬间发几十个请求直接把对方服务器打挂然后自己的IP被封。爬虫的核心不是快而是稳。第二道是主动观察网站的robots.txt这是每个爬虫工程师职业素养的底线。爬到Books to Scrape没问题因为它允许但如果是别的网站先看看哪些路径不允许抓取遵守对方的规则做一个有涵养的爬虫。这既是对别人的尊重也是保护自己。至于分布式爬虫那是爬虫工程化的高级阶段。当单机速度跟不上业务需求时你才会有动力去了解消息队列、调度器、去重器这些东西。读完案例3你只需要脑子里有这个概念分布式不是创造了新的爬虫方式而是把你现在写的这个循环拆成“调度”和“执行”两个部分让多台机器同时干同一件事。比如用Scrapy-Redis共享请求队列用Celery做任务分发。现在不用急着学等你的单机爬虫真的在说“我可以处理10万页数据只是要多花一小时”时自然就会有动力去折腾它了。作为这个系列案例的阶段性总结我个人的体会是爬虫能力成长的关键根本不是背语法而是在一次次调试中培养出对HTML结构的直觉。从案例1蹒跚学步到现在能独立抓完一个完整站点背后唯一的秘诀就是多写、多断点、多在某一段XPath返回空列表的时候静下心来,去看一眼目标页面真实的HTML长什么样。希望案例3的这套内容,能帮你把最基本的那条链路打得足够扎实。

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

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

免费获取报价 →
↑