之所以需要写网页爬虫,无非是因为无法直接读取对方数据库,需要借助于网页将需要的文字和图片等信息抓取下来。不管使用什么样的工具,我们最终都需要对抓取的网页做进一步的结构化处理以获取我们需要的信息,这一篇博客主要介绍基于 Python 的轻量级网页抓取及信息抽取工具。以代码实战分析为主,原理介绍为辅。
网页爬虫基础知识
在学习网页爬虫之前,我们需要了解网页的组成及浏览器渲染过程,了解浏览器渲染过程有助于我们使用浏览器自带的开发者工具高效定位到我们需要的元素。 现代网页结构还是由三剑客 HTML + CSS + JavaScript 组成,HTML 构成我们常常看到的文字等主体内容,CSS 则负责对主体内容进行排版及美化,JS(JavaScript) 主要用于和用户交互,具备动态改变网页内容及样式的能力,所以 JS 在现代网页中的使用也是越来越广泛。从以上三剑客扮演的不同角色我们可以看出如果我们要抓取的网页信息都在原始 HTML 中(也俗称静态网页),后期的信息抽取工作是最轻松的。然而现在越来越多的网站反爬虫措施逐步升级,很多关键信息都需要使用 JS 动态生成,这个时候我们往往需要借助浏览器的帮助。 网页基础部分不再赘述,左耳朵耗子博客上的 浏览器的渲染原理简介 对浏览器的工作过程介绍的非常好。
网页信息抽取 - PyQuery
对于静态网页,我倾向于使用 pyquery 来对网页进行解析抽取,依赖于 lxml 解析 HTML, 速度非常快,而且 PyQuery 使用起来和 jQuery 的语法特别相似,抽取指定 id/class 等可以说是得心应手,基本不需要依赖正则表达式进行抽取,与 XPath 查询相比显得简单多了。PyCon Taiwan 2017 有一个很好的介绍 - 比美麗的湯更美麗:pyquery, 相应的 YouTuBe 视频, 这段视频详细介绍了 PyQuery 和其他同类工具的优劣及与其他框架结合使用的经验。
动态网页信息抽取
对于动态网页,我们需要结合 JS 引擎动态生成所需内容,这里我们可以使用 Selenium 浏览器自动化测试框架打开一个浏览器供程序操作 - Selenium with Python 对于反爬限制较为严格的网站可以试试 Gecko(Firefox)
静态网页代码实战
这里我们以 Lintcode 为例(仅作为技术交流用途…),爬虫的目的是抓取题目标题、描述、标签和难度等信息并转写为 markdown. 源网页地址:http://www.lintcode.com/en/proble/palindrome-number/ 完整代码可参考 https://github.com/billryan/algorithm-exercise/blob/master/scripts/lintcode.py
- 使用 PyQuery/requests 打开链接
对于不需要登录即可打开的网页,我们可以直接在 PyQuery 中打开,如果需要其他登录信息,我们还可以借助 requests 模拟登录,将最终获取的网页原始文件传给 pq 即可。对于部分登录需要图形验证码的网站,我们还可以借助百度提供的个人免费 OCR API 进行破解。
from pyquery import PyQuery as pq
class Lintcode(object):
def __init__(self):
self.driver = None
def open_url(self, url):
self.url = url
print('open URL: {}'.format(url))
self.driver = pq(url=url)
- 获取网页标题
网页标题通常在 title 全局属性中,在 pq 中调用 title
即可得
def get_title(self):
print('get title...')
title = self.driver('title').text()
return title
- 获取题目描述
从 Chrome 『查看元素』可知 Lintcode 中的题目描述由 description id 下的前两个 m-t-lg
类组成。这里我们可以使用 jQuery 中 .classname:nth-child(n)
语法获取。
def get_description(self):
print('get description...')
desc_pq = self.driver('#description')
desc_html = desc_pq('.m-t-lg:nth-child(1)').html()
example_html = desc_pq('.m-t-lg:nth-child(2)').html()
return desc_html + example_html
- 获取题目难度
从 Chrome 『查看元素』可知题目难度信息隐藏于一 CSS 类的属性 data-original-title
中,故可使用 .attr()
获取。
def get_difficulty(self):
print('get difficulty...')
progress_bar = self.driver('.progress-bar')
original_title = progress_bar.attr('data-original-title')
splits = original_title.strip().split(' ')
difficulty = splits[1]
ac_rate = splits[-1]
return difficulty
- 获取题目标签信息
题目标签信息在 tags id 的 CSS 类 tags
的 a
标签属性列表的文本中,我们可对其进行迭代获取之。
def get_tags(self):
print('get tags...')
tags = []
for i in self.driver('#tags.tags a'):
tags.append(i.text)
return tags
动态网页代码实战
动态网页中我们可以使用浏览器生成 JS 内容进而进行信息抽取。这里我们以 Leetcode 为例(仅作为技术交流用途…),爬虫的目的同 Lintcode. 源网页地址:https://leetcode.com/problems/palindrome-number/ 完整代码可参考 https://github.com/billryan/algorithm-exercise/blob/master/scripts/leetcode.py
- 使用 webdriver 打开
使用 headless 的 Chrome webdriver 打开,同时禁用 GPU 渲染,使用 Chrome 打开后才能获得 JS 动态生成的文本,由于 Selenium 自带的信息抽取方法凑合够用,这里使用 webdriver 自带的方法进行演示,同时便于和 PyQuery 的使用方法进行对比。
from selenium import webdriver
class Leetcode(object):
def __init__(self):
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
self.driver = webdriver.Chrome(chrome_options=chrome_options)
def open_url(self, url):
self.url = url
print('open URL: {}'.format(url))
self.driver.get(url)
- 获取题目描述
Leetcode 的题目描述在 CSS 类 question-description
中,这里我们可以通过调用 find_element_by_class_name
, 返回 HTML 时可使用 get_attribute('innerHTML')
def get_description(self):
print('get description...')
elem = self.driver.find_element_by_class_name('question-description')
return elem.get_attribute('innerHTML')
其它如获取标题等方法不再赘述,参考完整版代码。
番外篇 - 使用 AnyProxy 分析没有网站的移动端应用
除了通常的网页抓取外,有些应用可能只有移动端而并没有网页端,这个时候我们就需要借助网络代理对 HTTP/HTTPS 请求进行拦截分析,能完成这一需求的有 Windows 下的 fiddle, Mac 下的 Charles(需要付费) 除了这些特定平台的代理诊断工具外,我们还可以使用阿里开源的 node 应用 AnyProxy, HTTPS 中间人拦截使用也非常方便。