使用Python3编写一个爬虫

使用Python3编写一个爬虫

需求简介

最近厂里有一个新闻采集类的需求,细节大体如下:

  1. 模拟登录一个内网网站(SSO)
  2. 抓取新闻(支持代理服务器的方式访问)
  3. 加工内容样式,以适配手机屏幕
  4. 将正文中的图片转存到自已的服务器,并替换img标签中的url
  5. 图片存储服务器需要复用已有的FastDFS分布式文件系统
  6. 采集结果导入生产库
  7. 支持日志打印

初学Python3,正好用这个需求练练手,最后很惊讶的是只用200多行代码就实现了,如果换成Java的话大概需要1200行吧。果然应了那句老话:人生苦短,我用Python

登录页面抓包

第一步当然是抓包,然后再根据抓到的内容,模拟进行HTTP请求。

常用的抓包工具,有Mac下的Charles和Windows下的Fiddler。
它们的原理都是在本机开一个HTTP或SOCKS代理服务器端口,然后将浏览器的代理服务器设置成这个端口,这样浏览器中所有的HTTP请求都会先经过抓包工具记录下来了。

这里推荐尽量使用Fiddler,原因是Charles对于cookie的展示是有bug的,举个例子,真实情况:请求A返回了LtpaToken这个cookie,请求B中返回了sid这个cookie。但在Charles中的展示是:请求A中已经同时返回了LtpaToken和sid两个cookie,这就很容易误导人了。
另外Fiddler现在已经有了Linux的Beta版本,貌似是用类似wine的方式实现的。

如果网站使用了单点登录,可能会涉及到手工生成cookie。所以不仅需要分析每一条HTTP请求的request和response,以及带回来的cookie,还要对页面中的javascript进行分析,看一下是如何生成cookie的。

模拟登录

将页面分析完毕之后,就可以进行模拟HTTP请求了。
这里有两个非常好用的第三方库, requestBeautifulSoup

requests 库是用来代替urllib的,可以非常人性化的的生成HTTP请求,模拟session以及伪造cookie更是方便。
BeautifulSoup 用来代替re模块,进行HTML内容解析,可以用tag, class, id来定位想要提取的内容,也支持正则表达式等。

具体的使用方式直接看官方文档就可以了,写的非常详细,这里直接给出地址:
requests官方文档
BeautifulSoup官方文档

通过pip3来安装这两个模块:

sudo apt-get install python3-pip
sudo pip3 install requests
sudo pip3 install beautifulsoup4

导入模块:

import requests
from bs4 import BeautifulSoup

模拟登录:

def sso_login():
    # 调用单点登录工号认证页面
    response = session.post(const.SSO_URL,
                            data={'login': const.LOGIN_USERNAME, 'password': const.LOGIN_PASSWORD, 'appid': 'np000'})

    # 分析页面,取token及ltpa
    soup = BeautifulSoup(response.text, 'html.parser')
    token = soup.form.input.get('value')
    ltpa = soup.form.input.input.input.get('value')
    ltpa_value = ltpa.split(';')[0].split('=', 1)[1]

    # 手工设置Cookie
    session.cookies.set('LtpaToken', ltpa_value, domain='unicom.local', path='/')

    # 调用云门户登录页面(2次)
    payload = {'token': token}
    session.post(const.LOGIN_URL, data=payload, proxies=const.PROXIES)
    response = session.post(const.LOGIN_URL, data=payload, proxies=const.PROXIES)
    if response.text == "success":
        logging.info("登录成功")
        return True
    else:
        logging.info("登录失败")
        return False

这里用到了BeautifulSoup进行HTML解析,取出页面中的token、ltpa等字段。
然后使用session.cookies.set伪造了一个cookie,注意其中的domain参数,设置成1级域名。
然后用这个session,去调用网站页面,换回sid这个token。并可以根据页面的返回信息,来简单判断一下成功还是失败。

列表页面抓取

登录成功之后,接下来的列表页面抓取就要简单的多了,不考虑分页的话,直接取一个list出来遍历即可。

def capture_list(list_url):
    response = session.get(list_url, proxies=const.PROXIES)
    response.encoding = "UTF-8"
    soup = BeautifulSoup(response.text, 'html.parser')
    news_list = soup.find('div', 'xinwen_list').find_all('a')
    news_list.reverse()
    logging.info("开始采集")
    for news_archor in news_list:
        news_cid = news_archor.attrs['href'].split('=')[1]
        capture_content(news_cid)
    logging.info("结束采集")

这里使用了response.encoding = "UTF-8"来手工解决乱码问题。

新闻页面抓取

新闻页面抓取,涉及到插临时表,这里没有使用每三方库,直接用SQL方式插入。
其中涉及到样式处理与图片转存,另写一个模块pconvert来实现。

