一、引言与前期准备
1.1 深入了解一个网站的方法
检查robots.txt(了解抓取该网站时存在哪些限制)
检查sitemap(一般在robots.txt中能找到,这里提供了该网站的所有网页的链接)
估算网站大小(一个简便方法是检查Google爬虫的结果 -> site:www.baidu.com)
识别网站所用技术(python中的builtwith方法 -> builtwith.parse("www.baidu.com") )
寻找网站所有者(whois查询:站长或者python中的 -> print(whois.whois("www.baidu.com")) )
1.2 网页访问error异常问题
#此代码实现并测试爬取下载网页中的几个问题或方法:遇到5xx错误重试下载;设置用户代理
import urllib.request
import urllib.error
def download(url, user_agent = "brain", num_retries = 2): #下载网页
print("downloading:",url)
header = {"user-agent": user_agent} #设置用户代理,而不使用python默认的用户代理Python-urllib/3.6
req = urllib.request.Request(url, headers = header)
try:
html = urllib.request.urlopen(req).read()
except urllib.error.URLError as e: #下载过程中出现问题
print("download error:",e.reason)
html = None
if num_retries > 0: #错误4XX发生在请求存在问题,而5XX错误则发生在服务端存在问题,所以在发生5XX错误时重试下载
if hasattr(e, "code") and 500<= e.code <600:
return download(url, user_agent, num_retries-1) # recursively retry 5XX HTTP errors
return html
#download("http://example.webscraping.com") #访问正常
download("http://httpstat.us/500") #这个网页测试用,一直是5XXerror
运行结果:
downloading: http://httpstat.us/500
download error: Internal Server Error
downloading: http://httpstat.us/500
download error: Internal Server Error
downloading: http://httpstat.us/500
download error: Internal Server Error
二、爬取网站
爬取网站,我们通常需要做两件事:下载网页(该过程一般称为爬取 crawling);提取数据(或保存数据,或继续分析数据等)
2.1 下载网页(爬取 crawling)
三种爬取网站的常见方法:爬取网站地图、遍历每个网页的数据库ID、跟踪网页链接
2.1.1 爬取网站地图sitemap中的url链接(使用正则表达式)
#def download()实现并测试爬取下载网页中的几个问题或方法:遇到5xx错误重试下载;设置用户代理
#def crawl_sitemap(url)实现爬取一个网站sitemap中url数据(用到正则表达式),并且将每个url下载
import urllib.request
import urllib.error
import re
def download(url, user_agent = "brain", num_retries = 2): #下载url网页
print("downloading:",url)
header = {"user-agent": user_agent} #设置用户代理,而不使用python默认的用户代理Python-urllib/3.6
req = urllib.request.Request(url, headers = header)
try:
html = urllib.request.urlopen(req).read()
except urllib.error.URLError as e: #下载过程中出现问题
print("download error:",e.reason)
html = None
if num_retries > 0: #错误4XX发生在请求存在问题,而5XX错误则发生在服务端存在问题,所以在发生5XX错误时重试下载
if hasattr(e, "code") and 500<= e.code <600:
return download(url, user_agent, num_retries-1) # recursively retry 5XX HTTP errors
return html
#download("http://example.webscraping.com") #访问正常
#download("http://httpstat.us/500") #这个网页测试用,一直是5XXerror
def crawl_sitemap(url): ##爬取一个网站sitemap中url,此url为网站sitemap的url
sitemap = download(url) # download the sitemap file
sitemap = str(sitemap) #把bytes类型转成str类型,以便下面用正则表达式提取数据
links = re.findall("<loc>(.*?)</loc>", sitemap) # extract the sitemap links这里用了正则表达式,返回的是结果列表
for link in links:
html = download(link) #把爬取的url都下载下来
crawl_sitemap("http://example.webscraping.com/sitemap.xml")
2.1.2 ID遍历爬虫
#def download()实现并测试爬取下载网页中的几个问题或方法:遇到5xx错误重试下载;设置用户代理
#def crawl_sitemap(url)实现爬取一个网站sitemap中url数据(用到正则表达式),并且将每个url下载
import urllib.request
import urllib.error
import re #正则表达式
import itertools #使用ID遍历
def download(url, user_agent = "brain", num_retries = 2): #下载url网页
print("downloading:",url)
header = {"user-agent": user_agent} #设置用户代理,而不使用python默认的用户代理Python-urllib/3.6
req = urllib.request.Request(url, headers = header)
try:
html = urllib.request.urlopen(req).read()
except urllib.error.URLError as e: #下载过程中出现问题
print("download error:",e.reason)
html = None
if num_retries > 0: #错误4XX发生在请求存在问题,而5XX错误则发生在服务端存在问题,所以在发生5XX错误时重试下载
if hasattr(e, "code") and 500<= e.code <600:
return download(url, user_agent, num_retries-1) # recursively retry 5XX HTTP errors
return html
#download("http://example.webscraping.com") #访问正常
#download("http://httpstat.us/500") #这个网页测试用,一直是5XXerror
""" #使用正则表达式的爬虫
def crawl_sitemap(url): ##爬取一个网站sitemap中url,此url为网站sitemap的url
sitemap = download(url) # download the sitemap file
sitemap = str(sitemap) #把bytes类型转成str类型,以便下面用正则表达式提取数据
links = re.findall("<loc>(.*?)</loc>", sitemap) # extract the sitemap links这里用了正则表达式,返回的是结果列表
for link in links:
html = download(link) #把爬取的url都下载下来"""
#使用ID遍历的爬虫
max_errors = 5
num_errors = 0
for page in itertools.count(1): #如果ID在数据库中不连续,则设置不连续次数max_errors达到时退出
url = "http://example.webscraping.com/view/%d" % page
html = download(url)
if html is None: #download出错html为None,说明遇到不连续的ID了
num_errors += 1
if num_errors == max_errors:
break
else:
num_errors = 0
crawl_sitemap("http://example.webscraping.com/sitemap.xml")
2.1.3 跟踪网页链接
ID遍历在数据库中多数为大数的不连续ID情况下很不奏效,当上述两种方法效果都不理想时我们需要让爬虫表现得更像普通用户,跟踪链接访问感兴趣的内容。通过跟踪所有链接,我们可以很容易地下载整个网站的页面,但这种方法会下载大量我们并不需要的网页,因此我们这里也需要结合正则表达式来确定需要下载哪些页面,明确范围和缩小下载数量。
import urllib.request
import urllib.error
import re #正则表达式
import urllib.parse #将url链接从相对路径(浏览器可懂但python不懂)转为绝对路径(python也懂了)
def download(url, user_agent = "brain", num_retries = 2): #下载url网页
print("downloading:",url)
header = {"user-agent": user_agent} #设置用户代理,而不使用python默认的用户代理Python-urllib/3.6
req = urllib.request.Request(url, headers = header)
try:
html = urllib.request.urlopen(req).read()
except urllib.error.URLError as e: #下载过程中出现问题
print("download error:",e.reason)
html = None
if num_retries > 0: #错误4XX发生在请求存在问题,而5XX错误则发生在服务端存在问题,所以在发生5XX错误时重试下载
if hasattr(e, "code") and 500<= e.code <600:
return download(url, user_agent, num_retries-1) # recursively retry 5XX HTTP errors
return html
#download("http://example.webscraping.com") #访问正常
#download("http://httpstat.us/500") #这个网页测试用,一直是5XXerror
"""
#使用正则表达式的爬虫
def crawl_sitemap(url): ##爬取一个网站sitemap中url,此url为网站sitemap的url
sitemap = download(url) # download the sitemap file
sitemap = str(sitemap) #把bytes类型转成str类型,以便下面用正则表达式提取数据
links = re.findall("<loc>(.*?)</loc>", sitemap) # extract the sitemap links这里用了正则表达式,返回的是结果列表
for link in links:
html = download(link) #把爬取的url都下载下来
#使用ID遍历的爬虫
max_errors = 5
num_errors = 0
for page in itertools.count(1): #如果ID在数据库中不连续,则设置不连续次数max_errors达到时退出
url = "http://example.webscraping.com/view/%d" % page
html = download(url)
if html is None: #download出错html为None,说明遇到不连续的ID了
num_errors += 1
if num_errors == max_errors:
break
else:
num_errors = 0
"""
#跟踪链接的爬虫
#link_crawler()函数传入两个参数:要爬取的网站URL、用于跟踪链接的正则表达式。
def link_crawler(seed_url, link_regex):
"""先下载 seed_url 网页的源代码,然后提取出里面所有的链接URL,接着对所有匹配到的链接URL与link_regex 进行匹配,
如果链接URL里面有link_regex内容,就将这个链接URL放入到队列中,
下一次 执行 while crawl_queue: 就对这个链接URL 进行同样的操作。
反反复复,直到 crawl_queue 队列为空,才退出函数。"""
crawl_queue = [seed_url]
seen = set(crawl_queue) #有可能链接中互相重复指向,为避免爬取相同的链接,所以我们需要记录哪些链接已经被爬取过(放在集合seen中),若已被爬取过,不再爬取
while crawl_queue:
url = crawl_queue.pop()
html = download(url)
html = str(html)
#filter for links matching our regular expression
if html == None:
continue
for link in get_links(html):
if re.match(link_regex, link):
link = urllib.parse.urljoin(seed_url, link) #把提取的相对url路径link(view/178)转化成绝对路径(/view/Poland-178)link
if link not in seen: #判断是否之前已经爬取
seen.add(link) #之前没有的话加在集合中以便后续继续判断
crawl_queue.append(link) #之前没有的话这个链接可用,放在列表中继续进行爬取
def get_links(html):
"""用来获取一个html网页中所有的链接URL"""
#做了一个匹配模板 webpage_regex,匹配 <a href="xxx"> or <a href='xxx'>这样的字符串,并提取出里面xxx的URL,请注意这里的xxxURL很可能是源码中相对路径,eg view/1 正常访问肯定是打不开的
webpage_regex = re.compile('<a href=["\'](.*?)["\']', re.IGNORECASE)
return re.findall(webpage_regex,html)
#return re.findall('<a[^>]+href=["\'](.*?)["\']', html)也可以这样实现,但没有上面的先编译模板再匹配好
#只想找http://example.webscraping.com/index... or http://example.webscraping.com/view...
link_crawler("http://example.webscraping.com", "/(index|view)")
以上代码的详解可以看这篇文章,特别详细:
Python 网络爬虫 009 (编程) 通过正则表达式来获取一个网页中的所有的URL链接,并下载这些URL链接的源代码。以下是他对本文爬虫的总结:
这样,我们就已经介绍了3种爬取一个站点或者一个网页里面所有的链接URL的源代码。这些只是初步的程序,接下来,我们还可能会遇到这样的问题:
1 . 如果一些网站设置了 禁止爬取的URL,我们为了执行这个站点的规则,就要按照它的 robots.txt
文件来设计爬取程序。
2 . 在国内是上不了google的,那么如果我们想要使用代理的方式上谷歌,就需要给我们的爬虫程序设置代理。
3 . 如果我们的爬虫程序爬取网站的速度太快,可能就会被目标站点的服务器封杀,所以我们需要限制下载速度。 (这个爬虫就遇到了,今后一定注意)
4 . 有一些网页里面有类似日历的东西,这个东西里面的每一个日期都是一个URL链接,我们有不会去爬取这种没有意义的东西。日期是无止境的,所以对于我们的爬虫程序来说,这就是一个爬虫陷阱,我们需要避免陷入爬虫陷阱。
我们需要解决上这4个问题。才能得到最终版本的爬虫程序。
三、自己编写的完整网络爬虫
以下是自己学习到的第一个网络爬虫,之后将自己写的与实例版本进行对比
3.1 自己学习写的最终版本
import urllib.request
import urllib.error
import re #正则表达式
import urllib.parse #将url链接从相对路径(浏览器可懂但python不懂)转为绝对路径(python也懂了)
import urllib.robotparser #爬取数据前解析网站robots.txt文件,避免爬取网站所禁止或限制的
import datetime #下载限速功能所需模块
def download(url, user_agent = "brain", proxy = None, num_retries = 2): #下载url网页,proxy是支持代理功能,初始值为None,想要设置就直接传参数即可
print("downloading:",url)
header = {"user-agent": user_agent} #设置用户代理,而不使用python默认的用户代理Python-urllib/3.6
req = urllib.request.Request(url, headers = header)
opener = urllib.request.build_opener() #为支持代理功能时刻准备着
if proxy: #如果设置了proxy,那么就进行以下设置以实现支持代理功能
proxy_params = { urllib.parse.urlparse(url).scheme: proxy }
opener.add_handler(urllib.request.ProxyHandler(proxy_params))
response = opener.open(req)
try:
html = urllib.request.urlopen(req).read()
except urllib.error.URLError as e: #下载过程中出现问题
print("download error:",e.reason)
html = None
if num_retries > 0: #错误4XX发生在请求存在问题,而5XX错误则发生在服务端存在问题,所以在发生5XX错误时重试下载
if hasattr(e, "code") and 500<= e.code <600:
return download(url, user_agent, num_retries-1) # recursively retry 5XX HTTP errors
return html
#download("http://example.webscraping.com") #访问正常
#download("http://httpstat.us/500") #这个网页测试用,一直是5XXerror
#跟踪链接的爬虫
#link_crawler()函数传入两个参数:要爬取的网站URL、用于跟踪链接的正则表达式。
def link_crawler(seed_url, link_regex, max_depth=2):
"""先下载 seed_url 网页的源代码,然后提取出里面所有的链接URL,接着对所有匹配到的链接URL与link_regex 进行匹配,
如果链接URL里面有link_regex内容,就将这个链接URL放入到队列中,
下一次 执行 while crawl_queue: 就对这个链接URL 进行同样的操作。
反反复复,直到 crawl_queue 队列为空,才退出函数。"""
crawl_queue = [seed_url]
max_depth = 2 #为避免爬虫陷阱,将用于避免重复链接的seen记录值修改为字典,增加记录访问次数;如果想要禁用该功能,只需将max_depth设为一个负数即可,此时当前深度永远不会与之相等
seen = {seed_url:0} #初始化seed_url访问深度为0
#seen = set(crawl_queue) #有可能链接中互相重复指向,为避免爬取相同的链接,所以我们需要记录哪些链接已经被爬取过(放在集合seen中),若已被爬取过,不再爬取
while crawl_queue:
url = crawl_queue.pop()
rp = urllib.robotparser.RobotFileParser() #爬取前解析网站robots.txt,检查是否可以爬取网站,避免爬取网站禁止或限制的
rp.set_url("http://example.webscraping.com/robots.txt")
rp.read()
user_agent = "brain"
if rp.can_fetch(user_agent, url): #解析后发现如果可以正常爬取网站,则继续执行
#爬取网站的下载限速功能的类的调用,每次在download下载前使用
throttle = Throttle(delay=5) #这里实例网站robots.txt中的delay值为5
throttle.wait(url)
html = download(url) #html = download(url, hearders, proxy=proxy, num_retries=num_retries)这里可以传所需要的参数
html = str(html)
#filter for links matching our regular expression
if html == None:
continue
depth = seen[url] #用于避免爬虫陷阱的记录爬取深度的depth
if depth != max_depth:
for link in get_links(html):
if re.match(link_regex, link):
link = urllib.parse.urljoin(seed_url, link) #把提取的相对url路径link(view/178)转化成绝对路径(/view/Poland-178)link
if link not in seen: #判断是否之前已经爬取
seen[link] = depth + 1 #在之前的爬取深度上加1
crawl_queue.append(link) #之前没有的话这个链接可用,放在列表中继续进行爬取
else:
print("Blocked by %s robots,txt" % url)
continue
def get_links(html):
"""用来获取一个html网页中所有的链接URL"""
#做了一个匹配模板 webpage_regex,匹配 <a href="xxx"> or <a href='xxx'>这样的字符串,并提取出里面xxx的URL,请注意这里的xxxURL很可能是源码中相对路径,eg view/1 正常访问肯定是打不开的
webpage_regex = re.compile('<a href=["\'](.*?)["\']', re.IGNORECASE)
return re.findall(webpage_regex,html)
#return re.findall('<a[^>]+href=["\'](.*?)["\']', html)也可以这样实现,但没有上面的先编译模板再匹配好
class Throttle: #爬取网站的下载限速功能的类的实现,每次在download下载前使用
"""Add a delay between downloads to the same domain"""
def __init__(self, delay):
self.delay = delay # value of delay between downloads for each domain
self.domains = {} # timestamp of when a domain was last accessed记录上次访问的时间,小知识timestamp:时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
def wait(self, url):
domain = urllib.parse.urlparse(url).netloc
last_accessed = self.domains.get(domain)
if self.delay>0 and last_accessed is not None:
sleep_secs = self.delay - (datetime.datetime.now() - last_accessed).seconds
if sleep_secs > 0:
time.sleep(sleep_secs) #domain has been accessed recently,so need to sleep
self.domains[domain] = datetime.datetime.now()
#只想找http://example.webscraping.com/index... or http://example.webscraping.com/view...
link_crawler("http://example.webscraping.com", "/(index|view)")
3.2 示例网站提供的最终版本
这里看看就好,示例代码是用python2实现的
import re
import urlparse
import urllib2
import time
from datetime import datetime
import robotparser
import Queue
def link_crawler(seed_url, link_regex=None, delay=5, max_depth=-1, max_urls=-1, headers=None, user_agent='wswp', proxy=None, num_retries=1):
"""Crawl from the given seed URL following links matched by link_regex
"""
# the queue of URL's that still need to be crawled
crawl_queue = Queue.deque([seed_url])
# the URL's that have been seen and at what depth
seen = {seed_url: 0}
# track how many URL's have been downloaded
num_urls = 0
rp = get_robots(seed_url)
throttle = Throttle(delay)
headers = headers or {}
if user_agent:
headers['User-agent'] = user_agent
while crawl_queue:
url = crawl_queue.pop()
# check url passes robots.txt restrictions
if rp.can_fetch(user_agent, url):
throttle.wait(url)
html = download(url, headers, proxy=proxy, num_retries=num_retries)
links = []
depth = seen[url]
if depth != max_depth:
# can still crawl further
if link_regex:
# filter for links matching our regular expression
links.extend(link for link in get_links(html) if re.match(link_regex, link))
for link in links:
link = normalize(seed_url, link)
# check whether already crawled this link
if link not in seen:
seen[link] = depth + 1
# check link is within same domain
if same_domain(seed_url, link):
# success! add this new link to queue
crawl_queue.append(link)
# check whether have reached downloaded maximum
num_urls += 1
if num_urls == max_urls:
break
else:
print 'Blocked by robots.txt:', url
class Throttle:
"""Throttle downloading by sleeping between requests to same domain
"""
def __init__(self, delay):
# amount of delay between downloads for each domain
self.delay = delay
# timestamp of when a domain was last accessed
self.domains = {}
def wait(self, url):
domain = urlparse.urlparse(url).netloc
last_accessed = self.domains.get(domain)
if self.delay > 0 and last_accessed is not None:
sleep_secs = self.delay - (datetime.now() - last_accessed).seconds
if sleep_secs > 0:
time.sleep(sleep_secs)
self.domains[domain] = datetime.now()
def download(url, headers, proxy, num_retries, data=None):
print 'Downloading:', url
request = urllib2.Request(url, data, headers)
opener = urllib2.build_opener()
if proxy:
proxy_params = {urlparse.urlparse(url).scheme: proxy}
opener.add_handler(urllib2.ProxyHandler(proxy_params))
try:
response = opener.open(request)
html = response.read()
code = response.code
except urllib2.URLError as e:
print 'Download error:', e.reason
html = ''
if hasattr(e, 'code'):
code = e.code
if num_retries > 0 and 500 <= code < 600:
# retry 5XX HTTP errors
return download(url, headers, proxy, num_retries-1, data)
else:
code = None
return html
def normalize(seed_url, link):
"""Normalize this URL by removing hash and adding domain
"""
link, _ = urlparse.urldefrag(link) # remove hash to avoid duplicates
return urlparse.urljoin(seed_url, link)
def same_domain(url1, url2):
"""Return True if both URL's belong to same domain
"""
return urlparse.urlparse(url1).netloc == urlparse.urlparse(url2).netloc
def get_robots(url):
"""Initialize robots parser for this domain
"""
rp = robotparser.RobotFileParser()
rp.set_url(urlparse.urljoin(url, '/robots.txt'))
rp.read()
return rp
def get_links(html):
"""Return a list of links from html
"""
# a regular expression to extract all links from the webpage
webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
# list of all links from the webpage
return webpage_regex.findall(html)
if __name__ == '__main__':
link_crawler('http://example.webscraping.com', '/(index|view)', delay=0, num_retries=1, user_agent='BadCrawler')
link_crawler('http://example.webscraping.com', '/(index|view)', delay=0, num_retries=1, max_depth=1, user_agent='GoodCrawler')
3.3 测试此爬虫
我们可以将用户代理设置为BadCrawler,也就是本章前文所述的被robots.txt 屏蔽了的那个用户代理。从下面的运行结果中可以看出,爬虫果然被屏蔽了,代码启动后马上就会结束:
>>>seed_url = "http://example.webscraping.com/index"
>>>link_regex = "/(index/view)"
>>>link_crawler(seed_url, link_regex, user_agent="BadCrawler")
Blocked by robots.txt : http://example.webscraping.com/
现在,让我们使用默认的用户代理,并将最大深度设置为1,这样只有主页上的链接才会被下载:
downloading: http://example.webscraping.com
downloading: http://example.webscraping.com/index/1
downloading: http://example.webscraping.com/index/2
downloading: http://example.webscraping.com/index/0
downloading: http://example.webscraping.com/view/Barbados-20
downloading: http://example.webscraping.com/view/Bangladesh-19
downloading: http://example.webscraping.com/view/Bahrain-18
downloading: http://example.webscraping.com/view/Bahamas-17
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Azerbaijan-16
downloading: http://example.webscraping.com/view/Austria-15
downloading: http://example.webscraping.com/view/Australia-14
downloading: http://example.webscraping.com/view/Aruba-13
downloading: http://example.webscraping.com/view/Armenia-12
downloading: http://example.webscraping.com/view/Argentina-11
downloading: http://example.webscraping.com/view/Antigua-and-Barbuda-10
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Antarctica-9
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Anguilla-8
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Angola-7
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Andorra-6
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/American-Samoa-5
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Algeria-4
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Albania-3
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Aland-Islands-2
download error: TOO MANY REQUESTS
downloading: http://example.webscraping.com/view/Afghanistan-1
download error: TOO MANY REQUESTS