Python爬虫实例- CSDN博客爬虫

Python爬虫实例: CSDN博客爬虫

Tags: Python 爬虫


0x00 前言

关于爬虫原理、web基础等内容,请自行搜索。自己总结过一份,但觉得火候还不够,知道的内容感觉没什么必要写,想写的又总结不出来。
在这推荐一份教程:Python爬虫入门教程。

下面将用Python实现CSDN博客爬虫:输入用户ID,将该用户所有博文存至本地数据库。

0x01 页面源码分析

CSDN博客地址URL结构为”http://blog.csdn.net/“+用户id,博客首页默认为摘要视图。

点击第二页,易得某页的域名格式为”http://blog.csdn.net/“+用户id+”/article/list/”+页码

知道总页数后,即可自己构建所有目录页的URL。获取页数可从页面下方的统计数据获得

源码为

通过正则匹配即可,具体为

"([0-9]+)条数据"

在页面的HTML源码中,与博文有关的部分如下

每个

元素对应一个博文的摘要。
进一步查看

每个list_item元素中有三个有用的子元素:

  • article_title
  • article_description
  • article_manage

article_title中有博文的相对地址,博文标题。
article_description中为博文摘要。
article_manage中右下角的时间、阅读次数、评论次数

从article_title中提取出相对地址

'<span class="link_title"><a href="([\s\S]*?)">\s*([\s\S]*?)\s*a>span>', 

[\s\S]表示任意字符,如果有其他简洁的写法还请留言告知:P


元素对应博文内容,将这一段内容提取出即可。

'(<div id="article_details" class="details">[\s\S]*?div>)\s*
class="blog-associat-tag">'

为紧挨博文
的下一元素。

0x02 源码及注解

from optparse import OptionParser 
import queue
import re
import sqlite3
import threading
import urllib.request
import time
import ThreadPool

class Store(object):
    """结果存储类

    每次操作完成后将结果存入队列,最后把结果统一存进数据库
    """
    def __init__(self, db_name, table_name="blog"):
        """初始化函数

        Args:
            db_name: 数据库文件名
            table_name: 数据库表名
        """
        threading.Thread.__init__(self)
        self._work_queue = queue.Queue()
        if db_name != None:
            self._dbname = db_name
        else:
            self._dbname = "default"+str(int(time.time()))+".db"
        self._tablename = table_name

    def add_item(self, title, user_id, content):
        """添加一条记录

        Args:
            title: 博文标题
            user_id: 博文作者id
            content: 博文内容
        """
        self._work_queue.put((title, user_id, content))

    def store(self):
        """将所有记录存至数据库
        """
        conn = sqlite3.connect(self._dbname)
        c = conn.cursor()
        try:
            c.execute(("CREATE TABLE %s "
                       "(TITLE TEXT PRIMARY KEY NOT NULL, "
                       " USER_NAME TEXT NOT NULL, "
                       " CONTENT TEXT NOT NULL)"%self._tablename))
        except:
            pass
        purchases = []
        while self._work_queue.qsize() != 0:
            purchases.append(self._work_queue.get())
        c.executemany('INSERT INTO %s VALUES (?,?,?)'%self._tablename, 
                      purchases)
        conn.commit()
        conn.close()
        print("store end. dbfile name is %s" % (self._dbname))


def Download_URL(url, headers={}):
    """下载指定url页面

    Args:
        url: 要下载页面的URL
        headers: headers为字典结构。键为HTTP请求的消息报头名,值为对应的报头值

    Returns:
        页面下载成功,返回对应的HTML。

    Raises:
        None
    """
    req = urllib.request.Request(urllib.request.quote(url, ":?=/&%"), 
                                 headers=headers)
    while True:
        try:
            response = urllib.request.urlopen(req, timeout=5)  # timeout设置超时时间
            html = response.read().decode("utf-8") 
            print("success: " + req.full_url)
            return html
        except Exception as reason:
            print(reason)

def Store_Article(title, content):
    """存储文章

    Args:
        title: 文章标题
        content: 文章内容
    """
    global user_id
    global store

    # 新增一条要存储的结果
    store.add_item(title, user_id, content)

