python3网易新闻爬虫

python3编写网易爬虫,并部署至centos服务器运行。

作者 jooop 日期 2017-01-29
python3网易新闻爬虫

一、背景:

  因为另一项目新闻管理系统的需要,以及作为熟悉python基础的练手项目,现准备用python写一个网易新闻爬虫,为新闻管理系统项目获取新闻内容,以解决新闻源的自动获取、添加到数据库的问题。

二、目标:

  实现自动化获取网易新闻不同类别的新闻列表url,然后分别爬取新闻内容并存入新闻管理系统项目的mysql数据库,以供新闻管理系统的使用。

三、参考:

  因为对python的爬虫和html解析模块不是很了解,因此在写的过程中查阅参考了许多模块的教程和资料。另外对于html页面中的标签正则,经查阅后使用断言以解决。
  具体的爬虫结构,链接和正文内容的解析与正则模式、数据存储表均为自己设计。

参考资料:

  1. 编写简单的网络爬虫 (python3.2)
    http://blog.csdn.net/database_zbye/article/details/38826893
  2. Python requests模块学习笔记
    http://www.cnblogs.com/tangdongchu/p/4229049.html
  3. 采用beautifulsoup库 解析html页面
    http://blog.csdn.net/weiyuanke/article/details/16986639
  4. 《零基础学python》(第二版)»第七章 保存数据 »MySQL数据库(2)
    http://docs.pythontab.com/learnpython/231/
  5. 正则表达式分组、断言详解
    http://www.cnblogs.com/iyangyuan/archive/2013/05/30/3107390.html

四、分析:

(一)、新闻爬虫业务流程分析:

  1. 爬取新闻列表的html页面,解析出新闻的url,存入队列。
  2. 从队列中取出一个url,爬取新闻内容的html页面,进行解析,将解析出的标题、正文、来源、时间、图片等结果放入数据库。

(二)、实现过程中几个细节分析:

1. 模块的选择和列表页面的爬取:

  在看了一些入门文章后,开始打算使用requests模块爬取新闻列表和正文页面,然后使用BeautifulSoup加lxml对页面进行解析,在对网易新闻列表的加载方式经过一番研究后,通过浏览器的开发者工具提取到动态JS加载的新闻标题列表的JSON页面的链接,用手工提取不同分类的JSON链接地址,然后通过requests模块来获取页面。

2. 提取新闻url:

  对于列表地址的解析,因为没有了解过JSON数据的处理方式,为了方便起见使用了断言式正则规则来提取出新闻的url。

3. 将url存入队列

  为了标识已经访问过的新闻链接,防止程序突然崩溃导致的队列数据丢失和简单的链接去重。使用数据库作为存放新闻页面链接的队列,titles表结构如下:

字段名称 数据类型 Key Default 解释
url varchar(100) PRI NULL 存放新闻url
category varchar(10) NULL 存放分类信息
flag tinyint(4) NULL 标记是否获取过内容

  将url设置为主键,并且在存入新闻链接时使用insert ignore into ...语句来保证url不会重复,在新插入url的同时将flag设置为0,表示该页面待爬取
在存入解析完的新闻数据的同时,将titles表中,相对应的url的flag设置为1,表示已经爬取过该页面。

4. 获取新闻界面

  同第一步中获取新闻列表界面一样,直接使用requests模块获取页面。因此直接调用其函数。

5. 解析新闻界面

  对于新闻内容的提取用到了BeautifulSoup模块和正则表达式混合进行解析。另外因为网易新闻有些html样式不统一,此处解析只针对占主流的页面样式,对于无法解析的页面进行提示并标记。
  另外因为考虑到新闻管理系统需要取出新闻正文,再输出到html界面进行展示,因此对新闻正文的html标签没有使用正则去除、而是原样式保存到数据库。

6. 将解析出的数据存入数据库,同时对已获取内容的url标记

  将成功解析的数据存入news表中,并将titles表相对应的url的flag设置为1,表示已经获取过该新闻内容。此处遇到过正文、来源等数据长度超过数据库的表字段长度,导致程序中断的问题,对此解决方法是在执行该插入操作时捕获异常,然后对该新闻的url进行flag标记设置为2,跳过此新闻的获取。
  将内容格式无法解析的url的flag设置为2,表示已经试图爬取过该页面内容,但是无法获取该新闻。
  news表结构如下:

