Web Crawler

Python learning -- Web Crawler example

Posted by Bing Yan on December 1, 2018

前言

    Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。
Python 本身也是由诸多其他语言发展而来的,这包括 ABC、Modula-3、C、C++、Algol-68、SmallTalk、Unix shell 和其他的脚本语言等等。
像 Perl 语言一样,Python 源代码同样遵循 GPL(GNU General Public License)协议。
    Python 的设计具有很强的可读性,相比其他语言经常使用英文关键字,其他语言的一些标点符号,它具有比其他语言更有特色语法结构。
    而大多数人最开始接触Python都是通过网络爬虫。今天我就通过一个自己编写的网络爬虫例子来初步了解一下Python。

正文

什么是网络爬虫(Web Crawler)

    简单来说互联网是由一个个站点和网络设备组成的大网,我们通过浏览器访问站点,站点把HTML、JS、CSS代码返回给浏览器,这些代码经过浏览器解析、渲染,将丰富多彩的网页呈现我们眼前;
    如果我们把互联网比作一张大的蜘蛛网,数据便是存放于蜘蛛网的各个节点,而爬虫就是一只小蜘蛛,沿着网络抓取自己的猎物(数据)爬虫指的是:向网站发起请求,获取资源后分析并提取有用数据的程序。
    从技术层面来说就是 通过程序模拟浏览器请求站点的行为,把站点返回的HTML代码/JSON数据/二进制数据(图片、视频) 爬到本地,进而提取自己需要的数据,存放起来使用。

网络爬虫(Web Crawler)能做什么


  • 做为通用搜索引擎网页收集器(google,baidu)
  • 做垂直搜索引擎
  • 科学研究:在线人类行为,在线社群演化,人类动力学研究,计量社会学,复杂网络,数据挖掘,等领域的实证研究都需要大量数据,网络爬虫是收集相关数据的利器。
  • 偷窥,hacking,发垃圾邮件…
    我的理解:就是从网络中获取需要的大量数据,作为大数据等技术分析的原料。因为海量的数据本身是蕴含着很多规律,需要去发现和挖掘的。而这部分工作就需要之后的数据清洗、数据仓储、数据分析等技术手段。

为什么用Python写网络爬虫

所有语言都有相通的用处,比如写网络爬虫也有很多种语言选择。
工厂方法模式构成要素:

  • C,C++:高效率,快速,适合通用搜索引擎做全网爬取。缺点,开发慢,写起来又臭又长,例如:天网搜索源代码。
  • 脚本语言:Perl, Python, Java, Ruby。简单,易学,良好的文本处理能方便网页内容的细致提取,但效率往往不高,适合对少量网站的聚焦爬取
  • C#(貌似信息管理的人比较喜欢的语言)

那最终为什么python能够胜任这个工作,为大多数人接受?

  • 跨平台,对Linux和windows都有不错的支持。
  • 科学计算,数值拟合:Numpy,Scipy
  • 可视化:2d:Matplotlib(做图很漂亮), 3d: Mayavi2
  • 复杂网络:Networkx
  • 统计:与R语言接口:Rpy
  • 交互式终端
  • 网站的快速开发


爬虫的基本流程


用户获取网络数据的方式:

  • 浏览器提交请求—>下载网页代码—>解析成页面
  • 模拟浏览器发送请求(获取网页代码)->提取有用的数据->存放于数据库或文件中
    爬虫要做的就是方式2。


  1. 发起请求:

    使用http库向目标站点发起请求,即发送一个Request
    Request包含:请求头、请求体等
    Request模块缺陷:不能执行JS 和CSS 代码

  2. 获取响应内容:

    如果服务器能正常响应,则会得到一个Response
    Response包含:html,json,图片,视频等

  3. 解析内容:

    解析html数据:正则表达式(RE模块),第三方解析库如Beautifulsoup,pyquery等。 解析json数据:json模块
    解析二进制数据:以wb的方式写入文件

  4. 保存数据:

    数据库(MySQL,Mongdb、Redis)
    文件

HTTP协议复习

编写网络爬虫需要对HTTP协议有一定了解。通过修改请求方式、请求URL、在响应体中过滤有用信息才能实现网络爬虫的功能。

由图可知,用户通过发送请求给服务器,服务器根据请求发送响应。整个过程中有Request和Response两个消息。

  • Request:用户将自己的信息通过浏览器(socket client)发送给服务器(socket server)
  • Response:服务器接收请求,分析用户发来的请求信息,然后返回数据(返回的数据中可能包含其他链接,如:图片,js,css等)

