网页基本知识
- 统一资源标识符:URI - Universal Resource Identifier
- 统一资源定位符:URL - Universal Resource Locator
- HTTP协议- 请求响应式的协议
- HTTP请求
- 请求行 - GET / HTTP /1.1
- GET:从服务获取资源
- POST:向服务器提交数据
- 请求行 - GET / HTTP /1.1
- HTTP响应
- 响应行 - HTTP/1.1 200 OK
- 响应状态码 - 404 Not Found / 403 Fornbidden
- 2xx:成功
- 3xx:重定向
- 4xx:请求有问题
- 5xx:服务器有问题
- 响应状态码 - 404 Not Found / 403 Fornbidden
- 响应行 - HTTP/1.1 200 OK
HTML
HTML - 超文本标记语言
HTML页面源代码由三样东西构成
- 标签 - 数据(content - 承载内容)
- 层叠样式表(css) - 显示(display - 渲染页面)
- Javascript(JS) - 行为(behavior - 交互行为)
标签
-
结构
HTML文件开始 文件头开始
文件内容
文件头结束
文件体开始
文件体内容
文件体结束
HTML文件结束 -
常用标签
-
文本:
h1 ~ h6 :标题
p :段落
sup / sub :上标 / 下标
em / strong:强调
-
图像
img - src 属性
-
链接
页面链接
锚链接:a(anchor) - 属性有:href / name / target (_self / _blank / _parent / _top)
功能链接
-
列表
ul - 无序列表(unordered list) - li (列表项,list item)
ol - 有序列表(ordered list)
di - 定义列表(defintion list) - dt (定义标题) / dd (定义描述)
-
音视频
audio(音频)标签:
video(视频)标签:
更多标签可点击标签查看
CSS
-
样式表分类:
- 外部样式表
- 内部样式表
- 内嵌样式表(行内样式表)
-
选择器:
- 通配符选择器
- 标签选择器
- 类选择器(比标签选择器具体)
- ID选择器
- 父子选择器
- 后代选择器
- 相邻兄弟选择器
- 兄弟选择器
- 属性选择器
注意:如果为一个标签写了多套样式其不冲突,那么所有的样式会叠加,如果样式发生了冲突,需要遵循重要性原则(!important),具体性原则,就近原则
网络爬虫
网络爬虫:按照一定的规则自动浏览万维网并获取信息的机器人程序(或脚本)
应用领域:
- 搜索引擎
- 新闻聚合
- 社交应用
- 舆情监控
- 行业数据
如何写爬虫程序:
- 通过requests三方库来获取网络资源
- 解析页面:
- 正则表达式
- CSS选择器解析
- xpath解析
import re import requests resp = requests.get( url='https://movie.douban.com/top250', headers={ 'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' } ) patten = re.compile(r'<span class="title">(.*?)</span>') print(patten.findall(resp.text))
以上代码是爬取了豆瓣电影top250的电影名称,先使用requests.get请求来获取页面,在GET请求里需要添加URL参数和请求头,然后使用正则表达式捕获组解析"豆瓣电影Top250"中的中文电影名称
import bs4 import requests import re resp = requests.get('https://sohu.com/index.html') soup = bs4.BeautifulSoup(resp.text, 'html.parser') anchors = soup.select('a[href]') """ for anchor in anchors: 通过标签对象的attrs属性(attribute)的索引操作获取指定的属性值 print(anchor.attrs['href']) """ pattern = re.compile(r'(?<!https:)//') def fix_url(url): url = pattern.sub('', url) if not url.startswith('https://'): url ='https://' + url return url resp = requests.post('https://sohu.com/index.html') soup = bs4.BeautifulSoup(resp.text, 'html.parser') # anchors = soup.select('div.list-mod.list-mod-0 > div > ul > li > a') anchors = soup.select('div.list16 > ul > li > a') for anchor in anchors: print(anchor.attrs['title']) print(fix_url(anchor.attrs['href'])) print('程序结束!!!')
以上代码爬取了搜狐网界面的新闻标题以及链接:
-
通过resquests.get获取页面
-
选择beautifulsoup的解析器 —> ‘html.parser’
-
使用bs4的select搜索树节点
-
遍历文档书,通过标签对象的attrs属性的索引操作获取指定的属性值
-
当得到属性值之后发现部分属性值的格式不正确
href="//www.sohu.com/a/443278966_120698792"
-
所以我们定义了一个fix_url函数进行修改,在函数中使用了正则表达式的(?<!exp)零宽断言分类
-
接下来通过同样的方法获取新闻的标题
-
在这使用了css选择器获取标签下的内容
-
最后输出新闻标题和新闻链接
-
tip:使用css选择器不知道如何写的时候,可以在网页页面选择需要爬取的元素点击检查,在出现的界面中点击鼠标右键,点击Copy,选择Copy selector即可
用Python读写Excel文件
import requests
import bs4
import xlwt
import time
import random
def fetch_movie_detail(detail_url):
resp = requests.get(
url=detail_url,
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/87.0.4280.88 Safari/537.36'
}
)
doc = bs4.BeautifulSoup(resp.text, 'html.parser')
genre_spans = doc.select('span[property="v:genre"]')
genre = '/'.join([genre_span.text for genre_span in genre_spans])
country_span = doc.select('span.pl')[4]
country = country_span.next_sibling.strip()
# 获取到标签之后,可以通过next_sibling获取下一个兄弟节点(包括文本节点),
# 如果要获取下一个标签节点(不包括文本节点),可以使用next_element属性获取下一个标签
# previous_sibling(获取上一个兄弟节点)
# strip()将字符串开头和结尾的空格去掉
language_span = doc.select('span.pl')[5]
language = language_span.next_sibling.strip()
duration = doc.select_one('span[property="v:runtime"]').attrs['content']
return genre, country, language, duration
def main():
# 创建工作簿
wb = xlwt.Workbook()
# 创建工作表
sheet = wb.add_sheet('Top250')
col_names = ('排名', '名称', '类型', '评分', '制片国家', '语言', '时长')
for index, name in enumerate(col_names):
sheet.write(0, index, name)
rank = 0
for page in range(10):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={page * 25}',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/87.0.4280.88 Safari/537.36'
}
)
doc = bs4.BeautifulSoup(resp.text, 'html.parser')
info_divs = doc.select('div.info')
for info_div in info_divs:
rank += 1
anchor = info_div.select_one('div.hd>a')
detail_url = anchor.attrs['href']
title = anchor.select_one('span.title').text
score = info_div.select_one('div.bd>div.star>span.rating_num').text
# 创建一个列表保存表单元素
movie_details = [rank, title, score]
movie_details += fetch_movie_detail(detail_url)
for index, item in enumerate(movie_details):
# 写入行索引、列索引、元素
sheet.write(rank, index, item)
time.sleep(random.random() * 3 + 3)
wb.save('豆瓣电影.xls')
if __name__ == '__main__':
main()
以上代码是将"豆瓣电影Top250"的排名, 名称, 类型, 评分, 制片国家, 语言,时长写入Excel文件中
-
先将以上信息使用三方库requests,bs4模块爬取出来
-
导入xlwt模块 -> 创建工作簿 -> 创建工作表 -> 添加表头数据 -> 将数据写入单元格 -> 保存工作簿
注意:使用xlwt / slrd 模块保存的文件格式为xls,使用openpyxl导入的XML保存的格式为xlsx
-
最后导入了时间模块随机等待3 - 6秒的时间,让爬取的频率没那么快
CSV - Comma Seperated Values
csv(逗号分隔值文件)
import csv
with open('test.csv', 'w', encoding='utf-8') as file:
# csv.QUOTE_ALL将所有单引号变为双引号
csv_writer = csv.writer(file, quoting=csv.QUOTE_ALL)
csv_writer.writerow(['张飞', 90, 70, 90])
csv_writer.writerow(['赵云', 78, 79, 67])
csv_writer.writerow(['关羽', 84, 75, 87])
csv_writer.writerow(['马超', 96, 85, 97])
csv_writer.writerow(['黄忠', 88, 90, 89])
使用csv模块将writerow()里面的内容写入在text.csv里面
with open('2018年北京积分落户数据.csv', newline='', encoding='utf-8') as file:
reader = csv.reader(file)
for row in reader:
print(row)
使用csv模块将文件里的内容读出来
API - Application Programmering Interface
通过API获取数据
import requests
def download_picture(picture_url):
resp = requests.get(picture_url)
if resp.status_code == 200:
filename = picture_url[picture_url.rfind('/') + 1:]
with open(f'images/{filename}', 'wb') as file:
file.write(resp.content)
def main():
key = '密钥KEY'
for page in range(1, 2):
resp = requests.get(
f'http://api.tianapi.com/meinv/index?key={key}&page={page}&num=10'
)
beauty_list = resp.json()['newslist']
for beauty in beauty_list:
picture_url = beauty['picUrl']
download_picture(picture_url)
if __name__ == '__main__':
main()
以上代码是在某个API平台上获取了该页面meinv这一类的数据,可以直接获取JSON数据,然后使用三方库requests得到图片的数据,通过resp.content写入在文件中
在这里我们通过以上代码可以发现,在这段代码中存在了两个进程,一个是下载图片的链接,另一个是将下下来的图片链接写入到文件里去,但是写入文件的时候比较耗费时间,会阻塞下载图片的进程,这个时候我们就可以通过多线程的方式来将耗时间的任务放入线程池中执行
多线程
-
通过threading库中的Thread模块来创建多线程
import time def output1(): while True: print('Ping', end='', flush=True) time.sleep(0.001) def output2(): while True: print('pong', end='', flush=True) time.sleep(0.001) def main(): output1() output2() main()
在这段代码中写了2个死循环的函数,output1每隔0.01秒无限输出’ping’, output2每隔0.01秒无限输出’pong’,当我们把这两个函数放在一个主函数里面同时执行的时候,会发现输出台会无限输出’ping’, 而没有’pong’,说明只执行了output1,没有执行output2,如果想要两个程序同时执行,就需要多线程。
import time
from threading import Thread
def output(content):
while True:
print(content, end='', flush=True)
time.sleep(0.001)
def main():
t1 = Thread(target=output, args=('ping', ))
t1.start()
t2 = Thread(target=output, args=('pong', ))
t2.start()
if __name__ == '__main__':
main()
在这段代码中导入threading库,将需要执行的操作包装成一个函数,然后在main函数里使用Thread模块,里面的target后输入需要执行的函数,args后跟的是函数里的参数,需要注意的是参数必须是以元组的类型,然后将这段代码通过变量保存,在下面使用变量名.stat()执行,这样就可以是2个程序同时执行了。
- 但是在编写多线程程序时,一定要注意线程的创建和释放有较大的开销,而且创建了太多的线程,线程之间的调度切换本身也是有开销的,所以线程并不是越多越好,最好的用法是创建若干个线程,然后重复的使用他们。
- 线程池:先用一个容器,提前创建号若干个线程放进去,用线程的时候从线程池中借出一个线程用完了之后,不要释放线程,而是把这个线程放回池子,让线程可以被重复利用。这就是所谓的池化技术 — 空间换取时间的做法
from concurrent.futures.thread import ThreadPoolExecutor
import requests
def download_picture(picture_url):
resp = requests.get(picture_url)
if resp.status_code == 200:
filename = picture_url[picture_url.rfind('/') + 1:]
with open(f'images/{filename}', 'wb') as file:
file.write(resp.content)
def main():
# 上下文语法,创建线程池
with ThreadPoolExecutor(max_workers=10) as pool:
key = '密钥KEY'
for page in range(1, 4):
resp = requests.get(
f'http://api.tianapi.com/meinv/index?key={key}&page={page}&num=10'
)
beauty_list = resp.json()['newslist']
for beauty in beauty_list:
picture_url = beauty['picUrl']
# 将耗时间的任务放到线程池中交给线程来执行
pool.submit(download_picture, picture_url)
if __name__ == '__main__':
main()
还是那段通过API获取图片的代码,只不过我们在main函数的下面使用了with上下文语法,来创建线程池,这里使用的最多能同时运行的线程数目为10,然后在下面将耗时间的任务使用submit函数提交到到线程池中交给线程来执行,这样就可以大大减少程序运行的时间。
多个线程竞争一个资源 —> 临界资源
import random
import threading
import time
from concurrent.futures.thread import ThreadPoolExecutor
class Account:
"""银行账户"""
def __init__(self):
self.balance = 0
self.condition = threading.Condition(threading.RLock())
def deposit(self, money):
"""
存钱
:param money:存入的金额
"""
# 上下文语法
with self.condition:
new_balance = self.balance + money
time.sleep(0.01)
self.balance = new_balance
# 唤醒(通知)暂停的线程让它们有机会恢复执行
self.condition.notify_all()
def withdraw(self, money):
"""
取钱
:param money:取款的金额
:return: 取款成功返回True, 否则返回False
"""
with self.condition:
while self.balance < money:
# 如果线程执行条件不满足,可以让线程暂停并释放已经获得的锁对象
self.condition.wait()
new_balance = self.balance - money
time.sleep(0.01)
self.balance = new_balance
def put_money(account):
while True:
money = random.randint(5, 10)
account.deposit(money)
current = threading.current_thread()
print(f'{current.name}存入{money}元,当前余额{account.balance}元')
time.sleep(1)
def get_money(account):
while True:
money = random.randint(10, 20)
account.withdraw(money)
current = threading.current_thread()
print(f'{current.name}取出{money}元,当前余额{account.balance}元')
time.sleep(0.5)
def main():
account = Account()
with ThreadPoolExecutor(max_workers=10, thread_name_prefix='Joker') as pool:
for _ in range(5):
pool.submit(put_money, account)
pool.submit(get_money, account)
if __name__ == '__main__':
main()
- 在工作上我们会遇到临界资源的清况,所以我们需要保护我们的资源(在关键操作上只有一个线程能够访问到这个资源),这个时候就可以使用锁
以上代码我们定义了一个银行类和存取钱的对象方法,当我们向银行不断存钱的时候,我们能够让存钱的程序一步步执行,即第一次存完再存下一次,不能造成多次存钱的时候强占一个进程,而这就是多个线程竞争一个资源的情况。与此同时,我们也必须得做到存完钱后才能去取钱。
- 使用Conditon模块来对对象方法上锁,为了防止锁上之后程序崩溃无法释放锁,可以使用上下文语法来写
- 我们可以在里面使用Conditon的wait函数来时线程暂停并释放已经获得的锁对象,使用notify_all函数唤醒暂停的线程,让他们有机会恢复执行
- 通过threading.current_thread() 可以得到当前线程的名字,thread_name_prefix=’’ 对当前线程的名字进行重命名
- 再加上之前的多线程就可以使得必须得存钱之后才能进行取钱的行为
反扒措施
在某些网站中,当你爬取数据的时候可能会遇到爬了几次就无法爬取、爬取出来的数据什么都没有,这是因为当你爬取次数过多,速度过快,网站就会封禁了你的IP,所以我们就可以使用反扒措施来进行爬取。
- IP封禁
- 如果没登陆不能访问,登录之后可以访问,可以在请求头中强行添加登录信息(Cookie - 服务器保存在用户浏览器中的和用户身份相关的信息)
- 如果是商业项目,就需要提前创建好一个Cookies池,每次请求从池子中随机选择一组Cookie
- 不管是否登录都不能访问,需要使用IP代理,让代理服务器帮我们向目标网站发起请求,目标网站返回的响应由代理返回给我们。
import bs4
import requests
import time
import random
resp = requests.get('URL')
proxy_list = resp.json()['msg']
# 商业爬虫项目,要提前创建好代理池(很多组IP代理,失效的代理会被移除,代理会定时更新)
# 付费的商业代理一般购买之后都是提供一个网络API接口(URL), 通过请求这个接口就可以获得代理的信息
# 我们使用的代理提供的API接口返回JSON格式的数据,可以通过Response对象的json()方法将
# 返回的JSON的数据处理成字典,再从中提取出IP代理的信息.
for page in range(10):
proxy_dict = random.choice(proxy_list)
ip, port = proxy_dict['ip'], proxy_dict['port']
try:
resp = requests.get(
url=f'https://movie.douban.com/top250?start={page * 25}',
headers={
'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
},
proxies={
'https': f'http://{ip}:{port}'
}
)
except requests.exceptions.ConnectTimeout:
print('连接超时,可能是代理服务器已经失效')
except requests.exceptions.ProxyError:
print('代理失效')
else:
if resp.status_code == 200:
soup = bs4.BeautifulSoup(resp.text, 'html.parser')
# select方法会通过CSS选择器定位页面元素
spans = soup.select('div.hd > a > span:first-child')
for span in spans:
# 通过标签对象的text属性获取标签中文字
print(span.text)
time.sleep(random.random()*3+3)
else:
print('无法获取页面...')
以上代码是通过IP代理来获取"豆瓣电影网Top250"的中文电影名,我们通过付费的商业代理购买之后,会提供一个网络API接口(URL),通过请求这个接口就可以获得代理的信息,然后我们将代理提供的API接口返回的JSON格式的数据,通过Response对象的json()方法将返回的JSON的数据处理成字典,再从中提取出IP代理的信息。
- 将提取出IP代理的信息通过随机函数随机取一个出来,用变量保存里面的ip和port信息
- 然后在请求头中添加 proxies={‘https’: f’http://{ip}:{port}’},这样就可以使用代理来爬取数据了
- 但是因为代理失效的速度很快,所以我们在里面写了一段异常捕获的代码,来判断代码失效的原因
- 在最后用time函数让程序随机休眠3~6秒,减慢我们爬取的速度
注意:因为代理失效快,所以商业爬虫项目要提前创建好代理池,因为代理会定时更新,所以可以移除失效的代理,留下可以爬取的代理
- 带动态页面的爬虫
- 使用selenium库
from selenium import webdriver
import bs4
driver = webdriver.Chrome()
driver.get('https://image.so.com/z?ch=beauty')
# page_source是带动态内容的页面源代码
soup = bs4.BeautifulSoup(driver.page_source, 'html.parser')
imgs = soup.select('img')
for img in imgs:
print(img.attrs['src'])
将提取出IP代理的信息通过随机函数随机取一个出来,用变量保存里面的ip和port信息
2. 然后在请求头中添加 proxies={‘https’: f’http://{ip}:{port}’},这样就可以使用代理来爬取数据了
3. 但是因为代理失效的速度很快,所以我们在里面写了一段异常捕获的代码,来判断代码失效的原因
4. 在最后用time函数让程序随机休眠3~6秒,减慢我们爬取的速度
注意:因为代理失效快,所以商业爬虫项目要提前创建好代理池,因为代理会定时更新,所以可以移除失效的代理,留下可以爬取的代理
- 带动态页面的爬虫
- 使用selenium库
from selenium import webdriver
import bs4
driver = webdriver.Chrome()
driver.get('https://image.so.com/z?ch=beauty')
# page_source是带动态内容的页面源代码
soup = bs4.BeautifulSoup(driver.page_source, 'html.parser')
imgs = soup.select('img')
for img in imgs:
print(img.attrs['src'])
导入selenium库导入webdriver对网址进行操控,使用page_source方法得到动态内容的页面源代码,最后使用bs4解析就可以得到需要的内容