urllib

urllib是一个Python库,利用它可以实现HTTP请求的发送,我们要做的是指定请求的URL、请求头、请求体等信息。此外urllib还可以把服务器返回的对象转化为Python对象,通过该对象可以方便地获取响应的相关信息,如响应状态码、响应头、响应体。
urllib库包含如下4个模块。

  • request: 最基本的http请求模块,可以模拟请求的发送。
  • error: 异常处理模块。
  • parse: 一个工具模块。
  • robotparser: 主要用来识别网站的robots.txt文件,然后判断哪些网站可以爬,哪些网站不可以爬。

发送请求

urlopen

urllib.request模块提供了最基本的构造HTTP请求的方法,利用这个模块可以模拟浏览器的请求发起过程。用百度测试

1
2
3
import urllib.request
response = urllib.request.urlopen('https://www.baidu.com')
print(response.read().decode('utf-8'))

response.read()方法
属于urllib.response对象,用来从HTTP响应中读取数据。read()方法会读取HTTP响应的主体(body),这通常是你请求的页面的HTML内容或API返回的JSON数据等。read()方法返回的是字节串(bytes),因为HTTP协议传输的是字节数据。由于返回的是字节串,通常需要将其解码为字符串才能阅读。常见的解码方式是使用.decode(‘utf-8’),这假设响应内容是使用UTF-8编码的。除了read(),urllib.response对象还提供了其他方法来读取数据,如readline()(读取一行数据)和readlines()(读取所有行并返回一个列表)。

1
2
3
4
5
import urllib.request
response = urllib.request.urlopen('https://www.baidu.com')
print(type(response))
>>>
<class 'http.client.HTTPResponse'>

这里响应的是HTTPResponse类型的对象,包含read、readinto、getheader、getheaders、fileno等方法。得到响应把它赋值给了response,可以通过response对象来调用哪些方法和属性。

打印响应的属性

1
2
3
4
5
6
import urllib.request
response = urllib.request.urlopen('https://www.baidu.com')
print(response.status) //响应状态码
print(response.getheaders()) //响应头的信息
print(response.getheader("Server")) //提供参数,获取响应头对应参数的信息
print(response.read().decode('utf-8'))

data参数

  data参数可选。在添加该参数的时候,需要使用bytes方法将参数转化为字节流编码格式的内容,即bytes类型。如果传递了这个参数,那么它的请求方式就不再是GET而是POST了。
我们请求的站点是https://www.httpbin.org ,它可以提供HTTP请求测试。本次请求的URL是 https://www.httpbin.org/post,这个连接可以测试POST请求,能够输出请求的一些信息,其中包含我们传递的data参数。

1
2
3
4
5
import urllib.request
import urllib.parse
data = bytes(urllib.parse.urlencode({'name':'germy'}),encoding='utf-8')
response = urllib.request.urlopen('https://www.httpbin.org/post',data)
print(response.read().decode('utf-8'))

我们传递的参数出现在了form字段中,表明是模拟表单提交,以POST方式传输数据。

timeout参数

  设置超时时间,单位是秒,如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常,如果不指定这个参数,会使用全局默认时间。
实例

1
2
3
4
import socket
import urllib.request
response = urllib.request.urlopen('https://www.httpbin.org/get',timeout=0.1)
print(response.read().decode('utf-8'))

  可能会出现urllib.error.URLError: <urlopen error timed out>这种报错

实例

1
2
3
4
5
6
7
8
9
10
11
import socket
import urllib.request
import urllib.parse
import urllib.error
try :
response = urllib.request.urlopen('https://www.httpbin.org/get',timeout=0.1)
except urllib.error.URLError as e:
if isinstance(e.reason,socket.timeout):
print('Time Out')
>>>
Time Out

其他参数

  context参数,该参数必须是ssl.SSLContext类型,用来指定SSL的设置。
  cafilecapate这两个参数分别用来指定CA证书和其路径,在请求HTTPS连接时会用。

Request

  利用urlopen发起的最基本的请求,那几个参数并不足以构建一个完整的请求。如果想往请求中加入Headers信息,就要利用更强大的Request类来构建请求了。

1
2
3
4
import urllib.request
request = urllib.request.Request('http://www.baidu.com')
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))


这里我们依然用urlopen发送请求,但是这次该方法的参数不是URL了,而是一个Request类型的对象。
构造Request类:

1
class urllib.request.Request(url,data=None,headers={},origin_req_host,unverifiable=False,method=None)

  第一个参数url用于请求URL,是必传参数,其他都是可选参数。
  第二个参数data如果要传数据,必须传bytes类型的。如果数据是字典,可以先用urllib.parse模块里的urlencode方法进行编码。
  第三个参数headers是一个字典,就是请求头,在构造请求时,既可以通过headers参数直接构造此项,也可以通过调用请求实例的add_header方法添加。
  添加请求头的常见方法是,通过修改User-Agent来伪装浏览器。默认的User-Agent是Python-urllib。
  第四个参数origin_req_host指的是请求方的host名称或者IP地址。
  第五个参数unverifiable表示请求方是否是无法验证的,默认取值是false,指用户没有足够的权限来接收这个请求的结果。
  第六个参数是method是一个字符串,用来指示请求使用的方法。
  传入多个参数构建Request类:

1
2
3
4
5
6
7
8
9
from urllib import request,parse
url = 'http://www.httpbin.org/post'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
'Host':'www.httpbin.org'}
dict = {'name':'germy'}
data = bytes(parse.urlencode(dict),encoding='utf-8')
req = request.Request(url,data,headers,method='POST')
response = request.urlopen(req)
print(response.read().decode('utf-8'))

传入4个参数构造了一个Request类,data用urlencode方法和bytes方法把字典数据转换成字节流格式。
运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "germy"
},
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "10",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "www.httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"X-Amzn-Trace-Id": "Root=1-662f5c7d-78dad94c612f72536858fcd6"
},
"json": null,
"origin": "219.156.133.195",
"url": "http://www.httpbin.org/post"
}

成功设置了data,headers,method
通过add_header方法添加headers的方式

1
2
req=request.Request(url=url,data=data,method=method)
req.add_header('User-Agent','xxxxx')

高级用法

  学会构建请求之后,还有一些高级操作如Cookie处理、代理设置等,我们需要用Handler。Handler可以理解为各种处理器,有专门处理登录验证的、处理Cookie的、处理代理设置的。利用Handler,几乎可以实现HTTP请求中所有的功能。
  urllib.request模块的BaseHandler类是其他所有Handler类的父类。提供了最基本的方法,例如default_open,protocol_request等。
有各种Handler子类继承BaseHandler类

  • HTTPDefaultErrorHandler用于处理HTTP响应错误,所有错误都会抛出HTTPError类型的异常。
  • HTTPRedirectHandler用于处理重定向。
  • HTTPCookieProcessor用于处理Cookie。
  • ProxyHandler用于设置代理,代理默认为空。
  • HTTPPasswordMgr用于管理密码,它维护着用户名密码的对照表。
  • HTTPBasicAuthHandle用于管理认证,如果一个链接在打开时需要认证,那么可以用这个类来解决认证问题。
    还有一个重要的类OpenerDirector,我们可以称之为Opener。之前用过的urlopen方法,就是urllib库为我们提供的一个Opener。为了实现更高级的功能,前面使用的Request类和urlopen类相当于类库已经封装好的极其常用的方法,利用这两个类可以完成基本的请求,但现在要实现更高级的功能,就要深入一层进行配置,使用更底层的实例来完成操作,所以要用Opener。
    Opener类提供open方法,该方法返回的响应类型和urlopen方法如出一辙。使用Handler类来构建Opener类。

验证

  在访问某些网站时,例如 https://ssr3.scrape.center ,可能会弹出认证窗口

  这种情况是这个网站启用了基本身份认证,英文是HTTP Basic Access Authentication,一种登陆验证方式,允许网页浏览器或其他客户端程序在请求网站时提供用户名和口令形式的身份认证。
爬虫可以借助HTTPBasicAuthHandler模块完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from urllib.request import HTTPBasicAuthHandler,HTTPPasswordMgrWithDefaultRealm,build_opener
from urllib.error import URLError

username = 'admin'
password = 'admin'
url="https://ssr3.scrape.center/"

p=HTTPPasswordMgrWithDefaultRealm()
p.add_password(None,url,username,password)
auth_handler = HTTPBasicAuthHandler(p)
opener = build_opener(auth_handler)

try :
result = opener.open(url)
html = result.read().decode('utf-8')
print(html)
except URLError as e:
print(e.reason)

  这里首先实例化了一个HTTPBasicAuthHandler对象auth_handler,参数是HTTPPasswordMgrWithDefaultRealm对象,它利用add_passward方法添加用户名和密码,建立了一个用来处理验证的Handler类。
  将对象auth_handler作为参数传递给build_opener方法,构建一个Opener,它在发送请求时就相当于验证成功了。
  最后利用Opener类中的open方法打开连接,即可完成验证。这里获取的结果就是验证成功后的页面源码内容。

代理

添加代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from urllib.error import URLError
from urllib.request import ProxyHandler , build_opener

proxy_handler = ProxyHandler(
{'http' : 'http://127.0.0.1:8080',
'https' : 'https://127.0.0.1:8080'}
)

opener = build_opener(proxy_handler)

try:
response = opener.open('https://www.baidu.com')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

  在本地搭建HTTP代理,让其运行在8080端口上。使用了ProxyHandler,其参数是一个字典,键名是协议类型,键值是代理链接,可以添加多个代理。利用这个Handler和build_opener方法构建了一个Opener,之后发送请求即可。

处理Cookie需要用到相关的Handler。
先用实例看看如何获取Cookie

1
2
3
4
5
6
7
8
import http.cookiejar,urllib.request

cookie = http.cookiejar.CookieJar()
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('https://www.baidu.com')
for item in cookie:
print(item.name+"="+item.value)

  先声明CookieJar对象。然后利用HTTPCookieProcessor构建一个Handler,再利用build_opener方法构建Opener,执行open函数即可。

  分别输出了每个Cookie条目的名称和值。
  输出文件格式的内容,Cookie实际上也是以文本形式保存的。

1
2
3
4
5
6
7
8
import urllib.request,http.cookiejar

filename = 'cookies.txt'
cookie = http.cookiejar.MozillaCookieJar(filename)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('https://www.baidu.com')
cookie.save(ignore_discard=True,ignore_expires=True)

  这时将CookieJar换成MozillaCookieJar,它会在生成文件时用到,是CookieJar的子类,用来处理跟Cookie和文件相关的事件,如读取和保存Cookie可以将Cookie保存成Mozilla型浏览器的Cookie格式。

  LWPCookieJar同样可以读取和保存Cookie,只是Cookie文件的保存格式和MozillaCookieJar不一样,它会保存成LWP(libwww-perl)格式。
  要保存LWP格式的Cookie文件,只需在声明时修改:
cookie = http.cookiejar.LWPCookieJar(filename)

不同格式的Cookie有一定的差异。
生成Cookie之后,对其进行读取和利用
以LWPCookieJar格式为例:

1
2
3
4
5
6
7
8
import urllib.request,http.cookiejar

cookie = http.cookiejar.LWPCookieJar()
cookie.load('cookies.txt',ignore_discard=True,ignore_expires=True)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('https://www.baidu.com')
print(response.read().decode('utf-8'))

这里调用load方法来读取本地的Cookie文件,获取Cookie的内容。这样做的前提是我们已经生成了LWPCookieJar格式的Cookie,并保存成了文件,读取了Cookie之后,用同样的方法构建Handler类和Opener类即可完成操作。

处理异常

URLError

  URLError类来自urllib库的error模块,继承自OSError类,是error异常模块的基类,由request模块产生的异常可以通过捕获这个类来处理。
它的一个属性reaseon,返回错误的原因。
用一个实例来看看

1
2
3
4
5
6
7
from urllib import request,error
try:
response = request.urlopen('https://blttttt.com/404')
except error.URLError as e:
print(e.reason)
>>>
[Errno 11001] getaddrinfo failed

我们访问了一个不存在的页面,是会报错的,但是通过捕获URLError这个异常,没有直接报错,而是输出了报错的原因,可以避免程序异常终止。

HTTPError

HTTPError是URLError的子类,专门用来处理HTTP请求的错误,例如认证请求失败。
有三个属性:

  • code
    返回HTTP状态码
  • reason
    同父类,返回错误原因
  • headers
    返回请求头

实例

1
2
3
4
5
from urllib import request,error
try:
response = request.urlopen('https://cuiqingcai.com/404')
except error.HTTPError as e:
print(e.reason,e.code,e.headers,sep='\n')

运行结果

URLError是HTTPError的父类,所以可以先选择捕获子类的错误,再捕获父类的错误,代码写法可以改为:

1
2
3
4
5
6
7
8
9
from urllib import request,error
try :
response = request.urlopen('https://cuiqingcai.com/404')
except error.HTTPError as e:
print(e.reason,e.code,e.headers,sep='\n')
except error.URLError as e:
print(e.reason)
else:
print("Request succeeded")

这样可以先捕获HTTPError,捕获它的错误原因、状态码、请求头信息。如果不是HTTPError异常,就会捕获URLError异常,输出错误原因。最后用else语句来处理正常的逻辑。
有时reason属性返回的不一定是字符串,也可能是一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
import socket
import urllib.request
import urllib.error
try :
response = urllib.request.urlopen('https://cuiqingcai.com/404',timeout=0.01)
except urllib.error.URLError as e:
print(type(e.reason))
if isinstance(e.reason,socket.timeout):
print('TIME OUT')
>>>
<class 'socket.timeout'>
TIME OUT

reason的类型是socket.timeout类,这里使用了isinstance方法来判断它的类型。

解析链接

urllib库里还提供了parse模块,这个模块定义了处理URL的标准接口,如实现URL各部分的抽取、合并以及转换。它支持如下协议处理:file、ftp、gopher、hdl、http、https、imap、mailto、mms、news、nntp、prospero、rsync、rtsp、rtspu、sftp、sip、sips、snews、svn、svn+ssh、telnet和wais。

urlparse

这个方法可以实现url的识别和分段

1
2
3
4
5
6
7
8
from urllib.parse import urlparse

result = urlparse('https://www.baidu.com/index.html;user?id=5#comment')
print(type(result))
print(result)
>>>
<class 'urllib.parse.ParseResult'>
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')

解析的结果是ParseResult类型的对象,包含6部分,分别是scheme、netloc、path、params、query、fragment。
观察URL:https://www.baidu.com/index.html;user?id=5#comment
urlparse方法在解析URL时有特定的分隔符。如://前面的内容就是scheme,代表协议。第一个/符号前面便是netloc,即域名;后面是path,访问路径。分号;后面是params,参数。问号?后面是查询条件query,一般用作GET类型的URL。井号#后面是锚点fragment,用于直接定位页面内部的下拉位置。
标准链接格式:scheme://netloc/path;params?quert#fragment,一个标准的URL都会符合这个规则,利用urlparse方法就可以将它拆分开来。
urlparse的API用法:
urllib.parse.urlparse(urlstring,scheme='',allow_fragment=True)

  • urlstring:
    这是必填项,即待解析的URL。
  • scheme:
    这是默认的协议。如果待解析的URL没有带协议信息,就会将这个作为默认协议。
  • allow_fragment:
    是否忽略fragment。如果此项被设置为False,那么fragment部分就会被忽略,它会被解析为path、params或者query的一部分,而fragment部分为空。

urlunparse

此方法用于构造URL。接收的参数是一个可迭代对象,其长度必须是6,否则会抛出参数数量不足或者过多的问题。
实例

