爬虫近期学习总结

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) # 输出html代码

同时在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) #['m','m','m']
lst = re.findall(r"\d+","5点之前,你要给我5000万")
print(lst) #['5','5000']

search 会进行匹配,但是如果匹配到了第一个结果就会返回这个结果。如果匹配不上search返回值为None

1
2
ret = re.search(r'\d',"5点之前,你要给我5000万")
print(lst) # 5

Match

match 只能从字符串的开头进行匹配

1
2
ret = re.match('a','abc').group()
print(ret) # a

Finditer

finditerfindall 差不多,只不过返回的是迭代器(重点)

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()) # 结果: 123

正则中的内容如何单独提取?

单独获取到正则中的具体内容可以给分组起名字,来获取对应的值

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()) # 结果: <span id='10010'>中国联通</span>
print(result.group("id")) # 结果: 10010 # 获取id组的内容
print(result.group("name")) # 结果: 中国联通 # 获取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

# findall: 匹配字符串中所有符合正则的内容
lst = re.findall(r"\d+", "我的电话是:10086,我女朋友的电话是:10010")
print(lst)

# finditer: 匹配字符串中所有的内容[返回的是迭代器],从迭代器中拿到内容需要 .group()
it = re.finditer(r"\d+", "我的电话是:10086,我女朋友的电话是:10010")
print(it)
for i in it:
print(i.group())

# search:全文匹配找到一个结果就返回,返回的结果是match,从中拿到内容需要 .group()
s = re.search(r"\d+", "我的电话是:10086,我女朋友的电话是:10010")
print(s.group())

# match: 从头开始匹配
s = re.match(r"\d+", "我的电话是:10086,我女朋友的电话是:10010") # success
print(s.group())
s = re.match(r"\d+", "r我的电话是:10086,我女朋友的电话是:10010") # wrong
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>
"""

# (?P<分组名字>正则)可以单独从正则匹配的内容中提取到XXX名字的内容
obj = re.compile(r"<div class='(.*?)'><span id='(.*?)'>(.*?)</span></div>", re.S) # 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")) # 从name组里面拿取数据

BS4 模块使用

bs4模块安装

1
pip install bs4

bs4模块中我们主要用到的是BeautifulSoup,通过BeautifulSoup我们可以导入网页的源代码进行分析,从而提取我们想要的数据。

样例:

1
2
3
4
5
6
7
8
9
10
11
12
import requests
from bs4 import BeautifulSoup
resp =requests.get(url) # 对指定url发起请求
page = BeautifulSoup(resp.text, "html.parser") # BeautifulSoup对数据处理,并声明传入文本为html
# 比如我要找标签table,其class是hq_table
table = page.find("table", class_="hq_table") # 找到table标签,且class是hq_table
# 提取到所有tr
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
pip install lxml

基本概念

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中 :

  1. book, id, name, price…都被称为节点.
  2. Id, name, price, author被称为book的⼦节点
  3. book被称为id, name, price, author的⽗节点
  4. id, name, price,author被称为同胞节点

基本使用

  1. 将要解析的html内容构造出etree对象.
  2. 使⽤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") # /表示层级关系,第一个/是根节点
# result = tree.xpath("/book/name")
# result = tree.xpath("/book/name/text()") # text() 拿文本
# result = tree.xpath("/book/author//nick/text()") # // 后代
# result = tree.xpath("/book/author/*/nick/text()") # * 任意的节点,通配符
result = tree.xpath("/book//nick/text()")

print(result)

一些提取数据写法

[@class='xxx']属性选取
text()获取⽂本
@href获取对应href里的数据
./从上一个结点继续往下寻找

一般的请求方式如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
session = requests.session()
# 准备⽤户名密码
data = {
"loginName": "xxxxxx",
"password": "xxxxxx"
}
# UA
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)
# cookie中的东⻄
print(session.cookies)
# 带着cookie请求书架
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 # 防盗链 意义:本次请求是由哪个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 Thread
def 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 Thread
class 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 Process
def 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 Process
class 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需要将num1后再使用,线程B需要将num1后再使用,而线程C则是需要使用num原来的值8。由于这三个线程都是共享存储num值的内存空间的,并且这三个线程是可以同时并发执行的,当三个线程同时对num操作时,因为num只有一个,所以肯定会存在不同的操作顺序。

因此出于程序稳定运行的考虑,对于线程需要调用内存中的共享数据时,我们就需要为线程加锁。

1
2
3
lock = threading.RLock()    # 调用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, ProcessPoolExecutor

def 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}")


爬虫近期学习总结
https://equinox-shame.github.io/2022/03/14/爬虫近期学习总结/
作者
梓曰
发布于
2022年3月14日
许可协议