如何制作一个爬虫

如何制作一个爬虫

参考链接:

https://cuiqingcai.com/5052.html

环境与库

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
请求库
Requests
Selenium
ChromeDriver
GeckoDriver
PhantomJS
aiohttp

解析库
lxml
Beautiful Soup
pyquery
tesserocr

数据库
MySQL
MongoDB
Redis

存储库
PyMySQL
PyMongo
redis-py
RedisDump

Web 库
Flask
Tornado

App 爬取相关库
Charles
mitmproxy
Appium

爬虫框架
pyspider
Scrapy
Scrapy-Splash
Scrapy-Redis

部署相关库
Docker
Scrapyd
Scrapyd-Client
Scrapyd API
Scrapyrt
Gerapy

其中我这边使用的库是:Beautiful Soup ,Requests ,Selenium ,ChromeDriver 等

其他的库根据不同场景的需要进行处理即可

Robots 协议

参考链接:

创建并提交 robots.txt 文件 | Google 搜索中心 | 文档 | Google for Developers

Robots 协议也称作爬虫协议、机器人协议,它的全名叫作网络爬虫排除标准(Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以抓取,哪些不可以抓取

对于该协议,如果存在,搜索爬虫会根据其中定义的爬取范围来爬取。如果没有找到这个文件,搜索爬虫便会访问所有可直接访问的页面。(当然,也有很多人不会管这些)

example

1
2
3
User-agent: *
Disallow: /
Allow: /public/

实用的 robots.txt 规则

实用规则
禁止抓取整个网站 请注意,在某些情况下,Google 即使未抓取网站中的网址,仍可能将其编入索引。注意:这不适用于各种 AdsBot 抓取工具,此类抓取工具必须明确指定。User-agent: * Disallow: /
禁止抓取某一目录及其内容 在目录名后添加一道正斜线,即可禁止抓取整个目录。注意:请勿使用 robots.txt 禁止访问私密内容;请改用正确的身份验证机制。对于 robots.txt 文件所禁止抓取的网址,Google 仍可能会在不进行抓取的情况下将其编入索引;另外,由于 robots.txt 文件可供任何人随意查看,因此可能会泄露您的私密内容的位置。User-agent: * Disallow: /calendar/ Disallow: /junk/ Disallow: /books/fiction/contemporary/
仅允许某一抓取工具访问网站内容 只有 googlebot-news 可以抓取整个网站。User-agent: Googlebot-news Allow: / User-agent: * Disallow: /
允许除某一抓取工具以外的其他所有抓取工具访问网站内容 Unnecessarybot 不能抓取相应网站,所有其他漫游器都可以。User-agent: Unnecessarybot Disallow: / User-agent: * Allow: /
禁止抓取某一网页 例如,禁止抓取位于 https://example.com/useless_file.htmluseless_file.html 页面和 junk 目录中的 other_useless_file.htmlUser-agent: * Disallow: /useless_file.html Disallow: /junk/other_useless_file.html
禁止抓取除子目录以外的整个网站 抓取工具只能访问 public 子目录。User-agent: * Disallow: / Allow: /public/
禁止 Google 图片访问某一特定图片 例如,禁止访问 dogs.jpg 图片。User-agent: Googlebot-Image Disallow: /images/dogs.jpg
禁止 Google 图片访问您网站上的所有图片 如果无法抓取图片和视频,则 Google 无法将其编入索引。User-agent: Googlebot-Image Disallow: /
禁止抓取某一特定文件类型的文件 例如,禁止抓取所有 .gif 文件。User-agent: Googlebot Disallow: /*.gif$
禁止抓取整个网站,但允许 Mediapartners-Google 访问内容 实施此规则会阻止您的网页显示在搜索结果中,但 Mediapartners-Google 网页抓取工具仍能分析这些网页,以确定要向访问您网站的用户显示哪些广告。User-agent: * Disallow: / User-agent: Mediapartners-Google Allow: /
使用 *$ 通配符匹配以特定字符串结尾的网址 例如,禁止抓取所有 .xls 文件。User-agent: Googlebot Disallow: /*.xls$

robotparser

用于自动分析路径是否可以爬取

1
2
3
4
5
6
7
from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.set_url('http://www.jianshu.com/robots.txt')
rp.read()
print(rp.can_fetch('*', 'http://www.jianshu.com/p/b67554025d7d'))
print(rp.can_fetch('*', "http://www.jianshu.com/search?q=python&page=1&type=collections"))

requests

对于一些基础的用法不再介绍,例如get,post请求,cookie设置,header的设置,状态码的判断等都自行学习,主要聊一下高级用法,对于爬虫的一些各种场景的绕过

文件上传

1
2
3
4
5
import requests

files = {'file': open('favicon.ico', 'rb')}
r = requests.post("http://httpbin.org/post", files=files)
print(r.text)

Cookie以及会话

1
2
3
4
5
6
import requests

r = requests.get("https://www.baidu.com")
print(r.cookies)
for key, value in r.cookies.items():
print(key + '=' + value)
1
2
3
4
5
6
import requests

s = requests.Session()
s.get('http://httpbin.org/cookies/set/number/123456789')
r = s.get('http://httpbin.org/cookies')
print(r.text)

采用session的时候,相当于我们维持在一个网页上面

SSL证书验证

如果请求一个 HTTPS 站点,但是证书验证错误的页面时,就会报这样的错误,那么如何避免这个错误呢?很简单,把 verify 参数设置为 False 即可。相关代码如下:

1
2
3
4
import requests

response = requests.get('https://test.cn', verify=False)
print(response.status_code)

不过我们发现报了一个警告,它建议我们给它指定证书。我们可以通过设置忽略警告的方式来屏蔽这个警告:

1
2
3
4
5
6
import requests
from requests.packages import urllib3

urllib3.disable_warnings()
response = requests.get('https://test.cn', verify=False)
print(response.status_code)

或者通过捕获警告到日志的方式忽略警告:

1
2
3
4
5
import logging
import requests
logging.captureWarnings(True)
response = requests.get('https://test.cn', verify=False)
print(response.status_code)

当然,我们也可以指定一个本地证书用作客户端证书,这可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组:

1
2
3
4
import requests

response = requests.get('https://www.12306.cn', cert=('/path/server.crt', '/path/key'))
print(response.status_code)

代理设置

1
2
3
4
5
6
7
8
import requests

proxies = {
"http": "http://user:password@10.10.1.10:3128/",
'http': 'socks5://user:password@host:port',
'https': 'socks5://user:password@host:port'
}
requests.get("https://www.taobao.com", proxies=proxies)

身份认证

在访问网站时,我们可能会遇到这样的认证页面

此时可以使用 requests 自带的身份认证功能

1
2
3
4
import requests

r = requests.get('http://localhost:5000', auth=('username', 'password'))
print(r.status_code)

如果需要OA认证

1
pip3 install requests_oauthlib

使用 OAuth1 认证的方法如下:

1
2
3
4
5
6
7
import requests
from requests_oauthlib import OAuth1

url = 'https://api.twitter.com/1.1/account/verify_credentials.json'
auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET',
'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')
requests.get(url, auth=auth)

Prepared Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from requests import Request, Session

url = 'http://httpbin.org/post'
data = {
'name': 'germey'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'
}
s = Session()
req = Request('POST', url, data=data, headers=headers)
prepped = s.prepare_request(req)
r = s.send(prepped)
print(r.text)

这里我们引入了 Request,然后用 urldataheaders 参数构造了一个 Request 对象,这时需要再调用 Sessionprepare_request() 方法将其转换为一个 Prepared Request 对象,然后调用 send() 方法发送即可,运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
"Content-Length": "11",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36"
},
"json": null,
"origin": "182.32.203.166",
"url": "http://httpbin.org/post"
}

可以看到,我们达到了同样的 POST 请求效果。

有了 Request 这个对象,就可以将请求当作独立的对象来看待,这样在进行队列调度时会非常方便。后面我们会用它来构造一个 Request 队列。

解析html

任选一个就行

lxml

xpath语法

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from lxml import etree

text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive"><a href="link3.html">third item</a></li>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a>
</ul>
</div>
'''

# 使用 lxml 解析 HTML 文本
html = etree.HTML(text)

# 使用 XPath 表达式获取所有 <li> 元素
li_elements = html.xpath("//li")

# 打印所有 <li> 元素
for li in li_elements:
print(etree.tostring(li, encoding='unicode'))

1
2
3
4
5
from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))
1
2
3
4
5
6
7
8
result = html.xpath('//*') # 所有节点
result = html.xpath('//li/a') # 选取li节点的所有直接a子结点
result = html.xpath('//ul//a') # 选取li节点的所有a子孙结点
result = html.xpath('//a[@href="test"]/../@class') # 选取href 属性为 test 的 a 节点,然后再获取其父节点,然后再获取其 class 属性
result = html.xpath('//a[@href="test"]/parent::*/@class') # 选取href 属性为 test 的 a 节点,然后再获取其父节点,然后再获取其 class 属性
result = html.xpath('//li/a/test()') # 用 XPath 中的 text() 方法获取节点中的文本
result = html.xpath('//li[contains(@class, "li")]/a/text()') # 多值匹配,多个class的时候,需要使用contains函数

Beautiful Soup

基础解析

1
2
soup = BeautifulSoup('<p>Hello</p>', 'lxml')
soup = BeautifulSoup('<p>Hello</p>', 'html.parser')

之前我是一直使用的是html.parser,来学一下lxml

1
2
3
4
5
6
7
8
9
10
11
12
13
soup = BeautifulSoup(html, 'lxml')
print(soup.prettify()) # 打印出html缩进之后的全部html,并且完成补全
print(soup.p.string) # 打印出第一个匹配的节点的内容
print(soup.p.attrs) # 获取全部属性,字典形式
print(soup.p['name']) # 直接得到p节点的name
print(soup.p.children) # 子节点
print(soup.p.descendants) # 全部子孙节点
print(soup.a.parent) # 父节点
print(soup.a.parents) # 祖先节点
print(soup.a.next_sibling) # 上一个兄弟节点
print(soup.a.previous_sibling) # 下一个兄弟节点
for i, child in enumerate(soup.p.children):
print(i, child)

方法选择器,直接得到特定条件的节点

1
2
3
4
5
6
print(soup.find_all(name='ul'))
find_parents() 和 find_parent():前者返回所有祖先节点,后者返回直接父节点。
find_next_siblings() 和 find_next_sibling():前者返回后面所有的兄弟节点,后者返回后面第一个兄弟节点。
find_previous_siblings() 和 find_previous_sibling():前者返回前面所有的兄弟节点,后者返回前面第一个兄弟节点。
find_all_next() 和 find_next():前者返回节点后所有符合条件的节点,后者返回第一个符合条件的节点。
find_all_previous() 和 find_previous():前者返回节点后所有符合条件的节点,后者返回第一个符合条件的节点。

css 选择器,获取div标签下面的节点

1
2
3
print(soup.select('.panel .panel-heading')) # class为panel下的class为panel-heading的节点
print(soup.select('ul li')) # ul标签下面的li标签
print(soup.select('#list-2 .element')) # ID为list-2的元素下所有类为element的子元素

实战

Ajax

对于前后端进行数据交互之后在进行渲染的情况,存在下面的方法对数据进行爬取

首先Ajax的交互方式,前端发包情况隶属于XHR的发包,可以在F12之后看到,具体方法可以查看下面的文章:

https://cuiqingcai.com/5597.html

实战可以查看:

https://cuiqingcai.com/5609.html

但是对于这样的实战手法,仅仅考虑明文传输,假设出现密文传输的方式,在安全的领域下面需要进行前端的逆向操作,