ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从“抓不到“到“抓得全“:requests-html网页解析实战,3招拿下JS动态数据

2026/8/15 21:05:24 拓冰建站 浏览量
从“抓不到“到“抓得全“:requests-html网页解析实战,3招拿下JS动态数据 从抓不到到抓得全requests-html网页解析实战3招拿下JS动态数据【免费下载链接】requests-htmlPythonic HTML Parsing for Humans™项目地址: https://gitcode.com/gh_mirrors/re/requests-html凌晨一点同事把一段爬虫代码拍在我桌上帮我看下这个页面明明有数据我抓出来却是空的。他抓的是一个内部培训站的课程列表浏览器里整整齐齐排着三十行课程requests拿回来的 HTML 里却只有一张空壳。这种浏览器有、源码没有的诡异现象几乎每个做数据抓取的人都撞过——问题不在网络请求而在 JavaScript 渲染。这篇文章就用 requests-html 这个网页解析库从一次真实救火出发把静态抓取、JS 渲染、异步批量这三关逐一打通。先搞清抓不到的三种病因排查问题之前先给数据抓取做个快速体检。网页上的数据大体分三种对应三种完全不同的处理策略数据形态典型特征传统方案是否有效纯静态 HTML数据写在返回的源码里✅ 有效懒加载 / 异步接口滚动后 data 才从接口拉回❌ 拿不到JS 动态渲染DOM 由脚本生成❌ 拿不到同事抓的就是第三种。他的原方案是经典的三件套urlopen拿源码、正则抠字符串、BeautifulSoup 收尾。这套组合对付静态页面绰绰有余但遇到前端框架React/Vue 之类生成的页面就彻底哑火——服务端返回的是空骨架真正的数据要靠浏览器里的 JavaScript 去填充。要治这种病思路只有一条把发请求和跑浏览器合并成一件事。requests-html 干的就是这个活它外面套着 requests 的皮里面装着解析引擎还内置了一个无头 Chromium能在抓取时像真人浏览器一样把脚本执行完。第一关HTMLSession 取代裸 requests三行代码建会话先看最基础的动作。requests-html 对 requests 的老用户几乎零迁移成本把requests.get()换成session.get()即可返回的对象在请求能力之外多了一个会解析的.html属性from requests_html import HTMLSession session HTMLSession() response session.get(https://python.org/) # 这行是关键响应对象自带解析能力 page response.html # 一行拿到全部链接自动去重、剔除锚点 print(len(page.links)) print(len(page.absolute_links)) # 绝对地址版本HTMLSession并不是简单包一层它会自动模拟浏览器 User-Agent、自动跟随重定向、复用连接池、持久化 Cookie。这些特性在后面的反爬环节都会派上用场。第二关CSS 选择器与 XPath两把手术刀交替用会话建好了接下来是定位数据。requests-html 的find()方法在项目源码 requests_html.py 的BaseParser类中定义支持完整的 CSS 选择器语法用法和 jQuery 几乎一致# 取第一个 #about 元素 about page.find(#about, firstTrue) # 按 class 筛选所有链接 nav_links page.find(nav a) # 按属性筛选找所有外链 external page.find(a[href^http]) # 按包含文本筛选找含 python 的元素 matches page.find(containingpython)定位到元素后.text拿文本、.attrs拿属性字典、.html拿内层 HTML.links和.absolute_links拿元素内的链接集合。项目测试文件 tests/test_requests_html.py 里有一整套断言可以当用法速查表来读。习惯 XPath 的也不用纠结xpath()方法平行存在项目文档 docs/source/index.rst 里两者都有示例# XPath 同样支持拿整个文档根节点 root page.xpath(/html, firstTrue) print(root.attrs[class]) # 输出 no-js # 直接取所有 a 标签的 href 属性值列表 hrefs page.xpath(//a/href)第三关render() 唤醒沉睡的数据——真正的重头戏现在回到同事的问题。静态抓取解决了但目标站点是 JS 渲染的怎么办答案是render()方法——它在源码 requests_html.py 的HTML类中实现会启动无头 Chromium把页面完整执行一遍再拿回渲染后的 DOM# 渲染整页等 JS 跑完 response.html.render( retries3, # 加载失败自动重试 wait1, # 打开页面后先等 1 秒 scrolldown2, # 向下滚动 2 次触发懒加载 sleep1 # 每次滚动后歇 1 秒 ) # 渲染之后之前空荡荡的选择器终于有结果了 courses response.html.find(.course-item) print(f渲染后抓到 {len(courses)} 个课程)❗注意render()首次运行会往主目录~/.pyppeteer/下载一份 Chromium耗时几分钟之后就不再下载。在服务器上跑之前先确认磁盘和网络环境。render()的几个参数对应着真实用户的行为scrolldown模拟滚动很多页面滚动到哪才加载到哪、sleep给异步请求留出返回时间、retries兜底页面偶发超时。如果页面滚动后仍缺数据把scrolldown和sleep一起调大通常能解决九成问题。⚡进阶玩法render()还能直接执行自定义 JavaScript 并把返回值带回来等于在浏览器里装了个数据探头script () { const items document.querySelectorAll(.course-item); return items.map(el ({ title: el.querySelector(.title).textContent, url: el.querySelector(a).href, })); } result response.html.render(scriptscript) print(result) # 直接拿到结构化列表相对链接不用愁一个方法自动转绝对地址抓链接时最烦的一件事页面里全是/course/42这种相对路径存下来没法直接用。requests-html 内置的_make_absolute()同样定义在BaseParser中专门治这个测试文件里test_links就是靠它把 6 个相对链接全部转成了绝对地址from requests_html import HTML # 不经过网络直接解析 HTML 字符串也行 doc a href/course/42Python 入门/a html HTML(htmldoc, urlhttps://example.com/) # 相对路径自动拼接成完整 URL for link in html.links: print(link) # /course/42 print(html._make_absolute(link)) # https://example.com/course/42_make_absolute()的实现逻辑很实在没有域名就拼上基础地址有域名缺协议就补http(s)已经完整的原样返回。比自己手写urljoin省心得多。批量抓取提速AsyncHTMLSession 一次开多个页面同事的需求很快变成了帮我把十个栏目都抓下来。这时候逐页串行抓就太慢了——每页都要等渲染动辄几秒。requests-html 自带异步方案AsyncHTMLSession对应源码里的arender()方法import asyncio from requests_html import AsyncHTMLSession asession AsyncHTMLSession() async def fetch_courses(url): response await asession.get(url) # 异步渲染效果等同 render() await response.html.arender(scrolldown1, sleep1) return { url: response.html.url, count: len(response.html.find(.course-item)), } async def main(): urls [ https://example.com/python, https://example.com/data, https://example.com/web, ] # 并发跑起来各自返回结果 results await asyncio.gather(*[fetch_courses(u) for u in urls]) for r in results: print(r) asyncio.run(main())提示异步版本能并发但也要节制目标站点扛不住瞬时压力会直接把你 IP 拉黑。批量大时建议分批每批 5~10 个页面并加随机延时。把前面全串起来一个能直接跑的通关脚本下面这个脚本把三关全打通——静态抓取、JS 渲染、批量并发加上去重和结果输出就是同事最终交出去的版本from requests_html import HTMLSession, AsyncHTMLSession import asyncio BASE https://example.com/training def extract_from_page(html): 从渲染完成的页面里抠出课程清单 items [] for el in html.find(.course-item): title_el el.find(.title, firstTrue) link_el el.find(a, firstTrue) if title_el is not None: items.append({ title: title_el.text.strip(), url: html._make_absolute(link_el.attrs[href]), }) return items def fetch_static(url): 第一关不渲染直接抓静态内容 session HTMLSession() response session.get(url) session.close() return response.html def fetch_dynamic(url): 第二关渲染 JS 后再抓 session HTMLSession() response session.get(url) response.html.render(scrolldown2, sleep1) session.close() return response.html async def fetch_many(urls): 第三关异步并发批量抓 asession AsyncHTMLSession() async def one(url): response await asession.get(url) await response.html.arender(scrolldown1, sleep1) return extract_from_page(response.html) return await asyncio.gather(*[one(u) for u in urls]) if __name__ __main__: # 单页 渲染 html fetch_dynamic(f{BASE}/index.html) print(本页课程数:, len(extract_from_page(html))) # 批量异步 pages [f{BASE}/list/{i}.html for i in range(1, 4)] all_items asyncio.run(fetch_many(pages)) merged {item[url]: item for batch in all_items for item in batch} print(批量去重后共:, len(merged), 门课程) for item in list(merged.values())[:5]: print(-, item[title])三个实战中高频踩到的坑坑一抓回来还是空。先确认是不是render()没等够。用response.html.search(xxx...{}yyy)或直接len(html.text)对比渲染前后的内容量能快速判断没抓到还是没渲染完。项目 README 里那个 pythonclock 例子就是标准诊断流程。坑二被反爬拦截。先把render()停掉观察——有时数据其实在静态页里渲染反而触发风控。请求头方面HTMLSession默认已经带了浏览器风格的 User-Agent如果还不行在会话上手动补Accept和Accept-Language并优先通过 Cookie 保持会话状态。坑三下载音频、文件这类资源。抓链接只是第一步真正下载时注意用流式写入别一把梭进内存同时控制并发别让目标服务器过载。让这套抓取方案自己转起来同事的脚本最后跑在了服务器上配了一个简单的定时任务每天凌晨抓一次更新课程表数据落成 JSON 供内部系统消费。想更进一步你可以把抓下来的数据接上 SQLite 做增量归档、接到企业微信/钉钉机器人做变更推送或者把课程页面整页导出成 Markdown 存档。requests-html 的价值在于它把请求—解析—渲染揉成了一个顺手的小工具静态页面用find()动态页面加一个render()量大就换AsyncHTMLSession。同样的三板斧换成抓图片、抓视频、抓文档思路完全一致只是选择器里的标签名不同。下次再遇到浏览器有、源码没有的页面别急着上 Selenium 全家桶——先试试把浏览器塞进 requests 里三行代码问题可能就没了。【免费下载链接】requests-htmlPythonic HTML Parsing for Humans™项目地址: https://gitcode.com/gh_mirrors/re/requests-html创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考