描述

上一篇(一个六年经验的python后端是怎么学习用java写API的(1)设定场景,选择框架dropwizard)确定需求后,第一步需要实现实现 Extracter 模块来抓取微信文章,代码为 pirate

pirate 是由我的 django 脚手架 original 实现的,文件上传提供了七牛和腾讯云两个 backend,部署提供了默认的配置文件,因此只要关注具体的微信的抓取逻辑即可。

核心表讲解

pirate/original/extracter/models.py

微信文章表,正常设计,大字段拆表。

 13 class WXArticle(TimeStampedModel):
 14     raw_url = models.CharField(max_length=1023)
 15     title = models.CharField(max_length=255)
 16     cover = models.CharField(max_length=255)
 17     description = models.CharField(max_length=255, default='')
 18     is_active = models.BooleanField(default=False)
 19     is_delete = models.BooleanField(default=False)
 
 
 98 class WXArticleContent(TimeStampedModel):
 99     article_id = models.IntegerField(unique=True)
100     content = models.TextField()
101     body_script = models.TextField(default='')

pirate/original/extracter/models.py

图片上传缓存表,因为有时候会重复抓取一篇文章(例如编辑后需要重新写文章),而每次抓取文章时会将里面的图片上传到我们的 cdn,添加上传文件缓存表来减少重复的图片上传操作。

188 class URLFileUploadCache(TimeStampedModel):
189     raw_url = models.CharField(blank=True, default='', max_length=512)
190     url = models.CharField(blank=True, default='', max_length=255)
191     raw_url_md5 = models.CharField(blank=True, default='', max_length=64, db_index=True)

抓取

观察微信文章发现,文章 div id=js_content 为文章正文,其中标题和 cover 图在 meta 里面,使用 requests 抓取文章原文,BeautifulSoup 过滤需要的 dom,正则找出需要替换的图片,upload_handler 选择七牛去上传图片并且替换掉原图地址。

 21     @classmethod
 22     def spider_url(cls, raw_url):
 23         assert raw_url.startswith('https://mp.weixin.qq.com/') or raw_url.startswith('http://mp.weixin.qq.com/')
 24         content = tools.spider_request(raw_url)
 25         description = content_text = title = cover = ''
 26         body_script = ''
 27         if content:
 28             b = BeautifulSoup(content, 'html.parser')
 29             content_dom = b.find('div', id='js_content')
 30             title_dom = b.find('meta', property="og:title")
 31             cover_dome = b.find('meta', property="og:image")
 32             if title_dom:
 33                 title = title_dom.attrs['content']
 34             if cover_dome:
 35                 cover = cover_dome.attrs['content']
 36                 raw_url = cover
 37                 upload_cache = URLFileUploadCache.get_cached_objects(raw_url, get_last=True)
 38                 if upload_cache:
 39                     cover = upload_cache[0].url
 40                 else:
 41                     upload_data = tools.upload_file_from_url(raw_url)
 42                     url = upload_data['url']
 43                     if url:
 44                         URLFileUploadCache.new_cache(raw_url, url)
 45                         cover = url
 46             if content_dom:
 47                 content_text = unicode(content_dom)
 48                 ss = b.body.find_all('script')
 49                 body_script = ''.join(unicode(s) for s in ss)
 50                 urls = []
 51                 for _re in constants.WEIXIN_IMAGE_RES:
 52                     urls.extend(_re.findall(content_text))
 53                 descriptions = constants.WEIXIN_DESCRIPTION_RE.findall(content)
 54                 if descriptions:
 55                     description = BeautifulSoup(descriptions[0]).meta.attrs.get('content', '')
 56                 mapper = {}
 57                 for raw_url in urls:
 58                     if raw_url in mapper:
 59                         continue
 60                     upload_cache = URLFileUploadCache.get_cached_objects(raw_url, get_last=True)
 61                     if upload_cache:
 62                         mapper[raw_url] = upload_cache[0].url
 63                     else:
 64                         upload_data = tools.upload_file_from_url(raw_url, )
 65                         url = upload_data['url']
 66                         if url:
 67                             URLFileUploadCache.new_cache(raw_url, url)
 68                             mapper[raw_url] = url
 69                 for raw_url, url in mapper.iteritems():
 70                     if not url:
 71                         continue
 72                     content_text = content_text.replace(raw_url, url)
 73         return {
 74             'raw_url': raw_url,
 75             'title': title,
 76             'cover': cover,
 77             'content': content_text,
 78             'description': description,
 79             'body_script': body_script,
 80         }
 
 

