Scrapy爬虫重复抓取且遗漏唯一条目,每次运行结果不同
首先,我帮你梳理下核心问题的根源,然后一步步给出修复方案:
1. 复用Item对象导致数据覆盖(最关键问题)
你在parse方法里把items = HotelinfoItem()放在了循环外面,这会导致所有酒店条目共用同一个Item实例。Scrapy是异步框架,当你yield items时,后续的循环迭代会修改这个Item的内容,最终多条数据指向同一个对象,就会出现重复或者数据混乱的情况——这也是为什么每次重复的条目都不一样的原因。
修复方法:把Item的初始化移到循环内部,确保每个酒店都有独立的Item对象:
def parse(self, response): all_single_entries = response.css("div.listItem") for entry in all_single_entries: items = HotelinfoItem() # 移到循环里面! # 后续的赋值逻辑保持不变,继续处理当前酒店的数据 ... yield items
2. 未启用去重Pipeline,重复条目无法被过滤
你已经写了DuplicatesPipeline,但在settings.py的ITEM_PIPELINES里完全没配置它!这就导致即使抓到重复条目,也不会被丢弃,反而占用了总条数的名额,导致其他正常酒店条目被挤掉,出现“部分酒店缺失”的情况。
修复步骤:
- 先修改Item的字段赋值逻辑:你现在把所有字段都存成了列表(比如
items["hotel_ids"] = [str(...)]),但每个酒店对应一个值,列表是不可哈希的,没法直接放到集合里去重。所以要把字段改成单个字符串:
# 提取酒店名称:用extract_first()替代extract()+字符串分割,更稳定 hotel_name = entry.css(".listing_title [target=_blank]::text").extract_first(default="NA").strip() items["hotel_names"] = hotel_name # 提取酒店链接 hotel_link = entry.css(".listing_title a::attr(href)").extract_first(default="") items["hotel_links"] = f"https://www.tripadvisor.com{hotel_link}" if hotel_link else "NA" # 提取酒店ID hotel_id = entry.css(".listing_title a::attr(id)").extract_first(default="") items["hotel_ids"] = hotel_id.split("_")[1] if "_" in hotel_id else "NA" # 提取价格 hotel_price = entry.css(".premium_offer_container div::attr(data-pernight)").extract_first(default="NA") items["hotel_displayed_price"] = hotel_price # 提取酒店类型 hotel_type = entry.css(".mb10 .label::text").extract_first(default="Hotel").strip() items["hotel_type"] = hotel_type
- 然后在
settings.py里启用去重Pipeline,注意优先级要比存储Pipeline高(数字越小,优先级越高):
ITEM_PIPELINES = { 'Hotelinfo.pipelines.DuplicatesPipeline': 100, # 先执行去重 'Hotelinfo.pipelines.HotelinfoPipeline': 300, # 再执行存储 }
- 最后更新
DuplicatesPipeline的逻辑,确保判断的是单个ID值:
class DuplicatesPipeline: def __init__(self): self.ids_se跳过 io Respectenter_lookingexpand留下Over_临时 elabor streaming广大 Official定想,哦不,初始化去重集合: self.ids_seen = set() def process_item(self, item, spider): adapter = ItemAdapter(item) hotel_id = adapter['hotel_ids'] if hotel_id in self.ids_seen: raise DropItem(f"Duplicate item found: {item['hotel_names']} (ID: {hotel_id})") else: self.ids_seen.add(hotel_id) return item
3. 分页请求的Splash配置有问题
你在初始请求里用了wait=10,但分页请求里却用了wait=5,而且还用了response.follow来发起分页请求——response.follow是针对普通HTTP请求的,没法正确触发Splash的页面渲染,这会导致分页页面加载不充分,出现条目重复或者漏抓。
修复方法:分页请求改用SplashRequest,统一等待时间为10秒,甚至可以添加JS等待逻辑确保页面加载完成:
next_page = response.css("a.next::attr(href)").get() if next_page is not None: next_page_url = f"https://www.tripadvisor.com{next_page.split('#')[0]}" yield SplashRequest( url=next_page_url, callback=self.parse, args={ "wait": 10, # 可选:等待列表元素加载完成再返回,比固定等待更可靠 "js_source": """ function main(splash) { return splash.wait_for_element('div.listItem'); } """ } )
4. 数据库层面添加双重保障
你的HotelinfoPipeline每次运行都会删除原表重建,而且没有主键约束。建议给hotel_ids添加主键,这样即使Pipeline去重失效,数据库也会拒绝插入重复ID的数据:
def create_table(self): self.curr.execute("""DROP TABLE IF EXISTS hotel_links_with_prices_tb""") self.curr.execute(""" CREATE TABLE hotel_links_with_prices_tb ( hotel_ids VARCHAR(50) PRIMARY KEY, # 用hotel_id作为主键,避免重复 hotel_names TEXT NOT NULL, hotel_links TEXT NOT NULL, hotel_displayed_price TEXT, hotel_type TEXT NOT NULL )""")
最后建议
- 开启Scrapy的日志(在
settings.py里设置LOG_LEVEL = 'INFO'),这样可以看到DuplicatesPipeline是否在正常丢弃重复条目,方便排查问题。 - 可以在
parse方法里打印每个条目的ID,观察是否有重复提取的情况,确认数据提取逻辑是否正确。
按照这些步骤修改后,应该就能解决重复条目和部分酒店缺失的问题了!
内容的提问来源于stack exchange,提问作者KindLundt

