这篇博客承接前面的HTTP基本原理,对requests、Xpath和selenium三个库/工具做个简单介绍,并且用三个爬虫实例由浅到深理解爬虫的构思和实现过程,最后是用selenium+chromedriver模拟浏览器,实现对微信公众号文章的爬取。
1. requests requests是python最常见的HTTP客户端库,可以调用requests模块的API伪装成浏览器对网站发起请求。
前面一篇爬虫博客介绍了requests的六种方法,这里不多赘述,主要回顾下发送请求和获得响应的过程。
requests库有两个重要的对象,Request 和Response ,Request对象对应的是请求,向目标网址发送一个请求访问服务。而Response对象,是包含了爬虫返回的内容。
Request对象完整的发起get和post请求方式:
1 2 3 4 5 6 7 requests.get(url, params=None , **kwargs) requests.post(url, data=None , **kwargs)
当服务器正常响应时,返回状态码200 ,这个时候就可以用Response对象的属性来获取网页信息。
Response对象属性:
.status_code:HTTP响应状态码,200表示成功,其他状态码详见上篇博客
.text:HTTP响应体内容的字符串格式
.content:HTTP响应体内容的二进制格式
.encoding:从HTTP header中猜测的响应内容编码方式
.apparent_encoding:从内容中分析出的响应内容编码方式(备选编码方式)
这里需要注意,如果我们获得的响应内容是图片视频和音频的话,需要用二进制格式进行储存。
有了以上基础知识,就可以用request写一个爬虫项目了,我们现在目的是爬取豆瓣电影古装排行榜前20的电影图片。
进入豆瓣电影排行榜网页,按F12进入浏览器开发者工具,点击网页页面分类中的“古装”,对网页数据进行抓包。当鼠标滚轮往下滚动的时候我们可以发现,每次滚动更新,有一个名字很长的数据包会不断更新,还伴随着一大堆jpg图片的出现,很明显这个数据包是我们要抓取的对象。
点击表头选项的响应头,我们看到返回的数据是json格式,编码方式是utf-8。请求url栏中问号之前的部分是我们要的url,参数可以设置一个字典传入。
在负载部分,多次抓包以后可以看到前三个参数是一直不变 的,猜测这几个参数可能是和电影类型和页面布局相关,这个可以不用管。翻页刷新后总是固定显示20个电影,因此limit和数据包内抓取的电影数相关,start和这个数据库中起始的电影编号相关。
再看看响应的json数据中,有一个“cover_url”的键对应值是电影的图片地址,至此思路就很明确了。
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 import requestsurl = 'https://movie.douban.com/j/chart/top_list' param = { 'type' : '30' , 'interval_id' : '100:90' , 'action' :'' , 'start' : '0' , 'limit' : '10' , } headers = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.46' } response = requests.get(url = url, params = param, headers = headers) ls = response.json() for p in ls: with open ('./' + str (p['title' ]) + '.jpg' ,'wb' ) as img: response1 = requests.get(url = p["cover_url" ], headers = headers) img.write(response1.content) print ('over!!!' )
.json()
这个requests库自带的函数还是挺有意思的,在这个例子中是将返回的字符串JSON数据反序列化为list数据,list中嵌套了字典数据,每个电影的信息都储存在字典中。因此这里也可以用.json()['cover_url']
直接对图片网址进行抓取,注意下这里第二次抓取的是图片,返回的是二进制数据,所以用content而不是text。
2. Xpath 前面例子抓包返回的是JSON字符串,所以可以直接提取我们要的信息。如果返回的是HTML源代码,就可以用正则或者Xpath 来解析我们想要的数据。
Xpath是一种解析XML文档信息的工具,我们可以通过lxml库(XML和HTML的解析库)中导入etree模块,实例化etree对象,使用xpath函数结合xpath表达式进行标签定位和指定数据的获取。
Xpath常用规则:
表达式
描述
nodename
选取此节点的所有子节点
/
从当前节点选取直接子节点
//
从当前节点选取所有子孙节点
.
选取当前节点
..
选取当前节点的父节点
@
选取属性
Xpath常用表达式:
1 2 3 4 5 6 7 8 9 10 11 12 属性定位 //div[@class="slist"] # 找到所有class属性值为slist的div标签 层级定位 //div[@class="slist"]/../li[2] # 找到class属性值为slist的div的父标签下的第二个直系子标签li 多属性解析 //div[@class="slist" and @name="Id"] # 找到class属性为slist以及name属性为Id的div标签 模糊匹配 //div[contains(@class, "slist")] # 找到class属性中包含slist值得div标签 文本获取 //li[@class="item"]//text() # 取出class属性值为item的所有li标签下的所有标签文本(包括子标签) 获取属性 //li/a/@href # 取出所有li标签下a标签下的href属性值
etree对象实例化:
1 2 3 4 5 6 7 本地文件(解析保存在本地的HTML文件): tree = etree.parse(文件名) tree.xpath("xpath表达式") 网络数据(实例化一个html类): tree = etree.HTML(网页内容字符串) tree.xpath("xpath表达式")
注意下xpath解析出来的数据是以列表 形式存储的,接下来示范一下requests结合Xpath写一个爬虫程序,目的是抓取彼岸图网 的4k动漫封面图。
进入页面以后同样F12审查元素,点击不同页抓取返回的数据包,发现翻到第x页能抓到index_x.html,但是第一页没有下划线和其他页稍有不同,为了方便起见就从第二页开始抓。
仔细观察可以发现,所有封面图都在属性值为slist的div标签下的ul标签下的li标签中(这么说着好绕= =),前面http原理的博客说过,这样的标签可以看出是是无序列表,我们要找的封面图片地址可以通过img标签的src属性获得,图片名称可以通过alt属性获得,因此可以写如下的爬虫代码:
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 import requestsfrom lxml import etreeimport osfor i in range (2 ,3 ): url = 'http://pic.netbian.com/4kdongman/index_' +str (i)+'.html' headers = { 'User-Agent' :'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.3' } response = requests.get(url = url, headers = headers) page_text = response.text tree = etree.HTML(page_text) li_list = tree.xpath('//div[@class="slist"]/ul/li' ) if not os.path.exists('./pic' ): os.mkdir('./pic' ) for li in li_list: img_src = 'http://pic.netbian.com' + li.xpath('./a/img/@src' )[0 ] img_name = li.xpath('./a/img/@alt' )[0 ] + '.jpg' img_name = img_name.encode('iso-8859-1' ).decode('gbk' ) img_data = requests.get(url = img_src, headers = headers).content img_path = 'pic/' + img_name with open (img_path,'wb' ) as fp: fp.write(img_data) print (img_name,'下载成功!!!' )
当然,上面爬虫抓取的只是封面图片,并不是高清图片,因为高清图片是需要登录账号花钱下载的….我们只能合法地从我们能在浏览器中看到的图片爬取信息。而且如果你在短时间内发起大量请求的话,ip是很有可能被封的,以后再讲一些预防反爬的措施。
3. selenium selenium是一个自动化测试工具,本质是通过驱动浏览器,完全模拟浏览器中的操作 ,比如拖动、点击下拉等等。为什么要模仿浏览器中的操作呢?因为很多网站是动态加载的,requests这一类的模块无法直接执行JavaScript代码,这个时候就可以通过测试工具selenium模仿人在浏览器中的操作,从而获得网页渲染之后的结果。
selenium官方网站:Selenium
(很暖心地有中文文档)以最新的selenium指南为基础,简单介绍一下其用法。
强调两点:
简单地以淘宝首页作为例子:
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 from selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.chrome.service import Servicefrom time import sleepbro = Service(executable_path = './chromedriver.exe' ) driver = webdriver.Chrome(service = bro) driver.get('https://www.taobao.com/' ) search_input = driver.find_element(By.ID, 'q' ) search_input.send_keys('Iphone' ) btn = driver.find_element(By.CSS_SELECTOR, '.btn-search' ) btn.click() driver.get('https://www.baidu.com' ) sleep(2 ) driver.back() sleep(2 ) driver.forward() sleep(2 ) driver.quit()
简单来说,流程可以分为
创建驱动实例开启会话 driver = webdriver.Chrome()
在浏览器中执行操作 driver.get("https://www.baidu.com")
请求浏览器信息 title = driver.title
建立等待策略(隐式或显示) driver.implicitly_wait(0.5)或者用sleep(1)
定位标签 text_box = driver.find_element(by=By.NAME, value="my-text")
节点交互 text_box.send_keys("Selenium")
获取信息 value = message.text
结束会话,关闭浏览器 driver.quit()
浏览器创建 selenium支持的浏览器有Chrome、Firefox、Edge、Internet Explorer和Safari。参考支持的浏览器列表 | Selenium
元素定位 webdriver提供了8种内置的定位方法:
1 2 3 4 5 6 7 8 9 10 from selenium.webdriver.common.by import Byfind_element(By.ID, 'value' ) find_element(By.NAME, 'value' ) find_element(By.CLASS_NAME, 'value' ) find_element(By.TAG_NAME, 'value' ) find_element(By.LINK_TEXT, 'value' ) find_element(By.PARTIAL_LINK_TEXT, 'value' ) find_element(By,XPATH, 'value' ) find_element(By.CSS_SELECTOR, 'value' )
节点交互 常见的节点交互有以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 driver.get("https://www.baidu.com" ) driver.back() driver.forward() driver.refresh() driver.add_cookie({"name" : "key" , "value" : "value" }) driver.find_element(By.LINK_TEXT, "new window" ).click() driver.find_element(By.ID, 'value' ).send_keys('value' ) driver.switch_to.new_window('tab' ) driver.switch_to.new_window('window' ) driver.switch_to.window(original_window) driver.close() driver.quit()
动作链 上面说的交互是对页面中存在的标签或者说是元素进行交互,而对于没有特定对象的,比对鼠标的双击、拖拽,鼠标滚轮的操作,键盘的输入 等,这些操作就需要动作链来执行。该部分内容官网有详细介绍 ,这里就举个例子了解一下怎么用的就行。
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 import timefrom selenium.webdriver.common.by import Byfrom selenium import webdriverfrom time import sleepfrom selenium.webdriver import ActionChainsbro = webdriver.Chrome(executable_path = './chromedriver' ) bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable' ) bro.switch_to.frame('iframeResult' ) div = bro.find_element(By.ID, 'draggable' ) action = ActionChains(bro) action.click_and_hold(div) for i in range (10 ): action.move_by_offset(5 ,0 ) action.perform() time.sleep(3 ) action.release() bro.quit()
微信公众号爬虫范例 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import randomimport refrom pandas import DataFrameimport osimport requestsimport timefrom selenium import webdriverfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.common.by import Byfrom time import sleepfrom selenium.webdriver import ChromeOptionsfrom lxml import etreemodern='author' keyword = '冷兔' page = 2 headers = { 'User-Agent' :'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' } option = ChromeOptions() option.add_experimental_option('excludeSwitches' , ['enable-automation' ]) bro = webdriver.Chrome(executable_path = './chromedriver' , options=option) bro.get("https://www.sogou.com/index.php" ) wait = WebDriverWait(bro, 2 ) search_input = bro.find_element(By.ID, 'query' ) search_input.send_keys(keyword) btn = bro.find_element(By.ID, 'stb' ) btn.click() time.sleep(2 ) btn = bro.find_element(By.XPATH, '//*[@id="loginBtn"]' ) btn.click() time.sleep(2 ) btn = bro.find_element(By.XPATH, '/html/body/div[3]/div[3]/div[2]/div[4]/div/a[2]' ) btn.click() time.sleep(10 ) button = bro.find_element(By.ID, 'sogou_weixin' ) button.click() article_button = bro.find_element(By.XPATH, '//*[@id="scroll-header"]/form/div/input[1]' ) article_button.click() url_list=[] for i in range (page): page_text = bro.page_source tree = etree.HTML(page_text) author=tree.xpath('/html/body/div[2]/div[1]/div[3]/ul/li/div[2]/div/a/text()' ) print (author) url_page_list=tree.xpath('/html/body/div[2]/div[1]/div[3]/ul/li/div[2]/h3/a/@href' ) for j in range (len (author)): if author[j]==keyword: url_list.append(url_page_list[j]) elif modern=='article' : url_list.append(url_page_list[j]) else : continue if i != page - 1 : next_button = bro.find_element(By.ID, 'sogou_next' ) next_button.click() time.sleep(2 ) url_list=['https://weixin.sogou.com/' +i for i in url_list] title_list=[] content_list=[] time_list=[] author_list=[] for url in url_list: response = bro.get(url=url) time.sleep(random.randint(1 , 3 )) page_text = bro.page_source tree_content = etree.HTML(page_text) title = tree_content.xpath('/html/body/div[1]/div[2]/div[1]/div/div[1]/h1/text()' ) title = re.sub(r'\s' , '' , '' .join(title)) content = tree_content.xpath('/html/body/div[1]/div[2]/div[1]/div/div[1]/div[3]//text()' ) content = re.sub(r'\s*' , '' , '' .join(content)) time1=tree_content.xpath('//*[@id="publish_time"]/text()' )[0 ] author=tree_content.xpath('//*[@id="js_name"]/text()' )[0 ] author=re.sub(r'\s' , '' , '' .join(author)) title_list.append(title) content_list.append(content) time_list.append(time1) author_list.append(author) data = {'title' : title_list, 'time' :time_list,'author' :author_list,'content' : content_list} df = DataFrame(data) df.to_excel('./微信公众号_' +keyword+'.xlsx' )
这里规避检测识别是设置Chromedriver的启动参数,在启动Chromedriver之前,为Chrome开启实验性功能参数excludeSwitches
,它的值为[‘enable-automation’]
。这个参数有什么作用呢?
我们正常运行selenium时,最上方是有提示”Chrome正受到自动测试软件的控制“的,这个参数设置就是禁用浏览器的提示 。如果我们用selenium模拟chrome浏览器访问网站,网站可以通过检查window.navigator.webdriver
返回值判断我们是用正常的浏览器访问还是用selenium模拟浏览器发起的访问。
如果返回值为undefined,说明是正常浏览器;如果返回true说明用selenium模拟的浏览器 。
为Chrome开启实验性功能参数excludeSwitches后,和正常浏览器一样返回的是undefined。
当然,反爬手段不仅仅是这一个,这个以后的有空再细说。上面的爬虫代码参考了刘丹老师,不是最终版本,还可以对输出内容和样式继续做优化。简单看一下实现的结果: