网络爬虫
网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。
传统爬虫从一个或若干初始网页的URL开始,获得初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。
爬虫的合法性
目前法律尚在建立和完善中,现在的爬虫暂时还是灰色地带,但是在爬取网站的时候,我们需要注意不要去爬取网站后台的私密敏感信息,否则容易吃官司。
关于网站的Robots文件
大多数网站都会定义robots.txt文件,对爬虫进行一些限制。
在网页域名后面直接加上 ‘/robots.txt’ 然后回车就可以看到该网页的robots文件,格式如:
User-agent: Baiduspider
Allow: /article
Allow: /oshtml
Disallow: /product/
Disallow: /
表示禁止'Baiduspider'爬取除Allow规定的其他页面
我们在写爬虫的时候也应该限制自己的爬虫遵守所爬取网页的Robots协议。
一个简单爬虫基本流程
数据采集(网页下载)—> 数据处理(网页解析)—> 数据存储(将有用的信息持久化)
- 设定抓取目标(种子页面/起始页面)并获取网页。
- 当服务器无法访问时,按照指定的重试次数尝试重新下载页面。
- 在需要的时候设置用户代理或隐藏真实IP,否则可能无法访问页面。
- 对获取的页面进行必要的解码操作然后抓取出需要的信息。
- 在获取的页面中通过某种方式(如正则表达式)抽取出页面中的链接信息。
- 对链接进行进一步的处理(获取页面并重复上面的动作)。
- 将有用的信息进行持久化以备后续的处理。
常用工具:
- 下载数据(种子页面) - urllib / requests / aiohttp
- 解析数据 - re / lxml / beautifulsoup4(bs4)/ pyquery
- 缓存和持久化 - pymysql / redis / sqlalchemy / peewee / pymongo
- 生成摘要 - hashlib
- 序列化和压缩 - pickle / json / zlib
- 调度器 - 进程 / 线程 / 协程
爬虫注意事项
- 处理相对链接: 有时候我们从页面中获取的链接是一个相对链接,需要将获取到的连接与URL前缀进行拼接( urllib.parse中的urljoin() )
- 设置代理业务:有些网站限制了访问的区域,一些爬虫就需要隐藏自己的身份(也就是设置使用代理服务器,urllib.request中的ProxyHandler,国内免费代理服务如:西刺代理)
- 限制下载速度:如果爬虫获取网页的速度过快,那么一些网站就会对对应ip进行封禁
- 避免爬虫陷阱:一些网站会动态生成内容,这会导致产生无限多的页面,可以通过记录到达当前页面经过了多少个链接(链接深度)来解决该问题,当达到事先设定的最大深度时爬虫就不再向队列中添加该网页中的链接。
- SSl相关问题:使用urlopen打开一个HTTPS链接时会验证一次SSL证书,如果不做出处理会产生错误提示“SSL: CERTIFICATE_VERIFY_FAILED”,可以通过两种方式加以解决:
1,使用未经验证的上下文
import ssl
request = urllib.request.Request(url='...', headers={...})
context = ssl._create_unverified_context()
web_page = urllib.request.urlopen(request, context=context)
2,设置全局的取消证书验证
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
从搜狐体育抓取nba相关新闻标题和链接
- 导入需要用到的模块
from urllib.error import URLError
from urllib.request import urlopen
import re
import pymysql
import ssl
from pymysql import Error
- 通过指定的字符集对页面进行解码(不是每个网站都将字符集设置为utf-8)
# charsets=('utf-8',)默认为utf-8
def decode_page(page_bytes, charsets=('utf-8',)):
page_html = None
for charset in charsets:
try:
page_html = page_bytes.decode(charset)
break
except UnicodeDecodeError:
pass
# logging.error('Decode:', error)
return page_html
- 获取页面的HTML代码(通过递归实现指定次数的重试操作)
def get_page_html(seed_url, *, retry_times=3, charsets=('utf-8',)):
page_html = None
try:
page_html = decode_page(urlopen(seed_url).read(), charsets)
except URLError:
# logging.error('URL:', error)
if retry_times > 0:
return get_page_html(seed_url, retry_times=retry_times - 1,
charsets=charsets)
return page_html
- 从页面中提取需要的部分(通常是链接,也可以通过正则表达式进行指定)
def get_matched_parts(page_html, pattern_str, pattern_ignore_case=re.I):
pattern_regex = re.compile(pattern_str, pattern_ignore_case)
return pattern_regex.findall(page_html) if page_html else []
- 运行爬虫程序并对指定的数据进行持久化操作
def start_crawl(seed_url, match_pattern, *, max_depth=-1):
conn = pymysql.connect(host='localhost', port=3306,
database='crawler', user='root',
password='123456', charset='utf8')
try:
with conn.cursor() as cursor:
url_list = [seed_url]
# 通过下面的字典避免重复抓取并控制抓取深度
visited_url_list = {seed_url: 0}
while url_list:
current_url = url_list.pop(0)
depth = visited_url_list[current_url]
if depth != max_depth:
# 尝试用utf-8/gbk/gb2312三种字符集进行页面解码
page_html = get_page_html(current_url, charsets=('utf-8', 'gbk', 'gb2312'))
links_list = get_matched_parts(page_html, match_pattern)
param_list = []
for link in links_list:
if link not in visited_url_list:
visited_url_list[link] = depth + 1
page_html = get_page_html(link, charsets=('utf-8', 'gbk', 'gb2312'))
headings = get_matched_parts(page_html, r'<h1>(.*)<span')
if headings:
param_list.append((headings[0], link))
cursor.executemany('insert into tb_result values (default, %s, %s)',
param_list)
conn.commit()
except Error:
pass
# logging.error('SQL:', error)
finally:
conn.close()
def main():
ssl._create_default_https_context = ssl._create_unverified_context
start_crawl('http://sports.sohu.com/nba_a.shtml',
r'<a[^>]+test=a\s[^>]*href=["\'](.*?)["\']',
max_depth=2)
if __name__ == '__main__':
main()
使用BeautifulSoup简单抓取搜狐体育nba相关页面
import re
from bs4 import BeautifulSoup
import requests
def main():
# 通过requests第三方库的get方法获取页面
resp = requests.get('http://sports.sohu.com/nba_a.shtml')
# 对响应的字节串(bytes)进行解码操作(搜狐的部分页面使用了GBK编码)
html = resp.content.decode('gbk')
# 创建BeautifulSoup对象来解析页面(相当于javascript的DOM)
soup = BeautifulSoup(html, 'lxml')
# 通过CSS先择器语法查找并通过循环进行处理
for elem in soup.select('a[test=a]'):
# 通过attrs属性(字典)获取元素的属性值
link_url = elem.attrs['href']
resp = requests.get(link_url)
bs_sub = BeautifulSoup(resp.text, 'lxml')
# 使用正则表达式对获取的数据做进一步的处理
print(re.sub(r'[\r\n]', '', bs_sub.select_one('h1').text))
# print(bs_sub.find('h1').text)
if __name__ == '__main__':
main()