Python爬虫实现腾讯招聘自动翻页采集

发布 : 2017-04-22 分类 : Python 浏览 :

spider

1
2
3
4
5
6
7
8
9
10
11
Spider类定义了如何爬取某个(或某些)网站。
包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。
换句话说,Spider就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。

class scrapy.Spider是最基本的类,所有编写的爬虫必须继承这个类。

主要用到的函数及调用顺序为:

__init__() : 初始化爬虫名字和start_urls列表
start_requests() 调用make_requests_from url():生成Requests对象交给Scrapy下载并返回response
parse() : 解析response,并返回Item或Requests(需指定回调函数)。Item传给Item pipline持久化 , 而Requests交由Scrapy下载,并由指定的回调函数处理(默认parse()),一直进行循环,直到处理完所有的数据为止。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#所有爬虫的基类,用户定义的爬虫必须从这个类继承
class Spider(object_ref):

#定义spider名字的字符串(string)。spider的名字定义了Scrapy如何定位(并初始化)spider,所以其必须是唯一的。
#name是spider最重要的属性,而且是必须的。
#一般做法是以该网站(domain)(加或不加 后缀 )来命名spider。 例如,如果spider爬取 mywebsite.com ,该spider通常会被命名为 mywebsite
name = None

#初始化,提取爬虫名字,start_ruls
def __init__(self, name=None, **kwargs):
if name is not None:
self.name = name
# 如果爬虫没有名字,中断后续操作则报错
elif not getattr(self, 'name', None):
raise ValueError("%s must have a name" % type(self).__name__)

# python 对象或类型通过内置成员__dict__来存储成员信息
self.__dict__.update(kwargs)

#URL列表。当没有指定的URL时,spider将从该列表中开始进行爬取。 因此,第一个被获取到的页面的URL将是该列表之一。 后续的URL将会从获取到的数据中提取。
if not hasattr(self, 'start_urls'):
self.start_urls = []

# 打印Scrapy执行后的log信息
def log(self, message, level=log.DEBUG, **kw):
log.msg(message, spider=self, level=level, **kw)

# 判断对象object的属性是否存在,不存在做断言处理
def set_crawler(self, crawler):
assert not hasattr(self, '_crawler'), "Spider already bounded to %s" % crawler
self._crawler = crawler

@property
def crawler(self):
assert hasattr(self, '_crawler'), "Spider not bounded to any crawler"
return self._crawler

@property
def settings(self):
return self.crawler.settings

#该方法将读取start_urls内的地址,并为每一个地址生成一个Request对象,交给Scrapy下载并返回Response
#该方法仅调用一次
def start_requests(self):
for url in self.start_urls:
yield self.make_requests_from_url(url)

#start_requests()中调用,实际生成Request的函数。
#Request对象默认的回调函数为parse(),提交的方式为get
def make_requests_from_url(self, url):
return Request(url, dont_filter=True)

#默认的Request对象回调函数,处理返回的response。
#生成Item或者Request对象。用户必须实现这个类
def parse(self, response):
raise NotImplementedError

@classmethod
def handles_request(cls, request):
return url_is_from_spider(request.url, cls)

def __str__(self):
return "<%s %r at 0x%0x>" % (type(self).__name__, self.name, id(self))

__repr__ = __str__

主要属性和方法

name

1
2
3
定义spider名字的字符串。

例如,如果spider爬取 mywebsite.com ,该spider通常会被命名为 mywebsite

allowed_domains

1
包含了spider允许爬取的域名(domain)的列表,可选。

start_urls

1
初始URL元祖/列表。当没有制定特定的URL时,spider将从该列表中开始进行爬取。

start_requests(self)

1
2
3
4
该方法必须返回一个可迭代对象(iterable)。
该对象包含了spider用于爬取(默认实现是使用 start_urls 的url)的第一个Request。

当spider启动爬取并且未指定start_urls时,该方法被调用。

parse(self, response)

1
2
当请求url返回网页没有指定回调函数时,默认的Request对象回调函数。
用来处理网页返回的response,以及生成Item或者Request对象。

log(self, message[, level, component])

1
使用scrapy.log.msg()方法记录(log)message。

Python实现数据自动翻页采集

新建Scrapy项目

1
scrapy startproject tencentspider

Markdown

创建爬虫

1
scrapy genspider tencent "tencent.com"

Markdown

编写items.py

1
获取职位名称、详细信息
1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-

import scrapy

class TencentspiderItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field()
detailLink = scrapy.Field()
positionInfo = scrapy.Field()
peopleNumber = scrapy.Field()
workLocation = scrapy.Field()
publicTime = scrapy.Field()

编写tencent.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -*- coding: utf-8 -*-
import scrapy
import re
from tencentspider.items import TencentspiderItem


