爬取网站思路

分析网站

爬取一个网站首先要了解这个网站,可以从以下几点去分析你要爬取的网站。

爬取内容

分析你需要哪些内容? 标题,文本内容,链接,tag,图片等等。

分析页面

分析你的入口页面,入口页面中要解析的url,如何进入下一页(深搜,广搜),如何循环。

破防

  • 头文件设置,header,UA
  • 是否使用cookies
  • 验证码如何应对
  • 有没有ip限制,接口调用频次限制

考虑存储设计

考虑用哪种方式存储数据?

  • json
  • mysql
  • ElasticSearch

新建scrapy

新建爬虫虚拟环境

mkvirtualenv my_spider --python==python3.7/python.exe
workon my_spider

安装 scrapy

pip install scrapy

运行命令生成scrapy及爬虫模板

scrapy startproject Lagou_Spider
scrapy genspider Lagou www.lagou.com

把生成的文件放对位置

新建main方法

# _*_ coding: utf-8 _*_

from scrapy.cmdline import execute

import sys
import os

print(os.path.dirname(os.path.abspath(__file__)))  # 打印目录
sys.path.append(os.path.dirname(os.path.abspath(__file__)))  # 寻找项目工程目录
execute(["scrapy", "crawl", "lagou"])

写爬虫

用选择器定位元素

使用chrome找

打开 chrome 按F12,慢慢找

scrapy shell 调试

scrapy shell http://某某网址.com

有些网址有反爬虫机制,会返回403,可以添加-s USER_AGENT='Mozilla/5.0'

scrapy shell -s USER_AGENT='Mozilla/5.0' https://movie.douban.com/subject/1292052/

写Item(scrapy.Item)

字段 = scrapy.Field()

字段处理

create_date = scrapy.Field(
        input_processor=MapCompose(date_convert),
    )

对数据处理写出方法,还能复用,美滋滋,

MapCompose()方法可以接受多个方法

插入数据的sql写成方法

get_insert_sql()

def get_insert_sql(self):
        insert_sql = """
            insert into jobbole_article(title, url, create_date, fav_nums)
            VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE content=VALUES(fav_nums)
        """
        params = (self["title"], self["url"], self["create_date"], self["fav_nums"])

        return insert_sql, params

写入ES的方法(选用)

实例化ES的model

def save_to_es(self):
        article = ArticleType()
        article.title = self['title']
        article.create_date = self["create_date"]
        article.content = remove_tags(self["content"])  # 删掉html的tag
        article.front_image_url = self["front_image_url"]
        if "front_image_path" in self:
            article.front_image_path = self["front_image_path"]
        article.praise_nums = self["praise_nums"]
        article.fav_nums = self["fav_nums"]
        article.comment_nums = self["comment_nums"]
        article.url = self["url"]
        article.tags = self["tags"]
        article.meta.id = self["url_object_id"]

        article.suggest = gen_suggests(ArticleType._doc_type.index, ((article.title,10),(article.tags, 7)))

        article.save()

        redis_cli.incr("jobbole_count")

        return




def gen_suggests(index, info_tuple):
    #根据字符串生成搜索建议数组
    used_words = set()
    suggests = []
    for text, weight in info_tuple:
        if text:
            #调用es的analyze接口分析字符串
            words = es.indices.analyze(index=index, analyzer="ik_max_word", params={'filter':["lowercase"]}, body=text)
            anylyzed_words = set([r["token"] for r in words["tokens"] if len(r["token"])>1])
            new_words = anylyzed_words - used_words
        else:
            new_words = set()

        if new_words:
            suggests.append({"input":list(new_words), "weight":weight})

    return suggests

如果有对每一个字段都需要处理的方法可以 重写ItemLoader

class ArticleItemLoader(ItemLoader):
    #自定义itemloader
    default_output_processor = TakeFirst()

如果不需要初始方法可以用 output_processor

return_value()

def return_value(value):
    return value

output_processor

front_image_url = scrapy.Field(
        output_processor=MapCompose(return_value)
    )

使用Item

实例化

item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response)

加载

def parse_detail(self, response):
front_image_url = response.meta.get("front_image_url", "")  # 文章封面图
        item_loader = ArticleItemLoader(item=JobBoleArticleItem(), response=response)
        item_loader.add_css("title", ".entry-header h1::text")
        item_loader.add_value("url", response.url)
        item_loader.add_value("url_object_id", get_md5(response.url))
        item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text")
        item_loader.add_value("front_image_url", [front_image_url])
        item_loader.add_css("praise_nums", ".vote-post-up h10::text")
        item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")
        item_loader.add_css("fav_nums", ".bookmark-btn::text")
        item_loader.add_css("tags", "p.entry-meta-hide-on-mobile a::text")
        item_loader.add_css("content", "div.entry")

        article_item = item_loader.load_item()


        yield article_item

中间件(选用)

  • 随机UA
  • 随机IP代理

管道

默认的管道

class ArticlespiderPipeline(object):
    def process_item(self, item, spider):
        return item

