Requests 的基本使用
用requests
构建一个请求实现得到网页对应的源代码,对应构造方式有许多,大致整理如下:
get请求
1 2 import requests resp = requests.get(url,headers,data)
post请求
1 2 import requests resp = requests.post(url,headers,data)
获取响应码
1 2 3 import requests resp = requests.post(url,headers,data)print (resp.status_code)
将请求的数据保存在一个变量中,我们可以加入一个.text
实现转换成Html
文件,同时在转换之前可以加入一个编码,将得到的数据进行对应的编码
例如:
1 2 3 4 import requests resp = requests.post(url,headers,data) resp.encoding = "xxx" print (resp.text)
同时在requests
之中的url、headers、data
分别对应着请求的链接、请求头、请求数据。
一般请求头是用于伪装自己是浏览器,而避免服务器端拒绝访问。可以通过浏览器的F12
进行抓取,请求数据我们可以写入一定Cookies
,来实现某些需要登录等,才能完成的抓取。
正则表达式
Regular Expression, 正则表达式, ⼀种使⽤表达式的⽅式对字符串 进⾏匹配的语法规则. 我们抓取到的⽹⻚源代码本质上就是⼀个超⻓的字符串, 想从⾥⾯提 取内容.⽤正则再合适不过了. 正则的优点: 速度快, 效率⾼, 准确性⾼ 正则的缺点: 新⼿上⼿难度有 点⼉⾼. 不过只要掌握了正则编写的逻辑关系, 写出⼀个提取⻚⾯内容的正则 其实并不复杂 正则的语法: 使⽤元字符进⾏排列组合⽤来匹配字符串。
在线测试正则表达式https://tool.oschina.net/regex/
常用元字符:具有固定含义的特殊符号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \w 匹配字母数字下划线\d 匹配数字\s 匹配空白符\t 匹配一个制表符 ^ 匹配字符串的开始 $ 匹配字符串的结尾\W 匹配非字母数字下划线\D 匹配数字\S 匹配空白符 a|b 匹配字符a或者b () 匹配括号内的表达式,也表示一个组 [...] 匹配字符串中的字符 [^...] 匹配除了字符组中字符的所有字符
量词:控制前面元字符出现的次数
1 2 3 4 5 6 * 重复零次或者更多次 + 重复一次或者多次 ? 重复零次或者一次 {n } 重复n 次 {n ,} 重复n 次或者更多次 {n ,m} 重复n 到m次
贪婪匹配和惰性匹配
1 2 .* 贪婪匹配(尽可能多的匹配内容) .* ? 惰性匹配(尽可能少的匹配内容)
案例
案例一
1 2 3 4 5 6 7 8 案例 1<div class ="jay" > 周杰伦</div > <div class ="jj" > 林俊杰</div > 测试表达式<div class =".*?" > .*?</div > 测试结果 两处匹配<div class ="jay" > 周杰伦</div > <div class ="jj" > 林俊杰</div >
案例二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 案例 2 str: 玩⼉吃鸡游戏, 晚上⼀起上游戏, ⼲嘛呢? 打游戏啊 reg: 玩⼉.*?游戏 此时匹配的是: 玩⼉吃鸡游戏 reg: 玩⼉.*游戏 此时匹配的是: 玩⼉吃鸡游戏, 晚上⼀起上游戏, ⼲嘛呢? 打游 戏 str: <div > 胡辣汤</div > reg: <.*> 结果: <div > 胡辣汤</div > str: <div > 胡辣汤</div > reg: <.*?> 结果:<div > </div > str: <div > 胡辣汤</div > <span > 饭团</span > reg: <div > .*?</div > 结果: <div > 胡辣汤</div >
RE 模块
那么又一个问题来了,正则我会写了,怎么在python
程序中使用呢?答案便是 re 模块。
Findall
findall
查找所有,返回list
1 2 3 4 lst = re.findall("m" ,"mai le fo len,mai ni mei!" )print (lst) lst = re.findall(r"\d+" ,"5点之前,你要给我5000万" )print (lst)
Search
search
会进行匹配,但是如果匹配到了第一个结果就会返回这个结果。如果匹配不上search返回值为None
1 2 ret = re.search(r'\d' ,"5点之前,你要给我5000万" )print (lst)
Match
match
只能从字符串的开头进行匹配
1 2 ret = re.match('a' ,'abc' ).group()print (ret)
Finditer
finditer
和 findall
差不多,只不过返回的是迭代器(重点)
1 2 3 it = re.finditer("m" ,"mai le fo len,mai ni mei!" )for el in it: print (el.group())
Compile
compile()
可以将⼀个⻓⻓的正则进⾏预加载. ⽅便后⾯的使⽤
1 2 3 4 5 obj = re.compile (r'\d{3}' ) ⼀个 正则表达式对象, 规则要匹配的是3 个数字 ret = obj.search('abc123eeee' ) ⽤search, 参数为待匹配的字符串print (ret.group())
正则中的内容如何单独提取?
单独获取到正则中的具体内容可以给分组起名字,来获取对应的值
1 2 3 4 5 6 7 8 s = """ <div class='⻄游记'><span id='10010'>中国联通</span></div> """ obj = re.compile (r"<span id='(?P<id>\d+)'>(?P<name>\w+)</span>" , re.S) result = obj.search(s)print (result.group()) print (result.group("id" )) print (result.group("name" ))
这⾥可以看到我们可以通过使⽤分组. 来对正则匹配到的内容进⼀步的进⾏筛选
练习代码
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 import re lst = re.findall(r"\d+" , "我的电话是:10086,我女朋友的电话是:10010" )print (lst) it = re.finditer(r"\d+" , "我的电话是:10086,我女朋友的电话是:10010" )print (it)for i in it: print (i.group()) s = re.search(r"\d+" , "我的电话是:10086,我女朋友的电话是:10010" )print (s.group()) s = re.match(r"\d+" , "我的电话是:10086,我女朋友的电话是:10010" ) print (s.group()) s = re.match(r"\d+" , "r我的电话是:10086,我女朋友的电话是:10010" ) print (s.group()) obj = re.compile (r"\d+" ) ret = obj.finditer("我的电话是:10086,我女朋友的电话是:10010" )for it in ret: print (it.group()) ret = obj.findall("我的电话是:10086,我女朋友的电话是:10010" )print (ret) s = """ <div class='beijing'><span id='1'>北京</span></div> <div class='shanghai'><span id='1'>上海</span></div> <div class='chongming'><span id='1'>崇明</span></div> <div class='guangzhou'><span id='1'>广州</span></div> <div class='fujian'><span id='1'>福建</span></div> """ obj = re.compile (r"<div class='(.*?)'><span id='(.*?)'>(.*?)</span></div>" , re.S) ret = re.compile (r"<div class='(?P<class>.*?)'><span id='(?P<id>.*?)'>(?P<name>.*?)</span></div>" , re.S) result = obj.finditer(s)for i in result: print (i.group()) print (i.group("name" ))
BS4 模块使用
bs4模块安装
在bs4
模块中我们主要用到的是BeautifulSoup
,通过BeautifulSoup
我们可以导入网页的源代码进行分析,从而提取我们想要的数据。
样例:
1 2 3 4 5 6 7 8 9 10 11 12 import requestsfrom bs4 import BeautifulSoup resp =requests.get(url) page = BeautifulSoup(resp.text, "html.parser" ) table = page.find("table" , class_="hq_table" ) tr_list = table.find_all("tr" )for tr in tr_list: td_list = tr.find_all("td" ) name = td_list[0 ].text print (name)
如果想要找到p
标签里的img
标签呢?我们可以使用find
样例: img = p.find('img')
那我们想要获取img
标签里的链接src
那么我们可以再使用get
样例: src = img.get("src")
XPath 模块使用
XPath
是⼀⻔在 XML
⽂档中查找信息的语⾔。XPath
可⽤来在 XML
⽂档中对元素和属性进⾏遍历⽽我们熟知的Html
恰巧属于XML
的 ⼀个⼦集。 所以完全可以⽤XPath
去查找Html
中的内容。
XPath 模块安装
基本概念
1 2 3 4 5 6 7 8 9 <book > <id > 1</id > <name > 野花遍地⾹</name > <price > 1.23</price > <author > <nick > 周⼤强</nick > <nick > 周芷若</nick > </author > </book >
在上述html中 :
book, id, name, price…都被称为节点.
Id, name, price, author被称为book的⼦节点
book被称为id, name, price, author的⽗节点
id, name, price,author被称为同胞节点
基本使用
将要解析的html内容构造出etree
对象.
使⽤etree
对象的xpath()
⽅法配合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 25 26 27 28 29 30 31 32 33 34 from lxml import etree xml = """ <book> <id>1</id> <name>野花遍地香</name> <price>1.23</price> <nick>臭豆腐</nick> <author> <nick id="beijing">北京</nick> <nick id="shanghai">上海</nick> <nick id="fujian">福建</nick> <nick id="tianjin">天津</nick> <nick id="yunnan">云南</nick> <div> <nick id="chengdu">成都</nick> </div> </author> <partner> <nick id="ppc">碰碰车</nick> <nick id="ppbc">频频爆出</nick> </partner> </book> """ tree = etree.XML(xml) result = tree.xpath("/book//nick/text()" )print (result)
一些提取数据写法
[@class='xxx']
属性选取
text()
获取⽂本
@href
获取对应href
里的数据
./
从上一个结点继续往下寻找
Requests 处理 Cookie 模拟登录
一般的请求方式如requests.get()
和requests.post()
在请求时不是连续的,在面对登录抓取的时候这就会陷入困境,因而我们引入一个requests.session()
样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import requests session = requests.session() data = { "loginName" : "xxxxxx" , "password" : "xxxxxx" } headers = { "user-agent" :"Mozilla/5.0 (Macintosh; IntelMac OS X 10_15_4) AppleWebKit/537.36 (KHTML, likeGecko) Chrome/87.0.4280.141 Safari/537.36" } resp = session.post(url=url, data=data, headers=headers)print (session.cookies) resp =session.get(url)print (resp.text)
防盗链处理
在爬取有些网站时,会有对应的防盗链进行反爬取,本质原理上就是一个判定,判断你这个请求是由哪一个url
所产生的,如果定位不到对应的url
链接那么就会直接拒绝访问,我们对应的处理方式则是在headers
里面加入一个Referer
1 2 3 4 headers = { "user-agent" :"Mozilla/5.0 (Macintosh; IntelMac OS X 10_15_4) AppleWebKit/537.36 (KHTML, likeGecko) Chrome/87.0.4280.141 Safari/537.36" "Referer" :url }
代理使用
当我们反复抓取⼀个⽹站时, 由于请求过于频繁, 服务器很可能会将你的IP进⾏封锁来反爬.。为了避免IP
被服务器封锁,我们需要使用代理进行爬取。
样例:
1 2 3 4 5 6 7 8 9 import requests headers = { "User-Agent" : "Mozilla/5.0 (Macintosh; IntelMac OS X 10_15_4) AppleWebKit/537.36 (KHTML, likeGecko) Chrome/87.0.4280.141 Safari/537.36" , } proxies = { "https" : "https://xxxx.xxxx.xxxx.xxxx" } resp = requests.get("https://www.baidu.com" ,headers=headers, proxies=proxies) print (resp.text)
**注:**代理IP⼀般属于灰⾊产业,故不深入讨论。
多线程
实现多线程我们需要导入包Thread
1 from threading import Thread
那么怎么实现呢?我们通过函数为一个线程,主函数一个线程而实现双线程。如果需要多线程,则可以添加多个函数。
写法一:
1 2 3 4 5 6 7 8 9 from threading import Threaddef func (): for i in range (1000 ): print ("func" , i)if __name__ == '__main__' : t = Thread(target=func) t.start() for i in range (1000 ): print ("main" , i)
写法二:
1 2 3 4 5 6 7 8 9 10 11 from threading import Threadclass MyThread (Thread ): def run (self ): for i in range (1000 ): print ("func" , i)if __name__ == '__main__' : t = MyThread() t.start() for i in range (1000 ): print ("main" , i)
多进程
多进程的写法与多线程十分相似,同样有两种写法,就不过多阐述了。
写法一:
1 2 3 4 5 6 7 8 9 from multiprocessing import Processdef func (): for i in range (1000 ): print ("func" , i)if __name__ == '__main__' : t = Process(target=func) t.start() for i in range (1000 ): print ("main" , i)
写法二:
1 2 3 4 5 6 7 8 9 10 11 from multiprocessing import Processclass MyProcess (Thread ): def run (self ): for i in range (1000 ): print ("func" , i)if __name__ == '__main__' : t = MyProcess() t.start() for i in range (1000 ): print ("main" , i)
线程锁
我们知道,不同进程之间的内存空间数据是不能够共享的,试想一下,如果可以随意共享,谈何安全?但是一个进程中的多个线程是可以共享这个进程的内存空间中的数据的,比如多个线程可以同时调用某一内存空间中的某些数据(只是调用,没有做修改)。
试想一下,在某一进程中,内存空间中存有一个变量对象的值为num=8
,假如某一时刻有多个线程需要同时使用这个对象,出于这些线程要实现不同功能的需要,线程A
需要将num
减1
后再使用,线程B
需要将num
加1
后再使用,而线程C
则是需要使用num
原来的值8
。由于这三个线程都是共享存储num
值的内存空间的,并且这三个线程是可以同时并发执行的,当三个线程同时对num
操作时,因为num
只有一个,所以肯定会存在不同的操作顺序。
因此出于程序稳定运行的考虑,对于线程需要调用内存中的共享数据时,我们就需要为线程加锁。
1 2 3 lock = threading.RLock() lock.acquire() lock.release()
线程池和进程池
当我们对某些⽹站内容进⾏抓取的时候⾮常容易遇到这样⼀种情况,我们发现这⽹站的数据太多了。有⼀万多⻚, 也就对应着 ⼀万多个url
。那我们设计多线程的时候如果每个url
对应⼀个线程就会产⽣新问题。朋友,你⼀定要知道。创建线程本身也是要消耗你的计算机资源的。线程不是变魔术变出来的。那这时我们就可以考虑能不能重复的使⽤线程呢? 答案当然可以。线程池就可以帮你搞定。
工作原理
创建⼀个⼤池⼦,存放固定数量的线程。然后把我们要执⾏的任务丢给线程池。由线程池去分配哪个线程来完成该任务。
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutordef fn (name ): for i in range (1000 ): print (name, i)if __name__ == '__main__' : with ThreadPoolExecutor(50 ) as t: for i in range (100 ): t.submit(fn, name=f"线程{i} " ) with ProcessPoolExecutor(50 ) as t: for i in range (100 ): t.submit(fn, name=f"进程{i} " )