有一些站点它在 robots.txt 文件中设定了禁止浏览该站点的代理用户。既然目标站点有这样的规矩,我们就要遵循它。
使用urllib的robotparser模块来解析robots.txt文件,以避免下载禁止爬取的url 然后通过can_fetch()函数来判断指定的用户代理是否符合解析出的robots.txt规则。
urllib包含四大模块:request(请求)、error(异常处理)、parse(url解析,拼接,合并,编码)、robotparser(解析robots.txt文件)。
使用步骤:
1.创建一个解析器 rp = robotparser.RobotFileParser()
#返回一个解析器对象
2.设置robots.txt的url rp.set_url('http://example.python-scraping.com/robots.txt')
3.获取robots.txt的解析 rp.read()
#读出解析内容保存到解析对象中
4.匹配解析出的规则
allow = rp.can_fetch('BadCrawler','http://example.python-scraping.com/robots.txt')
#如果允许 useragent 按照被解析 robots.txt 文件中的规则来获取 url 则返回 True。对user_agent和要访问的url的双重限定。
from urllib import robotparser
#将创建RobotFileParser对象封装成函数,传入文件的链接,返回解析器对象
def get_robots_parser(robots_url):
rp = robotparser.RobotFileParser()
rp.set_url(robots_url)
rp.read()
return rp
不同于用户代理(代理浏览器标识),这里的代理指的是服务器代理,比如当本机ip被屏蔽访问不到该网页的服务器,就可以使用代理来逃过屏蔽。
相比于urllib库,python有一个处理http更友好的库requests库。 urllib是python标准库,就是你安装了python,这个库就已经可以直接使用了。
requests是第三方库,需要独立安装:pip install requests。一般建议使用requests,它是对urllib的再次封装,各种操作会更加简单。这里我们还用urllib来熟悉底层的操作。
操作十分简单,代码如下
from urllib import request
proxy = '60.7.208.137:9999'
proxy_support = urllib.request.ProxyHandler({'http':proxy})
opener = urllib.request.build_opener(proxy_support)
urllib.request.install_opener(opener)
爬取速度过快有时ip会被封禁或者造成服务器过载的风险,所以我们要在两次爬取之间添加一个延时
这里我们要用到urlparse模块解析url
python3中urlparse模块和urllib模块合并,urlparse()在urllib.parse中进行调用。 urlparse()把url拆分为6个部分,scheme(协议),netloc(域名),path(路径),params(可选参数), query(连接键值对),fragment(特殊锚),并且以元组形式返回。
创建Throttle类用于记录每个域名(域名由urlparse解析出)上次访问的时间,如果两次访问时间小于给出的时延,则执行睡眠操作暂停爬行一会儿
from urllib.parse import urlparse
import time
class Throttle:
def __init__(self, delay):
self.delay = delay #保存设定的两次下载之间的间隔
self.domains = {} #保存域名下载最新一次的时间戳
def wait(self, url):
domain = urlparse(url).netloc #netloc获取域名
last_accessed = self.domains.get(domain) #获取上次下载的时间
if self.delay > 0 and last_accessed is not None: #时延大于零并且有上次下载的时间戳
sleep_sec = self.delay - (time.time() - last_accessed) #本次应该休眠=设定的时延-据上次下载已用的时间
if sleep_sec > 0:
time.sleep(sleep_sec)
self.domains[domain] = time.time() #记录这次下载的时间戳
调用:
throttle = Throttle(delay)
......
throttle.wait(url)
html = GetData(link)
对于一些会动态生成页面内容的网页。比如一个在线日历网站,提供访问下个月和下一年的链接, 下个月又包含下下个月的页面,这样就会一直爬取,掉入爬虫陷阱。
简单的避免爬虫陷阱的方法是记录到达当前页面经过了多少个链接即深度, 设置一个阈值,到达最大深度时,爬虫不再向队列中添加该网页中的链接了。
方法:
将seen改为字典,保存已发现链接的深度记录。
其中用到了字典的get(key, parameter)函数 ,当能查询到匹配key的value时,就会返回相应key对应的value,如果不能的话,就会返回后面的这个参数。
将parameter设置为0,这就使得第一次爬行的域名理所当然的深度初始化为0。
以前面的爬行链接追踪为例修改为
def scrap_link(start_url, link_regex, robots_url = None, user_agent = 'wswp', max_depth =5):
if not robots_url:
robots_url = '{}/robots.txt'.format(start_url)
rp = get_robots_parser(robots_url)
crawl_queue = [start_url]
#seen = [start_url] # seen = set(crawl_queue) set函数返回‘set’对象,可以用seen.add()添加元素
seen = {}
while crawl_queue :
url = crawl_queue.pop()
if rp.can_fetch(user_agent, url):
depth = seen.get(url, 0)
if depth == max_depth:
print('深度超过最大深度,跳过该页')
continue
html = GetData(url, user_agent = user_agent)
if html is None:
continue
for link in getlinks(html):
if re.match(link_regex,link):
abs_link = urljoin(start_url, link)
if abs_link not in seen:
crawl_queue.append(abs_link)
seen[abs_link] = depth + 1 #其实depth在这里就反映了循环的层数,类似于树的按层遍历,每多一层深度加一
#返回一个页面的所有a标签的链接
def getlinks(html):
url_regex = re.compile("""]+href=["'](.*?)["']""", re.IGNORECASE)
return url_regex.findall(html) #或者re.findall(url_regex, html, re.IGNORECASE)
其中关键修改的地方为
def scrap_link(start_url, link_regex, robots_url = None, user_agent = 'wswp', max_depth =5):
.........
seen = {}
........
if rp.can_fetch(user_agent, url):
depth = seen.get(url, 0)
if depth == max_depth:
print('深度超过最大深度,跳过该页')
continue
.....
for link in getlinks(html):
if re.match(link_regex,link):
abs_link = urljoin(start_url, link)
if abs_link not in seen:
crawl_queue.append(abs_link)
seen[abs_link] = depth + 1 #其实depth在这里就反映了循环的层数,
requests是对urllib的再次封装,各种操作会更加简单。推荐大家先学习urllib库熟悉机理,之后可以主要使用requests库。
在我写的GetData函数中,部分功能可以用requests替换的更加简单,如:
1. #添加用户代理
request = urllib.request.Request(url)
request.add_header('User-Agent', user_agent)
response = urllib.request.urlopen(request)
#添加服务器代理
proxy_support = urllib.request.ProxyHandler({'http': proxy})
opener = urllib.request.build_opener(proxy_support)
urllib.request.install_opener(opener)
替换为
response = requests.get(url, headers = header, proxies = proxy)
2.#处理解码
cs = response.headers.get_content_charset()
html = response.read().decode(cs)
替换为
html = response.text
这里解释一下requests的各个函数的功能
requests.get()以get方式请求http页面
.text属性,http响应内容的字符串形式,自动化测试字符编码然后解码为字符串输出
更加详细的函数及属性见图
替换后的代码为
import requests
def GetData(url, user_agent = 'wswp', retry=2, proxy = None):
print('download : ' + url)
header = {'User-Agent', user_agent}
try:
response = requests.get(url, headers = header, proxies = proxy)
html = response.text
if response.status_code >= 400:
print('Download error', response.text)
html = None
if retry > 0 and 500 <= response.status_code <600:
return GetData(url, proxy_support,retry - 1)
except (URLError, HTTPError, ContentTooShortError) as e:
print('download error :', e.reason)
html = None
return html
相对原来的代码更加简短、结构也更清晰。