
最近要学爬虫(web crawler)啦~这篇文章来记录我学习的知识点。
另外,我做了一个测试请求的接口方便我自己的练习,支持显示GET请求和POST请求,返回Json数据。 API地址:https://oneteam.jixiaob.cn/urltest GET请求示例:https://oneteam.jixiaob.cn/urltest?sample=喵&num=1 返回示例: {"method": "GET", "sample": "喵", "num": "1"} POST请求示例: 先直接访问oneteam.jixiaob.cn/urltest,然后按F12,右键代码,选择Edit as HTML,把下面的东西复制到合适的地方,然后点击框框以外的空白处,使代码生效。然后在页面输入想要测试的内容,点击Submit提交测试。 <div> <form method="post" action="https://oneteam.jixiaob.cn/urltest"> <input name="test"><button>Submit</button></form> </div> 返回示例: {"method": "POST", "test": "好家伙"} 特别注意,使用POST请求时一定要使用https,否则会301跳转变为GET请求! 当前仅支持GET请求或POST请求,如果使用其它方式会返回Not Allowed 错误返回: {"msg": "not allowed"}
上面那个是老接口,已经关掉了,我搞了个新接口,可以显示更多信息
API地址:https://oneteam.jixiaob.cn/urltest
支持GET、POST请求。
请求示例:https://oneteam.jixiaob.cn/urltest?name=zkg&age=16
返回示例:
Message:success META: {'QUERY_STRING': 'name=zkg&age=16', 'REQUEST_METHOD': 'GET', 'CONTENT_TYPE': '', 'CONTENT_LENGTH': '', 'REQUEST_URI': '/urltest?name=zkg&age=16', 'PATH_INFO': '/urltest', 'DOCUMENT_ROOT': '/www/wwwroot/120.79.174.94', 'SERVER_PROTOCOL': 'HTTP/2.0', 'REQUEST_SCHEME': 'https', 'HTTPS': 'on', 'REMOTE_ADDR': '1.71.187.116', 'REMOTE_PORT': '15098', 'SERVER_PORT': '443', 'SERVER_NAME': '120.79.174.94', 'UWSGI_SCRIPT': 'djangoProject_OneTeam.wsgi', 'UWSGI_CHDIR': '/www/wwwroot/120.79.174.94/', 'HTTP_HOST': 'oneteam.jixiaob.cn', 'HTTP_SEC_CH_UA': '" Not A;Brand";v="99", "Chromium";v="90", "Microsoft Edge";v="90"', 'HTTP_SEC_CH_UA_MOBILE': '?0', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.56', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'HTTP_SEC_FETCH_SITE': 'none', 'HTTP_SEC_FETCH_MODE': 'navigate', 'HTTP_SEC_FETCH_USER': '?1', 'HTTP_SEC_FETCH_DEST': 'document', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'HTTP_COOKIE': '_ga=GA1.2.2010036960.1614158313; csrftoken=fs9FNBEqHoVp8pYV8sbYCS0RKgby7R5xnGnNc4H8wn8kX7oQCQxEJXvMb0FlDpQk', 'wsgi.input': <uwsgi._Input object at 0x7f68cbd3b0b0>, 'wsgi.file_wrapper': <built-in function uwsgi_sendfile>, 'wsgi.version': (1, 0), 'wsgi.errors': <_io.TextIOWrapper name=2 mode='w' encoding='UTF-8'>, 'wsgi.run_once': False, 'wsgi.multithread': True, 'wsgi.multiprocess': True, 'wsgi.url_scheme': 'https', 'uwsgi.version': b'2.0.19.1', 'uwsgi.core': 1, 'uwsgi.node': b'iZwz9ck5wgjfca17qoifv6Z', 'SCRIPT_NAME': ''} Cookies: {'_ga': 'GA1.2.2010036960.1614158313', 'csrftoken': 'fs9FNBEqHoVp8pYV8sbYCS0RKgby7R5xnGnNc4H8wn8kX7oQCQxEJXvMb0FlDpQk'} Request_info: {'method': 'GET', 'name': 'zkg', 'age': '16'}
正则表达式
————————————————————
正则表达式对于爬虫来说是很基础而且很重要的东西
正则表达式是用来匹配内容的,而学习这种东西,最快的方法是学与练习结合。
于是,我在学习的时候一直在用这个在线验证正则表达式的网站:http://tool.chinaz.com/regex
[]:这个括号表示一个字符。
里面可以直接放一些字符,如[abcde]就会匹配其中的一个字符
特别的,如果在这些字符前面加上^就会匹配不是这里面的字符
如[^bcdef]
也可以放范围,如
[A-Z]所有大写字母 [a-z]所有小写字母 [0-9]所有单个数字
匹配所有,[\s] [\S](不包含非空字符,换行符)
[\w]匹配字母、数字、下划线,同 [A-Za-z0-9_]
还有一些看不见的字符,比如\f换页符 \n换行符 \r回车符 \t制表符 \v垂直制表符
有一些特殊字符,在正则中需要加上一个\才能作为字符匹配。
如^开头 $结尾 \b单词边界(空格的地方) \B非单词边界
*匹配前面子表达式0次或多次 +匹配前面子表达式1次或多次 ?匹配前面子表达式0次或1次
.匹配除换行符\n外的任意单字符 [中括号开始 \转义 {标记限定符表达式的开始 |两项中的一个选择
限定符
{n}匹配确定的n次
{n,}匹配n+次(包含n)
{n,m}匹配n~m次(包含n,m)逗号两边不能有空格
最小匹配(非贪婪)
正则表达式默认尽可能多的匹配内容,所以如果想匹配一个小标签
^<w*?>$
以下是我自己造的用来练习的正则表达式,可能匹配的非常不准确,仅供娱乐。
1.匹配邮箱:[\w]+@[\w]+\.[\w]+
2.匹配URL:[A-Za-z]+://[\w]+\.[\w]+
3.匹配磁力链接:magnet:\?xt=urn:btih:[0-9A-z]{40}
Python中的使用方法:
提取信息
1.match()
match更适合检测某个字符串是否符合正则表达式的规则。
会尝试从字符串的起始位置匹配正则,匹配就返回成功匹配的部分,不匹配返回None
result = re.match('正则表达式', '要匹配的文本', 修饰符)
然后result.group()就是匹配的结果了。 reslut.span()是匹配出的范围
如何从匹配结果中提取一部分内容呢?将想提取的信息使用(括号)括起来即可。
这时候result.group()还是整个匹配的内容,result.group(1)就是第一个括号里面的内容,依此类推。
关于修饰符:
re.I 不区分大小写
re.L 本地化识别匹配
re.M 多行匹配(影响^和$)
re.S 使.匹配包括换行符在内的所有字符
re.U 使用Unicode字符集解析字符 影响\w \W \b \B
re.X 更灵活的格式便于正则更加易于理解
2.search()
与match不同的是,search在匹配时会扫描整个字符串,返回第一个成功匹配的结果。
3.findall()
获取匹配正则表达式的所有内容,返回一个列表。
修改信息
sub()
当然也可以用replace,但是这样太繁琐了。
比如要替换掉一段文本中的数字:
54aKS4yrsoiRS4ixSL2g
text = '54aKS4yrsoiRS4ixSL2g' text = re.sub('\d+', '', text) print(text) #==》aKSyrsoiRSixSLg
创建正则表达式pattern
compile()
patten = re.compile('正则表达式')
这样以后在用的时候直接写patten就行了,不用再写一遍正则表达式了。
Urllib库
————————————————————
urllib库是Python内置的库,不需要额外安装。
他有request请求模块、error异常处理模块、parse工具模块和robotparser模块判断哪些网站可以爬。
request模块 —— Urlopen()
可以方便地实现请求的发送并得到响应。
我们先使用urlopen()的方法,来实现简单的get请求。
有时候你很高兴地写了一段代码,想获取网站的html代码:
import urllib response = urllib.request.urlopen(‘https://www.jixiaob.cn’) print(response.read().decode('utf-8'))
你很高兴的运行了它,却收获了一个报错:module 'urllib' has no attribute 'request'
urllib里面明明有request鸭,怎么没导进来呢?
我去搜了一下,他们说Python3有时候会抽风,不会将子模块自动导入进去。
有两个解决方法,一是import urllib.request as ul 二是import urllib.request
我采用了第二种方法,修改代码如下:
import urllib.request response = urllib.request.urlopen('https://www.jixiaob.cn') print(response.read().decode('utf-8'))
然后运行了一下,就成了,控制台里面显示了我网站主页的html源码。
我们看一下response的type,发现是<class 'http.client.HTTPResponse'>
这是个HTTPResponse类型的对象,包含read() readinto() getheader(name) getheaders() fileno()等方法
还有msg version status reason debuglevel closed 等属性
read() 返回网页的内容
getheaders() 获取响应的所有头信息
getheader(name) 获取name的响应头对应的信息
status 返回状态码
data参数
使用urlopen方法时可以加入data可选参数,这时请求方式将变为POST
传参需要使用字节流类型(bytes),可以通过字典转换得到。
比如我请求我的测试API:
import urllib.request import urllib.parse data_dict = { 'word': 'hello' } data = bytes(urllib.parse.urlencode(data_dict), encoding='utf-8') response = urllib.request.urlopen('https://oneteam.jixiaob.cn/urltest', data=data) print(response.read().decode('utf-8'))
然后返回了正确的结果。
{"method": "POST", "word": "hello"}
这种请求方式是模拟表单的提交方式。
timeout参数
设置超时时间,单位秒,超时会抛异常。支持HTTP HTTPS FTP请求。
这个可以设置,如果网页长时间没有相应,就跳过抓取,可以用try实现。
我们用我的API模拟一下超时请求(我的网站反应有点快,,1秒和0.1秒都能相应,,):
我们在上面代码的基础上修改一句:
response = urllib.request.urlopen(url, data=data, timeout=0.01)
然后运行,你就会得到这个异常
urllib.error.URLError: <urlopen error timed out>
import urllib.request import urllib.parse import socket import urllib.error data_dict = { 'word': '好家伙' } data = bytes(urllib.parse.urlencode(data_dict), encoding='utf-8') try: response = urllib.request.urlopen('https://oneteam.jixiaob.cn/urltest', data=data, timeout=0.01) except urllib.error.URLError as e: if isinstance(e.reason, socket.timeout): print('Time OUT')
我们把它改成异常处理,可以发现确实是因为超时导致的错误。
context参数 ssl.SSLContext类型 指定SSL设置
cafile参数 指定CA证书
capath 指定CA证书的路径
但有时我们使用urlopen()直接请求的时候,会遇到HTTP 418错误。这是反爬虫程序返回的。
这时候就需要使用下面的request()来添加header信息,来绕过反爬虫。
request模块 —— Request()
上面的urlopen()仅支持基本的请求,不能添加header信息。如果要加入header信息,就需要用到Request请求了。
Request请求的方式是先创建一个Request类型的对象,然后在对象里面可以灵活的设置参数。
首先我们简单写一个只有url的request:
import urllib.request request = urllib.request.Request('https://oneteam.jixiaob.cn/urltesst') response = urllib.request.urlopen(request) print(response.read().decode('utf-8'))
然后加入多个参数构建请求。
构建request的时候可以加入可选参数。
data 的加入方法同上面的urlopen 的方法。
headers 字典类型,可以创建的时候加入,也可以通过add_header()进行添加。
通常会通过修改UA(User-Agent)的方法来伪装浏览器,默认的UA是Python-urllib
origin_req_host 请求方的host名称或IP地址
unverifiable 请求无法验证,默认是False
method 字符串,指示请求的方法,如GET、POST、PUT等
我们在上面的基础上加入header等信息,把代码修改为下面的样子:
import urllib.request import urllib.parse headers = { 'Host': 'oneteam.jixiaob.cn' } data_dict = { 'name': 'ZhaoKugua' } data = bytes(urllib.parse.urlencode(data_dict), encoding='utf-8') request = urllib.request.Request(url='https://oneteam.jixiaob.cn/urltest', data=data, headers=headers, method='POST') request.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46 ') response = urllib.request.urlopen(request) print(response.read().decode('utf-8'))
另外,除了这样添加header,还可以使用add_header()方法
request.add_header('User-Agent', '这里放UA信息')
高级用法:
以上的请求还没有办法处理Cookie还有代理等一些高级的内容。
urllib.request 模块里面有BaseHandler类,提供了一些基本方法。
比如:
HTTPDefalutErrorHandler:用于处理HTTP相应错误,抛出HTTPError类型的异常。
HTTPRedirectHandler:用于处理重定向
HTTPCookieProcesser:处理Cookies
ProxyHandler:设置代理,默认为空
HTTPPasswordMgr:管理密码,维护用户名和密码的表
HTTPBasicAuthHandler:管理认证
更详细的说明详见官方文档:
urllib.request — Extensible library for opening URLs — Python 3.9.5 documentation
还有一个OpenerDirector类,也叫Opener,urlopen()就算一个Opener,是人家给你封装好的比较常用的请求。
Opener使用open()的方法,和urlopen()类似。我们可以使用Handler构建Opener
1.登录验证
比如我请求了我朋友的一个网站,它是需要登录验证的:
def urllib_advanced_auth(): from urllib.error import URLError from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener # 输入用户名和密码,还有url username = '这是用户名' password = '这是密码' url = '这是url' # 构建密码管理对象(WithDefaultRealm包含默认的远程服务器的域信息,一般没人管是None) p = HTTPPasswordMgrWithDefaultRealm() p.add_password(None, url, username, password) # 构建管理认证对象 auth_handler = HTTPBasicAuthHandler(p) # 构建opener对象 opener = build_opener(auth_handler) # 尝试登录进入 try: result = opener.open(url) html = result.read().decode('utf-8') return html except URLError as e: # 错误处理,返回错误类型 return e.reason
2.代理请求
导包ProxyHandler
# 这是个字典,协议类型: 代理链接
proxy_handler = ProxyHandler({
'http': 'http://127.0.0.1:1080'
'https': 'https://127.0.0.1:1080'
})
opener = build_opener(proxy_handler)
try:
response = opener.open('https://www.google.com')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)
3.Cookies
首先可以通过cookiejar加上请求来获取cookie
def urllib_advanced_cookie(): import http.cookiejar import urllib.request # 声明CookieJar对象 cookie = http.cookiejar.CookieJar() # Cookie管理对象 handler = urllib.request.HTTPCookieProcessor(cookie) # 构建opener opener = urllib.request.build_opener(handler) # 发送请求 response = opener.open('https://www.baidu.com') # 打印cookie结果 for item in cookie: print(item.name + " = " + item.value) return 0 ''' 打印结果: BAIDUID = 1D22557FD5297F0D991E8A61A2B20C4D:FG=1 BIDUPSID = 1D22557FD5297F0DF3624871F1907025 PSTM = 1620722979 BD_NOT_HTTPS = 1 '''
可以发现百度给了我们一些cookie
当然,我们也可以以文件形式存储下来
把上面声明CookieJar的语句改为:
# 声明保存为文件的CookieJar对象
cookie = http.cookiejar.MozillaCookieJar('Cookies.txt') # 这里面是保存的文件名
然后请求之后来一个save
# ignore_discard即使cookies将被丢弃也将它保存下来;ignore_expires如果cookies已经过期也将它保存并且文件已存在时将覆盖
cookie.save(ignore_discard=True, ignore_expires=True)
运行之后我们就得到了Mozilla型浏览器的格式的Cookie
# Netscape HTTP Cookie File # http://curl.haxx.se/rfc/cookie_spec.html # This is a generated file! Do not edit. .baidu.com TRUE / FALSE 1652260281 BAIDUID C352B4182A11A0B44F65E6350DE7AFBA:FG=1 .baidu.com TRUE / FALSE 3768207928 BIDUPSID C352B4182A11A0B4CC508C85C7932A6F .baidu.com TRUE / FALSE 3768207928 PSTM 1620724281 www.baidu.com FALSE / FALSE 1620724581 BD_NOT_HTTPS 1
当然也可也选择另一种保存方式——libwww-perl(LWP)格式,声明CookieJar时改为:
cookie = http.cookiejar.LWPCookieJar('Cookies.txt')
即可。
#LWP-Cookies-2.0 Set-Cookie3: BAIDUID="179B3D514E4AFD2EC04D591806452377:FG=1"; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2022-05-11 09:15:02Z"; comment=bd; version=0 Set-Cookie3: BIDUPSID=179B3D514E4AFD2E47BB0E6A6174ADBC; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2089-05-29 12:29:09Z"; version=0 Set-Cookie3: PSTM=1620724503; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2089-05-29 12:29:09Z"; version=0 Set-Cookie3: BD_NOT_HTTPS=1; path="/"; domain="www.baidu.com"; path_spec; expires="2021-05-11 09:20:02Z"; version=0
可以看到同样的Cookie,存储的方式大有不同。
加载和save的方法基本一致,使用
cookie.load('文件名', ignore_discard=True, ignore_expires=True)
即可。
4.异常处理
urllib中的error模块定义了由request模块产生的异常,出现了问题,request模块就会抛出error里面定义的异常
可以先捕获异常,然后print(e.reason),就可以使异常得到有效处理了。
1.URLError:访问了不存在的页面等。
2.HTTPError:URLError的子类,处理http请求的错误,如认证请求失败。有code(http状态码)、reason(原因)和headers(请求头)三个属性。
parse模块——解析链接
urllib中还有个parse模块,用于实现url各部分的抽取、合并以及链接转换。
1.urlparse() 实现URL的识别和分段
result = urlparse('这里填url')
def parse_url(): from urllib.parse import urlparse result = urlparse('https://oneteam.jixiaob.cn/urltest?code=114514') print(result) ''' 输出结果: ParseResult(scheme='https', netloc='oneteam.jixiaob.cn', path='/urltest', params='', query='code=114514', fragment='') '''这个结果是一个元组,可以通过下标或者名字访问。如result.scheme result[0]
可以看到scheme(协议)、netloc(域名)、path(访问路径)、params(参数)、query(GET请求的参数)、fragment(锚点)等信息
其实urlparse第二个参数是协议,如果url里面没有指定协议的话,在这里可以指定默认协议。这个指定的协议只会在url里面没有的情况下生效。第三个参数allow_fragments,默认是True,设置为False的话fragment(锚点)会变成path或者query的一部分
2.urlunparse() 可以看作urlparse的逆过程,用于URL构造
接受的参数是可迭代对象(如list列表),长度必须为6
如data = ['http', 'jixiaob.cn', 'index.html', 'user', 'a=6', 'comment']
则urlunparse的结果就是http://jixiaob.cn/index.html;user?a=6#comment
3.urlsplit() 不解析params,合并到path中,与urlparse类似。
4.urlunsplit() urlsplit的逆过程
参数是可迭代对象,长度必须为5
5.urljoin() 生成链接的另一种方法
urljoin('base_url', 'newurl')
分析base_url的scheme(协议)、netloc(域名)和path(路径),如果newurl有缺失的话就会拿base_url解析出来的这三个东西补充。
比如('https://www.baidu.com?keyword=miao', '?category=2')的结果就是https://www.baidu.com?category=2
6.urlencode() GET请求的神器
可以将字典的参数转换为get请求的参数
# get请求参数转换 get_data_dict = { 'name': 'zkg', 'sex': '男' } base_url = 'https://oneteam.jixiaob.cn/urltest' # 这里同时也练习一下urljoin result2 = urljoin(base_url, 'urltest?' + urlencode(get_data_dict)) print(result2) ''' 结果: https://oneteam.jixiaob.cn/urltest?name=zkg&sex=%E7%94%B7 '''
可以看到请求的参数被正确转换了。
7.parse_qs() 将get的参数转换回字典
需要注意的是,传的参数是URL中?以后的get参数的内容(query),如果直接传入URL,第一个名字会出错。
所以可以搭配urlparse('URL').query使用。
8.parse_qsl() 将get的参数转换成元组,与上面的7类似。具体结构是外面是list,里面是两个长度的元组,元组里面第一个是参数名,第二个是参数值。
9.quote() 将内容转换为URL编码格式
比如quote('赵苦瓜')就是%E8%B5%B5%E8%8B%A6%E7%93%9C
这种方法可以防止中文参数乱码。
10.unquote() 将URL编码格式还原为内容
比如unquote('%E8%B5%B5%E8%8B%A6%E7%93%9C')就是赵苦瓜
有了上面的方法,就可以灵活的进行URL的解析和构造。
Robots.txt Robots协议
————————————————————
进行爬虫时有爬虫协议要遵守,该爬的爬,不该爬的不能爬,防止对自己造成风险。
robots协议一般放在网站根目录下,如https://www.so.com/robots.txt
User-agent: 允许的搜索爬虫UA名称,至少指定一条。
Disallow:不允许抓取的目录。留空表示允许所有爬虫访问任何目录。当然允许所有也可也直接把robots.txt留空。
Allow:与Disallow连用,一般不单独使用,用来排除Disallow的限制。比如Disallow是/,表示所有目录都不允许抓取,而Allow是/public/,表示public目录可以被抓取。
爬虫名称:如Baiduspider(百度)、Googlebot(谷歌)、MSNBot(必应)、YoudaoBot(有道)、Sogou web spider(搜狗)、360Spider(360)等。
可以使用robotparser进行解析
urllib.robotparser.RobotFileParser(url='') # 这里也可以不加参数
定义完之后有几个方法:
set_url() 传入链接
read() 读取文件并分析。进行其它操作之前必须先读取,否则所有判断都是False。
parse() 解析robots.txt文件。传入参数是robots.txt某些行的内容,会按照语法规则分析这些内容。
(read和parse选一个使用即可)
can_fetch() 第一个参数User-agent,第二个参数是要抓取的URL。根据是否可以抓取返回True或False。
mtime() 返回上次抓取和分析robots.txt的时间。(对于长时间分析和抓取很必要)
modified() 将当前时间设置为上次抓取和分析robots.txt的时间
def robot_parse(): from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url('https://www.mi.com/robots.txt') rp.read() print(rp.can_fetch('*', 'https://www.mi.com/comment/index.html')) print(rp.can_fetch('*', 'https://www.mi.com/mi11?product_id=1205000029&cfrom=search')) return 0
这里没研究明白,,用Pycarm写的都是True,用IDLE写的都是False,,就没真正判断对过,,
这,,,我,,,
本文地址:https://blog.jixiaob.cn/?post=53
版权声明:若无注明,本文皆为“赵苦瓜のBlog~”原创,转载请保留文章出处。