class TencentSpider(scrapy.Spider):
name = "tencent"
allowed_domains = ["hr.tencent.com"]
start_urls = ['http://hr.tencent.com/position.php?&start=0#a']

def parse(self, response):
oddList = response.xpath('//*[@class="odd"]')
evenList = response.xpath('//*[@class="even"]')

for odd,even in zip(oddList, evenList):
odditem = TencentspiderItem()
evenitem = TencentspiderItem()
odditem['name'] = odd.xpath('./td[1]/a/text()').extract()[0]
odditem['detailLink'] = odd.xpath('.//td[1]/a/@href').extract()[0]
odditem['positionInfo'] = odd.xpath('./td[2]/text()').extract()[0]
odditem['peopleNumber'] = odd.xpath('./td[3]/text()').extract()[0]
odditem['workLocation'] = odd.xpath('./td[4]/text()').extract()[0]
odditem['publicTime'] = odd.xpath('./td[5]/text()').extract()[0]
evenitem['name'] = even.xpath('./td[1]/a/text()').extract()[0]
evenitem['detailLink'] = even.xpath('.//td[1]/a/@href').extract()[0]
evenitem['positionInfo'] = even.xpath('./td[2]/text()').extract()[0]
evenitem['peopleNumber'] = even.xpath('./td[3]/text()').extract()[0]
evenitem['workLocation'] = even.xpath('./td[4]/text()').extract()[0]
evenitem['publicTime'] = even.xpath('./td[5]/text()').extract()[0]
# 设置自动翻页
curpage = re.search('(\d+)', response.url).group(1)
page = int(curpage) + 10
url = re.sub('(\d+)', str(page), response.url)
# 发送新的url请求加入待爬队列,并调用回调函数self.parse
yield scrapy.Request(url, callback=self.parse)
# 将获取的数据交给pipeline
yield odditem
yield evenitem

编写pipeline.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html

import json


class TencentspiderPipeline(object):
def __init__(self):
self.file = open('tencent.json', 'wb')

def process_item(self, item, spider):
content = json.dumps(dict(item), ensure_ascii=False) + "\n"
self.file.write(bytes(content, encoding='utf-8'))
return item

def close_spider(self, spider):
self.file.close()

在setting.py里设置ITEM_PIPELINES

1
2
3
ITEM_PIPELINES = {
'tencentspider.pipelines.TencentspiderPipeline': 300,
}

Markdown

执行爬虫

1
scrapy crawl tencent

Markdown

parse()方法的工作机制

1
2
3
4
5
6
7
8
9
10
1.因为使用的yield,而不是return。parse函数将会被当做一个生成器使用。
scrapy会逐一获取parse方法中生成的结果,并判断该结果是一个什么样的类型;
2.如果是request则加入爬取队列,如果是item类型则使用pipeline处理,其他类型则返回错误信息。
3.scrapy取到第一部分的request不会立马就去发送这个request,只是把这个request放到队列里,然后接着从生成器里获取;
4.取尽第一部分的request,然后再获取第二部分的item,取到item了,就会放到对应的pipeline里处理;
5.parse()方法作为回调函数(callback)赋值给了Request,指定parse()方法来处理这些请求 scrapy.Request(url, callback=self.parse)
6.Request对象经过调度,执行生成scrapy.http.response()的响应对象,并送回给parse()方法,直到调度器中没有Request(递归的思路)
7.取尽之后,parse()工作结束,引擎再根据队列和pipelines中的内容去执行相应的操作;
8.程序在取得各个页面的items前,会先处理完之前所有的request队列里的请求,然后再提取items。
7.这一切的一切,Scrapy引擎和调度器将负责到底。

CrawlSpiders

创建CrawlSpider模板

1
scrapy genspider -t crawl tencent tencent.com
1
2
3
4
5
6
7
class scrapy.spiders.CrawlSpider

它是Spider的派生类,Spider类的设计原则是只爬取start_url列表中的网页,

而CrawlSpider类定义了一些规则(rule)来提供跟进link的方便的机制,

从爬取的网页中获取link并继续爬取的工作更适合。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class CrawlSpider(Spider):
rules = ()
def __init__(self, *a, **kw):
super(CrawlSpider, self).__init__(*a, **kw)
self._compile_rules()

#首先调用parse()来处理start_urls中返回的response对象
#parse()则将这些response对象传递给了_parse_response()函数处理,并设置回调函数为parse_start_url()
#设置了跟进标志位True
#parse将返回item和跟进了的Request对象
def parse(self, response):
return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)

#处理start_url中返回的response,需要重写
def parse_start_url(self, response):
return []

def process_results(self, response, results):
return results

