爬虫知识练习
为了使用XPath,需要安装第三方库lxml
核心**:写XPath就是写地址 获取文本:
//标签1[@属性1="属性值1"]/标签2[@属性2="属性值2"]/.../text()
获取属性值:
//标签1[@属性1="属性值1"]/标签2[@属性2="属性值2"]/.../@属性n
@属性="属性值"不是必需的。作用是帮助过滤相同的标签。在不需要过滤相同标签时可以省略
可以直接从html最外层的标签开始,一层一层往下找
/html/body/div[@class="useful"]/ul/li/text()
当html开头时,前面是单斜线。但没必要,只需要找到一个标志性的"地标",从这开始往下找就可以
如何确定从哪个标签开始?原理:"倒着找地标"
<div class="useful">
<ul>
<li class="info">首页1</li>
<li class="info">首页2</li>
<li class="info">首页3</li>
</ul>
</div>
如上<ul>标签本身没有属性,写XPath时可以省略
标签有属性,但属性值都相同,例如
- 以相同字符串开头
<body>
<div id="test-1">首页1</div>
<div id="test-2">首页2</div>
<div id="userful-test">首页3</div>
</body>
需要抓取"首页1""首页2",如果不指定
//标签[starts-with(@属性名, "相同的开头部分")]
//div[starts-with(@id, "test")]/text()
- 属性值包含相同字符串 寻找属性值包含某些字符串的元素时,XPath的写法格式和某些以字符串开头的写法格式相同,只不过关键字从"starts-with"变成了"contains".
//标签[contains(@属性名, "相同的开头部分")]
//div[contains(@id, "test")]/text()
lxml的XPath不支持直接提取属性值以某些字符串结尾的情况,如果遇到,建议使用contains代替
- 对XPath返回的对象执行XPath XPath也支持先抓大再抓小
useful = selector.xpath('//div[@class="useful"]')
info_list = useful[0].xpath('ul/li/text()')
对XPath返回的对象再次执行XPath的时候,子XPath开头不需要添加斜线,直接以标签名开始即可
- 不同标签的文字
<body>
<div id="test">青龙
<span id="tiget">
白虎
</sapn>
龙
</div>
</body>
data = selector.xpath('//div[@id="test"]')[0]
info = data.xpath('string(.)')
要提取不同标签的文字,就需要使用string(.)关键字。首先像先抓大再抓小一样,先获取
python的第三方库,用来从html和xml中提取数据。某些方面比XPath易懂,但不如XPath简洁。是使用python开发的,因此速度比XPath慢
使用beautifulsoup4提取html内容,一般经过以下两个步骤:
- 处理源代码生成beautifulsoup对象
- 使用find_all()或者find()来查找内容
解析代码生成beautifulsoup对象
soup = BeautifulSoup(网页源代码, '解析器')
这里的解析器,可以使用html.parser:
soup = BeautifulSoup(网页源代码, 'html.parser')
如果安装了lxml,还可以使用lxml:
soup = BeautifulSoup(网页源代码, 'lxml')
查找内容
info_2 = soup.find(class_="test")
print(info_2.string)
由于html中class属性与python的class关键字相同,因此为了不产生冲突,BS4规定,如果遇到查询class的情况,使用"class_"代替。通过.string属性 就可以读出标签中的文字信息
先抓大再抓小,获取
userful = soup.find(class_="useful")
all_content = userful.find_all('li')
for li in all_content:
print(li.string)
find_all(name, attrs, recursive, text, **kwargs)
name html的标签
attrs 字典,字典的key是属性名,字典的value是属性值 例:attrs={'class': 'useful'},这种写法,class不需要加下划线
recursive 值为False时,BS4不会搜索子标签
text 可以是一个字符串或者是正则表达式,用于搜索标签里面的文本信息。 例:soup.find(text=re.compile('我需要'))
find()与find_all()不同点:
- find_all()返回的是beautifulsoup Tag对象组成的列表,如果没有找到满足要求的标签,返回空列表
- find()返回的是一个beautifulsoup Tag对象,如果有多个符合条件的标签,返回第一个对象,如果找不到,返回None
使用异步加载技术的网站,被加载的内容是不能在源代码中找到的。
并非所有的异步加载都会向后台发送请求。
打开http://exercise.kingname.info/exercise_ajax_2.html,可以看到并没有请求后台的行为,打开网页源代码也没有"行动代号:天王盖地虎"这几个汉字
那这个页面上的文字如何加载进来?这种情况称为伪装成异步加载的后端渲染。数据就在源代码里,但却不直接显示出来。注意源代码是JavaScript代码。
这种假的异步加载页面,处理思路一般是使用正则表达式从页面中把数据提取出来,然后直接解析。
显示在页面上的内容经过多次异步请求才能得到。第一个请求返回的是第2个请求的参数,只有得到上一个请求里面的有用信息,才能发起下一个请求
网站怎么知道现在是在计算机浏览器还是手机浏览器访问这个页面?网站怎么能记住地理位置呢?这就要归功于Headers了。浏览器可以将一些信息通过Headers传递 给服务器,服务器也可以将一些信息通过Headers传递给浏览器。
复制浏览器请求头
下载:https://chromedriver.storage.googleapis.com/index.html
ChromeDriver是Chrome浏览器的一个驱动程序。Selenium需要使用WebDriver才能处理网页,这里的WebDriver可以理解为浏览器或者浏览器驱动程序。可以是
Firefox,可以是Chrome,也可以是PhantomJS。前两者是有界面的,在处理网页时会弹出一个浏览器窗口,使用者可以直观的看到网页的内容是如何被自动操作的。
PhantomJS是没有界面的,因此适合在服务器上使用。
- 强制等待
time.sleep(5)
- 智能等待网页加载完成,需要使用"WebDriverWait"、"By"、"expected_conditions"这三个关键字
# 等待网页加载,直到class为content的HTML元素里面的文本中包含了"通关"两个汉字
WebDriverWait(driver, 30).until(expected_conditions.text_to_be_present_in_element((By.CLASS_NAME, "content"), '通关'))
关键字WebDriverWait会阻碍程序的运行,第二个参数30表示最多等待30s。在这30s内,每0.5s检查一次网页
until 直到等到某个条件满足才会继续执行后面的代码。这个被等待的条件就是expected_conditions(期待条件)。这个条件就是"presence_of_element_located",
"located"是"locate"的被动式,表示被定位的,presence(出现)。这个方法的作用"被定位的元素出现"。被定位的元素通过By来指定
- text_to_be_present_in_element 某个元素的text里面出现了某些文本。参数有2个,第一个为元组,第二个为部分或全部文本或正则表达式
- presence_of_element_located 某个元素出现 参数是一个元组
- 如果能找到元素,"find_element_by_xxx"返回的内容是一个Element对象;如果找不到,将会抛出一个Exception。因此,如果不确定是否存在, 必须使用"try...except Exception"包起来。如果使用"find_elements_by_xxx",返回的是一个列表,列表元素是Element对象,找不到返回空列表,不会异常
- 如果使用XPath,无论是"find_element_by_xxx"还是"find_elements_by_xxx",只要是想获取HTML标签里面的文本信息,那么就不能在XPath的末尾加 上"text()"。必须先定位到标签,然后读取返回的Element对象的".text"属性。
from selenium.webdriver.common.keys import Keys
driver = webdriver.Chrome('./chromedriver')
driver.get('https://www.zhihu.com/#signin')
elem = driver.find_element_by_name("account")
elem.clear()
elem.send_keys("xxx@163.com")
password = driver.find_element_by_name('password')
password.clear()
password.send_keys('123456')
input('请在网页上点击倒立的文字,完成以后回到这里按任意键继续。')
elem.send_keys(Keys.RETURN) # 模拟键盘回车键
优点:代码少,效果好
缺点:速度太慢。如果一个网页有很多图片又有很多异步加载,使用selenium完成网页要十几秒。如果服务器上使用PhantomJS作为WebDriver,还会出现内存泄漏
因此,不适合大规模的爬虫开放发
session = requests.Session()
source = session.get('https://zhihu.com', headers = headers, verify=False).content.decode()
使用了requests的Session模块。所谓Session,是指一段会话。网站会把一个会话的ID(session ID)保存在浏览器的cookies中用来标识用户的身份。
requests的Session模块可以自动保存网站返回的一些信息。
requests.get()在底层还是会先创建一个Session,然后用Session去访问。但每次调用requests.get(),都会创建一个新的Session。对于服务器来说,每次
新开浏览器去访问,不正常。使用Session模块每次都用这个Session去访问,这种比较正常。requests的官方建议,如果多次对同一个网站发送请求,那么应该使用Session
模块,会带来显著的性能提升。
对于HTTPS的网站,请求时带上verify=False,否则爬虫会报错。
表单登录成功以后会进行页面跳转,相当于开了一个新的网页
- 借助浏览器
使用cookies绕过登录,直接访问。收到通过浏览器登录获取cookies。比如:单击或者拖动滑块验证 - 不借助浏览器
仅需要识别图片的验证码,可以使用--先把验证码下载到本地,让肉眼识别并手动输入给爬虫。
- 爬虫访问登录页面
- 分析网页源代码,获取验证码地址
- 下载验证码到本地
- 打开验证码,人眼读取内容
- 构造post的数据,填入验证码
- post提交
- python图像识别
开源的OCR库pytesseract配合图像识别引擎tesseract,可以用来将图片中的文字转换为文本
这种方式爬虫应用中并不多见。因为现在大部分的验证码都加上了干扰的纹理。如果使用这种方式,只有两种情况:网站的验证码极其简单工整,使用大量的验证码来训练 tesseract。
mac安装:
brew install tesseract
安装python库
使用tesseract进行图像识别,需要安装两个第三方库
pip install Pillow(专门用来处理图像的第三方库)
pip install pytesseract(专门用来操作tesseract的第三方库)
tesseract的使用
- 导入pytesseract和Pillow
- 打开图片
- 识别
- 打码网站
在线验证码识别的网站,简称打码网站。这些网站有一些是使用深度学习技术识别验证码,有一些是雇佣的很多人来人肉识别。
流程:
- 将验证码上传到网站服务器
- 网站服务器将验证码分发给打码工人
- 打码工人人肉识别并上传结果
- 网站将结果返回
中间人攻击是指攻击者与通信的两端创建独立的联系,并交换其所收到的数据,使通信的两端都认为其正在通过一个私密的连接与对方直接对话,但事实上整个会话都被
攻击者控制。
通俗点讲:上课传纸条。A要传给B,但是A与B距离太远,于是让C转交。C就是中间人,他有两种攻击方式:仅仅偷偷查看纸条的内容,或者篡改再传给B。
数据抓包就是中间人爬虫的一个简单应用。所以使用charles也是一种中间人攻击。
是一个命令行下的抓包工具,作用和charles差不多,但可以在终端下工作。使用mitmproxy就可以实现自动化的抓包并从数据包里面得到有用的信息。
安装
pip install mitmproxy
要启动 mitmproxy 用 mitmproxy、mitmdump、mitmweb 这三个命令中的任意一个即可,这三个命令功能一致,且都可以加载自定义脚本,唯一的区别是交互界面的不同。
默认监听端口8080
- mitmproxy 命令启动后,会提供一个命令行界面,用户可以实时看到发生的请求,并通过命令过滤请求,查看请求数据。
- mitmweb 命令启动后,会提供一个 web 界面,用户可以实时看到发生的请求,并通过 GUI 交互来过滤请求,查看请求数据。
- mitmdump 命令启动后——你应该猜到了,没有界面,程序默默运行,所以 mitmdump 无法提供过滤请求、查看数据的功能,只能结合自定义脚本,默默工作。加载 python 脚本
- -h 帮助信息
- -p 修改监听端口
- -s 加载 python 脚本
- 可以通过上下移动鼠标滚轮的切换请求,选中某一请求后单击可查看请求详情
- 然后通过q命令可以返回到主界面,然后通过f命令加上要过滤的参数可以过滤请求,如下是只展示包含mitmproxy的请求
: Command prompt
E View event log 查看事件日志
O View options 视图选项
enter Select
q Exit the current view 退出当前视图
tab Next
Keybindings for this view
; Add comment to flow 向流程添加注释
A Resume all intercepted flows 恢复所有被拦截的流
D Duplicate flow 重复流
F Set focus follow 设置焦点跟随
L Load flows from file 从文件加载流
M Toggle viewing marked flows 切换查看标记流
S Start server replay 启动服务器重放
U Un-set all marks 取消所有标记
V Revert changes to this flow 将更改恢复到此流
X Kill this flow 杀死这个流
Z Purge all flows not showing 清除所有未显示的流
a Resume this intercepted flow 恢复截取的流
b Save response body to file 保存响应正文到文件
d Delete flow from view 从视图中删除flow
e Export this flow to file 导出该流到文件
f Set view filter 设置视图过滤器
m Toggle mark on this flow 此流上的切换标记
n Create a new flow 创建新的流程
o Set flow list order 设置流-列表按照啥顺序
r Replay this flow 重新进行请求
v Reverse flow list order 流列表-倒序排序
w Save listed flows to file 保存列出的流程到文件
z Clear flow list 清除流程列表
| Run a script on this flow 在这个流上运行一个脚本
ctrl l Send cuts to clipboard 发送剪切到剪贴板
Global Keybindings
- Cycle to next layout 循环到下一个布局
: Command prompt 命令提示符
? View help
B Start an attached browser 启动附加浏览器
C View commands 查看命令
E View event log
G Go to end 到终点
I Toggle whether the filtering via the intercept option is enabled 切换是否过滤通过拦截选项是启用的
K View key bindings Key Bindings界面
O View options 视图选项
P View flow details Flow Details界面
Q Exit immediately 立即退出
W Stream to file
enter Select
g Go to start
h Left
i Set intercept 设置拦截
j Down
k Up
l Right
q Exit the current view
space Page down
tab Next
ctrl b Page up
ctrl f Page down
ctrl right Focus next layout pane 聚焦下一个布局窗格
shift tab Focus next layout pane
例子:
i 进入请求拦截 ~u /api/coach_info & ~q 拦截指定url,过滤器~q仅拦截请求,不拦截响应
通过手机浏览器访问http://mitm.it/下载代理
mitmdump命令可以用来运行符合一定规则的python脚本,并在python脚本里面直接操作HTTP和HTTPS的请求,以及返回的数据包。
文档:https://docs.mitmproxy.org/stable/
创建一个parse_request.py文件
def request(flow):
print(flow.request.headers)
在命令行执行命令:
mitmdump -s parse_request.py
运行命令之后,打开一个app,可以看到app请求的头部信息已经出现在终端窗口中
也可以查看cookie、body信息等
def request(flow):
req = flow.request
print(f'当前请求的URL为:{req.url}')
print(f'当前请求的请求方式为:{req.method}')
print(f'当前请求的Cookies为:{req.cookies}')
print(f'当前请求的body为:{req.text}')
创建一个parse_response.py文件
import json
def response(flow):
resq = flow.response
print(f'返回的头部为:{resq.headers}')
print(f'返回的body为:{json.loads(resq.content)}')
针对性处理某个网站返回的数据。此时将请求和返回内容放在一起,且函数名必须为"response"
import json
def response(flow):
req = flow.request
resq = flow.response
if 'kingname.info' in req.url:
print('这是kingname的网站,也是我的目标网站')
print(f'请求的headers为:{req.headers}')
print(f'请求的UA为:{req.headers["User-Agent"]}')
print(f'返回的内容为:{response.text}')
网站返回的headers中经常有cookies,将得到的cookies后直接塞进Redis里面,但这样目前并不行。因为mitmdump的脚本对第三方库的支持有缺陷,很多第三方库不能运行。
为了解决这个问题,需要用到管道。在终端里面就是一根竖线,可以把左边的内容传递给右边。
mitmdump的脚本使用print()函数把cookies打印出来,再通过管道传递给另一个普通的正常的python脚本。
mitmdump -s mitmproxy_practice/print_cookies.py | python mitmproxy_practice/extract.py
到目前为止,自动获取cookies的功能已经实现了。当然headers里面的所有数据、请求发送的body里面的所有数据都可以使用此方式来截取。
from selenium import webdriver
import time
service_args = ["--proxy=127.0.0.1:8080", '--ignore-ssl-errors=yes']
def run():
print('start to token')
driver = webdriver.phantomjs(service_args=service_args)
driver.get('http://xxx')
time.sleep(5)
driver.close()
scrapy startproject <工程名>
cd 工程目录
scrapy genspider example baidu.com
scrapy genspider命令中有两个参数,第一个参数example是爬虫的名字,第二个参数是需要爬取的网站。
进入工程根目录
scrapy crawl <爬虫名>
scrapy crawl example
执行之后会发现,并没有百度首页上的任何文字出现。这是由于Scrapy的爬虫默认是遵守robots.txt协议的,而百度的首页在robots.txt协议
中是禁止爬虫的。
要让Scrapy不遵守robots.txt协议,需要修改一个配置。在爬虫的工程文件夹下面找到settings.py文件,修改如下
ROBOTSTXT_OBEY = False
再次执行,就可以了
title = response.xpath('//title/text()').extract()
search_button_text = response.xpath('//input[@class="bg s_btn"]/@value').extract()
可以看出,scrapy与lxml使用XPath唯一不同之处在于,scrapy的xpath语法后面需要使用.extract()这个方法
"extract"英文解释为提取,这个作用就是把获取到的字符串提取出来。
如果不使用.extract(),那么得到的结果是保存在一个SelectorList中的,直到调用.extract(),才会将结果以
列表的形式生成出来。
SelectorList,本身像一个列表。可以直接使用下标读取里面的每一个元素,也可以for循环展开,然后在.extract()。同时也可以先
执行SelectorList的.extract()方法,得到一个列表,再使用下标来获取每一个元素。
title = response.xpath('//title/text()')
title_1 = title.extract()[0]
title_2 = title[0].extract()