soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
soup.find_all(input,attrs={"name":"_xsrf","class":"c1","id":"link2"})
# 属性也可以以字典的格式传
3)text 参数
通过 text 参数可以搜搜文档中的字符串内容,与 name 参数的可选值一样, text 参数接受 字符串 , 正则表达式 , 列表
soup.find_all(text="Elsie")
# [u'Elsie']
soup.find_all(text=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']
soup.find_all(text=re.compile("Dormouse"))
[u"The Dormouse's story", u"The Dormouse's story"]
2.find(name, attrs, recursive, text, **kwargs)
find方法和find_all的使用方法是一样的,只不过find只找一个值,find_all返回的是一个列表
CSS选择器
这就是另一种与 find_all 方法有异曲同工之妙的查找方法.
写 CSS 时,标签名不加任何修饰,类名前加.
,id名前加#
在这里我们也可以利用类似的方法来筛选元素,用到的方法是 soup.select()
,返回类型是 list
(1)通过标签名查找
print soup.select('title')
#[<title>The Dormouse's story</title>]
print soup.select('a')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
print soup.select('b')
#[<b>The Dormouse's story</b>]
(2)通过类名查找
print soup.select('.sister')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
(3)通过 id 名查找
print soup.select('#link1')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
(4)组合查找
组合查找即和写 class 文件时,标签名与类名、id名进行的组合原理是一样的,例如查找 p 标签中,id 等于 link1的内容,二者需要用空格分开
print soup.select('p #link1')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
直接子标签查找,则使用 >
分隔
print soup.select("head > title")
#[<title>The Dormouse's story</title>]
(5)属性查找
查找时还可以加入属性元素,属性需要用中括号括起来,注意属性和标签属于同一节点,所以中间不能加空格,否则会无法匹配到。
print soup.select('a[class="sister"]')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
print soup.select('a[href="http://example.com/elsie"]')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
同样,属性仍然可以与上述查找方式组合,不在同一节点的空格隔开,同一节点的不加空格
print soup.select('p a[href="http://example.com/elsie"]')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
(6) 获取内容
以上的 select 方法返回的结果都是列表形式,可以遍历形式输出,然后用 get_text() 方法来获取它的内容。
soup = BeautifulSoup(html, 'lxml')
print type(soup.select('title'))
print soup.select('title')[0].get_text()
for title in soup.select('title'):
print title.get_text()
4 # 请求报头
5 headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"}
7 def captcha(captcha_data):
8 """
9 处理验证码
10 :return:
11 """
12 with open("captcha.jpg",'wb')as f:
13 f.write(captcha_data)
14 text = input("请输入验证码:")
15 return text # 返回用户输入的验证码
17 def zhihuLogin():
18 """
19 获取页面的_xsrf
20 验证码-抓包工具
21 发送post表单请求-抓包工具
22 :return:
23 """
25 # 相当于urllib2,通过HTTPCookieProcessor()处理器类构建一个处理器对象,
26 # 再用urllib2.build_opener构建一个自定义的opener来保存cookie
27 # 构建一个Session对象,可以保存页面Cookie
28 sess = requests.session() # 创建一个能够储存cookie的opener
30 # 首先获取登录页面,找到需要POST的数据(_xsrf),同时会记录当前网页的Cookie值
31 # 也可以直接用requests.get("...")发送请求,但这样就没法保存cookie值了
32 # 获取HTML内容可以用.text/.content来获取
33 html = sess.get('https://www.zhihu.com/#signin',headers=headers).text # get <==> 发送get请求
35 # 调用lxml解析库
36 bs = BeautifulSoup(html, 'lxml')
38 # _xsrf 作用是防止CSRF攻击(跨站请求伪造),通常叫跨域攻击,是一种利用网站对用户的一种信任机制来做坏事
39 # 跨域攻击通常通过伪装成网站信任的用户的请求(利用Cookie),盗取用户信息、欺骗web服务器
40 # 所以网站会通过设置一个隐藏字段来存放这个MD5字符串,这个字符串用来校验用户Cookie和服务器Session的一种方式
42 # 找到name属性值为 _xsrf 的input标签,再取出value 的值
43 _xsrf = bs.find("input", attrs={"name":"_xsrf"}).get("value") # 获取_xsrf
44 # 根据UNIX时间戳,匹配出验证码的URL地址
45 # 发送图片的请求,获取图片数据流,
46 captcha_data = sess.get('https://www.zhihu.com/captcha.gif?r=%d&type=login'%(time.time()*1000),headers=headers).content
47 # 调用上面的方法(需要手动输入),获取验证码里的文字
48 captcha_text = captcha(captcha_data)
49 data = {
50 "_xsrf": _xsrf,
51 "phone_num": "xxx",
52 "password": "xxx",
53 "captcha": captcha_text
54 }
55 # 发送登录需要的POST数据,获取登录后的Cookie(保存在sess里)
56 sess.post('https://www.zhihu.com/login/phone_num',data=data,headers=headers)
58 # 用已有登录状态的Cookie发送请求,获取目标页面源码
59 response = sess.get("https://www.zhihu.com/people/peng-peng-34-48-53/activities",headers=headers)
61 with open("jiaxin.html",'wb') as f:
62 f.write(response.content)
64 if __name__ == '__main__':
65 zhihuLogin()
示例:通过bs获取_xsrf登录知乎
二、JSON与JsonPATH
json简单说就是javascript中的对象和数组,所以这两种结构就是对象和数组两种结构,通过这两种结构可以表示各种复杂的结构
对象:对象在js中表示为{ }
括起来的内容,数据结构为 { key:value, key:value, ... }
的键值对的结构,在面向对象的语言中,key为对象的属性,value为对应的属性值,所以很容易理解,取值方法为 对象.key 获取属性值,这个属性值的类型可以是数字、字符串、数组、对象这几种。
数组:数组在js中是中括号[ ]
括起来的内容,数据结构为 ["Python", "javascript", "C++", ...]
,取值方式和所有语言中一样,使用索引获取,字段值的类型可以是 数字、字符串、数组、对象几种。
json模块提供了四个功能:dumps
、dump
、loads
、load
,用于字符串 和 python数据类型间进行转换。
1. json.loads() 字符串 ==> python类型
把Json格式字符串解码转换成Python对象 从json到python的类型转化对照如下:
import json
strList = '[1, 2, 3, 4]'
strDict = '{"city": "北京", "name": "大猫"}'
print(json.loads(strList))
# [1, 2, 3, 4]
print(json.loads(strDict)) # python3中json数据自动按utf-8存储
# {'city': '北京', 'name': '大猫'}
2. json.dumps() python类型 ==> 字符串
实现python类型转化为json字符串,返回一个str对象 把一个Python对象编码转换成Json字符串
从python原始类型向json类型的转化对照如下:
import json
dictStr = {"city": "北京", "name": "大猫"}
# 注意:json.dumps() 序列化时默认使用的ascii编码
# 添加参数 ensure_ascii=False 禁用ascii编码,按utf-8编码
# chardet.detect()返回字典, 其中confidence是检测精确度
print(json.dumps(dictStr))
# '{"city": "\\u5317\\u4eac", "name": "\\u5927\\u5218"}'
print(json.dumps(dictStr, ensure_ascii=False))
# {"city": "北京", "name": "大刘"}
3. json.dump() # 基本不用
将Python内置类型序列化为json对象后写入文件
import json
listStr = [{"city": "北京"}, {"name": "大刘"}]
json.dump(listStr, open("listStr.json","wb"), ensure_ascii=False)
dictStr = {"city": "北京", "name": "大刘"}
json.dump(dictStr, open("dictStr.json","w"), ensure_ascii=False)
4. json.load() # 基本不用
读取文件中json形式的字符串元素 转化成python类型
# json_load.py
import json
strList = json.load(open("listStr.json"))
print strList
# [{u'city': u'\u5317\u4eac'}, {u'name': u'\u5927\u5218'}]
strDict = json.load(open("dictStr.json"))
print strDict
# {u'city': u'\u5317\u4eac', u'name': u'\u5927\u5218'}
JsonPath
python3中没有jsonpath,改为jsonpath_rw,用法不明
JsonPath 是一种信息抽取类库,是从JSON文档中抽取指定信息的工具,提供多种语言实现版本,包括:Javascript, Python, PHP 和 Java。
JsonPath 对于 JSON 来说,相当于 XPATH 对于 XML。
下载地址:https://pypi.python.org/pypi/jsonpath
安装方法:点击Download URL
链接下载jsonpath,解压之后执行python setup.py install
官方文档:http://goessner.net/articles/JsonPath
JsonPath与XPath语法对比:
Json结构清晰,可读性高,复杂度低,非常容易匹配,下表中对应了XPath的用法。
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
4 import urllib2
5 # json解析库,对应到lxml
6 import json
7 # json的解析语法,对应到xpath
8 import jsonpath
10 url = "http://www.lagou.com/lbs/getAllCitySearchLabels.json"
11 headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"}
13 request = urllib2.Request(url, headers = headers)
15 response = urllib2.urlopen(request)
16 # 取出json文件里的内容,返回的格式是字符串
17 html = response.read()
19 # 把json形式的字符串转换成python形式的Unicode字符串
20 unicodestr = json.loads(html)
22 # Python形式的列表
23 city_list = jsonpath.jsonpath(unicodestr, "$..name")
25 #for item in city_list:
26 # print item
28 # dumps()默认中文为ascii编码格式,ensure_ascii默认为Ture
29 # 禁用ascii编码格式,返回的Unicode字符串,方便使用
30 array = json.dumps(city_list, ensure_ascii=False)
31 #json.dumps(city_list)
32 #array = json.dumps(city_list)
34 with open("lagoucity.json", "w") as f:
35 f.write(array.encode("utf-8"))
示例:拉勾网json接口
三、多线程爬虫案例
python多线程简介
一个CPU一次只能执行一个进程,其他进程处于非运行状态
进程里面包含的执行单位叫线程,一个进程包含多个线程
一个进程里面的内存空间是共享的,里面的线程都可以使用这个内存空间
一个线程在使用这个共享空间时,其他线程必须等他结束(通过加锁实现)
锁的作用:防止多个线程同时用这块共享的内存空间,先使用的线程会上一把锁,其他线程想要用的话就要等他用完才可以进去
python中的锁(GIL)
python的多线程很鸡肋,所以scrapy框架用的是协程
python多进程适用于:大量密集的并行计算
python多线程适用于:大量密集的I/O操作
Queue(队列对象)
Queue是python中的标准库,可以直接import Queue引用;队列是线程间最常用的交换数据的形式
python下多线程的思考
对于资源,加锁是个重要的环节。因为python原生的list,dict等,都是not thread safe的。而Queue,是线程安全的,因此在满足使用条件下,建议使用队列
初始化: class Queue.Queue(maxsize) FIFO 先进先出
包中的常用方法:
Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]])获取队列,timeout等待时间
class ThreadCrawl(threading.Thread):
def __init__(self,thread_name,pageQueue,dataQueue):
super(ThreadCrawl,self).__init__() # 调用父类初始化方法
self.thread_name = thread_name # 线程名
self.pageQueue = pageQueue # 页码队列
self.dataQueue = dataQueue # 数据队列
self.headers = {"User-Agent" : "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;"}
def run(self):
print("启动"+self.thread_name)
while not self.pageQueue.empty(): # 如果pageQueue为空,采集线程退出循环 Queue.empty() 判断队列是否为空
# 取出一个数字,先进先出
# 可选参数block,默认值为True
#1. 如果对列为空,block为True的话,不会结束,会进入阻塞状态,直到队列有新的数据
#2. 如果队列为空,block为False的话,就弹出一个Queue.empty()异常,
page = self.pageQueue.get(False)
url = "http://www.qiushibaike.com/8hr/page/" + str(page) +"/"
html = requests.get(url,headers = self.headers).text
time.sleep(1) # 等待1s等他全部下完
self.dataQueue.put(html)
except Exception as e:
print("结束" + self.thread_name)
class ThreadParse(threading.Thread):
def __init__(self,threadName,dataQueue,lock):
super(ThreadParse,self).__init__()
self.threadName = threadName
self.dataQueue = dataQueue
self.lock = lock # 文件读写锁
self.headers = {"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;"}
def run(self):
print("启动"+self.threadName)
while not self.dataQueue.empty(): # 如果pageQueue为空,采集线程退出循环
html = self.dataQueue.get() # 解析为HTML DOM
text = etree.HTML(html)
node_list = text.xpath('//div[contains(@id, "qiushi_tag_")]') # xpath返回的列表,这个列表就这一个参数,用索引方式取出来,用户名
for i in node_list:
username = i.xpath('.//h2')[0].text # 用户名
user_img = i.xpath('.//img/@src')[0] # 用户头像连接
word_content = i.xpath('.//div[@class="content"]/span')[0].text # 文字内容
img_content = i.xpath('.//img/@src') # 图片内容
zan = i.xpath('./div[@class="stats"]/span[@class="stats-vote"]/i')[0].text # 点赞
comments = i.xpath('./div[@class="stats"]/span[@class="stats-comments"]//i')[0].text # 评论
items = {
"username": username,
"image": user_img,
"word_content": word_content,
"img_content": img_content,
"zan": zan,
"comments": comments
# with 后面有两个必须执行的操作:__enter__ 和 _exit__
# 不管里面的操作结果如何,都会执行打开、关闭
# 打开锁、处理内容、释放锁
with self.lock:
with open('qiushi-threading.json123','ab') as f:
# json.dumps()时,里面一定要加 ensure_ascii = False 否则会以ascii嘛的形式进行转码,文件中就不是中文了
f.write((self.threadName+json.dumps(items, ensure_ascii = False)).encode("utf-8") + b'\n')
except Exception as e:
print(e)
def main():
pageQueue = Queue(10) # 页码的队列,表示10个页面,不写表示不限制个数
for i in range(1,11): # 放入1~10的数字,先进先出
pageQueue.put(i)
dataQueue = Queue() # 采集结果(每页的HTML源码)的数据队列,参数为空表示不限制个数
crawlList = ["采集线程1号", "采集线程2号", "采集线程3号"] # 存储三个采集线程的列表集合,留着后面join(等待所有子进程完成在退出程序)
threadcrawl = []
for thread_name in crawlList:
thread = ThreadCrawl(thread_name,pageQueue,dataQueue)
thread.start()
threadcrawl.append(thread)
for i in threadcrawl:
i.join()
print('1')
lock = threading.Lock() # 创建锁
# *** 解析线程一定要在采集线程join(结束)以后写,否则会出现dataQueue.empty()=True(数据队列为空),因为采集线程还没往里面存东西呢 ***
parseList = ["解析线程1号","解析线程2号","解析线程3号"] # 三个解析线程的名字
threadparse = [] # 存储三个解析线程,留着后面join(等待所有子进程完成在退出程序)
for threadName in parseList:
thread = ThreadParse(threadName,dataQueue,lock)
thread.start()
threadparse.append(thread)
for j in threadparse:
j.join()
print('2')
print("谢谢使用!")
if __name__ == "__main__":
main()
四、动态HTML处理
获取JavaScript,jQuery,Ajax...加载的网页数据
Selenium
Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,类型像我们玩游戏用的按键精灵,可以按指定的命令自动操作,不同是Selenium 可以直接运行在浏览器上,它支持所有主流的浏览器(包括PhantomJS这些无界面的浏览器)。
Selenium 可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。
Selenium 自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。但是我们有时候需要让它内嵌在代码中运行,所以我们可以用一个叫 PhantomJS 的工具代替真实的浏览器。
可以从 PyPI 网站下载 Selenium库https://pypi.python.org/simple/selenium ,也可以用 第三方管理器 pip用命令安装:pip install selenium
Selenium 官方参考文档:http://selenium-python.readthedocs.io/index.html
PhantomJS
PhantomJS 是一个基于Webkit的“无界面”(headless)浏览器,它会把网站加载到内存并执行页面上的 JavaScript,因为不会展示图形界面,所以运行起来比完整的浏览器要高效。
如果我们把 Selenium 和 PhantomJS 结合在一起,就可以运行一个非常强大的网络爬虫了,这个爬虫可以处理 JavaScrip、Cookie、headers,以及任何我们真实用户需要做的事情。
注意:PhantomJS 只能从它的官方网站http://phantomjs.org/download.html) 下载。 因为 PhantomJS 是一个功能完善(虽然无界面)的浏览器而非一个 Python 库,所以它不需要像 Python 的其他库一样安装,但我们可以通过Selenium调用PhantomJS来直接使用。
PhantomJS 官方参考文档:http://phantomjs.org/documentation
Selenium 库里有个叫 WebDriver 的 API。WebDriver 有点儿像可以加载网站的浏览器,但是它也可以像 BeautifulSoup 或者其他 Selector 对象一样用来查找页面元素,与页面上的元素进行交互 (发送文本、点击等),以及执行其他动作来运行网络爬虫。
# 导入 webdriver
from selenium import webdriver
# 要想调用键盘按键操作需要引入keys包
from selenium.webdriver.common.keys import Keys
# 调用环境变量指定的PhantomJS浏览器创建浏览器对象
driver = webdriver.PhantomJS()
# 如果没有在环境变量指定PhantomJS位置
# driver = webdriver.PhantomJS(executable_path="./phantomjs"))
# get方法会一直等到页面被完全加载,然后才会继续程序,通常测试会在这里选择 time.sleep(2)
driver.get("http://www.baidu.com/")
# 获取页面名为 wrapper的id标签的文本内容
data = driver.find_element_by_id("wrapper").text
# 打印数据内容
print data
# 打印页面标题 "百度一下,你就知道"
print driver.title
# 生成当前页面快照并保存
driver.save_screenshot("baidu.png")
# id="kw"是百度搜索输入框,输入字符串"长城"
driver.find_element_by_id("kw").send_keys(u"长城")
# id="su"是百度搜索按钮,click() 是模拟点击
driver.find_element_by_id("su").click()
# 获取新的页面快照
driver.save_screenshot("长城.png")
# 打印网页渲染后的源代码
print driver.page_source
# 获取当前页面Cookie
print driver.get_cookies()
# ctrl+a 全选输入框内容
driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'a')
# ctrl+x 剪切输入框内容
driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'x')
# 输入框重新输入内容
driver.find_element_by_id("kw").send_keys("itcast")
# 模拟Enter回车键
driver.find_element_by_id("su").send_keys(Keys.RETURN)
# 清除输入框内容
driver.find_element_by_id("kw").clear()
# 生成新的页面快照
driver.save_screenshot("itcast.png")
# 获取当前url
print driver.current_url
# 关闭当前页面,如果只有一个页面,会关闭浏览器
# driver.close()
# 关闭浏览器
driver.quit()
Selenium 的 WebDriver提供了各种方法来寻找元素,假设下面有一个表单输入框:
<input type="text" name="user-name" id="passwd-id" />