#从response中抽取符合任一用户定义'规则'的链接,并构造成Resquest对象返回
def _requests_to_follow(self, response):
if not isinstance(response, HtmlResponse):
return
seen = set()
#抽取之内的所有链接,只要通过任意一个'规则',即表示合法
for n, rule in enumerate(self._rules):
links = [l for l in rule.link_extractor.extract_links(response) if l not in seen]
#使用用户指定的process_links处理每个连接
if links and rule.process_links:
links = rule.process_links(links)
#将链接加入seen集合,为每个链接生成Request对象,并设置回调函数为_repsonse_downloaded()
for link in links:
seen.add(link)
#构造Request对象,并将Rule规则中定义的回调函数作为这个Request对象的回调函数
r = Request(url=link.url, callback=self._response_downloaded)
r.meta.update(rule=n, link_text=link.text)
#对每个Request调用process_request()函数。该函数默认为indentify,即不做任何处理,直接返回该Request.
yield rule.process_request(r)

#处理通过rule提取出的连接,并返回item以及request
def _response_downloaded(self, response):
rule = self._rules[response.meta['rule']]
return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow)

#解析response对象,会用callback解析处理他,并返回request或Item对象
def _parse_response(self, response, callback, cb_kwargs, follow=True):
#首先判断是否设置了回调函数。(该回调函数可能是rule中的解析函数,也可能是 parse_start_url函数)
#如果设置了回调函数(parse_start_url()),那么首先用parse_start_url()处理response对象,
#然后再交给process_results处理。返回cb_res的一个列表
if callback:
#如果是parse调用的,则会解析成Request对象
#如果是rule callback,则会解析成Item
cb_res = callback(response, **cb_kwargs) or ()
cb_res = self.process_results(response, cb_res)
for requests_or_item in iterate_spider_output(cb_res):
yield requests_or_item

#如果需要跟进,那么使用定义的Rule规则提取并返回这些Request对象
if follow and self._follow_links:
#返回每个Request对象
for request_or_item in self._requests_to_follow(response):
yield request_or_item

def _compile_rules(self):
def get_method(method):
if callable(method):
return method
elif isinstance(method, basestring):
return getattr(self, method, None)

self._rules = [copy.copy(r) for r in self.rules]
for rule in self._rules:
rule.callback = get_method(rule.callback)
rule.process_links = get_method(rule.process_links)
rule.process_request = get_method(rule.process_request)

def set_crawler(self, crawler):
super(CrawlSpider, self).set_crawler(crawler)
self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True)
1
CrawlSpider继承于Spider类,除了继承过来的属性外(name、allow_domains),还提供了新的属性和方法:

LinkExtractors

1
2
3
4
5
6
7
class scrapy.linkextractors.LinkExtractor

Link Extractors的目的很简单:提取链接。

每个LinkExtractor有唯一的公共方法是extract_links(),它接收一个Response对象,并返回一个scrapy.link.Link对象。

Link Extractors要实例化一次,并且extract_links方法会根据不同的response调用多次提取链接。
1
2
3
4
5
6
7
8
9
10
11
12
13
class scrapy.linkextractors.LinkExtractor(
allow = (),
deny = (),
allow_domains = (),
deny_domains = (),
deny_extensions = None,
restrict_xpaths = (),
tags = ('a','area'),
attrs = ('href'),
canonicalize = True,
unique = True,
process_value = None
)

主要参数

1
2
3
4
5
6
7
8
9
allow:满足括号中"正则表达式"的值会被提取,如果为空,则全部匹配

deny:与这个正则表达式(或正则表达式列表)不匹配的URL一定不提取

allow_domains:会被提取的链接的domains

deny_domains:一定不会被提取链接的domains

restrict_xpaths:使用xpath表达式,和allow共同作用过滤链接

rules

1
2
3
在rules中包含一个或多个Rule对象,每个Rule对爬取网站的动作定义了特定操作

如果多个rule匹配了相同的链接,则根据规则在本集合中被定义的顺序,第一个会被使用
1
2
3
4
5
6
7
8
class scrapy.spiders.Rule(
link_extractor,
callback = None,
cb_kwargs = None,
follow = None,
process_links = None,
process_request = None
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
link_extractor:是一个Link Extractor对象,用于定义需要提取的链接。

callback:从link_extractor中每获取到链接时,参数所指定的值作为回调函数,该回调函数接受一个response作为其第一个参数。

注意:当编写爬虫规则时,避免使用parse作为回调函数。
由于CrawlSpider使用parse方法来实现其逻辑,
如果覆盖了parse方法,crawl spider将会运行失败。

follow:是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进。 如果callback为None,follow默认设置为True ,否则默认为False。

process_links:指定该spider中哪个的函数将会被调用,
从link_extractor中获取到链接列表时将会调用该函数,该方法主要用来过滤。

process_request:指定该spider中哪个的函数将会被调用,
该规则提取到每个request时都会调用该函数。(用来过滤request)

爬取规则(Crawling rules)

1
以上面获取腾讯招聘的数据为例,给出配合rule使用CrawlSpider的例子:

运行Scrapy shell

1
scrapy shell http://hr.tencent.com/position.php?&start=0#a

导入LinkExtractor,创建LinkExtractor实例对象

1
2
3
from scrapy.linkextractors import LinkExtractor

page_lx = LinkExtractor(allow=('position.php?&start=\d+'))
1
2
3
4
5
allow : LinkExtractor对象最重要的参数之一,这是一个正则表达式,
必须要匹配这个正则表达式(或正则表达式列表)的URL才会被提取,
如果没有给出(或为空), 它会匹配所有的链接。

deny : 用法同allow,只不过与这个正则表达式匹配的URL不会被提取)。它的优先级高于 allow 的参数,如果没有给出(或None), 将不排除任何链接。

