在上一节里面,我定义了结构化字段Item,然而并没有用到它。
所以,为了能够将有用的信息整理到Item中去,我们需要了解一下提取页面有效信息的办法。
- 这里要用到一个小工具——Selectors(选择器)。
Selector是Scrapy内置的功能,它支持使用XPath和CSS Selector两种表达式来对信息进行搜索。
如果有同学用过正则表达式的话,对以上两种语言应该有些感性认识了。不过比起正则,也许BeautifulSoup插件中的find Tag与Selector更加相似,它们都可以很方便地对提取出html中的标签。(其实写这段我有点不确定的,因为据说XPath应该是一门在 XML 文档中查找信息的语言,XML和HTML之间的差异,我理解得不够深刻。)
这里附上XPath的教程:http://www.w3school.com.cn/xpath/index.asp
Step1:网页源代码分析
现在,我们就打开上一节中我们保存下来的tencent.txt,我从中选取了一小段我们比较感兴趣的数据。
<tr class="odd">
<td class="l square"><a target="_blank" href="position_detail.php?id=42477&keywords=&tid=0&lid=0">SNG16-腾讯音乐多媒体AI研究员(深圳)</a></td>
<td>技术类</td>
<td>3</td>
<td>深圳</td>
<td>2018-07-14</td>
</tr>
<tr class="even">
<td class="l square"><a target="_blank" href="position_detail.php?id=42471&keywords=&tid=0&lid=0">22989-专有云网络运维工程师(北京/上海/深圳)</a><span class="hot"> </span></td>
<td>技术类</td>
<td>2</td>
<td>深圳</td>
<td>2018-07-14</td>
</tr>
<tr class="odd">
<td class="l square"><a target="_blank" href="position_detail.php?id=42472&keywords=&tid=0&lid=0">22989-专有云数据库运维工程师(北京/上海/深圳)</a><span class="hot"> </span></td>
<td>技术类</td>
<td>2</td>
<td>深圳</td>
<td>2018-07-14</td>
</tr>
初看杂乱的信息中也是有不少规律的嘛。
看第一行中的odd
和下面几行中的even
,再结合刚才看到的原网页,可以猜出这分别代表了表格中的奇数行和偶数行,他们的背景色不一样的。
经过浏览器的渲染,摘录的这段呈现在我们眼中,就是表格的最后三行了。
现在回头看一下上节笔记中定义的Item信息,就可以一一对应上了,以最后一条为例:
- name = 22989-专有云数据库运维工程师(北京/上海/深圳)
- detailLink = position_detail.php?id=42472&keywords=&tid=0&lid=0
- catalog = 技术类
- recruitNumber = 2
- workLocation = 深圳
- publishTime = 2018-07-14
固然我们可以手动从中挑出想要的信息来,但最终的目的依然是让程序帮我们完成一切,我们对html源代码的关注,是为了找出有用信息的特征。
- 可以看出一对
<tr>
包括了一条招聘信息,而<td>
括起了一条信息中的各个元素。
那是不是如果按顺序提取出(tr[1],tr[2],……)再索引到(td[1],td[2],……)就可以对应上我们想要的信息了呢?
没错,不过在XPath里有更简单形式的语法能够帮我们找到他们。
Step2:XPath语法
表达式 | 描述 |
---|---|
nodename | 选取nodename节点的所有子节点。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
[ ] | 根据中括号的内容作筛选 |
* | 通配符:选择所有元素节点 |
还有一个表达式符号我想单独拿出来说,就是/
,大家知道/
在系统里用作文件夹层次的分隔,同样地,/
应该配合以上表达式食用。单独的/
表示根节点位置。
除了表达式,XPath还有一类构成要素:运算符。运算符可以连接表达式,以更方便地完成任务。我列出了感觉会常用的,想了解更多可以查看这个链接:http://www.w3school.com.cn/xpath/xpath_operators.asp
运算符 | 描述 | 实例 | 返回值 |
---|---|---|---|
| | 计算两个节点集 | //book | //cd | 返回所有拥有 book 和 cd 元素的节点集 |
>= | 大于或等于 | price>=9.80 | 如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。 |
or | 或 | price=9.80 or price=9.70 | 如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。 |
and | 与 | price>9.00 and price<9.90 | 如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。 |
<tr class="odd">
<td class="l square"><a target="_blank" href="position_detail.php?id=42477&keywords=&tid=0&lid=0">SNG16-腾讯音乐多媒体AI研究员(深圳)</a></td>
<td>技术类</td>
<td>3</td>
<td>深圳</td>
<td>2018-07-14</td>
</tr>
表格中信息的特征很好辨认,奇数行的内容一定具有属性 "odd",我们可以编写表达式把奇数行的找出来:
'//*[@class="odd"]'
- 表达式的解释
刚才提到/
可以表示根结点,那么//
又是什么意思呢,它表示从当前节点开始递归下降,此路径运算符出现在模式开头时,表示应从根节点递归下降。
所以代码的意思就是:从根节点开始寻找所有符合要求的节点,要求是该行属性为奇数行。
可是一般来说我们关注的对象可不仅仅是奇数行,还有偶数行呢。所以啊用上面提到的节点集符号|
修改一下表达式就可以了:
//*[@class="even"] | //*[@class="odd"]
这样,我们就找到了一条招聘信息的节点,为了能够分别保存职位名称、类型、工作地点等信息,我们需要对上一步找到的节点做进一步处理:
'./td[1]/a/text()'#当前节点中,第1个td节点里,a节点内的文字:name
'./td[1]/a/@href'#当前节点中,第1个td节点里,a节点内,herf属性的内容:detailLink
'./td[2]/text()'#当前节点中,第2个td节点里,a节点内的文字:catalog
'./td[3]/text()'#当前节点中,第3个td节点里,a节点内的文字:recruitNumber
'./td[4]/text()'#当前节点中,第4个td节点里,a节点内的文字:workLocation
'./td[5]/text()'#当前节点中,第5个td节点里,a节点内的文字:publishTime
一一对应的关系找到了,恭喜我们,终于理解XPath的初级用法了。接下来要把命令交给爬虫执行,就需要把它放在tencent.py的parse函数中。
Step3:编写爬取代码
打开tencent.py,在开头添加上我们对items.py里定义好的结构化字段的引用,然后修改整个文档的代码如下:
import scrapy
from tutorial.items import RecruitItem
class RecruitSpider(scrapy.Spider):
name = "tencent"
allowed_domains = ["hr.tencent.com"]
start_urls = [
"http://hr.tencent.com/position.php?&start=0#a"
]
def parse(self, response):
for sel in response.xpath('//*[@class="even"] | //*[@class="odd"]'):
name = sel.xpath('./td[1]/a/text()').extract()[0]
detailLink = sel.xpath('./td[1]/a/@href').extract()[0]
catalog = sel.xpath('./td[2]/text()').extract()[0]
recruitNumber = sel.xpath('./td[3]/text()').extract()[0]
workLocation = sel.xpath('./td[4]/text()').extract()[0]
publishTime = sel.xpath('./td[5]/text()').extract()[0]
item = RecruitItem()
item['name'] = name.encode('utf-8')
item['detailLink'] = detailLink.encode('utf-8')
item['catalog'] = catalog.encode('utf-8')
item['recruitNumber'] = recruitNumber.encode('utf-8')
item['workLocation'] = workLocation.encode('utf-8')
item['publishTime'] = publishTime.encode('utf-8')
yield item
代码执行部分结构有3层:
-
for sel in response.xpath('//*[@class="even"] | //*[@class="odd"]'):
首先找出页面中所有符合表格奇行和偶行特征的节点,然后用sel这个临时变量去遍历他们; -
name = sel.xpath('./td[1]/a/text()').extract()[0]
用XPath表达式去寻找sel中符合要求的元素,分别存入临时的字段里。
至于为什么语法是这样的,可以参考:
xpath().extract()和xpath().extract()[0] 的区别? - 知乎用户的回答 - 知乎
https://www.zhihu.com/question/63370553/answer/247633004 -
item['name'] = name.encode('utf-8')
将临时字段里的内容转码成UTF-8再存入item的各字段中。yield类似于C语言的return。
至此代码部分就编写完成啦~
Step4:爬取信息
来来,再度回到\tutorial文件夹下,运行终端,输入:
scrapy crawl tencent -o items.json
即在当前目录下生成了items.json。
JSON语言能够方便地被读取,我理解为轻量的数据库。
我是第一次处理json格式的语言,所以在用NPP记事本打开文档的时候,望着格式一团糟的json头晕了。不过为了降低学习成本,暂时还没有开数据库的坑。那就找一个能够方便地把UTF-8代码显示成汉字的工具吧。
我是选择了NPP的插件JSON Viewer,可以对JSON可视化排版,也能把UTF-8翻译成汉字,大概是这样的:
嗯……就是这样。
但是这个界面……求一款颜值高的本地工具,谢谢。