在简单完成了基金净值爬取以后,我们对中间的过程可能产生了很多疑惑,即使完成了目标,也仅仅是知其然而不知其所以然,而为了以后爬虫任务的顺利进行,对爬虫过程中所涉及的原理进行掌握是十分有必要的。

本文将会针对之前爬虫过程中所涉及到的几个爬虫原理进行简单的阐述。

 

url究竟是什么?它的构成有什么规律可循?

 URL和URI 

在访问任何一个网页时,我们都需要一个网页链接(如百度: www.baidu.com),这就相当于网页的“家庭地址”一样,只有在知道了这个“地址”,我们才能看到“这户人家”长得什么样。而这个“地址”在大部分时候也被称为URL,全称为Universal Resource Locator,即统一资源定位符。

除了URL,还有一个极少听到的名词——URI,全称为Uniform Resource Identifier,即统一资源标志符。

以获取基金代码列表时用到的链接为例——

http://fund.eastmoney.com/js/fundcode_search.js是天天基金网基金代码的数据存储链接,它是一个URL,也是一个URI。即有这样的数据资源,我们用URL/URl来唯一指定了它的访问方式,这其中包括了访问协议http、访问路径(/即根目录)和资源名称fundcode_search.js。通过这样的一个链接,我们便可以从互联网上找到这个资源,这就是URL/URI。

URL是URI的自己,也就是说每个URL都是URI,但不是每一个URI都是URL,URI的子集中还包括URN,它在目前的互联网中用得非常少,几乎所有的URI都是URL,因此,一般的网页链接我们都可以直接、也惯称为URL。

 URL的解析 

在爬取基金代码和基金净值数据时,仔细观察相关的URL,我们可以发现它们的构成并非是无规律可循的。而事实上,URL的构成也确实存在一套统一的标准。

protocol://domain[:port]/path/[?parameters]#fragment

  • protocol 协议:标明了请求需要使用的协议,通常使用的是HTTP协议或者安全协议 HTTPS.其他协议还有mailto:用户打开邮箱的客户端,和ftp:用来做文件的转换, file用来获取文件,data获取外部资源等
  • domain 域名:标明了需要请求的服务器的地址,一个URL中也可以使用IP地址作为域名使用
  • port 端口:标明了获取服务器资源的入口端口号用于区分服务的端口,一台拥有IP地址的服务器可以提供许多服务,比如Web服务、FTP服务、SMTP服务等。那么,服务器的资源通过“IP地址+端口号”来区分不同的服务。如果把服务器比作房子,端口号可以看做是通向不同服务的门。端口不是一个URL必须的部分,如果省略端口部分,将采用默认端口,一般为80。
  • path 路径:表示服务器上资源的路径,从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。过去这样的路径标记的是服务器上文件的物理路径,但是现在,路径表示的只是一个抽象地址,并不指代任何物理地址。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名。
  • parameter 参数:从“?”开始到“#”为止之间的部分为参数部分,又称搜索部分、查询部分。这些参数是以键值对的形式,通过&符号分隔开来,服务器可以通过这些参数进行相应的个性化处理。
  • fragment 片段:可以理解为资源内部的书签,用来想服务器指明展示的内容所在的书签的点。例如对于HTML文件来说,浏览器会滚动到特定的或者上次浏览过的位置,对于音频或者视频资源来说,浏览器又会跳转到对应的时间节点。锚部分也不是一个URL必须的部分。

requests.get()中的headers和params参数又是什么?

当我们尝试获取网页内容时,我们会用到requests.get()访问网站的服务器,然后获取想到得到的网页内容。

 params参数 

requests.get()中的params参数就是为了将一些特别长,且明显有规律的URL,如:

http://api.fund.eastmoney.com/f10/lsjz?callback=jQuery18303213780505917203_1548395296124&fundCode=000001&pageIndex=1&pageSize=20&startDate=&endDate=&_=1548395296139

以参数化的方式传入,让其URL组合更为简洁和格式化。

 headers参数 

而我们在获取基金净值数据时发现,直接用URL访问并不能获得我们想要的内容,而是加上参数headers才成功。

这是因为对一些网页进行访问时,在你发送请求给服务器的过程中,需要使用一些附加信息,在获得服务器的识别和准许后,才能返回给你你所想要的内容。这就像我们平时到某个小区去看望朋友时,保安会在需要确认你的信息后才会放你同行一样。

常用头信息

Accept

请求报头域,用于指定客户端可接受哪些类型的信息

Accept-Language

指定客户端可接受的语言类型

Accept-Encoding

指定客户端接受的内容编码
Host
用于指定请求资源的主机IP和端口号,其内容为请求URL的原始服务器或网关的位置
Cookie
也常用复数形式Cookies,这是网站为了辨别用户进行会话跟踪而存储在用户本地的数据,它的主要功能是维持当前访问会话
Referer
此内容用来标识这个请求是从哪个页面发过来的,服务器可以拿到这一信息并做相应的处理,如做来源统计、防盗链处理等
User-Agent
特殊的字符串头,可以使服务器识别客户使用的操作系统及版本、浏览器及版本等信息。
Content-Type
在HTTP协议消息头中,它用来表示具体请求中的媒体类型信息

 

是否只有get一种访问形式?

每一次访问网页都是一次向服务器发出请求的过程。更具体的说,在浏览器中输入URL,回车之后会在浏览器观察到页面内容,这个过程就是浏览器向网站所在的服务器发送一个请求,网站服务器接收到这个请求后进行处理和解析,然后返回对应的响应,接着传回给浏览器。响应里包含了页面的源代码等内容,浏览器再对其进行解析,便将网页呈现出来,也就是最终我们在浏览器上所看到的效果。

请求由客户端(手机或PC浏览器)向服务器发出,可分为四部分:请求方法、请求的网址、请求头和请求体。而get则是一种请求方法。

 请求方法 

常见的请求方法:GET和POST。

基金净值数据的访问,就是一种GET请求,链接为——

http://api.fund.eastmoney.com/f10/lsjz?callback=jQuery18303213780505917203_1548395296124&fundCode=000001&pageIndex=1&pageSize=20&startDate=&endDate=&_=1548395296139

其中URL中包含了请求的参数信息。
POST请求大多在表单提交时发起,常见于登录过程中发送的登录表单,在输入用户名和密码后点击登录,通常便会发起一个POST请求,其数据通常以表单形式传输,不会体现在URL中。

其他请求方法:

  • GET:请求页面,并返回页面的内容
  • HEAD:类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头
  • POST:大多用于提交表单或上传文件,数据包含在请求体中
  • PUT:从客户端向服务器传送的数据取代指定文档中的内容
  • DELETE:请求服务器删除指定的页面
  • CONNECT:把服务器当做跳板,让服务器代替客户端访问其他网页
  • OPTIONS:允许客户端查看服务器的性能
  • TRACE:回显服务器收到的请求,主要用于测试或诊断

 请求的网址 

即URL

 请求头 

 用来说明服务器要使用的附加信息,比如:Cookie、Referer、User-Agent等。

 请求体 

一般承载的内容是POST请求中的表单数据,而对于GET请求,请求体则为空。

在爬虫中,如果要构造POST请求,需要使用正确的Content-Type,并了解各种请求库的各个参数设置时使用的是哪种Content-Type,不然可能会导致POST提交后无法正常响应

 

当我们尝试直接用循环去爬取所有内容的时候,是否会遭遇反爬?又该如何解决反爬?

如果直接用循环去爬取网页内容时,经常,通常会由于被判别为爬虫而被禁止,也就是常说的反爬虫。

这种现象出现的原因往往是因为网址采取的反爬虫措施,比如,服务器会检测某个IP在单位时间内的请求次数,如果超过了这个阈值,就会直接拒绝服务,返回一些错误信息,统称这种情况被称为封IP。

遭遇反爬虫时,通常由两种思路:

1. 延长访问间隔时间:在每次循环访问时,用time.sleep方法,设置访问的间隔时间,但这种方法会降低爬虫的效率。

2. 代理IP:既然封IP是由于同一IP访问次数太多,那么我们如果借助代理IP的方式让服务器以为是不同的IP在访问服务器,就能有效的防止被封IP,但有效的代理IP通常也并不容易找到,需要付费购买。