调用LinkExtractor实例的extract_links()方法查询匹配结果

1
page_lx.extract_links(response)

没有查到

1
[]

注意转义字符的问题,继续重新匹配

1
2
page_lx = LinkExtractor(allow=('position\.php\?&start=\d+'))
page_lx.extract_links(response)

Markdown

Python实现数据自动翻页采集(CrawlSpider版本)

由于CrawlSpider使用parse方法来实现其逻辑,如果覆盖了 parse方法,crawl spider将会运行失败。

1
scrapy shell测试完成之后,修改以下代码

tencent.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# -*- coding: utf-8 -*-
import scrapy
from scrapy.spider import CrawlSpider, Rule
import re
from tencentspider.items import TencentspiderItem
from scrapy.linkextractors import LinkExtractor


class TencentSpider(CrawlSpider):
name = "tencent"
allowed_domains = ["hr.tencent.com"]
start_urls = ['http://hr.tencent.com/position.php?&start=0#a']

page_lx = LinkExtractor(allow=("start=\d+"))

rules = [
Rule(page_lx, callback="parseContent", follow=True)
]

def parseContent(self, response):
oddList = response.xpath('//*[@class="odd"]')
evenList = response.xpath('//*[@class="even"]')

for odd, even in zip(oddList, evenList):
odditem = TencentspiderItem()
evenitem = TencentspiderItem()
odditem['name'] = odd.xpath('./td[1]/a/text()').extract()[0]
odditem['detailLink'] = odd.xpath('.//td[1]/a/@href').extract()[0]
odditem['positionInfo'] = odd.xpath('./td[2]/text()').extract()[0]
odditem['peopleNumber'] = odd.xpath('./td[3]/text()').extract()[0]
odditem['workLocation'] = odd.xpath('./td[4]/text()').extract()[0]
odditem['publicTime'] = odd.xpath('./td[5]/text()').extract()[0]
evenitem['name'] = even.xpath('./td[1]/a/text()').extract()[0]
evenitem['detailLink'] = even.xpath('.//td[1]/a/@href').extract()[0]
evenitem['positionInfo'] = even.xpath('./td[2]/text()').extract()[0]
evenitem['peopleNumber'] = even.xpath('./td[3]/text()').extract()[0]
evenitem['workLocation'] = even.xpath('./td[4]/text()').extract()[0]
evenitem['publicTime'] = even.xpath('./td[5]/text()').extract()[0]
# 将获取的数据交给pipeline
yield odditem
yield evenitem

运行scrapy

1
scrapy crawl tencent

Markdown

Logging

1
Scrapy提供了log功能,可以通过logging模块使用

可以修改配置文件settings.py,任意位置添加下面两行,效果会清爽很多。

1
2
LOG_FILE = "TencentSpider.log"
LOG_LEVEL = "INFO"

Log levels

1
2
3
4
5
6
7
8
Scrapy提供5层logging级别:

CRITICAL - 严重错误(critical)

ERROR - 一般错误(regular errors)
WARNING - 警告信息(warning messages)
INFO - 一般信息(informational messages)
DEBUG - 调试信息(debugging messages)

logging设置

1
2
3
4
5
6
7
通过在setting.py中进行以下设置可以被用来配置logging:

LOG_ENABLED 默认: True,启用logging
LOG_ENCODING 默认: 'utf-8',logging使用的编码
LOG_FILE 默认: None,在当前目录里创建logging输出文件的文件名
LOG_LEVEL 默认: 'DEBUG'log的最低级别
LOG_STDOUT 默认: False如果为True,进程所有的标准输出(及错误)将会被重定向到log中。例如,执行print"hello",其将会在Scrapy log中显示。
本文作者 : Matrix
原文链接 : https://matrixsparse.github.io/2017/04/22/python爬虫实现腾讯招聘自动翻页采集/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

知识 & 情怀 | 二者兼得

微信扫一扫, 向我投食

微信扫一扫, 向我投食

支付宝扫一扫, 向我投食

支付宝扫一扫, 向我投食

留下足迹