字段名称 数据类型 Key Default 解释
newsId bigint(20) PRI NULL 新闻ID
title varchar(40) NULL 标题
content text NULL 正文
imageUrl varchar(180) NULL 首个图片地址(做封面)
date timestamp CURRENT_TIMESTAMP 日期
source varchar(50) NULL 来源
clickTraffic bigint(20) 0 点击量
category varchar(10) NULL 分类

五、开发:

(一)、开发环境及工具配置:

  开发环境:windows10
  开发语言:python3.5
  开发工具:IDEL、MySql5.7
  使用模块:BeautifulSoup、requests、lxml、pymysql

(二)、建表语句:

  title表:
  create table titles(   url varchar(100) primary key,   category varchar(10),   flag tinyint not null default 0   );
  news表:
  create table news(   newsId bigint primary key auto_increment,   title varchar(40),   content text,   imageUrl varchar(180),   date timestamp,   source varchar(50),   clickTraffic bigint default 0,   category varchar(10)   );

(三)、源码:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup
import requests
import lxml
import re
import pymysql
import warnings
warnings.filterwarnings("ignore")
#获取html页面
def GetPage(url):
headers={
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'
}
data=requests.get(url,headers=headers)
return data
#解析ListPage,返回新闻列表的链接
def TakenList(page):
href=re.compile(r'(?<="docurl":").*(?=",)')
urls=href.findall(page.text)#新闻列表的href正则规则
return urls
#将新闻url添加到数据库中
def UseSql(urls,category):
conn = pymysql.connect(host="localhost",user="root",passwd="root",db="news",port=3306,charset="utf8")
cur = conn.cursor() #利用连接对象得到游标对象
for url in urls:
cur.execute("insert ignore into titles (url,category) values (%s,%s)",(url,category))#此句行过程中,若数据库中已经存在目标utl,则不进行存储,并抛出一条警告
conn.commit()
cur.close()
conn.close()
#获取数据库中flag为0的url,返回urls[]
def GetSql():
conn = pymysql.connect(host="localhost",user="root",passwd="root",db="news",port=3306,charset="utf8")
cur = conn.cursor() #利用连接对象得到游标对象
cur.execute("select url,category from titles where flag=0")
lines=cur.fetchall()
cur.close()
conn.close()
return lines
#解析NewsPage
def TakenNews(data):
mdict={}
soup=BeautifulSoup(data.text,'lxml')
div1=soup.find('div',class_='post_content_main')
if repr(div1)!="None":
title=div1.find('h1').text#【标题】
post_time_source=div1.find('div',class_='post_time_source').text
#(post_time_source需要再分别正则出时间和来源)
opbody=div1.find('div',class_='post_text')
#(body需要再除去<div style="position:relative;">....</div>)
image=opbody.find("img")#因为需要在新闻系统列表上显示图片,所以需要一条图片地址
#(需要再正则出src内容)
time=re.search(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}\:\d{2}',post_time_source).group(0)#匹配格式2016-11-19 22:14:05
#【时间】
if repr(re.search(r'来源.*',post_time_source))!='None':
source=re.search(r'来源.*',post_time_source).group(0).strip()
source=' '.join(source.split())
else:
source='网易新闻'
#【来源】
body=re.sub(r'\n','',repr(opbody))
body=re.sub(r'<div class="gg200x300">(.*?)</div></div>','',body)
#【内容】
if repr(image)!='None': #若该新闻没有image,则将image标记为0
image=re.sub(r'data-src|data-origin-src','',repr(image)) #删除掉微信图片地址
image=re.search(r'(?<=src=")http:(.*?)(?=")',image).group(0)
else:
image=0
#【image的url】
mdict['解析状态']="成功解析"
mdict['title']=title
mdict['time']=time
mdict['source']=source
mdict['body']=body
mdict['image']=image
else:
mdict['解析状态']="无法解析"
return mdict
#将新闻数据存入数据库,同时将title的flag设为1
def Putsql(mdict,category,purl):
conn = pymysql.connect(host="localhost",user="root",passwd="root",db="news",port=3306,charset="utf8")
cur = conn.cursor()
cur.execute("insert into news (title,date,source,imageUrl,content,category) values (%s,%s,%s,%s,%s,%s)",
(mdict['title'],mdict['time'],mdict['source'],mdict['image'],mdict['body'],category))
cur.execute("update titles set flag=1 where url=%s",(purl))
conn.commit()
cur.close()
conn.close()
print("成功添加新闻",mdict['title'],"\n")
#无法解析的页面,将其忽略掉,同时将title的flag设为2
def Putsql(mdict,category,purl):
conn = pymysql.connect(host="localhost",user="root",passwd="root",db="news",port=3306,charset="utf8")
cur = conn.cursor()
try:
cur.execute("insert into news (title,date,source,imageUrl,content,category) values (%s,%s,%s,%s,%s,%s)",
(mdict['title'],mdict['time'],mdict['source'],mdict['image'],mdict['body'],category))
cur.execute("update titles set flag=1 where url=%s",(purl))
conn.commit()
except:
cur.execute("update titles set flag=2 where url=%s",(purl))
finally:
cur.close()
conn.close()
print("成功添加新闻",mdict['title'],"\n")
#main
count=0
NewsCategory={'国内':'http://temp.163.com/special/00804KVA/cm_guonei.js?callback=data_callback',
'国际':'http://temp.163.com/special/00804KVA/cm_guoji.js?callback=data_callback',
'社会':'http://temp.163.com/special/00804KVA/cm_shehui.js?callback=data_callback',
'军事':'http://temp.163.com/special/00804KVA/cm_war.js?callback=data_callback',
'体育':'http://sports.163.com/special/000587PR/newsdata_n_world.js?callback=data_callback',
'娱乐':'http://ent.163.com/special/000380VU/newsdata_index.js?callback=data_callback',
'科技':'http://tech.163.com/special/00097UHL/tech_datalist.js?callback=data_callback'
}
for category in NewsCategory:
print("正在获取"+category+"新闻列表")
category_url=NewsCategory[category]
listpage=GetPage(category_url) #获取标题列表页面
listurls=TakenList(listpage) #解析出标题列表数据
UseSql(listurls,category) #将列表放入数据库
print(category+"列表更新完成,开始获取新闻")
pageurls=GetSql() #从数据库获取未访问过的列表
#依次访问这些列表
for pageurl in pageurls:
purl=''.join(list(pageurl[0])) #取出新闻地址
pcategory=''.join(list(pageurl[1]))#取出新闻类型
newspage=GetPage(purl) #获取新闻内容页面
mdict=TakenNews(newspage) #解析出标题、内容等信息
if mdict['解析状态']=="成功解析":
Putsql(mdict,pcategory,purl,)
count=count+1
elif mdict['解析状态']=="无法解析":
Delsql(purl)
print("无法解析页面:"+purl+",已跳过")
print('本次新闻获取已完成,共更新"'+repr(count)+'"条新闻。')