def Download_Article(param):
    """下载博文

    Args:
        param: 字典。url键为博文地址,title键为文章标题
    """
    global user_id, headers   
    article_url = param['url']
    article_title = param['title']

    # 通过正则匹配,返回博文内容。
    # 随后保存博文内容,标题为:[作者ID]+博文名
    article_html = Download_URL(article_url, headers)
    content = re.findall('(
' '[\s\S]*?
)'
'\s*
', article_html)[0] Store_Article("[" + user_id + "]" + article_title, content) def Download_Page(param): """下载目录页 Args: param: 字典。url键中存有目录页的URL地址。 """ global headers, host global thread_pool page_url = param['url'] # 正则匹配,返回所有文章的链接和标题。格式为:(链接, 标题) # 随后下载每篇博文 page_html = Download_URL(page_url, headers) article_list = re.findall('' '' '\s*([\s\S]*?)\s*', page_html) for article in article_list: param = { 'url': host + article[0], 'title': re.sub(r'[/:*?<|\\]', '', article[1]) + ".html" } thread_pool.add_work(Download_Article, param) def Download_Front_Page(param): """下载博客首页 Args: param: 字典。url键为博客首页地址。 """ global headers, user_id global thread_pool front_page_url = param["url"] # 通过正则匹配,获得页数,博文数。 # 随后下载所有目录页 html = Download_URL(front_page_url, headers) article_count = int(re.findall("([0-9]+)条数据", html)[0]) page_count = int(re.findall("共([0-9]+)页", html)[0]) print("id: " + user_id + "\n" + "article count: " + str(article_count) + "\n" + "page count: " + str(page_count)) for page_num in range(1, page_count+1): param = { "url": front_page_url + "/article/list/" + str(page_num) } thread_pool.add_work(Download_Page, param) def option_parser(): """解析命令行参数 Returns: (user_id, thread_count, db_name) 分别为用户ID,线程池线程个数,数据库文件名 """ user_id = None thread_count = 1 db_name = None parser = OptionParser() parser.add_option("-i", "--id", dest="user_id", help="blog owner id", metavar="[user id]") parser.add_option("-t", "--thread", dest="thread_count", help="worker thread count", metavar="[count of thread]") parser.add_option("-f", "--filename", dest="dbfile", help="data file name", metavar="[data file name]") (options, args) = parser.parse_args() if options.user_id != None: user_id = options.user_id else: print("eg: blog_backup.py -i csdn_id") exit() if options.thread_count != None: thread_count = int(options.thread_count) if options.dbfile != None: db_name = options.dbfile return (user_id, thread_count, db_name) def main(): global thread_pool, store global headers, host, user_id user_id, thread_count, db_name = option_parser() store = Store(db_name) thread_pool = ThreadPool.Thread_Pool(thread_count) host = "http://blog.csdn.net" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36" " (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" } front_page_url = host + "/" + user_id param = { "url": front_page_url } # 向任务队列添加下载博客首页的任务 # 随后会自动解析下载所有目录页,进而解析下载所有博文 # 当所有下载任务均结束后,队列为空。在任务进行时队列不会为空。 # 所以可以通过等待队列为空进行线程同步 thread_pool.add_work(Download_Front_Page, param) thread_pool.wait_queue_empty() store.store() print("---end---") if __name__ == "__main__": main()

Python版本为3.4.2
ThreadPool为自己实现的线程池,详见Python线程池简单实现
通过命令行运行时,指定用户ID。程序先下载博客首页,获取博客页数,随后构建每一页的URL地址并下载,随后分析目录页源码,得到每篇博文的题目和URL,下载保存。
下载页面时,需指定User-Agent字段。网上说这是因为robots.txt文件中显示禁止任何爬虫。但是,访问robots文件如下:

User-agent: *
Disallow: /scripts
Disallow: /public
Disallow: /css/
Disallow: /images/
Disallow: /content/
Disallow: /ui/
Disallow: /js/
Disallow: /scripts/

Sitemap: http://blog.csdn.net/sitemap-index-1.xml

根据wiki百科的内容,robots协议并不具有强制性:

robots.txt协议并不是一个规范,而只是约定俗成的,所以并不能保证网站的隐私。
这个协议也不是一个规范,而只是约定俗成的,有些搜索引擎会遵守这一规范,而其他则不然。通常搜索引擎会识别这个元数据,不索引这个页面,以及这个页面的链出页面。

而且上述rotbots.txt文件只不允许访问特定目录,这些目录与博客首页如何产生联系,现在还没搞清。或许无法访问与robots协议并无关系?

0x03 Tips

博客搬家应用(Java):http://m.oschina.net/blog/194507
自己动手编写CSDN博客备份工具(C++):http://blog.csdn.net/gzshun/article/category/932960
简单网络爬虫抓取博客文章及思想介绍(Python):http://blog.csdn.net/Eastmount/article/details/39770543

大部分博文是在2012年写的,感觉自己在玩别人玩剩下的= =
欢迎讨论:)

你可能感兴趣的:(python)