因此,如果你的爬虫项目密度不大,可以采取第一种方式来语法反爬取,如果初始的爬虫量大,可以选择分批次爬取,整理后的代码如下:

 1 def get_fundcode():
 2     '''
 3     获取fundcode列表
 4     :return: 将获取的DataFrame以csv格式存入本地
 5     '''
 6     url = 'http://fund.eastmoney.com/js/fundcode_search.js'
 7     r = requests.get(url)
 8     cont = re.findall('var r = (.*])', r.text)[0]  # 提取list
 9     ls = json.loads(cont)  # 将字符串个事的list转化为list格式
10     fundcode = pd.DataFrame(ls, columns=['fundcode', 'fundsx', 'name', 'category', 'fundpy'])  # list转为DataFrame
11     fundcode = fundcode.loc[:, ['fundcode', 'name', 'category']]
12     fundcode.to_csv('./案例/基金净值爬取/fundcode.csv', index=False)
13 
14 
15 def get_one_page(fundcode, pageIndex=1):
16     '''
17     获取基金净值某一页的html
18     :param fundcode: str格式,基金代码
19     :param pageIndex: int格式,页码数
20     :return: str格式,获取网页内容
21     '''
22     url = 'http://api.fund.eastmoney.com/f10/lsjz'
23     cookie = 'EMFUND1=null; EMFUND2=null; EMFUND3=null; EMFUND4=null; EMFUND5=null; EMFUND6=null; EMFUND7=null; EMFUND8=null; EMFUND0=null; EMFUND9=01-24 17:11:50@#$%u957F%u4FE1%u5229%u5E7F%u6DF7%u5408A@%23%24519961; st_pvi=27838598767214; st_si=11887649835514'
24     headers = {
25         'Cookie': cookie,
26         'Host': 'api.fund.eastmoney.com',
27         'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
28         'Referer': 'http://fundf10.eastmoney.com/jjjz_%s.html' % fundcode,
29     }
30     params = {
31         'callback': 'jQuery18307633215694564663_1548321266367',
32         'fundCode': fundcode,
33         'pageIndex': pageIndex,
34         'pageSize': 20,
35     }
36     try:
37         r = requests.get(url=url, headers=headers, params=params)
38         if r.status_code == 200:
39             return r.text
40         return None
41     except RequestException:
42         return None
43 
44 
45 def parse_one_page(html):
46     '''
47     解析网页内容
48     :param html: str格式,html内容
49     :return: dict格式,获取历史净值和访问页数
50     '''
51     if html is not None:  # 判断内容是否为None
52         content = re.findall('\((.*?)\)', html)[0]  # 提取网页文本内容中的数据部分
53         lsjz_list = json.loads(content)['Data']['LSJZList']  # 获取历史净值列表
54         total_count = json.loads(content)['TotalCount']  # 获取数据量
55         total_page = math.ceil(total_count / 20)  #
56         lsjz = pd.DataFrame(lsjz_list)
57         info = {'lsjz': lsjz,
58                 'total_page': total_page}
59         return info
60     return None
61 
62 
63 def main(fundcode):
64     '''
65     将爬取的基金净值数据储存至本地csv文件
66     '''
67     html = get_one_page(fundcode)
68     info = parse_one_page(html)
69     total_page = info['total_page']
70     lsjz = info['lsjz']
71     lsjz.to_csv('./案例/基金净值爬取/%s_lsjz.csv' % fundcode, index=False)  # 将基金历史净值以csv格式储存
72     page = 1
73     while page < total_page:
74         page += 1
75         print(lsjz)
76         html = get_one_page(fundcode, pageIndex=page)
77         info = parse_one_page(html)
78         if info is None:
79             break
80         lsjz = info['lsjz']
81         lsjz.to_csv('./案例/基金净值爬取/%s_lsjz.csv' % fundcode, mode='a', index=False, header=False)  # 追加存储
82         time.sleep(random.randint(5, 10))
83 
84 
85 if __name__=='__main__':
86     # 获取所有基金代码
87     get_fundcode()
88     # fundcode = '519961'
89     fundcodes = pd.read_csv('./案例/基金净值爬取/fundcode.csv', converters={'fundcode': str})
90     # 获取所有基金净值数据
91     for fundcode in fundcodes['fundcode']:
92         print(fundcode)
93         main(fundcode)
94         time.sleep(random.randint(5, 10))
View Code

 

上述代码虽然提供了基金净值数据爬虫的完整代码,但在具体项目的实施中,还需要根据项目的更新需求和更新周期来增加更新机制,这时,一个较为完整的爬虫小程序才算完成~