def capture_content(news_cid):
    # 建立DB连接
    conn = mysql.connector.connect(user=const.DB_USERNAME, password=const.DB_PASSWORD, host=const.DB_HOST,
                                   port=const.DB_PORT, database=const.DB_DATABASE)
    cursor = conn.cursor()

    # 判断是否已存在
    cursor.execute('select count(*) from material_prepare where news_cid = %s', (news_cid,))
    news_count = cursor.fetchone()[0]
    if news_count > 0:
        logging.info("采集" + news_cid + ':已存在')
    else:
        logging.info("采集" + news_cid + ':新增')
        news_url = const.NEWS_BASE_URL + news_cid
        response = session.post(news_url, proxies=const.PROXIES)
        response.encoding = "UTF-8"
        soup = BeautifulSoup(response.text, 'html.parser')
        # logging.info(soup)
        news_title = soup.h3.text.strip()[:64]
        news_brief = soup.find('div', 'brief').p.text.strip()[:100]
        news_author = soup.h5.span.a.text.strip()[:100]
        news_content = soup.find('table', 'unis_detail_content').tr.td.prettify()[66:-7].strip()
        # 样式处理
        news_content = pconvert.convert_style(news_content)
        # 将图片转存至DFS并替换URL
        news_content = pconvert.convert_img(news_content)
        # 入表
        cursor.execute(
            'INSERT INTO material_prepare (news_cid, title, author, summary, content, add_time, status)  VALUES  (%s, %s, %s, %s, %s, now(), "0")'
            , [news_cid, news_title, news_author, news_brief, news_content])
    # 提交
    conn.commit()
    cursor.close()

样式处理

文本样式处理,还是要用到BeautifulSoup,因为原始站点上的新闻内容样式是五花八门的,根据实际情况,一边写一个test函数来生成文本,一边在浏览器上慢慢调试。

def convert_style(rawtext):
    newtext = '<div style="margin-left: 0px; margin-right:0px; letter-spacing: 1px; word-spacing:2px;line-height: 1.7em; font-size:18px;text-align:justify; text-justify:inter-ideograph">' \
              + rawtext + '</div>'
    newtext = newtext.replace(' align="center"', '')
    soup = BeautifulSoup(newtext, 'html.parser')
    img_tags = soup.find_all("img")
    for img_tag in img_tags:
        del img_tag.parent['style']
    return soup.prettify()

图片转存至DFS

因为原始站点是在内网中的,采集下来的HTML中,<img>标签的地址是内网地址,所以在公网中是展现不出来的,需要将图片转存,并用新的URL替换原有的URL。

def convert_img(rawtext):
    soup = BeautifulSoup(rawtext, 'html.parser')
    img_tags = soup.find_all("img")
    for img_tag in img_tags:
        raw_img_url = img_tag['src']
        dfs_img_url = convert_url(raw_img_url)
        img_tag['src'] = dfs_img_url
        del img_tag['style']
    return soup.prettify()

图片转存最简单的方式是保存成本地的文件,然后再通过nginx或httpd服务将图片开放出去:

pic_name = raw_img_url.split('/')[-1]
pic_path = TMP_PATH + '/' + pic_name
with open(pic_path, 'wb') as pic_file:
   pic_file.write(pic_content)

但这里我们需要复用已有的FastDFS分布式文件系统,要用到它的一个客户端的库fdfs_client-py
fdfs_client-py不能直接使用pip3安装,需要直接使用一个python3版的源码,并手工修改其中代码。操作过程如下:

git clone https://github.com/jefforeilly/fdfs_client-py.git
cd dfs_client-py
vi ./fdfs_client/storage_client.py
将第12行 from fdfs_client.sendfile import * 注释掉
python3 setup.py install

sudo pip3 install mutagen

客户端的使用上没有什么特别的,直接调用upload_by_buffer,传一个图片的buffer进去就可以了,成功后会返回自动生成的文件名。

from fdfs_client.client import *
dfs_client = Fdfs_client('conf/dfs.conf')
def convert_url(raw_img_url):
    response = requests.get(raw_img_url, proxies=const.PROXIES)
    pic_buffer = response.content
    pic_ext = raw_img_url.split('.')[-1]
    response = dfs_client.upload_by_buffer(pic_buffer, pic_ext)
    dfs_img_url = const.DFS_BASE_URL + '/' + response['Remote file_id']
    return dfs_img_url

其中dfs.conf文件中,主要就是配置一下 tracker_server

日志处理

这里使用配置文件的方式处理日志,类似JAVA中的log4j吧,首先新建一个log.conf

[loggers]
keys=root

[handlers]
keys=stream_handler,file_handler

[formatters]
keys=formatter

[logger_root]
level=DEBUG
handlers=stream_handler,file_handler

[handler_stream_handler]
class=StreamHandler
level=DEBUG
formatter=formatter
args=(sys.stderr,)

[handler_file_handler]
class=FileHandler
level=DEBUG
formatter=formatter
args=('logs/pspider.log','a','utf8')

[formatter_formatter]
format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s

这里通过配置handlers,可以同时将日志打印到stderr和文件。
注意args=('logs/pspider.log','a','utf8') 这一行,用来解决文本文件中的中文乱码问题。

日志初始化:

import logging
from logging.config import fileConfig

fileConfig('conf/log.conf')

日志打印:

logging.info("test")

完整源码

到此为止,就是如何用Python3写一个爬虫的全部过程了。
采集不同的站点,肯定是要有不同的处理,但方法都是大同小异。
最后,将源码做了部分裁剪,分享在了GitHub上。
https://github.com/xiiiblue/pspider

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,289评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,566评论 18 139
  • HTTP cookie(也称为web cookie,网络cookie,浏览器cookie或者简称cookie)是网...
    留七七阅读 17,793评论 2 71
  • 遇见 夏至已至 流火七月 任暴风骤雨猝然来袭 任骄阳烈日烘烤大地 初心不忘 激情持续 让灿烂笑容从心底洋溢 世间事...
    梁子仗剑走天下阅读 572评论 3 11
  • 今天午睡,从13点一直睡到18点,睡得天昏地暗、迷迷糊糊、不分白昼,醒了之后状态也不是很好,不太精神,脑子像浆糊,...
    老鹿在跑步阅读 320评论 0 1