1
2
3
4
5
from urllib.parse import urlunparse
data = ['https','www.baidu.com','index.html','uesr','a=6','comment']
print(urlunparse((data)))
>>>
https://www.baidu.com/index.html;uesr?a=6#comment

这里的参数使用的data列表类型。也可以使用其他类型,如元组或特定的数据结构。

urlsplit

此方法和urlparse类似,不过它不再单独解析params这部分(params会合并到path中),只返回5个结果。
实例

1
2
3
4
5
6
7
from urllib.parse import urlsplit
result = urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result)
print(type(result))
>>>
SplitResult(scheme='https', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment')
<class 'urllib.parse.SplitResult'>

返回的结果是SplitResult,其实也是元组,可以通过属性取其值,也可以通过索引取值。

1
2
3
4
5
from urllib.parse import urlsplit
result = urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result.scheme,result[0])
>>>
https https

urlunsplit

与urlunparse类似,也是将链接各部分组合成完整链接的方法,传入的参数是可迭代对象,如列表,元组,参数长度必须是5。
实例

1
2
3
4
5
from urllib.parse import urlunsplit
data=['https','www.baidu.com','index.html','a=6','comment']
print(urlunsplit(data))
>>>
https://www.baidu.com/index.html?a=6#comment

urljoin

  urlunparse和urlunsplit方法都可以完成链接的合并,但前提是有特定的对象,链接的每一部分要清晰分开。
  urljoin也可以生成链接。要提供base_url(基础链接)作为该方法的第一个参数,将新链接作为第二个参数。urljoin方法会分析base_url的scheme、netloc和path三个内容,并对新链接缺失的部分进行补充,最后返回结果。
实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from urllib.parse import urljoin

print(urljoin('https://www.baidu.com', 'FAQ.html'))
print(urljoin('https://www.baidu.com', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html?question=2'))
print(urljoin('https://www.baidu.com?wd=abc', 'https://cuiqingcai.com/index,php'))
print(urljoin('https://www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com#comment', '?caetgory=2'))
>>>
https://www.baidu.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html?question=2
https://cuiqingcai.com/index,php
https://www.baidu.com?category=2#comment
www.baidu.com?category=2#comment
www.baidu.com?caetgory=2

  可以发现,base_url提供了scheme、netloc和path。如果新链接不存在这三项,就进行补充,如果存在,base_url是不起作用的。
  通过urljoin方法,实现链接的解析、拼合与生成。

urlencode

  它在构造GET请求参数的时候非常有用。
实例:

1
2
3
4
5
6
7
8
9
10
from urllib.parse import urlencode
params={
'name':'germy',
'age':25
}
base_url = 'http://www.baidu.com?'
url=base_url+urlencode(params)
print(url)
>>>
http://www.baidu.com?name=germy&age=25

  先声明了一个字典params,用于将参数显示出来,然后调用urlencode方法将params序列化为GET请求的参数。
  urlencode方法很常用,有时为了更方便地构造参数。我们会先用字典将参数表示出来,然后将字典转化为URL的参数时,调用该方法即可。

parse_qs

  有了序列化就会有反序列化。利用parse_qs方法,可以将一串GET请求参数转回字典。
实例:

1
2
3
4
5
from urllib.parse import parse_qs
query = 'name=germy&age=25'
print(parse_qs(query))
>>>
{'name': ['germy'], 'age': ['25']}

可以看到URL的参数车公共转换为了字典类型。

查询字符串是 URL 中 ? 符号后的部分,通常用于在 HTTP 请求中传递参数。
输出结果会是一个字典,其中每个键对应一个参数名,每个值是一个列表,包含该参数名对应的所有值。

parse_qsl

  此方法用于将参数转化为由元组组成的列表:

1
2
3
4
5
from urllib.parse import parse_qsl
query = 'name=germy&age=25'
print(parse_qsl(query))
>>>
[('name', 'germy'), ('age', '25')]

  运行结果是一个列表,该列表中的每一个元素是一个元组,元组第一个内容是参数名,第二个内容是参数值。

quote

  此方法可以将内容转化为URL编码的格式。当URL中带有中文参数的时候,有可能导致乱码,使用quote可以将中文字符转化为URL编码。

1
2
3
4
5
6
from urllib.parse import quote
keyword = '壁纸'
url='http://www.baidu.com/s?wd='+quote(keyword)
print(url)
>>>
http://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8

unquote

它可以进行URL解码。
实例:

1
2
3
4
5
from urllib.parse import unquote
url = 'http://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8'
print(unquote(url))
>>>
http://www.baidu.com/s?wd=壁纸

分析Robots协议

利用urllib库的robotparser模块,可以分析网站的Robots协议。

Robots协议

  该协议也称爬虫协议或机器人协议,全名是网络爬虫排除标准。来告诉爬虫和搜索引擎哪些页面可以抓取,哪些不可以。通常有一个叫robots.txt的文本文件,一般在网站的根目录下。
  爬虫在访问一个站点时,首先检查这个站点的根目录下是否存在robots.txt文件,如果存在,会根据其中定义的爬取范围来爬取。若没有这个文件,搜索爬虫会访问所有可直接访问的页面。

robotparser

  使用robotparser模块来解析robots.txt文件。该模块提供了一个RobotFileParser,它可以根据某网站的robots.txt文件判断一个爬取爬虫是否有权限爬取这个网页。
  这个类的使用只需在构造方法里传入robots.txt文件的链接即可。
  看它的声明urllib.robotparser.RobotFileParser(url='')
  也可以不在声明时传入robots.txt文件的链接,让其默认为空,最后使用set_url()方法去设置也可以。

  • set_url:
    用来设置robots.txt文件的链接。如果在创建RobotFileParser对象时传入了链接,就不需要使用这个方法设置了。
  • read:
    读取robots.txt文件并进行分析。这个方法执行读取和分析操作,如果不调用这个方法,接下来接下来的判断都会为False,所以一定要调用这个方法。这个方法虽然不返回内容,但是执行了读取操作。
  • parse:
    用来解析robots.txt文件,传入其中的参数是robots.txt文件中某些行的内容,会按照robots.txt的语法规则来分析这些内容。
  • can_fetch:
    该方法有两个参数,第一个是User-Agent,第二个是要抓取的URL。返回结果是True或False,表示搜索引擎是否可以抓取这个URL。
  • mtime:
    返回上次抓取和分析robots.txt文件的时间,这对于长时间分析和抓取robots.txt文件的搜索爬虫很有必要,可能需要定期检查以抓取最新的robots.txt文件。
  • modified:
    同样对长时间分析和抓取的搜索爬虫很有帮助,可以将时间设置为上次抓取和分析robots.txt文件的时间。
    实例

    首先创建一个RobotFileParser对象rp,然后通过set_url方法设置robots.txt文件的链接。利用can_fetch方法判断网页是否可以被抓取。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from urllib.robotparser import RobotFileParser
    rp = RobotFileParser()
    rp.set_url("https://www.baidu.com/robots.txt")
    rp.read()
    print(rp.can_fetch('Baiduspider','https://www.baidu.com'))
    print(rp.can_fetch('Baiduspider','https://www.baidu.com/homepage/'))
    print(rp.can_fetch('Googlebot','https://www.baidu.com/homepage/'))
    >>>
    True
    True
    False
      利用Baiduspider就可以抓取百度的首页以及homepage页面,但是Googlebot就不能抓取homepage页面。
      还可以用parse方法执行对robots.txt文件的读取和分析。
    实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from urllib.request import urlopen
    from urllib.robotparser import RobotFileParser

    rp = RobotFileParser()
    rp.parse(urlopen('https://www.baidu.com/robots.txt').read().decode('utf-8').split('\n'))
    print(rp.can_fetch('Baiduspider','https://www.baidu.com'))
    print(rp.can_fetch('Baiduspider','https://www.baidu.com/homepage/'))
    print(rp.can_fetch('Googlebot','https://www.baidu.com/homepage/'))
    >>>
    True
    True
    False
    运行结果一样

requests的使用

requests相比于urllib更加方便。

准备

首先确保安装了requests库,若未安装,使用pip3安装:
pip3 install requests

实例

  在urllib库中的urlopen方法实际上是以GET请求方式请求网页的,requests库中相应的方法就是get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

response = requests.get('http://www.baidu.com')
print(type(response))
print(response.status_code)
print(type(response.text))
print(response.text[:100])
print(response.cookies)
>>>
<class 'requests.models.Response'>
200
<class 'str'>
<!DOCTYPE html><!--STATUS OK--><html><head><meta http-equiv="Content-Type" content="text/html;charse
<RequestsCookieJar[<Cookie BAIDUID=1C74878EE150BFF62131B72042A1460F:FG=1 for .baidu.com/>, <Cookie BAIDUID_BFESS=1C74878EE150BFF62131B72042A1460F:FG=1 for .baidu.com/>, <Cookie BIDUPSID=1C74878EE150BFF62131B72042A1460F for .baidu.com/>, <Cookie H_PS_PSSID=40304_40499_40446_40080 for .baidu.com/>, <Cookie PSTM=1714996509 for .baidu.com/>]>

  这里调用get方法实现了与urlopen方法相同的操作,返回一个Response对象,将其存放在变量response中,然后输出响应的类型,状态码,响应体的类型,内容,以及Cookie。

GET请求

利用requests库构建GET请求的方法。

基本实例

  构建简单GET请求,请求链接为https://www.httpbin.org/get,该网站会判断客户端发起的请求是否为get请求,如果是,那么它将返回相应的请求信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = 'https://www.httpbin.org/get'
r=requests.get(url)
print(r.text)
>>>
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "www.httpbin.org",
"User-Agent": "python-requests/2.31.0",
"X-Amzn-Trace-Id": "Root=1-6638c88c-656171973be54b8465b565d1"
},
"origin": "219.156.133.195",
"url": "https://www.httpbin.org/get"
}

  成功发起了GET请求,返回结果中包含请求头、URL、IP等信息。
  对于GET请求,可以添加附加信息,如加两个参数name和age,其中name是germy、age是25,那么URL就可以写为https://www.httpbin.org/get?name=germy&age=25,但是这样写稍有麻烦,其实可以利用params参数直接传递这种信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
data={
'name':'germy',
'age':25
}
url = 'https://www.httpbin.org/get'
response = requests.get(url, params=data)
print(response.text)
>>>
{
"args": {
"age": "25",
"name": "germy"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "www.httpbin.org",
"User-Agent": "python-requests/2.31.0",
"X-Amzn-Trace-Id": "Root=1-6638ca91-53eeec34391520830e2e1c8a"
},
"origin": "219.156.133.195",
"url": "https://www.httpbin.org/get?name=germy&age=25"
}

  这样就把URL参数以字典的形式传给get方法的params参数,通过返回信息可以判断,请求链接自动被构造成了https://www.httpbin.org/get?name=germy&age=25,就不用自己构造URL了。
  网页返回的类型虽然是str类型,但是它很特殊,是JSON格式的。如果想直接解析返回结果,得到一个JSON格式的数据,可以直接调用json方法:

1
2
3
4
5
6
7
8
9
10
11
12
import requests
data={
'name':'germy',
'age':25
}
url = 'https://www.httpbin.org/get'
response = requests.get(url, params=data)
print(type(response.json()))
print(response.json())
>>>
<class 'dict'>
{'args': {'age': '25', 'name': 'germy'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'www.httpbin.org', 'User-Agent': 'python-requests/2.31.0', 'X-Amzn-Trace-Id': 'Root=1-6638cd0c-14525f1e05304540064c24db'}, 'origin': '219.156.133.195', 'url': 'https://www.httpbin.org/get?name=germy&age=25'}

  调用json方法可以将结果(JSON格式字符串)转化为字典。
  但是如果返回结果不是JSON格式,就会出现解析错误,出现异常。

对于上面的请求得到的响应,如果直接用print将响应的对象打印输出,得到的会是什么,我试了下,请求成功<Response [200]>,请求失败<Response [404]>

抓取网页

https://ssr1.scrape.center为例,往代码里加一点提取信息的逻辑:

1
2
3
4
5
6
7
8
9
10
11
import re
import requests

url = 'https://ssr1.scrape.center'

response = requests.get(url)
pattern = re.compile('<h2.*?>(.*?)</h2>',re.S)
titles = re.findall(pattern,response.text)
print(titles)
>>>
['霸王别姬 - Farewell My Concubine', '这个杀手不太冷 - Léon', '肖申克的救赎 - The Shawshank Redemption', '泰坦尼克号 - Titanic', '罗马假日 - Roman Holiday', '唐伯虎点秋香 - Flirting Scholar', '乱世佳人 - Gone with the Wind', '喜剧之王 - The King of Comedy', '楚门的世界 - The Truman Show', '狮子王 - The Lion King']

  这里提取出了所有的电影标题,只需一个最基本的抓取和提取流程即可完成。

抓取二进制数据

  上面例子抓取的是网站页面,返回的是HTML文档。
而图片、音频、视频这些文件本质上都是由二进制编码组成的,有特定的保存格式和对应的解析方式,我们能看到各种形色的多媒体,抓取他们要拿到他们的二进制数。

1
2
3
4
5
import requests
url = 'https://scrape.center/favicon.ico'
response = requests.get(url)
print(response.text)
print(response.content)

  这里response.text的运行结果是一串乱码,而response.content的运行结果是bytes类型的数据。

bytes类型的数据前面会带有一个b,也称为“字节串”的格式,字节串是由一系列字节组成的,每个字节可以表示一个字符或者数据的一部分。在Python中,字节串通常用单引号或双引号括起来,并且字节串中的每个字节用两个十六进制数表示。

  可以将图片的二进制数据保存下来。

1
2
3
4
5
import requests
url = 'https://scrape.center/favicon.ico'
response = requests.get(url)
with open('favicon.ico', 'wb') as f:
f.write(response.content)

  使用open方法,以二进制写的形式打开文件,向文件里写入二进制数据。

添加请求头

  在发起HTTP请求的时候,会有一个请求头Request Headers,我们可以通过设置headers参数完成。
  上面的实例中,是没有设置请求头信息的,某些网站会发现这不是一个正常浏览器发起的请求,可能返回异常结果,导致网页抓取失败。
  添加请求信息,如添加一个User-Agent字段:

1
2
3
4
5
6
7
import requests
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
}
url = 'https://ssr1.scrape.center/'
response = requests.get(url, headers=headers)
print(response.text)

  也可以在headers参数中添加其他字段信息。

POST请求

实例

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
import requests
data={
'name':'admin',
'age':25
}
url = 'https://httpbin.org/post'
response = requests.post(url,data=data)
print(response.text)
>>>
{
"args": {},
"data": "",
"files": {},
"form": {
"age": "25",
"name": "admin"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "17",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.31.0",
"X-Amzn-Trace-Id": "Root=1-6639927b-00d275d24c6775e71e150ebb"
},
"json": null,
"origin": "219.156.133.195",
"url": "https://httpbin.org/post"
}

form部分就是提交的数据,证明POST请求成功发送。

响应

  请求发送后会得到响应。上面实例中,用的是text和content获取了响应的内容,还有很多属性
和方法用来获取其他信息,如状态码、响应头、Cookie等。
实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
url = ('http://ssr1.scrape.center/')
response = requests.get(url)
print(type(response.status_code),response.status_code) //状态码
print(type(response.headers),response.headers) //响应头
print(type(response.cookies),response.cookies) //cookies
print(type(response.url),response.url) //URL
print(type(response.history),response.history) //请求历史
>>>
<class 'int'> 200
<class 'requests.structures.CaseInsensitiveDict'> {'Date': 'Tue, 07 May 2024 02:40:47 GMT', 'Content-Type': 'text/html; charset=utf-8', 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff', 'Expires': 'Tue, 07 May 2024 02:45:59 GMT', 'Strict-Transport-Security': 'max-age=15724800; includeSubDomains', 'Server': 'Lego Server', 'X-Cache-Lookup': 'Cache Miss, Cache Miss', 'Cache-Control': 'max-age=600', 'Age': '0', 'Content-Length': '41667', 'X-NWS-LOG-UUID': '10330054940065279203', 'Connection': 'keep-alive'}
<class 'requests.cookies.RequestsCookieJar'> <RequestsCookieJar[]>
<class 'str'> https://ssr1.scrape.center/
<class 'list'> [<Response [302]>]

  这里面headers和cookies这两个属性的结果分别是CaseInsensitiveDictRequestsCookieJar类型的对象。
  状态码200表示响应没有问题,而我们可以通过判断这个数字来确认爬虫是否爬取成功。
  requests库还提供了一个内置的状态码查询对象requests.codes
实例

1
2
3
4
5
6
import requests
url = 'https://ssr1.scrape.center/'
response = requests.get(url)
exit() if not response.status_code == requests.codes.ok else print('Request Successfully')
>>>
Request Successfully

  通过比较返回码和内置的表示成功的状态码,保证得到了正常响应,如果是就输出请求成功的消息,否则程序终止运行,用requests.codes.ok得到的成功状态码是200。

高级用法

文件上传

  requests库可以模拟提交一些数据。也可以上传文件。
实例

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 requests
url = 'https://httpbin.org/post'
file = {'favicon.ico': open('favicon.ico','rb')}
reponse = requests.post(url, files=file)
print(reponse.text)
>>>
{
"args": {},
"data": "",
"files": {
"favicon.ico": "data:application/octet-stream;base64,AAAB..."
},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "4440",
"Content-Type": "multipart/form-data; boundary=5dc8b6473c0c05dc184c55876cebce25",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.31.0",
"X-Amzn-Trace-Id": "Root=1-66399eb4-6599df655be9cf0a6a9ac001"
},
"json": null,
"origin": "219.156.133.195",
"url": "https://httpbin.org/post"
}

  上传文件后,网站返回响应,响应中包含files字段和form字段,而form字段是空的,说明文件上传部分会单独用一个files字段来标识。

Cookie设置

  前面使用过urllib库处理Cookie,写法较为复杂,而用requests库,获取和设置Cookie只需一部即可完成。
实例

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = 'https://www.baidu.com/'
response = requests.get(url)
print(response.cookies)
for key, value in response.cookies.items():
print(key+'='+value)
>>>
<RequestsCookieJar[<Cookie BAIDUID=0F50E1FD0E238874BD72A86D327F65CF:FG=1 for .baidu.com/>, <Cookie BAIDUID_BFESS=0F50E1FD0E238874BD72A86D327F65CF:FG=1 for .baidu.com/>, <Cookie H_PS_PSSID=40300_40079_40463_60174 for .baidu.com/>, <Cookie PSTM=1715055521 for .baidu.com/>, <Cookie BD_NOT_HTTPS=1 for www.baidu.com/>]>
BAIDUID=070B4458EAF888B767A06FC7F52B6DE1:FG=1
BAIDUID_BFESS=070B4458EAF888B767A06FC7F52B6DE1:FG=1
H_PS_PSSID=40303_40080_60174
PSTM=1715054787
BD_NOT_HTTPS=1

  这里先调用cookies属性,成功得到Cookie,发现它属于RequestsCookieJar类型。然后调用items方法将Cookie转化为由元组组成的列表,遍历输出每一个Cookie条目的名称和值,实现对Cookie的遍历和解析。
  我们也可以直接用Cookie来维持登录状态。以Github为例,我们先登录Github,然后将请求头中的Cookie内容复制下来

  将Cookie添加到请求头里,然后发送请求。
实例

1
2
3
4
5
6
7
import requests
headers = {
'Cookie': '_octo=GH1.1.1020779121.1709874774; _device_id=e5c616e3c9807057e79266383da8cd61; saved_user_sessions=155144413%3AetQxEGfBoTvbxcQPOuPZ0WyHSskj5GsMfXA2k8JqGVOz4pv0; user_session=etQxEGfBoTvbxcQPOuPZ0WyHSskj5GsMfXA2k8JqGVOz4pv0; __Host-user_session_same_site=etQxEGfBoTvbxcQPOuPZ0WyHSskj5GsMfXA2k8JqGVOz4pv0; logged_in=yes; dotcom_user=blttttt; has_recent_activity=1; color_mode=%7B%22color_mode%22%3A%22auto%22%2C%22light_theme%22%3A%7B%22name%22%3A%22light%22%2C%22color_mode%22%3A%22light%22%7D%2C%22dark_theme%22%3A%7B%22name%22%3A%22dark%22%2C%22color_mode%22%3A%22dark%22%7D%7D; preferred_color_mode=light; tz=Asia%2FShanghai; _gh_sess=cscvLFSGJcf6AUPq6fzQjoyyl0O89JlmxwVkS1%2BaEXzwsS5bDp%2FKxbAGbdkfKGN2PeDMiLcD2%2BnXCaJiYkjRjAncjALoQ0yR0XMFkNlI08mU2uBAY%2BO0eHJ78vnRiiRutYVCkBPJAQF0oKQ8P46vwW1esYkG3X7Hfs2scPXyDm5sUkW0AC%2B%2B4Xg4yyjQ2VMD96gSpB0SUk8CMugT4GaYSQ4jWzSys%2BffoehUzl0fC76EAhvzT7X7fw5Oa4c7NKoGSz4xn6DXMl8ZaoK%2F7a32i%2FPwYplpfpcd8k%2By36Q%2F533m23P8dVyB3SMO2UZRdIFTUbTi7wFomjcSvbIFJ8lW%2BNMtuYrrz9TZ8X90K1gdDzi4KcmCSXzXcbY25E%2BDMlco%2BmfPtPPax1%2BIaoGu5HYTpM2IAkRDheNvdzHE4pbBPQbdJlx4p6V1R9MWYVm3iKM1Jx6gkPfm92%2BcbznQ4AItn4%2BV40z7Aj4QnV7gG6c8lkyeOhRjVls9s%2F%2BrhDwkHMPjxDpRzruGN6YEPo%2F7X8gt4uIspHA0d4z%2BnNbR1mloiPXw%2B1EJ6gqCog%3D%3D--8YfmSu%2BHtVUuakSu--kppLpsZaDcPEkIPLocCpMA%3D%3D'
}
url = ('https://github.com/')
response = requests.get(url,headers=headers)
print(response.text)

  结果中包含了登录才能有的信息,其中包含用户名信息。
  得到此结果说明Cookie成功模拟了登录状态,这样就能爬取登录之后才能看到的页面了。
  也可以通过cookies参数来设置Cookie的信息,这里构造一个RequestsCookieJar对象,然后对刚才复制的Cookie进行处理以及赋值。
实例

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
cookies = '_octo=GH1.1.1020779121.1709874774; _device_id=e5c616e3c9807057e79266383da8cd61; saved_user_sessions=155144413%3AetQxEGfBoTvbxcQPOuPZ0WyHSskj5GsMfXA2k8JqGVOz4pv0; user_session=etQxEGfBoTvbxcQPOuPZ0WyHSskj5GsMfXA2k8JqGVOz4pv0; __Host-user_session_same_site=etQxEGfBoTvbxcQPOuPZ0WyHSskj5GsMfXA2k8JqGVOz4pv0; logged_in=yes; dotcom_user=blttttt; has_recent_activity=1; color_mode=%7B%22color_mode%22%3A%22auto%22%2C%22light_theme%22%3A%7B%22name%22%3A%22light%22%2C%22color_mode%22%3A%22light%22%7D%2C%22dark_theme%22%3A%7B%22name%22%3A%22dark%22%2C%22color_mode%22%3A%22dark%22%7D%7D; preferred_color_mode=light; tz=Asia%2FShanghai; _gh_sess=blrf5T4EJEg6JJ50EPrvPOFUhj0hBcd1MbdCCIQc8MS8Fe03V4WSkKIGvUY08G9m%2B4kh1Ir1%2F5fjayGfZFgwiz34xSZrtbWxNO6%2BVWD8yVdBxZYwmE%2ByX1ckwBVTlhVzWjuIg4cRuFc7Cf2a%2BEjNyWZ1SGWiEFqkBI4%2BNBFFWjySrCv8%2Bprb%2FGE6PYp5yMj5ErqhtrIprbxYYLeZujlF2xZKEtO5HEE%2FML%2BkhPd3zelGQ1xfsQCDZCtGMKzBl096OovkfZGd2neqwvadIN5BFbtXr%2F0Qrg0l5%2FgKZivdxH0FcFWjbbh6vBYC1Wdq6ddLAmtoK5tkzf65szPCym4wJWK8BHH5ui1g6gR2jCrKggtk7XDYJ%2FgfRdRFFhOFodqyUTAwliztTaEUqsZx10xvLGSzOa76fWfqN5DOktKMUqEVPi6RwRejzb2hf6CxNQ4fr1kR348GyuyNrQGpAi0%2BFod2%2B7r4VtIRXPmLX%2FhMLZqC4%2BMRiM1S9i3IeQdhj%2BF%2Bu1VBLuvVlg0hKX3xxm449GFW0tBXk345dTu89MLffCFI29X%2BUXV8jA%3D%3D--L4G%2B56Y9nCQAoeYL--nvFWkVjJSzdHJtWrW%2FW%2F4Q%3D%3D'
jar = requests.cookies.RequestsCookieJar()
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
}
url = ('https://github.com/')

for cookie in cookies.split(';'):
key, value = cookie.split('=',1) //这里 split 方法的第二个参数 1 表示最多分割一次,确保即使 value 中包含等号 = 也不会继续分割。
jar.set(key, value)
response = requests.get(url,cookies=jar,headers=headers)
print(response.text)

  这里首先创建了一个RequestCookieJar对象,然后利用split方法对复制下来的Cookie内容做分割,接着利用set方法设置好每个Cookie条目的键名和键值,最后把RequestCookieJar对象通过cookies参数传递,调用requests库的get方法,即可获取登录后的页面。
  也可以正常登录。

Session维持

  直接利用request库中的get或post方法的确可以做到模拟网页的请求,但这两种方法实际上相当于不同的Session,或者说是两个浏览器打开了不同的页面。
  有一个场景,第一个请求利用requests库的post方法登录了某个网站,第二次想获取成功登录后的自己的个人信息,于是又用了一次requests库的get方法去请求个人信息页面。实际上这相当于打开了两个浏览器,是两个完全独立的操作,对应两个完全不相关Session,并不能获取个人信息。
  如果在两次请求时设置一样的Cookie,这样做可以,但是过于繁琐。
  解决这个问题的方法是维持同一个Session,也就是第二次请求时打开一个新的浏览器选项卡而不是打开一个新的浏览器。利用Session对象,可以方便地维护一个Session,而且不用担心Cookie的问题,它会自动帮我们处理好。
实例:沿用之前写法

1
2
3
4
5
6
7
8
import requests
requests.get('https://httpbin.org/cookies/set/number/123456789')
r = requests.get('https://httpbin.org/cookies')
print(r.text)
>>>
{
"cookies": {}
}

  这里请求测试网址https://httpbin.org/cookies/set/number/123456789。在请求时设置了一个Cookie条目,名称是number,内容是123456789。然后请求https://httpbin.org/cookies,获取当前Cookie信息。并不能成功获取设置的Cookie。
  然后用Session试试看

1
2
3
4
5
6
7
8
9
10
11
import requests
S=requests.Session()
S.get('https://httpbin.org/cookies/set/number/123456789')
r = S.get('https://httpbin.org/cookies')
print(r.text)
>>>
{
"cookies": {
"number": "123456789"
}
}

  这样就能获取到设置的Cookie了。

SSL证书验证

  现在很多网站要求使用HTTPS协议,但有些网站可能并没有设置好HTTPS证书,或者网站的HTTPS证书可能不被CA机构认可,这些网站就可能出现SSL证书错误的提示。
  例如我们访问网站https://ssr2.scrape.center/,如果用Chrome浏览器打开它,会有如下提示:

  其实可以在浏览器中通过一些设置来忽略证书的验证。
但如果是使用requests库来请求这类网站

1
2
3
4
import requests
url = 'https://ssr2.scrape.center'
response = requests.get(url)
print(response.text)

  会出现SSLError错误,因为我们请求的URL的证书是无效的。
  但如果还是想爬这个网站的话,可以使用verify参数控制是否验证证书,将此参数设置为False,那么在请求时就不会再验证证书是否有效。不设置verify参数,其默认值是True,会自动验证。

1
2
3
4
5
6
7
8
import requests
url = 'https://ssr2.scrape.center/'
response = requests.get(url,verify=False)
print(response.status_code)
>>>
E:\JetBrains\python386\lib\site-packages\urllib3\connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host 'ssr2.scrape.center'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
200

  不过有警告,建议我们给它指定证书,我们可以通过设置忽略警告的方式来屏蔽这个警告。

1
2
3
4
5
6
7
8
import requests
import urllib3
url = 'https://ssr2.scrape.center/'
urllib3.disable_warnings()
response = requests.get(url,verify=False)
print(response.status_code)
>>>
200

  或捕获警告到日志的方式忽略警告:

1
2
3
4
5
6
import requests
import logging
url = 'https://ssr2.scrape.center/'
logging.captureWarnings(True)
response = requests.get(url,verify=False)
print(response.status_code)

  也可以指定一个本地证书作为客户端证书,可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组:

1
2
3
import requests 
response = requests.get('https://ssr2.scrape.center/',cert=('/path/server.crt','/path/server.key'))
print(response.status_code)

  这里需要有crt和key文件,并指定他们的路径,而且本地私有证书的key必须是解密状态加密状态的key是不支持的。

超时设置

  在本机网络状况不好或服务器网络响应太慢甚至无响应时,我们可能会等待特别久的时间才能收到响应,甚至到最后因为接收不到响应而报错。为了防止服务器不能及时响应,可以设置一个超时时间,如果超过了这个时间还没有得到响应,就报错。
  这里要用到timeout参数,其值是从发出请求到服务器返回响应的时间。
实例

1
2
3
4
5
6
import requests
url = 'https://httpbin.org/get'
response = requests.get(url,timeout=1)
print(response.status_code)
>>>
200

  如果在相应时间内没有响应就会抛出异常。
  请求分为两个阶段:连接(connect)和读取(read)。如果想要分别指定连接和读取的timeout,则可以传入一个元组response = requests.get(url,timeout=(5,30))

身份认证

  在访问启用了基本身份认证的网站时,首先会弹出一个认证窗口,如前面遇到的https://ssr3.scrape.center
  可以使用requests库自带的身份认证功能,通过auth参数即可设置。
实例

1
2
3
4
5
6
7
import requests
from requests.auth import HTTPBasicAuth
url = 'https://ssr3.scrape.center'
response = requests.get(url,auth=HTTPBasicAuth('admin','admin'))
print(response.status_code)
>>>
200

  这个实例网站的用户名和密码都是admin,这里可以直接设置。
  如果用户名和密码正确,那么请求时就会自动认证成功,返回200状态码;如果认证失败,则返回401状态码。
  参数都传一个HTTPBasicAuth类,显得有些繁琐,有更简单的写法,直接传一个元组,它会默认使用HTTPBasicAuth这个类来认证。

1
2
3
4
5
import requests

url = 'https://ssr3.scrape.center'
response = requests.get(url,auth=('admin','admin'))
print(response.status_code)

  还有其他认证方式,如OAuth认证,这个需要安装oauth包pip3 install requests_oauthlib
实例

1
2
3
4
5
6
7
8
import requests
from requests_oauthlib import OAuth1

url = 'https://ssr3.scrape.center'
auth = OAuth1('YOUR_API_KEY', 'YOUR_API_SECRET',
'USER_OAUTH_TOKEN','USER_OAUTH_TOKEN_SECRET')
response=requests.get(url,auth=auth)
print(response.status_code)

代理设置

  某些网站在测试的时候请求几次,都能正常获取内容。但是一旦大规模且频繁的请求时,这些网站就可能弹出验证码,或者跳转到登录认证界面,甚至会直接封禁客户端的IP,导致在一定时间内无法访问。
  为了防止这种情况发生,我们需要设置代理来解决这个问题,要用到proxies参数。可以用如下方式设置:

1
2
3
4
5
6
7
import requests
proxies = {
'http': 'http://10.10.10.10:1080',
'https': 'http://10.10.10.10:1080'
}
url = 'https://www.baidu.com'
requests.get(url, proxies=proxies)

  这里直接运行是不可以的,因为这个代理无效,可以找有效的代理替换试验。
  如果代理需要使用身份认证,可以使用类似https://user:password@host:port这样的语法来设置代理:

1
2
3
4
5
import requests
proxies = {
'https': 'http://user:password@10.10.10.10:1080',
}
requests.get('https://www.httpbin.org/get', proxies=proxies)

  除了基本的HTTP代理外,requsts库还支持SOCKS协议的代理。
  首先要安装socks这个库:pip3 install "requests[socks]"
  然后就可以使用SOCKS协议代理了
实例

1
2
3
4
5
6
import requests
proxies ={
'http': 'socks5://user:password@host:port',
'https': 'socks5://user:password@host:'
}
requests.get('http://www.httpbin.org/get', proxies=proxies)

Prepared Request

  我们可以直接使用requests库的get和post方法的请求,但是这个请求在requests内部是怎么实现的。
  实际上,requests在发送请求的时候,是在内部构造了一个Request对象,并给这个对象赋予了各种参数,包括url、headers、data等,然后直接把这个Request对象发出去,请求成功后会再得到一个Response对象,解析这个对象即可。而对于Request对象,其实它就是Prepared Request。
  不使用get,而是直接构造一个Prepared Request对象来试试:

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
from requests import Request,Session

url = 'https://www.httpbin.org/post'
data = {
'name':'germy'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0'
}
s = Session()
req = Request('POST',url,data=data,headers=headers)
prepped = s.prepare_request(req)
response = s.send(prepped)
print(response.text)
>>>
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "germy"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "10",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "www.httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"X-Amzn-Trace-Id": "Root=1-663a1529-42d19be2162994625bf75e11"
},
"json": null,
"origin": "219.156.133.195",
"url": "https://www.httpbin.org/post"
}

  我们引入了Request类,然后用url、data和headers参数构造了一个Request对象,这时调用Session类的prepare_request方法将其转换为Prepared Request对象,再调用send方法发送,即可达到和POST请求同样的效果。
  有了Request这个对象,就可以将请求当作独立的对象来看待,这里可以直接操作这个Request对象,更灵活地实现请求的调度和各种操作。

正则表达式

实现字符串的检索、替换、匹配验证。

实例引入

正则表达式测试工具
  在这里输入待匹配的文字,然后选择常用的正则表达式,就可以得出相应的匹配结果。

Hello, my phone number is 010-86432100 and email is cqc@cuiqingcai.com, and my website is https://cuiqigncai.com

  这段字符串包含一个电话号码,一个Email地址和一个URL,然后用正则表达式将这些内容提取出来。
  在网页右侧选择“匹配Email地址”,就可以看到出现文本中的E-mail。选择“匹配网址URL”可以看到文本中的URL。
  这里面用的就是正则表达式匹配,用一定的规则将特定的文本提取出来。
  E-mail地址开头是一段字符串,然后一个@符号,后面跟上的是域名。对于URL,开头是协议类型,然后是冒号加两个斜杠,最后是域名加路径。
  对于URL可以是[a-zA-z]+://[^\s]*a-z代表匹配任意的小写字母,\s代表匹配任意的空白字符,*代表匹配前面任意多个字符。
常用匹配规则

模式 描述
\w 匹配字母数字下划线
\W 匹配非字母数字下划线
\s 匹配任意空白字符,[\t\n\r\f]
\S 匹配任意非空字符
\d 匹配任意数字,等价于[0-9]
\D 匹配任意非数字
\A 匹配字符串开头
\Z 匹配字符串结尾。如果有换行,只匹配到换行前的结束字符串
\z 匹配字符串结尾。如果有换行,同时还会匹配换行符
\G 匹配最后匹配完成的位置
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配一行字符串的开头
$ 匹配一行字符串的结尾
. 匹配任意字符,除了换行符。当re.DOTALL标记被指定时,可以匹配包括换行符在内的任意字符
[…] 用来表示一组字符,单独列出,如[amk]匹配a、m、k
[^…] 匹配不在[]中的字符
* 匹配0个或多个表达式
+ 匹配一个或多个表达式
? 匹配0个或1个前面的正则表达式定义的片段,非贪婪方式
{n} 精确匹配n个前面的表达式
{n,m} 匹配n到m次由前面正则表达式顶一个的片段,贪婪方式
a|b 匹配a或b
() 匹配括号内的表达式,也表示一个组

  正则表达式并非Python独有,但再Python的re库提供了正正则表达式的实现,利用这个库,可以再Python中方便地使用正则表达式。

match

  这是一个常用的匹配方法,match方法会尝试从字符串的起始位置开始匹配正则表达式,如果匹配就返回成功的结果;否则返回None。
实例

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

content = 'Hello 123 4567 World_This is a Regex Demo'
print(len(content))
result = re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}',content)
print(result)
print(result.group())
print(result.span())
>>>
41
<re.Match object; span=(0, 25), match='Hello 123 4567 World_This'>
Hello 123 4567 World_This
(0, 25)

  首先声明字符串,^Hello\s\d\d\d\s\d{4}\s\w{10}是一个正则表达式。在match方法里,第一个参数是传入了正则表达式,第二个参数是要匹配的字符串。结果是re.Match对象,匹配成功了。对象包含两个方法:group方法可以输出匹配到的内容;span方法可以输出匹配的范围,结果是(0,25),是匹配到的结果字符串在原字符串中的位置范围。

匹配目标

  match实现匹配目标,对于从字符串中提取内容,可以用括号()将想提取的子字符串括起来。()实际上标记了一个子表达式的开始和结束位置,被标记的子表达式依次对应每一个分组,通过调用group方法传入分组索引获取提取结果。
实例

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d\d\d\s\d{4})\s(\w{10})',content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
>>>
<re.Match object; span=(0, 25), match='Hello 123 4567 World_This'>
Hello 123 4567 World_This
123 4567
(0, 25)

  把字符串中的123 4567提取出来了。group方法会输出完整的匹配结果。group(1)输出第一个被()包围的匹配结果,以此类推。

通用匹配

  有一个万能匹配符,*。它可以匹配任意字符(除了换行符),*代表匹配前面的字符无限次,组合在一起就可以匹配任意字符了。
实例

1
2
3
4
5
6
7
8
9
10
11
import re

content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello.*Demo$',content)
print(result)
print(result.group())
print(result.span())
>>>
<re.Match object; span=(0, 41), match='Hello 123 4567 World_This is a Regex Demo'>
Hello 123 4567 World_This is a Regex Demo
(0, 41)

  中间部分全用.*代替,最后加一个结尾字符串。

贪婪与非贪婪

  通用匹配.*匹配到的内容有时并不是我们想要的结果。
实例

1
2
3
4
5
6
7
8
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^H.*(\d+).*Demo$',content)
print(result)
print(result.group(1))
>>>
<re.Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>
7

  目标是匹配字符串中间的数字,用(\d+)来匹配数字的两侧都用.*,而结果只得到了7。
  这是因为在贪婪匹配下,.*会匹配尽可能多的字符,.*后面是\d+,至少匹配一个数字,但是这里并没有指定具体几个数字,.*就把123456都匹配了,只给\d+留下一个满足条件的7。
  使用非贪婪匹配来解决这个问题。非贪婪匹配写法是.*?

1
2
3
4
5
6
7
8
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^H.*?(\d+).*Demo$',content)
print(result)
print(result.group(1))
>>>
<re.Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>
1234567

  此时成功获取了1234567。贪婪匹配是匹配尽可能多的字符,非贪婪匹配是匹配尽可能少的字符。
  有一种情况,当匹配的结果在字符串的结尾,.*?有可能匹配不到任何内容。
实例

1
2
3
4
5
6
7
8
9
import re
content = 'Http://www.baidu.com/comment'
result1 = re.match('Http://www.baidu.com/(.*?)',content)
result2 = re.match('Http://www.baidu.com/(.*)',content)
print('result1',result1.group(1))
print('result2',result2.group(1))
>>>
result1
result2 comment

修饰符

  在正则表达式里,可以用一些可选标志来控制匹配的模式。修饰符被指定为一个可选的标志。
实例

1
2
3
4
5
import re
content = '''Hello 1234567 World_This
is a Regex Demo'''
result1 = re.match('^He.*?(\d+).*?Demo$',content)
print('result1',result1.group(1))

  由于字符串里加了换行符,报错了AttributeError: 'NoneType' object has no attribute 'group'。表明正则表达式没有匹配到这个字符串,结果是None。调用group方法,导致了Attribute。
  因为匹配的内容是除了换行符之外的任意字符,当遇到换行符时,.*?就不能匹配了。
  解决这个问题的方式是加上一个修饰符re.S,就可以了。

1
2
3
4
5
6
7
import re
content = '''Hello 1234567 World_This
is a Regex Demo'''
result1 = re.match('^He.*?(\d+).*?Demo$',content,re.S)
print(result1.group(1))
>>>
1234567

  re.S在网页匹配中经常用到。因为HTML节点经常会有换行,加上它,就可以匹配节点与节点之间的换行了。
  还有一些修饰符,在必要的情况下可以使用。

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 实现本地化识别(local-aware)匹配
re.M 多行匹配,影响^和$
re.S 使匹配内容包括换行符在内的所有字符
re.U 根据Unicode字符集解析字符。会影响\w、\W、\b、\B
re.X 给予灵活的格式,将正则表达式书写的更易于理解

转义匹配

  .用于匹配除换行符以外的任意字符,但目标字符串中可能包含.这个字符。
  我们需要用到转义匹配。
实例

1
2
3
4
5
6
import re
content = '''(百度)www.baidu.com'''
result1 = re.match('\(百度\)www\.baidu\.com',content,re.S)
print(result1)
>>>
<re.Match object; span=(0, 17), match='(百度)www.baidu.com'>

在特殊字符前加\转义一下即可。

  match方法是从字符串的开头开始匹配的,一旦开头不匹配,整个匹配就会失败。
实例

1
2
3
4
5
6
import re
content = 'Extra strings Hello 1234567 World_THis is a Regax Demo Strings'
result = re.match('Hello.*?(\d+).*?Demo',content)
print(result)
>>>
None

  match方法在使用时需要考虑目标字符串开头的内容,它更适合检测某个字符串是否符合某个正则表达式的规则。
  使用search方法在匹配时会扫描整个字符串,然后返回第一个匹配成功的结果。在匹配时,search会依次以每个字符作为开头扫描字符串,知道找到符合规则的字符串,然后返回匹配的内容;如果没有找到,就返回None。

1
2
3
4
5
6
7
8
import re
content = 'Extra strings Hello 1234567 World_THis is a Regax Demo Strings'
result = re.search('Hello.*?(\d+).*?Demo',content)
print(result)
print(result.group(1))
>>>
<re.Match object; span=(14, 54), match='Hello 1234567 World_THis is a Regax Demo'>
1234567

其他实例
有一段HTML文本,用几个正则表达式实例实现相应信息的提取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
html = '''<div id="songs-list">
<h2 class="title"> 经典老歌 </h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2"> 一路上有你 </li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐"> 沧海一声笑 </a>
</li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦"> 往事随风 </a>
</li>
<li data-view="6"><a href="/4.mp3" singer="beyond"> 光辉岁月 </a></li>
<li data-view="5"><a href="/5.mp3" singer="陈慧琳"> 记事本 </a></li>
<li data-view="5">
<a href="/6.mp3" singer="邓丽君"> 但愿人长久 </a>
</li>
</ul>
</div>'''

  ul节点里有很多li节点,li节点里有点包含a节点,有的不包含a节点。a节点还有一些相应的属性——超链接和歌手名。
  我们尝试提取 class 为 active 的 li 节点内部的超链接包含的歌手名和歌名,此时需要提取第三个 li 节点下 a 节点的 singer 属性和文本。
  正则表达式以li开头,然后找标志符active,中间部分用.*?来匹配。然后提取singer的值,可以使用singer="(.*?)"。接下来匹配a节点的文本,左边界是>,右边界是</a>,目标用(.*?)匹配。正则表达式就是:li.*?active.*?singer="(.*?)">(.*?)</a>。由于代码中有换行,所以search方法的第三个参数要传入re.S。:

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 re
html = '''<div id="songs-list">
<h2 class="title"> 经典老歌 </h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2"> 一路上有你 </li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐"> 沧海一声笑 </a>
</li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦"> 往事随风 </a>
</li>
<li data-view="6"><a href="/4.mp3" singer="beyond"> 光辉岁月 </a></li>
<li data-view="5"><a href="/5.mp3" singer="陈慧琳"> 记事本 </a></li>
<li data-view="5">
<a href="/6.mp3" singer="邓丽君"> 但愿人长久 </a>
</li>
</ul>
</div>'''
result = re.search('li.*?active.*?singer="(.*?)">(.*?)</a>',html,re.S)
print(result.group(1),result.group(2))
>>>
齐秦 往事随风

  这样就获取到了class为active的歌手名和歌曲名。
  试一下表达式不加active会如何

1
2
3
4
result = re.search('li.*?singer="(.*?)">(.*?)</a>',html,re.S)
print(result.group(1),result.group(2))
>>>
任贤齐 沧海一声笑

  会返回第一个符合条件的匹配目标
  接下来,把search方法的第三个参数re.S去掉

1
2
3
4
result = re.search('li.*?singer="(.*?)">(.*?)</a>',html)
print(result.group(1),result.group(2))
>>>
beyond 光辉岁月

绝大部分HTML文本包含换行符,所以要尽量加上re.s修饰符,避免出现匹配不到的问题。

findall

  对于search方法,它返回与正则表达式相匹配的第一个字符串。如果想获取与正则表达式相匹配的所有字符串,要使用findall方法了。
实例

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
import re
html = '''<div id="songs-list">
<h2 class="title"> 经典老歌 </h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2"> 一路上有你 </li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐"> 沧海一声笑 </a>
</li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦"> 往事随风 </a>
</li>
<li data-view="6"><a href="/4.mp3" singer="beyond"> 光辉岁月 </a></li>
<li data-view="5"><a href="/5.mp3" singer="陈慧琳"> 记事本 </a></li>
<li data-view="5">
<a href="/6.mp3" singer="邓丽君"> 但愿人长久 </a>
</li>
</ul>
</div>'''
result = re.findall('li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>',html,re.S)
print(result)
print(type(result))
for i in result:
print(i)
print(i[0],i[1],i[2])
>>>
[('/2.mp3', '任贤齐', ' 沧海一声笑 '), ('/3.mp3', '齐秦', ' 往事随风 '), ('/4.mp3', 'beyond', ' 光辉岁月 '), ('/5.mp3', '陈慧琳', ' 记事本 '), ('/6.mp3', '邓丽君', ' 但愿人长久 ')]
<class 'list'>
('/2.mp3', '任贤齐', ' 沧海一声笑 ')
/2.mp3 任贤齐 沧海一声笑
('/3.mp3', '齐秦', ' 往事随风 ')
/3.mp3 齐秦 往事随风
('/4.mp3', 'beyond', ' 光辉岁月 ')
/4.mp3 beyond 光辉岁月
('/5.mp3', '陈慧琳', ' 记事本 ')
/5.mp3 陈慧琳 记事本
('/6.mp3', '邓丽君', ' 但愿人长久 ')
/6.mp3 邓丽君 但愿人长久

  返回列表中的每个元素都是元组类型,可以用索引依次取出每个条目。当正则表达式中只有一个分组时,列表中的元组没有()

sub

  正则表达式除了提取信息,还可以修改文本。如把字符串的所有数字都去掉,如果只用字符串的replace方法,显得繁琐,这里可以借助sub方法。
实例

1
2
3
4
5
6
import re
content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+','',content)
print(content)
>>>
aKyroiRixLg

  这里sub的第一个参数\d+用来匹配所有的数字,第二个参数是把数字替换成的字符串,第三个参数是原字符串。
  获取前面HTML文本中所有li节点的歌名。
实例

1
2
3
4
5
6
7
8
9
10
result = re.findall('<li.*?>\s?(<a.*?>)?\s(\w+)\s(</a>)?\s?</li>',html,re.S)
for i in result:
print(i[1])
>>>
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久

  显得有些繁琐,但此时用sub就很简单了。用sub把a节点去掉,只留下文本,然后用findall提取:

1
2
3
4
5
6
7
8
9
10
11
12
html = re.sub('<a.*?>|</a>','',html)
result = re.findall('<li.*?>(.*?)</li>',html,re.S)
print(result)
for i in result:
print(i.strip())
>>>
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久

  经过sub方法处理过之后,a节点就没有了,然后通过findall方法直接提取。

compile

  这个方法可以将字符串编译成正则表达式对象,实现在后面的代码中复用。
实例

1
2
3
4
5
6
7
8
9
10
11
import re
content1 = '2019-12-15 12:00'
content2 = '2019-12-17 12:55'
content3 = '2019-12-19 13:14'
pattern = re.compile('\d{2}:\d{2}')
result1 = re.sub(pattern, '', content1)
result2 = re.sub(pattern, '', content2)
result3 = re.sub(pattern, '', content3)
print(result1,result2,result3)
>>>
2019-12-15 2019-12-17 2019-12-19

  这里有三个日期,我们用sub方法把日期中的时间去掉,用compile方法将正则表达式编译成一个正则表达式对象,实现复用。
  compile中还可以传递修饰符,如re.S,在search,findall方法中就不需要再传了。

httpx的使用

  urllib库和requests库已经可以爬取绝大多数网站的数据,但是由于有些网站强制使用HTTP/2.0协议访问,而urllib和requests只支持HTTP/1.1,不支持HTTP2.0,就无法爬取数据。
  这时使用支持HTTP/2.0的请求库就可以了,有代表性的是hyper和httpx。httpx使用起来更方便,功能也强大,requests库已有的功能它也几乎都支持。

示例

  https://spa16.scrape.center/就是一个强制使用HTTP/2.0访问的一个网站。这个网站用requests库是无法爬取的:

1
2
3
4
5
6
7
8
import requests
url = 'https://ssr1.scrape.center/'
response = requests.get(url)
print(response)
```
&emsp;&emsp;会抛出错误,错误信息很多,真实原因就是requests这个库是使用HTTP/1.1访问的目标网站,而目标网站会检测请求使用的协议是不是HTTP/2.0,如果不是就拒绝返回任何请求。
## 安装
&emsp;&emsp;httpx可以使用pip3工具直接安装,需要的Python版本是3.6及以上

pip3 install httpx

1
但是这样安装完的httpx是不支持HTTP/2.0的,如果想支持,可以这样安装  

pip3 install “httpx[http2]”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
就既安装了httpx,又安装了httpx对HTTP/2.0的支持模块。 
## 基本使用
&emsp;&emsp;基本GET请求用法
```py
import httpx
response = httpx.get('https://httpbin.org/get')
print(response.status_code)
print(response.headers)
print(response.text)
>>>
200
Headers({'date': 'Mon, 13 May 2024 03:59:08 GMT', 'content-type': 'application/json', 'content-length': '306', 'connection': 'keep-alive', 'server': 'gunicorn/19.9.0', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true'})
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-httpx/0.27.0",
"X-Amzn-Trace-Id": "Root=1-6641900c-25c2aaf626555ab94c910e08"
},
"origin": "219.156.133.195",
"url": "https://httpbin.org/get"
}

  输出有状态码,响应头,响应体。,可以在响应体里看到User-Agent是python-httpx/0.27.0,代表是httpx请求的。
  接下来更改User-Agent再请求一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import httpx
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0'
}
response = httpx.get('https://www.httpbin.org/get',headers=headers)
print(response.text)
>>>
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "www.httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"X-Amzn-Trace-Id": "Root=1-664192eb-4fab4c527b08851818d180d9"
},
"origin": "219.156.133.195",
"url": "https://www.httpbin.org/get"
}

  接下来使用httpx请求https://spa16.scrape.center/这个http/2.0的网站

1
2
3
4
import httpx
url = 'https://spa16.scrape.center/'
response = httpx.get(url)
print(response.text)

  还是会抛出错误,其实httpx默认是不会开启对HTTP/2.0的支持的,默认使用的是HTTP/1.1,需要手动声明一下才能使用HTTP/2.0

1
2
3
4
5
import httpx
client = httpx.Client(http2=True)
url = 'https://spa16.scrape.center/'
response = client.get(url)
print(response.text)


  这里声明了一个Client对象,赋值为client变量,将http2参数设置为True,就开启了对HTTP/2.0的支持。然后成功获取了HTML代码。印证了这个示例网站只能使用HTTP/2.0访问。
  httpx和requests有很多相似的API,对于POST请求、PUT请求和DELETE请求来说,实现方式是类似的:

1
2
3
4
5
6
7
import httpx

r=httpx.get('https://spa16.scrape.center/get',params={'name','germy'})
r=httpx.post('https://spa16.scrape.center/post',data={'name','germy'})
r=httpx.put('https://spa16.scrape.center/put')
r=httpx.delete('https://spa16.scrape.center/delete')
r=httpx.patch('https://spa16.scrape.center/patch')

  根据得到的Response对象,使用以下属性和方法获取想要的内容。

  • status_code:状态码
  • text: 响应体的文本内容
  • content: 响应体的二进制内容
  • headers: 响应头,是headers对象,可以用像获取字典中的内容一样获取其中某个Header的值。
  • json:方法,调用此方法将文本结果转化为JSON对象。

可以参考httpx官方文档

Client对象

  httpx中有一个Client对象,可以和requests中的Session对象类比学习。
  官方较推荐的使用Client对象的方式是with as语句:

1
2
3
4
5
6
7
import httpx

with httpx.Client() as client:
response = client.get('https://www.httpbin.org/get')
print(response)
>>>
<Response [200 OK]>

这种用法等价于:

1
2
3
4
5
6
7
8
import httpx

client = httpx.Client()
try:
response = client.get('http://httpbin.org')
finally:
client.close()
print(response)

  这两种方式的运行结果一样,这里需要在最后显式调用close方法来关闭Client对象。
  在声明Client对象时可以指定一些参数,headers,这样使用该对象发起的所有请求都会默认带上这些参数设置。
示例:

1
2
3
4
5
6
7
8
import httpx
url = 'https://www.httpbin.org/headers'
headers = {'User-Agent': 'my-app/0.0.1'}
with httpx.Client(headers=headers) as client:
r=client.get(url)
print(r.json()['headers']['User-Agent'])
>>>
my-app/0.0.1

  这里声明了headers变量,内容为User-Agent属性,然后将此变量传递给headers参数初始化一个Client对象,赋值给client变量,用client变量请求测试网站,打印User-Agent内容。
更多用法参考官方文档

支持HTTP/2.0

  要声明Client对象,然后将http2参数设置为True,如果不设置,就默认支持HTTP/1.1,是不能开启对HTTP/2.0的支持。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import httpx
client = httpx.Client(http2=True)
url = 'https://www.httpbin.org/get'
response = client.get(url)
print(response.text)
print(response.http_version)
>>>
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "www.httpbin.org",
"User-Agent": "python-httpx/0.27.0",
"X-Amzn-Trace-Id": "Root=1-6641b1c0-03c067ce641ca1223c927bb3"
},
"origin": "219.156.133.195",
"url": "https://www.httpbin.org/get"
}

HTTP/2

  我们输出了response变量的http_version属性,这时request中不存在的属性。
  这里输出的http_version属性值是HTTP/2,表明使用了HTTP/2.0协议传输。

客户端的httpx上启用对HTTP/2.0的支持并不意味着请求和响应都通过HTTP/2.0传输。需要客户端和服务端都支持HTTP/2.0才可以。

支持异步请求

  httpx还支持异步客户端请求(AsyncClient),支持Python的async请求模式。
写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import httpx
import asyncio

async def fetch(url):
async with httpx.AsyncClient(http2=True) as client:
response = await client.get(url)
print(response.text)

if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(fetch('https://www.httpbin.org/get'))
>>>
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "www.httpbin.org",
"User-Agent": "python-httpx/0.27.0",
"X-Amzn-Trace-Id": "Root=1-6641b38e-6d8e79bb0d6cf9c909831d2f"
},
"origin": "219.156.133.195",
"url": "https://www.httpbin.org/get"
}

这里了解即可。详细可参考官方文档

基础实例案例

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 requests
import logging # logging库用来输出信息
import re
from urllib.parse import urljoin
import json
from os import makedirs
from os.path import exists

RESULT_DIR = 'result' # 定义保存数据文件夹
exists(RESULT_DIR) or makedirs(RESULT_DIR)


# 定义日志输出级别和输出格式
# logging.basicConfig(level=logging.INFO,format='%(asctime)s-%(levelname)s: %(message)s')

BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10
detail_url=[]

# 列表页爬取,返回html
def scrape_page(url):
logging.info('scraping %s ...',url)
try:
response = requests.get(url)
if response.status_code == 200:
return response.text
else:
logging.error('get invalid status code %s while scraping %s',response.status_code,url)
except requests.RequestException:
logging.error('error occurred while scraping %s',url,exc_info=True)

# 获取带有页码的url
def scrape_idnex(page):
index_url = urljoin(BASE_URL,"/page/"+str(page))
return index_url

# 获取详情页URL
def parse_index(html):
pattern = re.compile('<a.*?href="(.*?)" class="name">')
items = re.findall(pattern,html)
if not items:
return []
else:
for item in items:
detail_url.append(urljoin(BASE_URL,item))
logging.info('get detail url %s',urljoin(BASE_URL,item))

# 详情页爬取
def scrape_detail(url):
return scrape_page(url)

# 详情页解析
def parse_detail(html):
cover_pattern = re.compile('id="detail".*?<a.*?>.*?<img.*?"".*?src="(.*?)".*?class="cover">.*?</a>', re.S)
name_pattern = re.compile('<h2.*?>(.*?)</h2>')
categories_pattern = re.compile('<button.*?category.*?>.*?<span>(.*?)</span>.*?</button>', re.S)
info_time_pattern = re.compile('<div.*?info">.*?<span.*?>(\d{4}-\d{2}-\d{2}) 上映</span>.*?</div>',re.S)
score_pattern = re.compile('<p.*?score.*?>(.*?)</p>',re.S)
drama_pattern = re.compile('<div.*?class="drama"><h3.*?>剧情简介</h3>.*?<p.*?>(.*?)</p></div>',re.S)
cover = re.search(cover_pattern,html).group(1).strip() if re.search(cover_pattern,html) else None
name = re.search(name_pattern,html).group(1).strip() if re.search(name_pattern,html) else None
categories = re.findall(categories_pattern,html) if re.findall(categories_pattern,html) else None
info_time = re.search(info_time_pattern,html).group(1) if re.search(info_time_pattern,html) else None
score = re.search(score_pattern,html).group(1).strip() if re.search(score_pattern,html) else None
drama = re.search(drama_pattern,html).group(1).strip() if re.search(drama_pattern,html) else None

return {
'cover':cover,
'name': name,
'categories': categories,
'info_time': info_time,
'score': score,
'drama':drama
}

# 保存数据
def save_data(data):
name = data.get('name')
data_path = f'{RESULT_DIR}/{name}.json'
json.dump(data,open(data_path,'w',encoding='utf-8'),ensure_ascii=False,indent=2)


def main():
for page in range(1,TOTAL_PAGE+1):
page_index=scrape_idnex(page)
html=scrape_page(page_index)
parse_index(html)
for items in detail_url:
print(items)
html_detail=scrape_page(items)
detail=parse_detail(html_detail)
save_data(detail)


if __name__ == '__main__':
main()

Http基本原理

URI和URL

  URI全称:Uniform Resource Identifier,即统一资源标志符;URL的全称为Uniform Resource Locator,即统一资源定位符。举例:https://github.com/favicon.ico 既是一个URI,也是一个URL。即有favicon.ico这样一个图标资源,我们用URI/URL指定了访问它的唯一方式,其中包括https、访问路径(根目录)和资源名称。通过这样一个链接,就可以从互联网中找到某个资源,这个链接就是URI/URL。
  URL是URI的子集。除了URL,URI还包括一个子类,是URN(Uniform Resource Name),即统一资源名称。URN只为资源命名,而不指定如何定位资源。
  URL基本格式:
scheme://[username:password@]hostname[:port][/path][;parameters][?query][#fragment] 其中,中括号包括的内容代表非必要部分,如 https://www.baidu.com 这个URL,这里只包含了scheme、hostname两部分,没有part、parameters、query、fragment。

  • scheme:协议。常见协议有http、https、ftp等,另外scheme也被称作protocol,都代表协议的意思。

  • username、passward: 用户名和密码。某些情况下URL需要提供用户名和密码才能访问,这时候把用户名和密码放在host前面。如https://ssr3.scrape.center 这个URL需要用户名和密码才能访问,直接写为https://admin:admin@ssr3.scrape.center 就能直接访问。

  • hostname:主机地址。可以是域名或IP地址,比如https://www.baidu.com这个URL中的hostname就是www.baidu.com,这就是百度的二级域名。比如https://8.8.8.8,这个URL中的hostname,它是一个IP地址。

  • port:端口。这是服务器设定的服务端口,比如https://8.8.8.8:12345,这个URL中的端口就是12345。但是有些URL中没有端口信息,使用了默认端口。http协议的默认端口是80,https协议的默认端口是443。所以 https://www.baidu.com 其实相当于https://www.baidu.com:443, http://www.baidu.com 相当于http://www.baidu.com:80

  • path:路径。指的是网络资源在服务器中的指定地址,比如 https://github.com/favicon.ico中的path就是 favicon.ico ,指的是访问Github根目录下的favicon.ico。

  • paremeters:参数。用来指定访问某个资源时的附加信息,比如https://8.8.8.8:12345/hello;user中的user就是parameters。但是parameters现在用的少,很多人把参数后面的query部分称为参数,甚至把parameters和query混用。严格来说,parameters是;后面的部分。

  • query: 查询。用于查询某类资源,如果有多个查询用&隔开。query很常见,比如https://www.baidu.com/s?wd=nba&ie=utf-8 ,其中query部分就是wd=nba&ie=utf-8,这里指定了wd是nba,ie是utf-8。我们平时见到的参数、GET请求参数、parameters、params等称呼多情况指代的也是query。

  • fragment: 片段。它是对资源描述的部分补充,可以理解为资源内部的书签。有两个主要应用,一个是单页面路由,如前端框架Vue、Reat都可以借助它来做到路由管理;另一个是坐HTML锚点,用它控制一个页面打开时自动下滑滚动到某个特定位置。

HTTP和HTTPS

  HTTP:超文本传输协议,其作用是把超文本数据从网络传输到本地浏览器,能够保证高效而准确地传输超文本文档。

  HTTPS:是以安全为目标的HTTP通道,简单讲就是HTTP安全版,即在HTTP下加入SSL层,简称HTTPS。
  HTTPS的安全基础是SSL,因此通过该协议传输的内容都是经过SSL加密的,SSL的主要作用有:

  • 建立安全通道,保证数据传输的安全性。
  • 确认网站的真实性。凡是使用了HTTPS协议的网站,都可以通过单击浏览器地址栏的锁头标志来查看网站认证之后的真实信息,此外还可以通过VA机构颁发的安全签章来查询。

  HTTP和HTTPS协议都属于计算机网络中的应用层协议,其下层是基于TCP协议实现的,TCP协议属于计算机网络中的传输层协议,包括建立连接时的三次握手和断开时四次挥手等过程。

HTTP请求过程

  在浏览器地址栏中输入一个URL,按下回车即可看到对应网页内容。这个过程是浏览器先向网站所在的服务器发送一个请求,网站服务器接收到请求后对其进行处理和解析,然后返回对应的相应,接着传回浏览器。由于响应里包含页面源代码内容,浏览器再对其进行解析,将网页呈现出来。
  使用Chrome浏览器开发者模式下的Network监听组件来演示。Network监听组件可以在访问当前请求的网页时,显示产生的所有网络请求和响应。
  打开浏览器,访问百度,单击鼠标右键选择“检查”菜单或者直接快捷键F12,打开浏览器的开发者工具。
  切换到Network面板,重新刷新页面,就可以看到在Network面板下出现很多条目,每一个条目代表依次发送请求和接收响应的过程。

  观察第一个网络请求www.baidu.com,各列含义:

  • Name:请求的名称。一般用URL的最后一部分内容作为名称。
  • Status: 响应状态码。这里显示200,代表响应是正常的。通过状态码判断发送请求之后是否得到正常响应。
  • Type: 请求的文档类型。document代表这次请求的是一个HTML文档,内容是一些HTML代码。
  • Initiator: 请求源。标记请求是由哪个对象或进程发起的。
  • Size: 从服务器下载的文件或请求的资源的大小。如果资源是从缓存中取得到的,则会显示memory cache(内存缓存)/disk cache(磁盘缓存)。
  • Time: 从发起请求到获取响应所花的总时间。
  • Waterfall: 网络请求的可视化瀑布流。
      单击www.baidu.com看详细信息

      首先是General部分,其中RequestURL为请求的URL,Request Method为请求的方法,Status Code是响应状态码,Remote Address是远程服务器的地址和端口,Referrer Policy为Referrer判别策略。
      往下有Response Headers和Request Headers,分别代表响应头和请求头。请求头包含许多请求信息,如浏览器标识、Cookie、Host等信息,服务器根据请求头的信息判断请求是否合法,做出响应。响应头是响应的一部分,其中包含服务器的类型、文档类型、日期等信息,浏览器在接收到响应后,对其进行解析,呈现网页内容。

请求

  请求,英文为Request,由客户端发往服务器,分为四部分:请求方法、请求网址、请求头、请求体。

  • 请求方法
    标识请求客户端请求服务端的方式,常见的有两种:GET和POST。
    在浏览器中直接输入URL并回车,便发起了GET请求,请求的参数会直接包含到URL里,例如我们搜索Python,就是一个GET请求,链接为https://www.baidu.com/s?wd=Python URL里包含了请求的query信息,这里的wd标识要搜寻的关键字。
    POST请求大多在提交表单时发起,如登录表单,输入用户名和密码后,单击“登录”,通常发起的就是POST请求,其数据通常以表单形式传输,而不会体现在URL中。
    Get请求和POST请求的区别:
    Get请求中的参数包含在URL里面,数据可以在URL中看到;而POST请求的URL不会包含这些数据,数据都是用过表单形式传输的,会包含在请求体中。
    登录时一般要提交用户名和密码,其中密码是敏感信息,使用GET方式请求,密码会暴露在URL里面,造成密码泄露,这时最好使用POST方式发送。上传文件时,由于文件内容比较大,也会选用POST方式。

请求方法

方法 描述
GET 请求页面,并返回页面内容
HEAD 类似GET,不过返回的响应中没有具体内容。用于获取报头
POST 大多用于提交表单或上传文件,数据包含在请求体中
PUT 用客户端传向服务器的数据取代指定文档中的内容
DELETE 请求服务器删除指定的页面
CONNECT 把服务器当跳板,让服务器代替客户端访问其他网页
OPTIONS 允许客户端查看服务器的性能
TRACE 回显服务器收到的请求。主要用于测试或诊断
  • 请求的网址
    URL,它可以唯一确定客户端想要请求的资源。
  • 请求头
    用来说明服务器要使用的附加信息,重要的有:Cookie、Referer、User-Agent等。
    Accept: 请求报头域,用于指定客户端可接受哪些类型的信息。
    Accept-Language: 用于指定客户都拿可接受的语言类型。
    Accept-Encoding: 用于指定客户端可接受的内容编码。
    Host: 指定请求资源的主机IP和端口号,其内容为请求URL的原始服务器或网关位置。
    Cookie: 网站为了辨别用户,进行会话跟踪而存储在用户本地的数据。主要功能是维持当前访问会话。
    Referer: 标识请求是从哪个页面发过来的,服务器可以拿到这一信息并做相应的处理,如源统计、防盗链处理。
    User-Agent: 简称UA,特殊字符串头,可以使服务器识别客户端使用的操作系统及版本、浏览器及版本信息。
    Content-Type: 用它来表示具体请求中的媒体类型信息。
  • 请求体
    一般承载的内容是POST请求中的表单数据,对于GET请求,请求体为空。
    如登录Github时捕获的响应

    登录前,要填写用户名和密码信息,登陆时将这些内容以表单数据的形式提交给服务器,要注意Content-Type为application/x-www-form-urlencoded,只有这样设置Content-Type,内容才会以表单数据形式提交。也可以将Content-Type设置为application/json来提交json数据,或者设置为multipart/form-data来上传文件。

    Content-Type和POST提交数据方式的关系
COntent-Type POST提交数据的方式
application/x-www-form-urlencoded 表单数据
multipart/form-data 表单文件上传
application/json 序列化json数据
text/xml XML数据

构造POST请求需要使用正确的Content-Type,并了解设置各种请求库的各个参数时使用的是哪种Content-Type。

响应

  • 响应状态码
    由服务器返回给客户端,可以分为三部分:响应状态码、响应头、和响应体。
    响应状态码,表示服务器的响应状态,如200代表服务器正常相应、404代表页面未找到、500嗲表服务器内部发生错误。
  • 响应头
    包含服务器对请求的应答信息。
    Date: 用于标识响应产生的时间。
    Last-Modified: 用于指定响应资源的最后修改时间。
    Content-Encoding: 用于指定响应内容的编码。
    Server: 包含服务器的信息,例如名称、版本号。
    Content-Type: 文档类型,指定返回的数据是什么类型
    Set-Cookie: 设置Cookie。响应头中的Set-Cookie用于告诉浏览器需要将此内容放在Cookie中,下次请求时将Cookie携带上。
    Expires:用于指定响应的过期时间,可以让代理服务器或浏览器将加载的 内容更新到缓存中。再次访问相同内容时就可以直接从缓存中加载,降低服务器负担,缩短加载时间。
  • 响应体
    响应的正文数据都存在于响应体中。爬虫请求网页时,要解析的就是响应体数据。

爬虫基本原理

爬虫就是获取网页并提取和保存信息的自动化程序。

  • 获取网页
    获取网页源代码,源代码里包含网页的部分有用信息,获取源代码就能从中提取有用信息。
    我们要做的就是构造一个请求并发送给服务器,然后接收到响应并对其进行解析。
    Python提供了许多库,如urllib、requests等,我们可以用这些库来完成HTTP请求操作。除此之外,请求和响应都可以用类库提供的数据结构来表示,得到相应后只需要解析数据结构中的body部分,即可看到网页的源代码。
  • 提取信息
    获取网页源代码后,接下来就是分析源代码,从中提取想要的数据。最常用的是正则表达式,构造正则表达式的过程比较负责且易出错。
    由于网页结构具有一定的规则,还有一些库是根据网页节点属性、CSS选择器或者XPath来提取网页信息的,如:Beautiful Soup、pyquery、lxml等,使用这些库,可以高效地从源代码中提取网页信息。
  • 保存数据
    保存数据的形式多种多样,可以简单保存为TXT文本或JSON文本,也可以保存到数据库,如MySQL或MongoDB,还有远程服务器。

下载安装

官网下载安装即可

配置

使用命令git -v查看git版本
初次使用git需要设置用户名以及邮箱

1
2
git config --global user.name "用户名"
git config --global user.email "邮箱"

然后使用git config -l查看配置信息

创建仓库

git init

找一个合适的地方创建一个空目空,然后在该目录下打开终端,输入git init命令,会创建出一个.git目录,这个目录存放了git仓库的所有数据。

git clone

这个命令可以从github或者gitee这种远程服务器上克隆一个已经存在的仓库。
使用格式就是git clone 仓库地址

工作区域和工作状态

三个工作区域

工作区

.git所在的目录

暂存区

.git/index

本地仓库

.git/objext

四种工作状态

  • 未跟踪(Untrack)
    指我们新建的还没有被git管理的文件
  • 未修改(Unmodified)
    指已经被git管理但是文件内容还没有发生变化
  • 已修改(Modified)
    指已经修改过但是还没有添加到暂存区里的文件
  • 已暂存(Staged)
    指修改后并且已经添加到了暂存区里的文件

添加和提交文件

涉及到的命令

1
2
3
git status //查看仓库的状态
git add //添加到暂存区
git commit //提交

可以先执行一下git status命令,会提示没有提交的文件,然后我们在仓库目录下创建一个txt文件,再执行git status,提示这个文件未被跟踪,我们使用git add xxx.txt,该文件已经被添加到了暂存区,接下来使用git commit命令提交文件到仓库中,使用这个命令时要用-m参数指定提交信息,如果不指定,那么提交时会进入一个交互式界面,默认用vim来编辑提交信息,然后提交完成。这里提交的文件是暂存区里的文件。

如何编写程序界面

通过最基本的方式去实现界面,编写XML代码,当掌握了使用XML来编写界面的方法之后,不管是进行高复杂度界面实现还是分析和修改当前现有的界面,都将手到擒来。

常见控件使用

新建一个UIWidgetTest项目,允许Android Studio自动创建活动,活动名和布局都使用默认值。

TextView

  TextView可以说是Android中最简单的空间了,它主要用于在界面上显示一段文本信息。
在res目录下创建一个layout目录,然后在其中创建一个activity_main.xml文件,往里面放置一个TextView。

1
2
3
4
5
6
7
8
9
10
11
12
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView" />

</LinearLayout>

  外层的LinearLayout(根元素)先忽略不看,在TextView中使用android:id给当前控件定义了一个唯一标识符。然后使用android:layout_width和android:layout_height指定控件的宽度和高度。Android中的所有控件的都有这两个属性,可选值有三种:match_parent、fill_parent和wrap_content,其中match_parent和fill_parent的功能相同。fill_parent是Android早期版本使用的属性,现在官方推荐使用match_parent,它的作用是让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。wrap_content表示让当前控件的大小能够刚好包含里面的内容,也就是由控件内容决定当前控件的大小。
  指定文字内容可以正常显示,但是TextView的宽度和屏幕是一样宽的。这是由于TextView中的文字默认是居左上角对其的,TextView的宽度充满了整个屏幕,由于文字内容不够长,所以效果不明显。我们可以修改文字对齐方式:

1
2
3
4
5
6
7
8
9
10
11
12
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="TextView" />
</LinearLayout>

我们还可以对TextVIew中文字的大小和颜色进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="24sp" //指定大小
android:textColor="#00ff00" //指定颜色
android:text="TextView" />
</LinearLayout>

Button

Button是程序用于和用户进行交互的一个重要控件,它的可配置的属性和TextView差不多,在activity_main.xml中加入Button:

1
2
3
4
5
6
7
8
9
10
11
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>

  而这里,布局文件里的文字是“Button”,但是最终的显示结果确实“BUTTON”。这是由于系统对Button中的所有英文字母自动进行了大写转换,我们可以禁用这一默认特性:

1
2
3
4
5
6
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button"
android:textAllCaps="false"/>

然后在MainActivity中为Button的点击事件注册一个监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//添加逻辑
}
});
}
}

当每次点击按钮时,就会执行监听器中的onClick()方法,我们只需要在方法中加入要处理的逻辑即可。如果不喜欢使用匿名类的方式来注册监听器,也可以使用实现接口的方式来注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.button:
//添加逻辑
break;
default:
break;
}
}
}

上面代码在IDE中提示由于switch语句标签太少,应该用if替换,而且在Android Gradle8.0版本中,资源ID默认非final,所有应该避免在switch case使用。修改后的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(this);
}

@Override
public void onClick(View v) {
if (v.getId()==R.id.button){
// 添加逻辑
}else{
// 添加逻辑
}
}
}

两种方式都可以实现对按钮点击事件的监听

EditText

  EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理。
  修改activity_main.xml中的代码

1
2
3
4
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/

这就是使用XML来编写界面,完全不用借助可视化工具来实现。运行程序,即可看到EditText的显示,我们可以在里面输入内容。
输入框提示文字,修改activity_main.xml:

1
2
3
4
5
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"/>

  这里android:hint属性指定了一段提示性文本,当我们输入任何内容时,这段文本就会自动消失。
  随内容不断增多,EditText会不断拉长,由于EditText的高度指定的是wrap_content,所以它总能包含住里面的内容,但是当输入的内容过多时,界面就会变的难看,我们可以使用android:maxLines属性来解决这个问题,修改activity_main.xml:

1
2
3
4
5
6
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="2"/>

  这里指定了ExitText的最大行数为两行,当输入的内容超过两行时,文本就会向上滚动,而EditText不会继续拉伸。
还可以结合使用EditText与Button实现一些功能,如通过点击按钮来获取EditText中输入的内容。修改MainActivity中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private EditText editText;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button1 = (Button) findViewById(R.id.button);
button1.setOnClickListener(this);
editText = (EditText) findViewById(R.id.edit_text);

}

@Override
public void onClick(View v) {
if (v.getId()==R.id.button){
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this,inputText,Toast.LENGTH_SHORT).show();
}else{
// 添加逻辑
}
}
}

  通过findViewById()方法得到EditText的实例,然后在按钮的点击事件里调用EditText的getText()方法获取到输入的内容,再调用toString()方法转为字符串,最后用Toast将输入的内容显示出来。

ImageView

  ImageView是用于在界面上展示图片的一个控件,它可以让程序更加丰富多彩。我们需要提前准备一些图片,图片通常放在以”drawable”开头的目录下。我们项目中已经有一个drawable目录了,但是还没有指定具体的分辨率,所以一般不使用它来放置图片。我们在res目录下新建一个drawable-xhdpi目录,放置两张图片img_1.png和img_2.png。修改activity_xml:

1
2
3
4
5
6
7
8
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img_1"/>
```
&emsp;&emsp;这里使用android:src属性给ImageView指定了一张图片。由于图片的宽和高都是未知,所以将ImageView的宽和高都设定为wrap_content。
&emsp;&emsp;我们还可以在程序中通过代码动态地更改ImageView中的图片,然后修改MainActivity的代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private EditText editText;
private ImageView imageView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button button1 = (Button) findViewById(R.id.button);
    button1.setOnClickListener(this);
    editText = (EditText) findViewById(R.id.edit_text);
    imageView = (ImageView) findViewById(R.id.image_view);

}

@Override
public void onClick(View v) {
    if (v.getId()==R.id.button){
        imageView.setImageResource(R.drawable.img_2);
    }else{
        // 添加逻辑
    }
}

}

1
2
3
&emsp;&emsp;在按钮点击事件里,通过调用ImageView的setImageResource()方法将显示的图片改成了img_2,运行程序,点击按钮就可以看到ImageView中显示的图片改变了。  
## ProgressBar
&emsp;&emsp;ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。用法很简单:

1
2
&emsp;&emsp;重新运行程序,屏幕中会有一个圆形进度条在旋转。为了能让进度条在数据加载完成后消失,我们要用到一个属性:Android控件的可见属性。所有的Android控件都具有这个属性,可以通过android:visibility进行指定,可选值有3种:visible、invisible和gone。visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。invisible表示控件不可见,但它还占据着原来位置的大小,可以理解为是控件变成透明状态了。gone表示控件不仅不可见,而且不再占据任何屏幕空间。我们可以通过代码来设置控件的可见性,使用的是setVisibility()方法,可以传入View.VISIBLE、View.INVISIBLE和View.GONE这三种值。  
&emsp;&emsp;我们通过点击按钮让进度条消失:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private EditText editText;
private ImageView imageView;

private ProgressBar progressBar;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button button1 = (Button) findViewById(R.id.button);
    button1.setOnClickListener(this);
    editText = (EditText) findViewById(R.id.edit_text);
    imageView = (ImageView) findViewById(R.id.image_view);
    progressBar = (ProgressBar) findViewById(R.id.progress_bar);

}

@Override
public void onClick(View v) {
    if (v.getId()==R.id.button){
        if (progressBar.getVisibility()==View.GONE){
            progressBar.setVisibility(View.VISIBLE);
        }else {
            progressBar.setVisibility(View.GONE);
        }
    }else{
        // 添加逻辑
    }
}

}

1
2
&emsp;&emsp;在按钮点击事件中,我们通过getVisibility()方法来判断ProgressBar是否可见,如果可见就隐藏掉,如果不可见就将ProgressBar显示出来。重新运行程序,不断点击按钮,就会看到进度条会在显示和隐藏之间来回切换。  
&emsp;&emsp;还可以给ProgressBar指定不同的样式,刚刚看到的是圆形进度条,通过style属性可以将它设置成水平进度条,修改activity_main.xml中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
&emsp;&emsp;指定为水平进度条之后,通过android:max属性给进度条设置一个最大值,然后在代码中动态地更改进度条的进度。修改MainActivity中的代码:
```java
@Override
public void onClick(View v) {
if (v.getId()==R.id.button){
int progress=progressBar.getProgress();
progress=progress+10;
progressBar.setProgress(progress);
}else{
// 添加逻辑
}
}

  每点击一次按钮,我们就获取当前进度,然后在现有的进度上加10作为更新后的进度。

AlertDialog

  AlertDialog可以在当前界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽掉其他控件的交互功能,因此,AlertDialog一般用于提示一些非常重要的内容或者警告信息。
  比如为例防止用户删除重要内容,在删除前弹出一个确认对话框。修改MainActivity中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void onClick(View v) {
if (v.getId()==R.id.button){
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("This is Dialog");
dialog.setMessage("Something important");
dialog.setCancelable(false);
dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
});
dialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
});
dialog.show();
}else{
// 添加逻辑
}
}

  首先通过AlertDialog.Builder创建一个AlertDialog的实例,然后可以为这个对话框设置标题、内容、可否取消等特性,接下来调用setPositiveButton()方法为对话框设置确定按钮的点击事件,调用setNegativeButton()方法设置取消按钮的点击事件,最后调用show()方法将对话框显示出来。

ProgressDialog

ProgressDialog和AlertDialog有点类似,都可以在界面上弹出一个对话框,都能够屏蔽掉其  他控件的交互能力。不同的是,ProgressDialog会在对话框中显示一个进度条,一般用于表示当前操作比较耗时,让用户耐心等待。用法也和AlertDialog相似,修改MainActivity代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onClick(View v) {
if (v.getId()==R.id.button){
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);
progressDialog.show();
}else{
// 添加逻辑
}
}

  这里也是先构建ProgressDialog对象,然后设置标题,内容,可否取消等属性,最后通过show()方法将ProgressDialog显示出来。
  注意,如果在setCancelable()中传入了false,表示ProgressDialog是不能通过Back键取消掉的,这时就一定要在代码中做好控制,当数据加载完成后必须调用ProgressDialog的dismiss()方法来关闭对话框,否则ProgressDialog将会一直存在。

四种布局

新建一个UILayoutTest项目,让Android Studio自动帮我们创建活动,活动名和布局名都是用默认值。

线性布局

  LinearLayout又称为线性布局,这个布局会将它所包含的控件在线性方向上依次排列。线性排列有两种方向,垂直方向排列和水平方向排列。而上面的案例中都是通过android:orientation属性指定了排列方向是vertical,如果指定的是horizontal,控件就会在水平方向上排列了。修改activity_main.xml中代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button2" />

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button3" />
</LinearLayout>

然后我们修改LinearLayout的排列方向:

1
2
3
4
5
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" //排列方向
android:layout_width="match_parent"
android:layout_height="match_parent">
...

  如果我们不指定android:orientation属性的值,默认排列方向就是horizontal。
  注意:如果LinearLayout的排列方向是horizontal,内部的控件就绝不能将宽度指定为match_parent,因为这样的话,单独一个控件就会将整个水平方向占满,其他控件就没有可放置的位置了。同样,如果LinearLayout的排列方向是vetical,内部控件就不能将高度指定为match_parent。
  首先看android:layout_gravity属性,它和android:gravity属性相似,android:gravity用于指定文字在控件中的对齐方式,而android:layout_gravity用于指定控件在布局中的对齐方式。需要注意的是,当LinearLayout的排列方式是horizontal时,只有垂直方向的对齐方式才会生效,因为水平方向的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样,当LinearLayout的排列方向是vertical时,只有水平方向的对齐方式才会生效。修改activity_main.xml中的代码:

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
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:text="Button1" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Button2" />

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="Button3" />
</LinearLayout>

由于目前LinearLayout的排列方向是horizontal,因此我们只能指定垂直方向上的排列方向。

  LineatLayout另一个重要属性——android:layout_weight这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用。比如我们编写一个消息发送界面,需要文本编辑框和一个发送按钮,修改activity_main.xml中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:ignore="Suspicious0dp"
android:layout_weight="3"
android:hint="Type somethind"/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="Send"/>

  这里将EditText和Button的宽度都指定成了0dp,但是我们使用了android:layout_weight属性,此时控件的宽度就不由android:layout_width来决定,这里指定为0dp是一种规范写法。dp是Android中指定控件大小、间距等属性的单位。
  在EditText和Button里都将android:layout_weight属性的值都指定为1,表示EditText和Button将在水平方向平分宽度。其原理就是系统会先把LinearLayout下所有的控件指定的Layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值。因此如果想让EditText占据屏幕宽度的3/5,Button占据屏幕宽度的2/5,只需将EditText的layout_weight改成3,Button的layout_weight改成2就可以了。

  还可以通过指定部分控件的layout_weight值来实现更好的效果。修改activity_main.xml代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:ignore="Suspicious0dp"
android:layout_weight="1"
android:hint="Type somethind"/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>

  这里仅指定了EditText的android:layout_weight属性,并将Button的宽度改回了wrap_content。表示Button的宽度仍然按照wrap_content来计算,而EditText则会沾满屏幕所有剩余空间。这种方式在屏幕适配方面效果比较好。

相对布局

RelativeLayout又称为相对布局,也是一种常用布局。和LinearLayout的排列规则不同,RelativeLayout显得更加随意,它可以通过相对定位的方式让控件出现在布局的任何位置。正是如此,RelativeLayout中的属性很多,但是这些属性都是有规律可循的,修改activity_main.xml中的代码:

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="Button 1"/>

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button 2"/>

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3"/>

<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="Button 4"/>

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Button 5"/>
</RelativeLayout>

效果:

  上面例子中每个控件都是相对于父布局进行定位的,我们可以让控件相对于控件进行定位,修改activity_main.xml中的代码:

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">


<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3"/>

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 1"/>

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 2"/>



<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 4"/>

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 5"/>
</RelativeLayout>

  要注意的是,当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。

  RelativeLayout中还有另外一组相对于控件进行定位的属性,android:layout_alignLeft表示让一个控件的左边缘和另一个控件的左边缘对齐,android:layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐。还有android:layout_alignTop和android:layout_alignBotton道理相同。

帧布局

  FrameLayout又称作帧布局,它相比前两种布局简单很多,应用场景也少很多。这种布局没有方便的定位方式,所有的控件都会默认在布局左上角。修改activity_main.xml中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is TextView"/>

<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
</FrameLayout>

  FrameLayout中只是放置了一个TextView和一个ImageView,这里我们直接使用了@mipmap来访问ic_launcher这张图。

  可以看到文字和图片都是位于布局左上角。由于ImageView是在TextView之后添加的,因此图片压在了文字的上面。
  除了这种默认效果,我们还可以使用layout_gravity属性来指定控件在布局中的对齐方式,这和LinearLayout中的用法是相似的。修改activity_main.xml中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="This is TextView"/>

<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:src="@mipmap/ic_launcher"/>
</FrameLayout>

指定TextView居左对齐,ImageView居右对齐。

由于FrameLayout的定位方式欠缺,它的应用场景也比较少。

百分比布局

  前面的布局中只有LinearLayout支持使用layout_weight属性来实现按比例指定大小的功能,其他两种都不支持。如果想要用RelativeLayout来实现让两个按钮评分布局宽度的效果,则比较困难。
  为此,Android引入了新的布局方式来解决这个问题——百分比布局。这种布局中,不再使用wrap_content、match_parent等方式指定控件的大小了,而是允许直接直指定控件所占的百分比,这样可以轻松实现平分布局甚至任意比例分割布局的效果。
  百分比布局提供了PercentFrameLayout和PercentRelativeLayout这两个全新的布局。Android将百分比布局定义在了suport库中,我们只需要在项目的build.gradle中添加百分比布局库的依赖,就能保证百分比布局在Android所有系统版本上的兼容性了。
  打开app/build.gradle文件,在dependencies闭包中添加如下内容:

1
2
3
dependencies {
implementation ("androidx.percentlayout:percentlayout:1.0.0")
}

每当修改了任何gradle文件时,Android Studio都会弹出提示,需要我们再次同步才能使项目正常工作,点击Sync Now即可。修改activity_main.xml中的代码:

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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">

<!-- Button 1 -->
<Button
android:id="@+id/button1"
android:text="Button1"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button2"
app:layout_constraintBottom_toTopOf="@id/button3" />

<!-- Button 2 -->
<Button
android:id="@+id/button2"
android:text="Button2"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button1"
app:layout_constraintBottom_toTopOf="@id/button4" />

<!-- Button 3 -->
<Button
android:id="@+id/button3"
android:text="Button3"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button4"
app:layout_constraintTop_toBottomOf="@id/button1" />

<!-- Button 4 -->
<Button
android:id="@+id/button4"
android:text="Button4"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/button3"
app:layout_constraintTop_toBottomOf="@id/button2" />

</androidx.constraintlayout.widget.ConstraintLayout>

```
![](android-UI/8.png)
# 创建自定义控件
控件和布局的继承结构
![](android-UI/9.png)
&emsp;&emsp;所用的控件都是直接或间接继承自View的,所用的布局都是直接或间接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能影响这块区域的各种事件,我们使用的各种控件其实就是在View的基础之上添加了各自特有的功能。而ViewGroup则是一种特殊的View,它包含了很多子View和子ViewGroup,是一个放置控件和布局的容器。
&emsp;&emsp;创建一个UICustomViews项目。
## 引入布局
&emsp;&emsp;在iPhone中,几乎每个应用的界面顶部都会有一个标题栏,标题栏上会有一到两个按钮可用于返回或其他操作。这里我们创建自定义标题栏。
&emsp;&emsp;只需要加入两个Button和一个TextView,然后在布局中摆放好就可以了。
为了减少代码重复,可以使用引入布局的方式来解决这个问题,新建一个布局title.xml:

<Button
    android:id="@+id/title_back"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="top"
    android:layout_margin="5dp"
    android:text="Back"
    android:textColor="#fff" />

<TextView
    android:id="@+id/title_text"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_gravity="top"
    android:layout_weight="1"
    android:gravity="center"
    android:text="Title Text"
    android:textColor="#000"
    android:textSize="24sp"/>

<Button
    android:id="@+id/title_edit"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="top"
    android:layout_margin="5dp"
    android:text="Edit"
    android:textColor="#fff"
    />
1
2
3
4
5
6
7
8
9
&emsp;&emsp;在这里使用了android:layout_margin属性,它可以指定控件在上下左右方向上偏移的距离,也可以用android:layout_marginLeft或android:layout_marginTop等属性单独指定控件在某个方向上偏移的距离。  
&emsp;&emsp;标题栏布局编写完成,剩下就是在程序中使用标题栏,修改activity_main.xml中代码:
```xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/title"/>

</LinearLayout>
  隐藏系统自带标题
1
2
3
4
5
6
7
8
9
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActionBar actionbar= getSupportActionBar();
if (actionbar!= null){
actionbar.hide();
}
}
  通过调用getSupportActionBar()方法来获得ActionBar的实例,然后调用ActionBar的hide()方法将标题隐藏起来。 ## 创建自定义控件   新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件:
1
2
3
4
5
6
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
}
}
  我们重写了LinearLayout中带有两个参数的构造函数,在布局中引入TitleLayout控件就会调用这个构造函数。然后在构造函数中需要对标题栏布局进行动态加载,借助LayoutInflater来实现。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法动态加载一个布局文件,inflate()方法接收两个参数,第一个参数是要加载布局文件的id这里我们传入R.id.title,第二个参数给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,传入this。   在布局文件中添加这个自定义控件,修改activity_main.xml中的代码:
1
2
3
4
5
6
7
8
9
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.uicustomviews.TitleLayout
tools:ignore="MissingClass"
android:layout_height="match_parent"
android:layout_width="wrap_content"/>
</LinearLayout>
  添加自定义控件和添加普通控件的方式基本一样,只不过在添加自定义控件时,我们要指明控件的完整类名。   为标题栏的按钮注册点击事件,修改TitleLayout中的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
Button titleedit = (Button) findViewById(R.id.title_edit);
Button titleback = (Button) findViewById(R.id.title_back);
titleback.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
((Activity)getContext()).finish();
}
});
titleedit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(),"You cliclk Edit button",Toast.LENGTH_SHORT).show();
}
});
}
}
  先通过findViewById()方法得到按钮实例然后调用setOnClickListener()方法给两个按钮注册点击事件,当点击返回按钮时,销毁当前的活动,当点击编辑按钮时弹出一段文本。   这样每当在一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件就已经自动实现好了。 # ListView   由于手机屏幕空间都比较有限,能够一次性在屏幕上显示的内容不多,当我们的程序中有大量的数据需要展示时,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据则会滚动出屏幕。 ## ListView的简单用法   先新建一个ListViewTest项目,让Android Studio自动帮我们创建好活动。修改activity_main.xml中的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
修改MainActivity中的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {
private String[] data = {"Apple","Banana","Orange","Watermelon",
"Pear","Grape","Pineapple","Strawberry","Cherry","Mango",
"Apple","Banana","Orange","Watermelon",
"Pear","Grape","Pineapple","Strawberry","Cherry","Mango"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1,data
);
ListView listView= (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
  我们用了data数组来测试,里面包含了很多水果的名称。不过数组中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android中提供了很多适配器的实现类,其中ArrayAdapter较好用。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。ArrayAdapter有多个构造函数的重载,根据实际情况选择最合适的一种。ArrayAdapter的构造函数中依次传入当前上下文、ListView子项布局的id,以及要适配的数据。我们使用了android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,用于简单地显示一段文本。   最后调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,ListView和数据之间的关联就建立完成了。 ## 定制ListView的界面   首先准备一组图片,分别对应上面提供的每一种水果,让这些水果名称的旁边都有一个图样。(资源文件名只能包含小写字母,不能包含大写字母)。   定义实体类,作为ListView适配器的适配类型。新建类Fruit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Fruit {
private String name;
private int imageId;
public Fruit(String name,int imageId){
this.name=name;
this.imageId=imageId;
}
public String getName(){
return name;
}
public int getImageId(){
return imageId;
}
}
  Fruit类中只有两个字段,name表示水果的名字,imageId表示水果对应图片的资源id。   为ListView的子项指定我们自定义的布局,在layout目录下新建fruit_item.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="10dp"/>
</LinearLayout>
  这个布局里,定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于显示水果名称,让TextView在垂直方向上居中显示。   然后创建自定义适配器,这个适配器继承自ArrayAdapter,并将泛型指定为Fruit类。新建类FruitAdapter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;

public FruitAdapter(@NonNull Context context, int textViewResourceId, @NonNull List<Fruit> objects) {
super(context,textViewResourceId, objects);
resourceId=textViewResourceId;
}

@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit = getItem(position); //获取当前项的Fruit实例
View view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
}
  FruitAdapter重写了父类的一组构造函数,用于将上下文、ListView子项布局的id和数据都传递过来。另外又重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。在getView()方法中,首先通过getItem()方法得到当前项的Fruit实例,然后使用LayoutInflater来为这个子项加载我们传入的布局。   这里LayoutInflater的inflate()方法,接收三个参数,前两个参数已经知道了,第三个参数指定成了false,表示只让我们在父布局中声明的layout属性生效,但不为这个View添加父布局,因为一旦View有了父布局之后,它就不能再添加到ListView中了。   往下,接下来调用View的findVIewById()方法分别获取到ImageView和TextView的实例,并分别调用他们的setImageResource()和setText()方法来设置显示的图片和文字,最后将布局返回,这样自定义的适配器就完成了。修改MainActivity中的代码:
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
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initFruits();
FruitAdapter adapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
private void initFruits(){
for ( int i = 0;i < 2;i++){
Fruit apple = new Fruit("Apple" , R.drawable.Apple);
fruitList.add(apple);
Fruit banana = new Fruit("banana",R.drawable.Banana);
fruitList.add(banana);
Fruit orange = new Fruit("orange",R.drawable.Orange);
fruitList.add(orange);
Fruit cherry = new Fruit("cherry",R.drawable.Cherry);
fruitList.add(cherry);
Fruit grape = new Fruit("grape",R.drawable.Grape);
fruitList.add(grape);
Fruit mongo = new Fruit("mongo",R.drawable.Mango);
fruitList.add(mongo);
Fruit peach = new Fruit("peach",R.drawable.Peach);
fruitList.add(peach);
Fruit pear = new Fruit("pear",R.drawable.Pear);
fruitList.add(pear);
Fruit strawberry = new Fruit("strawberry",R.drawable.Strawberry);
fruitList.add(strawberry);
Fruit watermelon= new Fruit("watermelon",R.drawable.Watermelon);
fruitList.add(watermelon);
}
}
}
这里添加了一个initFruits()方法,用于初始化所有的水果数据。在Fruit类的构造函数中将水果的名字和对应的id注入,然后把创建好的对象添加到水果列表中。然后用for循环将所有的水果数据添加了两遍,如果只添加一遍,数据还不足以充满整个屏幕。接着在onCreat()方法创建了FruitAdapter对象,将FruitAdapter作为适配器传递给ListView,定制ListView界面完成。 ## ListView的点击事件   修改MainActivity的点击事件:
1
2
3
4
5
6
7
8
    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
}
  使用setOnItemClickListener()方法为ListView注册了一个监听器,当用户点击了ListView中任何一个子项时,就会回调onItemClick()方法。在这个方法中通过position参数来判断用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果名字显示出来。 # 滚动控件RecyclerView   可以说是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。我们先新建一个RecycleView项目。 ## RecycleView基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  在布局中加入RecycleView也很简单,先为RecycleView指定id,然后将宽度和高度都设置为match_parent,这样Recycle就占满了整个布局的空间。需注意,RecycleView并不是内置在系统SDK中的,所以需要把完整的包路径写出来。   接下来想要使用RecycleView来实现和ListView相同的效果,就要准备一份同样的水果图片。我们直接把从ListViewTest项目中的图片复制过来就可以了,另外也把Fruit类和fruit_item.xml也复制过来。   接下来为RecycleView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecycleView.Adapter,并将泛型指定为FruitAdapter.ViewHolder。ViewHolder是我们在FruitAdapter中定义的一个内部类:
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
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {

private List<Fruit> mFruitList;

static class ViewHolder extends RecyclerView.ViewHolder{
ImageView fruitImage;
TextView fruitName;

public ViewHolder(View view){
super(view);
fruitImage=(ImageView) view.findViewById(R.id.fruit_image);
fruitName=(TextView) view.findViewById(R.id.fruit_name);
}
}


public FruitAdapter(List<Fruit> fruitList){
mFruitList = fruitList;
}
@NonNull
@Override
public FruitAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fruit_item,parent,false);
ViewHolder holder = new ViewHolder(view);
return holder;
}

@Override
public void onBindViewHolder(@NonNull FruitAdapter.ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());

}

@Override
public int getItemCount() {
return mFruitList.size();
}
}

  这段代码看上去有点长,但是它要比ListView的适配器要容易理解。我们先定义内部类ViewHolder,ViewHolder要继承自RecycleView.ViewHolder。然后ViewHolder的构造函数中传入一个View参数,这个参数通常就是RecycleView子项的最外层布局,那么我们可以通过findViewById()方法来获取ImageView和TextView的实例。   往下,FruitAdapter中也有一个构造函数,这个方法用于把要展示的数据源传进来,并赋值给一个全局变量mFruitList,后续操作都将在这个数据源的基础上进行。   由于FruitAdapter是继承自RecycleView.Adapter的,就必须重写onCreaterHolder()、onBindViewHolder()和getItemCount()这3个方法。onCreateViewHolder()方法用于创建ViewHolder实例,我们在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入到构造函数中,最后将ViewHolder的实例返回。onBindViewHolder()方法是用于对RecycleView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中。getItemCount()就非常简单,它用于告诉RecycleView一共有多少子项,直接返回数据源的长度就可以了。   适配器准备好,开始使用RecycleVIew了,修改MainActivity中的代码。
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
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initFruit();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycle_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}


private void initFruit(){
for (int i = 0;i<2;i++){
Fruit apple = new Fruit("Apple" , R.drawable.apple);
fruitList.add(apple);
Fruit banana = new Fruit("banana",R.drawable.banana);
fruitList.add(banana);
Fruit orange = new Fruit("orange",R.drawable.orange);
fruitList.add(orange);
Fruit cherry = new Fruit("cherry",R.drawable.cherry);
fruitList.add(cherry);
Fruit grape = new Fruit("grape",R.drawable.grape);
fruitList.add(grape);
Fruit mongo = new Fruit("mongo",R.drawable.mango);
fruitList.add(mongo);
Fruit peach = new Fruit("peach",R.drawable.peach);
fruitList.add(peach);
Fruit pear = new Fruit("pear",R.drawable.pear);
fruitList.add(pear);
Fruit watermelon= new Fruit("watermelon",R.drawable.watermelon);
fruitList.add(watermelon);
}
}
}

下载

官网搜索下载即可

连接Mysql

新建项目

连接Mysql

填写配置信息,这里只需填写user(root)和密码,然后下载驱动包,测试连接,成功后点击ok即可

选择展示所有数据库

使用

创建数据库 右键项目->new->schema
创建表 右键数据库->new->table

修改表 右键表->Modify Table

新建查询

右键数据库->New->Query/Console

基础知识

  在Java字节码中,寄存器都是32位的,能够支持任何类型,64位类型(Long/Double)用两个寄存器表示,寄存器的命名方式有p命名法和v命名法。p命名法通常用与表示函数参数,比如p0、p1等,v命名法用于表示函数内部的变量,比如v0、v1等。

阅读全文 »

010Editor

下载地址
吾爱破解010Editor
官网010Editor
激活码

1
2
3
4
5
用户名:www.budingwang.com 注册码:CR96-4B9C-6470-303F  
用户名:www.budingwang.com 注册码:CR71-DD9C-C1D3-55D8
用户名:www.budingwang.com 注册码:CRE7-D59C-98D4-EF4E
用户名:www.budingwang.com 注册码:CR2C-A19C-E8F5-6185
用户名:www.budingwang.com 注册码:CR5F-BA9C-6A95-9CF6

已无效

apktool

下载地址
apktool

首先,创建一个要放置apktool的文件夹
第一步,右键1,重命名为apktool.bat保存到创建的文件夹中
第二步,点击二,下载,下载好后移动到创建的文件夹中

第三步,添加到环境变量

最后,在终端输入cmd输入apktool验证是否成功安装

安装成功!

使用

反编译Android应用程序包
apktool d apk_file.apk

反编译结果

重打包
apktool b apk_file(反编译生成的文件夹) -o new.apk(打包生成的apk的名称)

如果被加upx壳的文件是ELF文件,那么直接将文件拖进Linux虚拟机中在命令行

1
upx -d 文件名

壳就被脱掉了,然后再将文件从Linux拖到Window中进行调试。

安装VMware

官网搜索VMware,找到对应的操作系统,下载
安装,安装的时候一直下一步就好了

阅读全文 »

App攻防技术发展

  随Smali工具(将dex反编译为smali语言的工具)以及baksmali工具(将Smali语言编译为dex二进制文件的工具)出现,Android安全起步。
  Android中的代码是运行在Dalvik或Art虚拟机中的,与ARM汇编类似,Smali语言可以当作Android虚拟机的汇编语言。
  

代码混淆

  如使用Google自带的混淆器ProGuard。ProGuard的作用是更改类名、函数名、变量名。好处就是增加攻击者在破解软件时猜到相应类的实际作用的难度;压缩文件大小。
  在Android中启用ProGuard,只需修改项目根目录下的App/build.gradle文件,将其中的buildTypes层级中的minifyEnable对应的值从false更改为true即可,这样最终编译生成的release版本便会启用代码混淆。

还有一个“代码混淆”工具——DexGuard,功能更丰富,不过它是个收费商业软件。

动态加载

  将需要保护的代码单独编译成一个二进制文件,将其进行加密后保存在一个外部的二进制文件中。在外部程序运行的过程中,再将被保护的二进制文件进行解密并使用ClassLoader类加载器来动态加载和运行被保护的代码。
  Android中每一个Java类都是由ClassLoader类加载器进行加载和运行的。
  由于Android中使用Java编写的App很容易被破解,故将所有的核心代码使用NDK套件(在Android中,NDK实际上是一个工具集,可以让开发者使用C/C++语言实现应用的各个部分)进行开发,结果就是外部的Java代码最终只是充当二进制文件装载器的角色,实际的业务逻辑都被放置在了更难破解的so文件中。甚至有的基于“放置在客户端”不可靠的原则,为了App安全,将重要功能和数据放置到云端,在客户端尽量只进行结果的展示。
  在这些保护手段下,单纯依靠apktool和Jadx这类静态反编译工具对App进行静态分析
已无法满足破解者要求,这时动态分析成为了主流。
  动态分析是通过附加调试或者注入进程来进行的分析。有Android Studio或者Jeb对App的dex进行调试,还有IDA、GDB等Native调试器对so文件进行单步调试,以及Hook和trace,也是动态分析手段。
  基于动态分析,动态加载保护就成了最脆弱的保护方式。只要通过动态分析,不管是在加载后的函数中设置断点以便从内存中dump出来被保护的内容,还是直接搜索进程的内存空间以便根据特征找到真实的dex文件并dump下来,都能应对这种保护方式。另外针对其他静态保护方式,由于动态分析是基于进程所处的运行状态,因此相比于在静态分析时得到的无意义代码而在动态分析时包含的都是真实的数据信息,使得破解者在逆向分析过程中有了很多真实数据,从而削减代码保护的作用。针对动态分析的对抗手段发展。
  在动态分析过程中,首先将调试器附加上进程或者通过注入将指令代码和数据注入目标进程中,然后才能对目标进行调试与内存监控。做到这个,最基本的方法就是调用ptrace()函数对进程进行附加或者基于二次打包的方式对程序进行修改,而对抗分两种:运行时检测和实现阻止。
  调用ptrace()函数对进程进行附加,会存在相应的特征点,比如/proc//status文件中的TracePid变量在进程被附加后,会由0变为附加进程的pid。如果此时代码本身单开一个线程对这个文件的TracePid值进行循环检测,检测到异常时就会自动退出进程,就可以做到阻止进程被破解者调试。
  App逆向分析过程中最难绕过的保护手段就是App加固。其逻辑和上面的动态加载类似,用加固厂商的壳程序包裹真实的App,在真实动态运行时再通过壳程序执行释放出来的真正App。App加固按照不同阶段加固特性重新分为三个不同阶段。
  第一阶段被认为是DEX整体加固,这是App加固初期。核心原理是将DEX整体加密后动态加载,刚开始App整体加固是需要先解密文件并在解密完成后写入到另外一个文件中,在解密完毕后调用DexClassLoader或者其他类加载器来加载解密后的文件。后来由于文件操作过于明显,进一步发展出将加密的DEX在内存中解密并直接在内存中进行加载的加固技术,这一阶段的加固技术没有明显写文件操作但同样无法阻止动态分析。后来为了防止根据特征进行内存搜索的方式,还出现了加载后抹去DEX文件头的手段,但都无法阻止设置断点和Hook的动态分析手段。DEX整体加固的致命之处在于,代码数据总是完整地存储在一段内存中,一旦反注入、反调试等措施被破解,保护就会门户洞开。
  第二阶段习惯上称为代码抽取保护。这一阶段App加固的关键在于真正的代码数据并不与DEX的整体结构数据存储在一起,就算DEX被完整地从内存中dump(转储)出来,也无法看到真正的函数代码。这种加固的核心原理是利用私有函数,通过对自身进程的Hook来拦截函数被调用时的路径,在抽取的函数被真实调用前,将无意义的代码数据填充到对应的代码区中。
  第三阶段加壳保护,将所有的Java代码变成最终的Native层代码。VMP与Dex2C。VMP加固技术最早起源于PC的虚拟机加固,其核心逻辑是将所有的代码使用自定义的解释器执行。代码不依赖于系统本身,即使获得所有的函数内容,也貌合神离,不知所云。这时唯一的解决方案可能是逆向对应解释器,找到与系统解释器的映射关系。Dex2C技术则是通过编译原理相关知识将原本的Java代码转化为native层代码,因为native层的二进制编码相比Java字节码更不容易被逆向。但实际上,具有C/C++ni’xai’g’n层的二进制编码相比Java字节码更不容易被逆向。但实际上,具有C/C++逆向经验的逆向开发和分析人员还是可以完成的。