Request详解:

  • 请求方式:常见的请求方式:GET / POST
  • 请求的URL:URL是全球统一资源定位符,用来定义互联网上一个唯一的资源 例如:一张图片、一个文件、一段视频都可以用url唯一确定
  • 请求头:
    • User-agent:请求头中如果没有user-agent客户端配置,服务端可能将你当做一个非法用户host
    • cookies:cookie用来保存登录信息
    • Referrer:访问源至哪里来(一些大型网站,会通过Referrer 做防盗链策略;所有爬虫也要注意模拟)
  • 请求体:如果是get方式,请求体没有内容 (get请求的请求体放在 url后面参数中,直接能看到;如果是post方式,请求体是format data

Response详解:

  • 响应状态码:
    • 200:代表成功
    • 301:代表跳转
    • 404:文件不存在
    • 403:无权限访问
    • 502:服务器错误
  • 响应头:
    • Set-Cookie:BDSVRTM=0; path=/:可能有多个,是来告诉浏览器,把cookie保存下来
    • Content-Location:服务端响应头中包含Location返回浏览器之后,浏览器就会重新访问另一个页面
  • preview:就是网页源代码,包括JSO数据、网页html、图片、二进制数据等。

Sina News Web Crawler

根据培训视频,一步一步完成了第一个网络爬虫–新浪新闻网络爬虫:
整个过程并不很顺利。遇到的困难主要有:

  • 网站地址发生变更频繁,需要经常维护。这也是各大网站反爬虫的一种手段。因为编写的爬虫依赖的就是URL格式的统一,否则全自动就要变成“全得自己动”了。
  • 很多网页上的信息是通过多个请求加载进来的,要知道一个网页的展示,可能需要发送了上百个请求,如何在这些Response中找到正确的信息也需要细心。比如此例子中的“评论数”。而且即使经过多次增加和修改,评论数量的收集也还是会有不完整。

此项目代码在Github

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
commentCountUrl = 'https://comment5.news.sina.com.cn/cmnt/count?format=json&newslist=gn:comos-{}:0'
commentCountUrl2 = 'https://comment.sina.com.cn/page/info?version=1&format=json&channel=cj&newsid=comos-{}'
import re
import requests
from bs4 import BeautifulSoup
import json
from datetime import datetime
def getCommentCount(newsurl):
    m = re.search('doc-i(.+).shtml',newsurl)  #根据新闻url找出新闻的id
    newsid= m.group(1)
   # print(newsid)
    commentCountUrlFormat = commentCountUrl.format(newsid)   #根据新闻id,组合出评论数量的链接
    commentCountUrlFormat2 = commentCountUrl2.format(newsid)
    #根据评论数量链接,取得评论数
    res = requests.get(commentCountUrlFormat)
    res2 = requests.get(commentCountUrlFormat2)
    res.encoding='utf-8'
   # print(res.text)
    mm = re.search('\"total\":.*?(?=,)',res.text)
    mm2 = re.search('\"total\":.*?(?=,)',res2.text) #这两个路径中只有一个是有返回值的,另一个没有返回值,需要判断
    commentCount1=0
    commentCount2=0
    if mm is not None:
        commentCountStr1 = mm.group(0).lstrip('"total":').strip()
        commentCount1 = int(commentCountStr1)
    if mm2 is not None:
        commentCountStr2 = mm2.group(0).lstrip('"total":').strip()
        commentCount2 = int(commentCountStr2)
    
    #当第二个地址返回正确 ,但是第一个地址返回也不是空,知识内容是0,所以不为空的都要取,然后返回大的评论数
    
    
    if commentCount1 > commentCount2:
        return commentCount1
    else:
        return commentCount2
  
1
2
3
4
5
6
7
8
9
10
def getAuthor(soup):
    
    show_author = soup.select('.show_author')
    article_editor = soup.select('.article-editor')
    if len(show_author)>0:
        return show_author[0].text.lstrip('责任编辑:') 
    elif len(article_editor)>0:
        return article_editor[0].text.lstrip('责任编辑:')
    else:
        return 'null'
1
2
3
4
5
6
7
8
9
10
11
12
def getNewsDetail(newsurl):
    result = {}
    res = requests.get(newsurl)
    res.encoding='utf-8'
    soup = BeautifulSoup(res.text,'html.parser')
    result['title'] = soup.select('.main-title')[0].text
    result['dt'] = datetime.strptime(soup.select('.date')[0].text,'%Y年%m月%d日 %H:%M')
    result['newssource'] = soup.select('.source')[0].text
    result['article'] = ' '.join([p.text.strip() for p in soup.select('.article p')[:-1]])
    result['editor'] = getAuthor(soup)
    result['commentsCount'] = getCommentCount(newsurl)
    return result
1
2
3
4
5
6
7
8
def parseListLinks(pageUrl):
    newsdetails = []
    res = requests.get(pageUrl)
    res.encoding='utf-8'
    jd = json.loads(res.text)
    for ent in jd['result']['data']:
        newsdetails.append(getNewsDetail(ent['url']))
    return newsdetails
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def getNewsTotal(start, end):
    pageCommonUrl = 'https://feed.mix.sina.com.cn/api/roll/get?pageid=153&lid=2509&k=&num=10&page={}'
    if str(start).isdigit() and str(end).isdigit():
        if int(start)< int(end):
            news_total = []
            for i in range(int(start),int(end)):
                newsurl = pageCommonUrl.format(i)
                newsary = parseListLinks(newsurl)
                news_total.extend(newsary)
            return news_total
        else:
            return None
    else:
        return None
1
2
3
import pandas
df = pandas.DataFrame(getNewsTotal(11,13))
df.head(20)


执行效果如图:


当然也可以通过命令将结果存储在数据库或者表格中。

1
df.to_excel('news_result.xlsx')

总结

    有人对Python的理解还仅仅是脚本语言。其实Python是一个全能选手,尤其是在科研领域更是大放异彩。 在知乎上看到关于“你都用 Python 来做什么?”问题的回复,可以说是只有想不到没有做不到的。

参考资料

此次学习主要依赖于下面技术网站:
https://study.163.com/course/courseMain.htm?courseId=1003285002