2.3 Selectors:到站后的拆包动作 本节摘要:选择器是从响应里拆出数据的唯一工具,Scrapy 基于 parsel 提供 CSS 与 XPath 两套语法。本节讲两套语法的舒适区分工、get 与 getall 的取值纪律,以及"先在 shell 里调通、再写回 Spider"的标准工作流——这是到站拆包环节效率最高的做法。 车票到站,包裹堆在月台上。这一节解决第 2 章第三个问题:怎么把网页拆成结构化数据。拆包动作看似琐碎,实则有一套可以少走弯路的工作方法——先选语法、再定取值口径、最后用试验台验证。 两套语法,两个舒适区 CSS 选择器写起来短,适合按类名、标签、层级取节点;XPath 表达能力强,能沿轴反向查找、按文本定位、取属性。
本节摘要:选择器是从响应里拆出数据的唯一工具,Scrapy 基于 parsel 提供 CSS 与 XPath 两套语法。本节讲两套语法的舒适区分工、get 与 getall 的取值纪律,以及"先在 shell 里调通、再写回 Spider"的标准工作流——这是到站拆包环节效率最高的做法。
车票到站,包裹堆在月台上。这一节解决第 2 章第三个问题:怎么把网页拆成结构化数据。拆包动作看似琐碎,实则有一套可以少走弯路的工作方法——先选语法、再定取值口径、最后用试验台验证。
CSS 选择器写起来短,适合按类名、标签、层级取节点;XPath 表达能力强,能沿轴反向查找、按文本定位、取属性。我的习惯是:结构定位用 CSS,一旦需要"根据文本找节点"或"向上找父级",立刻切 XPath。
# CSS 舒适区:结构定位 response.css("div.quote") # 所有引用块 response.css("div.quote span.text::text") # 进入文本节点:::text response.css("a::attr(href)") # 取属性:::attr(名字) # XPath 舒适区:文本定位与轴查找 response.xpath('//span[@class="text"]/text()') response.xpath('//a[contains(text(), "下一页")]/@href') # 按文本找链接 response.xpath('//div[h3="书名"]/parent::div') # 由子及父
两套语法可以混用,中间用 xpath 或 css 方法接力:
# CSS 定位区块,XPath 在区块内按文本精确定位 for card in response.css("div.product"): name = card.xpath('.//h3[text()="书名"]/following-sibling::dd[1]/text()').get()
取值方法的口径必须在写代码前定好:get 取第一条、没有就 None;getall 取全部、没有就空列表。口径混乱是选择器代码最常见的 bug 来源——对列表用 get 只拿到一条,对唯一元素用 getall 又得到单元素列表,下游管道处理两种形状的数据时必然出错。
titles = response.css("h3.book-title") # SelectorList,先圈后取 first_title = titles.get() # 第一条或 None all_titles = titles.getall() # 全部,或空列表 # 直接在结果上链式取值也可以,但别混淆两种返回形状 price = response.css("p.price::text").get() # '£23.19' 或 None prices = response.css("p.price::text").getall() # ['£23.19'] 或 []
工程上的纪律:列表型字段一律 getall 加循环,标量字段一律 get 加判空。字段口径在 Items(2.4 节)里固化成模型后,这个问题会自然消失。
别在 Spider 里盲写选择器然后整程重跑。正确动作是先进交互终端,把选择器调到能出数据,再搬回代码:
$ scrapy shell "https://books.example.com/list" >>> response.status 200 >>> response.css("article.product_pod h3 a::attr(title)").getall()[:3] ['A Light in the Attic', 'Tipping the Velvet', 'Soumission'] >>> response.css("p.price::text").getall()[:2] ['£51.77', '£23.19'] >>> response.xpath('//a[text()="next"]/@href').get() 'catalogue/page-2.html'
试验台上还常用一个技巧:页面存到本地反复试验,避免对目标站点重复请求:
>>> fetch("https://books.example.com/list") # 拉一次 >>> import scrapy; open("page.html","wb").write(response.body) 50234 # 之后可用 scrapy shell page.html 反复试验,不碰远端
💡 关键直觉:选择器调不通的第一反应不该是改语法,而是先确认"数据在不在原始 HTML 里"。如果浏览器看得到、shell 里看不到,那是动态渲染问题,属于第 5 章的范畴——选择器再怎么改都没用。
把上面验证过的选择器组装成解析回调,这是到站拆包的完整形态:
def parse(self, response): for card in response.css("article.product_pod"): yield { "title": card.css("h3 a::attr(title)").get(default="").strip(), "price": card.css("p.price::text").get(default="").replace("£", ""), "stock": card.css(".instock.availability::text").get(default="").strip(), "detail_url": card.css("h3 a::attr(href)").get(), } next_page = response.xpath('//a[text()="next"]/@href').get() if next_page: yield response.follow(next_page, callback=self.parse)
运行统计里,item_scraped_count 的数字就是拆包产出。若它远小于预期而日志无报错,第一件事是回 shell 检查选择器是否被页面改版击穿——拆包代码是全工程最依赖站点现状的部分,改版即失效,这也是第 5 章"站点分析"要建立监控意识的原因。
拆出来的数据现在是裸字典。下一节给它们上户口——Item 数据模型,让整个下游对字段口径有据可依。