上传文件到七牛的 upload_handler

 13 POLICY = settings.FILE_CALLBACK_POLICY or {
 14     'callbackUrl': settings.FILEUPLOAD_CALLBACK_URL,
 15     'callbackBody': 'bucket=$(bucket)&key=$(key)&filename=$(fname)&filesize=$(fsize)',
 16     'insertOnly': 1,
 17 }
 18
 19
 20 class UploadHandler(object):
 21
 22     def __init__(self, key, secret, bucket):
 23         self.key = key
 24         self.secret = secret
 25         self.bucket = bucket
 26         self.backend = qiniu
 27         self._backend_auth = Auth(self.key, self.secret)
 28
 29     def _upload_token(self, key, expires=3600, policy=None):
 30         token = self._backend_auth.upload_token(self.bucket, key=key, expires=expires, policy=policy)
 31         return token
 32
 33     def upload_file(self, key, data, policy=None, fname='file_name'):
 34         if policy is None:
 35             policy = POLICY
 36         uptoken = self._upload_token(key, policy=policy)
 37         return self.backend.put_data(uptoken, key, data, fname=fname)
 38
 39     def get_download_url(self, key):
 40         return '{}{}'.format(settings.FILE_DOWNLOAD_PREFIX, key)
 
 

CDN 配置

QQ20200216-234519@2x.png

  • 创建一个公开的存储空间,名字即 FILE_UPLOAD_BUCKET

QQ20200216-234809@2x.png

  • 为了让域名好看一些,配置 CDN 加速域名,根据步骤操作即可(有一些 dns 的操作)。FILE_DOWNLOAD_PREFIX

image.png

  • 个人中心秘钥管理获取 upload_handler 需要的 FILE_UPLOAD_KEY、FILE_UPLOAD_SECRET

image.png

执行部署

  • 创建对应 mysql 数据库
  • 创建 virtualenv,source 后 pip install 项目路径下的 requirements/base.txt
  • 创建 config/settings/private_production.py,见下面代码段
  • 数据库 migrate,cd 到 original 目录,执行./manage.py migrate --settings=config.settings.production
  • 启动项目 ./manage.py runserver 0.0.0.0:8976 (端口号随意)
  • 当然建议用 supervisor + nginx,见deploy目录下的相关例子
    import os
    
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'dbname', # 改成你的数据库名字
            'HOST': os.environ.get('ORIGINAL_MYSQL_HOST', 'localhost'), # 数据库host
            'USER': os.environ.get('ORIGINAL_MYSQL_USER', 'db_username'), # 改成你的数据库user
            'PASSWORD': os.environ.get('ORIGINAL_MYSQL_PASSWORD', 'db_password'), # 改成你的数据库password
            'PORT': os.environ.get('ORIGINAL_MYSQL_PORT', 3306),
            'OPTIONS': {'charset': 'utf8mb4'},
        }
    }

    FILE_UPLOAD_BACKEND = 'qiniu'
    FILE_UPLOAD_KEY = 'i6fdSECQjLfF' # 改成你的qiniu key,现在这里是假的
    FILE_UPLOAD_SECRET = 'adfiuerqp' # 改成你的qiniu secret
    FILE_UPLOAD_BUCKET = 'reworkdev' # 改成你的qiniu bucket
    FILE_CALLBACK_POLICY = {}
    FILE_DOWNLOAD_PREFIX = '' # 改成你的host, 例如 http://cdn.myhost.com/

    FILEUPLOAD_CALLBACK_URL = # 改成你自己host的对应地址, 例如 https://www.myhost.com/api/v1/file/upload/callback/

postman 测试接口

4444.png

查看数据库

QQ20200217-001013@2x.png

QQ20200217-001037@2x.png

接下来

基本物料接口准备就绪,下面就可以正式开始 Java 框架的学习。

一个六年经验的python后端是怎么学习用java写API的(3)Java 开发环境搭建


D咄咄
1.7k 声望257 粉丝

Life is to short, please use python.