六、测试:

(一)、本地运行测试:

  1. 运行环境:win10、python3.5
  2. 运行效果:
    成功添加新闻和此次获取数目统计
    Alt text
      对无法解析的页面将flag设置为2,跳过
    Alt text

(二)、服务器定时运行测试:

  1. 运行环境:centos6.5、python3.5

  2. 每小时定时运行设置:
      通过linux自带定时运行服务crontab进行设置,每小时自动运行。

    [root@iZm5e19ccp2hp43c52aze2Z ~]# crontab -l
    #!/bin/sh
    0 */1 * * * /usr/local/python3/bin/python3 /usr/local/news.py
  3. 运行效果:
      因定时运行服务不在控制台输出信息,效果可从网站前台页面查看:http://115.28.137.1:8080/,每个小时整点与网易新闻同步更新。

    七、工作评价:

      此次开发为首次接触爬虫、也是首次实现完整的python程序,因此在爬虫结构设计、代码的实现上显得不够简洁、扩展性较差,对于细节的处理不够到位,对于另外一种新闻正文的页面形式没有进行解析而是直接跳过(此页面下多为自媒体投稿,新闻质量较差,所以没有考虑另外解析)。
      每日新闻数据量较少、并且在使用的过程中没有出现反爬虫等情况,因此没有考虑多线程和使用代理IP优化。
      因为对java的jdbc较为熟悉,所以在使用pymysql对数据库操作时比较顺利。
      总的来说,此次项目仅为熟悉python的练手项目,并且项目目的只为了在自己的项目中使用,所以只做了主要功能的实现。