之前做了一些数据抓取的工作,期间也踩了一些坑,所以有了这篇文章。

动态网页数据源获取

需要抓取的页面是使用React JavaScript 框架开发的,所有的页面都是客户端渲染而成,这也就导致我只能看到一个个的 data-id ,没有办法直接获取数据。这就涉及到一个我之前没有接触过的领域——动态网页爬虫。 一番 Google 之后,我了解到动态网页爬虫大致上可以通过以下两种方法实现:

  • 分析网页代码结构和请求,找到数据源的请求链接
  • 调用Webkit渲染之后再进行抓取

第二种方法相当于在命令行中跑一个浏览器,一个页面一个页面的打开,效率可想而知。再加上待抓取页面的 DOM 结构本来就比较复杂,没有添加相应的 class 和 id,导致即使渲染出来了想要抓到自己需要的数据也非常费劲。 于是只能采用第一种方案:分析了一下网页的代码之后发现所有的数据都是通过一个接口返回的。使用 Chrome 审查工具中的 Network 工具可以获取到所有的网络请求,在里面搜索 JSON ,找到了一个 JSON 的请求。点开一看正是我们需要的数据,解决了动态网页数据源的问题。

分类不统一

这个坑主要出在自己对目标网页的数据特性挖掘的不够。一开始以为目标网页是按照一个特定的分类来区分的,但是后来发现这个标准并不统一,最后抓取到的数据不在一个维度上。正当自己准备开工写很多特判的时候发现,如果从另外一个维度来索取数据的话,所有的数据都是统一的。 在这个案例中,就是将人为的分类切换成通过价格来获取数据,通过选择所有价格,就能获取到所有的数据,不需要再对不同维度的分类进行特判。 这个与其说是技术问题,更多的是一个经验的问题。

页面内部JS执行

这个坑就比较有趣了。 目标网页除了通过一个特定的接口获取数据之外,还会在页面内部通过 JavaScript 来直接传递数据。背后的技术考量不得而知,但是摆在我面前的问题就是我要如何获取这些 JavaScript 代码中的数据。 思考了一下之后想到了两种方案:

  • 自行匹配需要的字符串
  • 通过 phatomjs 等工具执行页面内部的 js 代码,并输出需要的数据变量

自行匹配的问题在于,我需要匹配的字符串的格式不一,很难直接匹配出我需要的数据。而通过 phatomjs 执行,就能比较好的解决这个问题。

一个比较脏的解决方案是这样的:

  1. 下载整个HTML页面到 test.html
  2. 通过 bs4 获取到所有的 <script> 标签内部的内容
  3. 将我们需要的那个标签输出到一个 data.js 文件中
  4. 之后把将数据构造成 json 的 js 代码写入 data.js 文件
  5. 通过 phatomjs 来执行代码
  6. 将输出通过 json.loads 载入并 append 到我们的数据数组中

这样,我们就获得了页面内部js代码中数据的json形式。

phatomjs 中执行的代码最后,千万要记得加上 phatom.exit(),否则不会自行退出。

phatomjs报错

https://cli.xuanwo.io/Tools/phatomjs.html#qxcbconnection-could-not-connect-to-display

当代码放到服务器上运行时候,出现了这样的报错:

QXcbConnection: Could not connect to display

这是因为源中的phatomjs默认运行在图形界面下,只需要在运行前执行:

export QT_QPA_PLATFORM=offscreen

即可。

线程调度

这个坑就比较隐蔽了,重复调试了很久。 在前面的流程中,我们有一个下载HTML页面并使用bs4解析的步骤。我之前的实现是通过subprocess.Popen()直接调用 curl 之后,就打开test.html。这样的实现导致了这样的一个问题:有可能网页还没有下载完,我就开始进行解析了,这样就会导致我的解析内容跟本就不正确。也就是说,subprocess.Popen() 不是一个阻塞的过程,它在调用完 curl 之后不会等到 curl 返回再结束。 定位到问题的话,解决起来就很容易了。通过查阅文档,我知道了可以通过这种方法来保证命令执行完毕再执行下一行代码:

child = subprocess.Popon("curl xxxx.com > test.html", shell=True)
child.wait()

回顾 & 总结

这个小小的玩意儿开发没花多久,但是学到了很多东西。从之前自己一直以为很难不敢尝试的动态网页抓取到 Python subprocess 线程调度,果然不踩坑就不会有新的收获。 这次开发的东西比较敏感,涉及到公司内部的一些事务,所以代码就不开源出来了。有什么想法或者问题可以直接在评论区里提出来,我会尽量回复的。因为是一个一次性的小套件,所以没有怎么考虑优化上的事情,如果有更好的解决方案,也欢迎大家一起探讨,说不定下次就用上了呢~