自定义json文件的导出

class JsonWithEncodingPipeline(object):
    #自定义json文件的导出
    def __init__(self):
        self.file = codecs.open('article.json', 'w', encoding="utf-8")
    def process_item(self, item, spider):
        lines = json.dumps(dict(item), ensure_ascii=False) + "\n"
        self.file.write(lines)
        return item
    def spider_closed(self, spider):
        self.file.close()

采用同步的机制写入mysql

class MysqlPipeline(object):
    #采用同步的机制写入mysql
    def __init__(self):
        self.conn = MySQLdb.connect('192.168.0.106', 'root', 'root', 'article_spider', charset="utf8", use_unicode=True)
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        insert_sql = """
            insert into jobbole_article(title, url, create_date, fav_nums)
            VALUES (%s, %s, %s, %s)
        """
        self.cursor.execute(insert_sql, (item["title"], item["url"], item["create_date"], item["fav_nums"]))
        self.conn.commit()

采用异步的机制写入mysql

class MysqlTwistedPipline(object):
    def __init__(self, dbpool):
        self.dbpool = dbpool

    @classmethod
    def from_settings(cls, settings):
        dbparms = dict(
            host = settings["MYSQL_HOST"],
            db = settings["MYSQL_DBNAME"],
            user = settings["MYSQL_USER"],
            passwd = settings["MYSQL_PASSWORD"],
            charset='utf8',
            cursorclass=MySQLdb.cursors.DictCursor,
            use_unicode=True,
        )
        dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)

        return cls(dbpool)

    def process_item(self, item, spider):
        #使用twisted将mysql插入变成异步执行
        query = self.dbpool.runInteraction(self.do_insert, item)
        query.addErrback(self.handle_error, item, spider) #处理异常

    def handle_error(self, failure, item, spider):
        #处理异步插入的异常
        print (failure)

    def do_insert(self, cursor, item):
        #执行具体的插入
        #根据不同的item 构建不同的sql语句并插入到mysql中
        insert_sql, params = item.get_insert_sql()
        cursor.execute(insert_sql, params)

调用scrapy提供的json export导出json文件

class JsonExporterPipleline(object):
    #调用scrapy提供的json export导出json文件
    def __init__(self):
        self.file = open('articleexport.json', 'wb')
        self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)
        self.exporter.start_exporting()

    def close_spider(self, spider):
        self.exporter.finish_exporting()
        self.file.close()

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

保存图片

class ArticleImagePipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        if "front_image_url" in item:
            for ok, value in results:
                image_file_path = value["path"]
            item["front_image_path"] = image_file_path

        return item

将数据写入到es中

class ElasticsearchPipeline(object):
    #将数据写入到es中

    def process_item(self, item, spider):
        #将item转换为es的数据
        item.save_to_es()

        return item

settings.py配置

UA?

USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"

中间件

settings

DOWNLOADER_MIDDLEWARES = {
   #'douban250.middlewares.RandomProxyMiddleware': 543,
   'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 2,
}

middleware

# 随机UA
class RandomUserAgentMiddlware(object):
    #随机更换user-agent
    def __init__(self, crawler):
        super(RandomUserAgentMiddlware, self).__init__()
        self.ua = UserAgent()
        self.ua_type = crawler.settings.get("RANDOM_UA_TYPE", "random")

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request, spider):
        def get_ua():
            return getattr(self.ua, self.ua_type)

        request.headers.setdefault('User-Agent', get_ua())

遵循robots.txt协议?

ROBOTSTXT_OBEY = False

下载延时?

DOWNLOAD_DELAY = 10
RANDOMIZE_DOWNLOAD_DELAY = True

使用cookies?

COOKIES_ENABLED = True
COOKIES_DEBUG = True

下载中间件?

DOWNLOADER_MIDDLEWARES = {
    # 'ArticleSpider.middlewares.JSPageMiddleware': 1,
   # 'ArticleSpider.middlewares.RandomUserAgentMiddlware': 543,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 2,
}

管道选用?

ITEM_PIPELINES = {
   # 'ArticleSpider.pipelines.JsonExporterPipleline': 2,
   # # 'scrapy.pipelines.images.ImagesPipeline': 1,
   #  'ArticleSpider.pipelines.ArticleImagePipeline': 1,
   #  'ArticleSpider.pipelines.MysqlTwistedPipline': 1,
    'ArticleSpider.pipelines.ElasticsearchPipeline': 1
}

图片字段名?

IMAGES_URLS_FIELD = "front_image_url"

项目目录

project_dir = os.path.abspath(os.path.dirname(__file__))

图片下载路径

IMAGES_STORE = os.path.join(project_dir, 'images')

mysql

MYSQL_HOST = "127.0.0.1"
MYSQL_DBNAME = "article_spider"
MYSQL_USER = "root"
MYSQL_PASSWORD = "root"

时间格式方式

SQL_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
SQL_DATE_FORMAT = "%Y-%m-%d"