影刀RPA 网页分页采集的通用模式:下一页判断与循环控制
作者:林焱
大部分数据采集不是一页能搞定的——商品列表有几十页、搜索结果要翻页、订单记录按月分页。分页采集是RPA最经典的场景之一,但很多新手写出来的分页流程总是出问题——漏采最后一页、多采了一页报错、翻页按钮变了找不到、翻页太快被限制。
这篇文章把分页采集的通用模式讲清楚,做成模板拿来就能用。
一、分页采集的核心逻辑
不管什么网站,分页采集的核心逻辑都是这三步循环:
1. 采集当前页数据 2. 判断是否有下一页 3. 如果有 → 翻到下一页,回到第1步 如果没有 → 结束循环关键是第2步:怎么判断有没有下一页。
二、判断是否有下一页
2.1 检查"下一页"按钮状态
最常见的判断方式——看"下一页"按钮是否可点击:
defhas_next_page():"""检查是否有下一页"""next_btn=get_element("//a[contains(@class, 'next')]")ifnotnext_btn:returnFalse# 检查是否被禁用class_attr=next_btn.get_attribute("class")if"disabled"inclass_attr:returnFalse# 检查是否有disabled属性ifnext_btn.get_attribute("disabled"):returnFalsereturnTrue坑1:下一页按钮的选择器因网站而异
拼多多店群自动化报活动上架!
不同网站的下一页按钮结构不同,常见模式:
// 通用模式 //a[contains(text(), '下一页')] //a[contains(text(), 'Next')] //a[@class='next'] //li[@class='next']/a //button[contains(@class, 'next')] //a[contains(@class, 'pagination-next')]需要根据目标网站调整选择器。
坑2:最后一页的下一页按钮可能不存在
有些网站在最后一页时直接移除了下一页按钮,而不是禁用它。需要同时处理"按钮不存在"和"按钮被禁用"两种情况。
2.2 检查当前页码和总页数
defget_page_info():"""获取当前页码和总页数"""# 获取总页数文本,如"第 3/15 页"page_text=get_element_text("//span[@class='page-info']")importrematch=re.search(r'(\d+)\s*/\s*(\d+)',page_text)ifmatch:current=int(match.group(1))total=int(match.group(2))returncurrent,total returnNone,None# 使用current,total=get_page_info()ifcurrentandtotal:ifcurrent<total:has_next=Trueelse:has_next=False2.3 检查当前页数据量
defhas_next_page_by_data(expected_per_page=20):"""通过数据量判断是否有下一页"""rows=get_element_list("//table//tr")data_count=len(rows)-1# 减去表头# 如果当前页数据等于每页预期数量,可能还有下一页# 如果少于预期数量,说明是最后一页returndata_count>=expected_per_page坑:最后一页也可能正好满页
如果数据总量正好是每页数量的整数倍,最后一页也是满的,这种判断会误认为还有下一页。需要结合其他判断方式。
2.4 检查URL变化
current_url=get_current_url()# 翻页click_element(".next-page")wait_for_page_load()new_url=get_current_url()ifnew_url!=current_url:has_next=True# URL变了,说明翻页成功else:has_next=False# URL没变,可能到最后一页了三、翻页操作
3.1 点击下一页按钮
defgoto_next_page():"""翻到下一页"""# 记录当前URL或数据标识old_identifier=get_element_text("//table//tr[2]/td[1]")# 第一条数据的ID# 点击下一页click_element("//a[contains(@class, 'next')]")# 等待新数据加载time.sleep(1)# 等待数据变化(确保翻页成功)for_inrange(10):new_identifier=get_element_text("//table//tr[2]/td[1]")ifnew_identifier!=old_identifier:returnTrue# 数据变了,翻页成功time.sleep(1)returnFalse# 翻页可能失败3.2 修改URL参数翻页
有些网站通过URL参数控制页码(如?page=2),可以直接修改URL:
importredefgoto_page(page_num):"""直接跳转到指定页"""current_url=get_current_url()# 替换URL中的page参数if"page="incurrent_url:new_url=re.sub(r'page=\d+',f'page={page_num}',current_url)else:if"?"incurrent_url:new_url=current_url+f"&page={page_num}"else:new_url=current_url+f"?page={page_num}"# 打开新URLopen_url(new_url)wait_for_page_load()3.3 滚动翻页
有些网站没有分页按钮,而是滚动到底部自动加载下一页:
defscroll_to_load_more():"""滚动加载更多"""last_height=execute_js("return document.body.scrollHeight")whileTrue:# 滚动到底部execute_js("window.scrollTo(0, document.body.scrollHeight)")time.sleep(2)# 检查是否有新内容加载new_height=execute_js("return document.body.scrollHeight")ifnew_height==last_height:# 高度没变,可能到底了# 再等一次确认time.sleep(3)new_height=execute_js("return document.body.scrollHeight")ifnew_height==last_height:break# 确认到底了last_height=new_height四、完整的分页采集模板
importtimeimportpandasaspdfromdatetimeimportdatetimedefpaginated_collection(collect_func,has_next_func,next_page_func,max_pages=100,delay=2):""" 通用分页采集模板 参数: - collect_func: 采集当前页数据的函数 - has_next_func: 判断是否有下一页的函数 - next_page_func: 翻到下一页的函数 - max_pages: 最大页数(安全阀) - delay: 每页之间的延时 """all_data=[]page=1errors=[]print(f"开始分页采集 -{datetime.now().strftime('%H:%M:%S')}")whilepage<=max_pages:try:# 1. 采集当前页page_data=collect_func()ifnotpage_data:print(f"第{page}页无数据,停止采集")breakall_data.extend(page_data)print(f"第{page}页:采集{len(page_data)}条,累计{len(all_data)}条")# 2. 判断是否有下一页ifnothas_next_func():print(f"第{page}页是最后一页,采集完成")break# 3. 翻到下一页ifnotnext_page_func():print(f"翻页失败,停止采集")breakpage+=1time.sleep(delay)# 页间延时exceptExceptionase:error_info=f"第{page}页采集异常:{e}"errors.append(error_info)print(error_info)# 尝试继续下一页ifpage<max_pages:try:next_page_func()page+=1time.sleep(delay)except:print("翻页也失败,停止采集")breakelse:break# 汇总print(f"\n采集完成:共{page}页,{len(all_data)}条数据")iferrors:print(f"错误{len(errors)}个:")forerrinerrors:print(f"{err}")returnall_data,errors# ===== 使用示例 =====# 定义采集函数defcollect_current_page():"""采集当前页数据"""rows=get_element_list("//table[@class='data-table']//tr")data=[]forrowinrows[1:]:# 跳过表头cells=row.find_elements_by_css_selector("td")ifcells:row_data={"id":cells[0].text.strip(),"name":cells[1].text.strip(),"price":cells[2].text.strip(),}data.append(row_data)returndata# 定义判断函数defcheck_has_next():"""检查是否有下一页"""next_btn=get_element("//a[contains(@class, 'next')]")ifnotnext_btn:returnFalsereturn"disabled"notinnext_btn.get_attribute("class")# 定义翻页函数defgoto_next():"""翻到下一页"""old_first_id=get_element_text("//table//tr[2]/td[1]")click_element("//a[contains(@class, 'next')]")# 等待数据变化for_inrange(10):time.sleep(1)try:new_first_id=get_element_text("//table//tr[2]/td[1]")ifnew_first_id!=old_first_id:returnTrueexcept:continuereturnFalse# 执行采集data,errors=paginated_collection(collect_func=collect_current_page,has_next_func=check_has_next,next_page_func=goto_next,max_pages=50,delay=2)# 保存结果df=pd.DataFrame(data)df.to_excel("D:\\output\\collected_data.xlsx",index=False)五、防重复采集
5.1 记录已采集的页码
collected_pages=set()defcollect_current_page(page_num):"""采集指定页(避免重复)"""ifpage_numincollected_pages:print(f"第{page_num}页已采集,跳过")return[]data=do_collect()collected_pages.add(page_num)returndata5.2 记录已采集的数据ID
TEMU店群矩阵自动化运营核价报活动
collected_ids=set()defcollect_current_page():"""采集当前页,跳过已采集的数据"""raw_data=do_collect()new_data=[]foriteminraw_data:item_id=item["id"]ifitem_idnotincollected_ids:collected_ids.add(item_id)new_data.append(item)else:print(f"跳过重复数据:{item_id}")returnnew_data5.3 断点续采
importjsonimportosdefsave_progress(page,data):"""保存采集进度"""progress={"last_page":page,"data_count":len(data),"collected_ids":list(collected_ids),"timestamp":datetime.now().isoformat()}withopen("D:\\progress.json","w")asf:json.dump(progress,f)defload_progress():"""加载采集进度"""ifnotos.path.exists("D:\\progress.json"):returnNonewithopen("D:\\progress.json","r")asf:returnjson.load(f)# 使用:从上次中断的地方继续progress=load_progress()start_page=progress["last_page"]+1ifprogresselse1ifprogress:collected_ids=set(progress["collected_ids"])print(f"从第{start_page}页继续采集,已有{len(collected_ids)}条数据")# 每采集完一页保存进度forpageinrange(start_page,max_pages+1):data=collect_page(page)save_progress(page,all_data)六、避坑清单
坑1:翻页太快被限制
短时间内翻太多页,网站可能限制访问。加随机延时:
time.sleep(random.uniform(2,5))坑2:最后一页数据重复采集
翻到最后一页后,下一页按钮可能还在但点击无效,导致重复采集最后一页。用数据ID去重。
坑3:翻页后元素定位失效
翻页后页面重新渲染,之前的元素引用全部失效。每页重新获取元素。
坑4:页码跳转而非顺序翻页
有些网站支持直接跳转到指定页码。如果需要跳页,先跳页再等待加载。
坑5:数据量不确定导致死循环
如果has_next永远返回True,循环不会停止。必须设置max_pages安全阀。
坑6:翻页后URL没变
有些SPA网站翻页不改变URL。用数据变化来判断翻页是否成功,而不是URL变化。