安装 pipenv 依赖和包管理工具

brew install pipenv

切换 pip ( pypi 源为国内 USTC.EDU 中科大)

mkdir ~/.pip
cd .pip
vim pip.conf
# 填充以下内容
[global]
timeout = 60
index-url = https://mirrors.ustc.edu.cn/pypi/web/simple

建立项目目录

mkdir project-directory
cd project-directory
pipenv install
# ------------- 终端会反馈以下输出
# Creating a virtualenv for this project…
# Pipfile: /project-directory/Pipfile
# Using /usr/local/opt/python/bin/python3.7 (3.7.4) to create virtualenv…
# -------------

# 修改项目的 pypi 源地址为国内
vim Pipefile

#修改为以下设置

[[source]]
name = "pypi"
url = "https://mirrors.ustc.edu.cn/pypi/web/simple"
verify_ssl = true

[dev-packages]

[packages]
pymysql = "*"
pathlib = "*"

[requires]
python_version = "3.7

进入子 shell 环境

pipenv shell

注意: 如果不进入子 shell,则 pipenv 会调用系统安装的默认 python 的 pip 来安装库,比如 OSX 默认就是 2.7版本,这样就会爆出一堆的依赖错误问题。

进入子 shell 之后便可以方便使用 pipenv install 来管理项目使用的模块了, 比如安装 scrapy 的命令就是 pipenv install scrapy

其实一直一来早都想把环境切换到 Nginx ,因为即使 Apache 运行在 event 模式下,受限于自身的架构,其性能的表现和资源占用的情况依旧无法和 Nginx 站在同一基准线上。而促使我做这件事的原因很简单,Let's Encrypt的证书过期了,厌倦了每三个月去 manual renew 证书一次,遂将证书的管理转到 Acme.sh, 后者支持阿里云的 DNS api,可以方便的自动 renew 证书。证书的生成和配置很简单,然而却出现了另外一个问题,Apache 无法支持 ACME 获得证书,这里大概是因为证书使用了另外一种加密方式,而 Apache 无法 Cipher。我没有纠结于证书的类型和加密方式,对我来说自动续签更加重要。既然 Apache 不行,正好借此机会换到 Nginx。

由于以前的 LAMP 早已安装和配置好,这里需要做的工作就是安装 Nginx 和配置就好。Nginx 的安装就不说了,现在 Reposity 的源本身已经很安全,个人不必再迷信手动编译,因此我全部用的都是官网的包安装的,快速方便。PHP 我这里用的是 7.2版本。 以下就直接贴出配置文件。

php-fpm + php pool
# 查看 php-fpm 配置
# cat /etc/php/7.2/fpm/php-fpm.conf | grep -v "^;" | grep -v '^$'
[global]
pid = /run/php/php7.2-fpm.pid
error_log = /var/log/php7.2-fpm.log
include=/etc/php/7.2/fpm/pool.d/*.conf

# 查看 Pool 的配置
# cat /etc/php/7.2/fpm/pool.d/site-name.conf | grep -v "^;" | grep -v '^$'
[site-name]
user = www-data
group = www-data
listen = /run/php/site-name.sock
listen.owner = www-data
listen.group = www-data
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
request_terminate_timeout = 300s
request_slowlog_timeout = 10s
chdir = /var/www/html/site-name.com/public_html
slowlog = /var/www/html/site-name.com/logs/$pool.log.slow
php_flag[display_errors] = off
php_admin_value[display_errors]=0
php_admin_value[display_startup_errors]=0
php_admin_value[html_errors]=0
php_admin_value[define_syslog_variables]=0
php_admin_flag[file_uploads]=1
php_admin_flag[log_errors] = on
php_admin_value[log_errors]=1
php_admin_value[upload_tmp_dir]="/var/tmp"
php_admin_value[upload_max_filesize]="4M"
php_value[max_input_time]="120"
php_admin_value[max_input_time]=120
php_value[max_execution_time]="300"
php_admin_value[post_max_size]="4M"
php_value[session.gc_maxlifetime]=3600
php_admin_value[session.gc_probability]=1
php_admin_value[session.gc_divisor]=100
php_admin_value[error_log] = /var/log/fpm-php/site-name.log
php_admin_value[memory_limit] = 64M
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = off
php_admin_value[magic_quotes_gpc]=0
php_admin_value[register_globals]=0
php_admin_value[session.auto_start]=0
php_admin_value[mbstring.http_input]="pass"
php_admin_value[mbstring.http_output]="pass"
php_admin_value[mbstring.encoding_translation]=0
php_admin_value[expose_php]=0
php_admin_value[allow_url_fopen]=1
php_admin_value[safe_mode]=0
php_admin_value[cgi.fix_pathinfo]=1
php_admin_value[cgi.discard_path]=0

# Nginx 的配置分为 2 部分,Nginx全局环境 + Vhost 配置

# Nginx 全局配置

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
    worker_connections 768;
    # multi_accept on;
}
http {
    ##
    # Basic Settings
    ##
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    # server_tokens off;
    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    ##
    # SSL Settings
    ##
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;
    ##
    # Logging Settings
    ##
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    ##
    # Gzip Settings - 这里是比较重要的地方
    ##
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
  gzip_min_length 1100;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript font/truetypef font/opentype application/vnd.ms-fontobject text/x-component image/svg+xml;
 gzip_static on;
    ##
    # Virtual Host Configs
    ##
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

# Nginx - 针对 Drupal 站点的 vhost 配置
server {
    listen 80;
    server_name www.site-name.com site-name.com;
    add_header Strict-Transport-Security max-age=15768000;
    #永久重定向到 https 站点
    return 301 https://$server_name$request_uri;
    }

server {
    listen 443 ssl;
    server_name www.site-name.com site-name.com;
    root /var/www/html/site-name.com/public_html;
    keepalive_timeout   120;
    #证书文件
    ssl_certificate     /etc/certs/site-name.com/fullchain.pem;
    #私钥文件
    ssl_certificate_key /etc/certs/site-name.com/key.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    access_log /var/www/html/site-name.com/logs/access.log;
    error_log /var/www/html/site-name.com/logs/error.log;

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Very rarely should these ever be accessed outside of your lan
    location ~* \.(txt|log)$ {
        allow 192.168.0.0/16;
        deny all;
    }

    location ~ \..*/.*\.php$ {
        return 403;
    }

    location ~ ^/sites/.*/private/ {
        return 403;
    }

    # Block access to scripts in site files directory
    location ~ ^/sites/[^/]+/files/.*\.php$ {
        deny all;
    }

    # Allow "Well-Known URIs" as per RFC 5785
    location ~* ^/.well-known/ {
        allow all;
    }

    # Block access to "hidden" files and directories whose names begin with a
    # period. This includes directories used by version control systems such
    # as Subversion or Git to store control files.
    location ~ (^|/)\. {
        return 403;
    }

    location / {
        # try_files $uri @rewrite; # For Drupal <= 6
        try_files $uri /index.php?$query_string; # For Drupal >= 7
    }
    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;
    }
    # Don't allow direct access to PHP files in the vendor directory.
    location ~ /vendor/.*\.php$ {
        deny all;
        return 404;
    }
    location ~ '\.php$|^/update.php' {
        # Ensure the php file exists. Mitigates CVE-2019-11043
        try_files $uri =404;
        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
        include fastcgi_params;
        # Block httpoxy attacks. See https://httpoxy.org/.
        fastcgi_param HTTP_PROXY "";
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_intercept_errors on;
        fastcgi_pass unix:/run/php/site-name.sock;
    }
    location ~ ^/sites/.*/files/styles/ { # For Drupal >= 7
        try_files $uri @rewrite;
    }
    location ~ ^(/[a-z\-]+)?/system/files/ { # For Drupal >= 7
        try_files $uri /index.php?$query_string;
    }
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;
    }
   # # Enforce clean URLs
    # Removes index.php from urls like www.example.com/index.php/my-page --> www.example.com/my-page
    # Could be done with 301 for permanent or other redirect codes.
    if ($request_uri ~* "^(.*/)index\.php(.*)") {
        return 307 $1$2;
    }
}

基于http 的 web 程序,由于其无状态的属性,很多时候并不是很适合循环定时执行的任务。Drupal 本身对于这种需求有多种解决方案,比如核心的 hook_cron, 第三方的现有模块 Utilimate Cron, Ellisa Cron, Scheduler Job等。然而核心 API 和现有模块的问题在于构建在在 drupal 的 cron 之上,本身核心的 cron 已经负担足够沉重,比较轻量的或者时间跨度比较长的任务比较适合通过这些途径来实现。当任务是长时间短间隔高频率循环执行的时候,通过这种方式很可能让核心的 cron 负担过于称重,一方面影响系统的稳定性,同时也会影响本身需求目标的实现。

以上解决办法主要是针对 shared host 用户,由于这一类用户没有完整的 Unix/Linux 主机权限,因此无法利用 Unix/Linux 核心自带的 crontab 带来的稳定性和遍历性。话说9102年都已经快结束了,这年头还没上云的用户或者还在用 sharedhost 跑 drupal 的用户不多了吧。

我要跑的任务是高频率定时循环执行的,所以以上的解决方案并不能很好的满足我的需求。

废话不多说,上解决办法。


解决方案1 - Crontab

思路如下

  1. 根据任务的需求定义路径
  2. 使用 crontab + curl 的方式请求路径,调用后台的 callback 执行相关的任务
  3. 这里需要注意的是需要做请求验证,验证其是否来自 127.0.0.1, token 是否正确,否则返回 403 错误

针对不同时段需要执行的任务,可以在 web 界面输出格式化的 crontab 列表,然后直接进入 shell 界面将这些任务加入 Linux 的 crontab 列表中即可

这里需要区分两种任务类型,一种是定时执行如每天12点钟运行,一种是循环执行如每隔10分钟执行一次。

比如说,每天的早上8点和晚上8点各运行一次更新任务,更新最新的产品信息和行业信息。

如何设计逻辑?(这里只针对目前手上已经有处理好的产品信息和行业信息的情况)

~~这里就需要用到 Drupal 核心的 Queue API + Utimate Cron API ~~
之前使用的是 Utimate Cron,这个模块的问题在于官方的文档实在是太烂了,使用他的最简设置是没问题的,比如我要使用数据库 logger 而非默认的缓存 logger, 该如何配置文档中没有说明。说是可以直接对接 drupal 的 queue, 我没有找到具体的设置方法。

考虑到快速上手解决问题,最终选取了 Queue API + Elysia Cron, 这里选用 Queue 的主要原因是当任务的负载很重且执行的时间很长,可能会超过PHP的 max_execution_time 时,使用队列的处理方式,每次执行一个任务或一小批任务,不至于进程卡死。

具体使用方式如下:

# 1.  使用  hook_cronapi() 定义定时任务,注意这里我的模块名是 jobs_every_minute
 /**
  * Implements hook_cronapi().
  */

  function jobs_every_minute_cronapi($op, $job = NULL){
    $items = array();
    $items['ec_job_test'] = array(
      'description' => 'Send nid to Queue',
      'rule' => '* * * * *',
      'arguments' => array(20),
      'callback' => 'jobs_every_minute_cron_callback',
    );
    return $items;
  }

# 2. 定义定时任务的队列
  /**
   * Implements hook_cron_queue_info().
   */
  function jobs_every_minute_cron_queue_info() {
    $queues = array();
    $queues['1st_test_queue'] = array(
      'worker callback' => 'jobs_every_minute_insert_sql_record',
      'time' => 2,
      'skip on cron' => TRUE,
    );
    return $queues;
  }

# 3. 定义定时任务执行时的回调

  function jobs_every_minute_cron_callback($arg){

    // 这里一定要用 get 的方法,而不是直接 new 一个 Queue 的对象
    // 因为 hook_cron_queue_info() 已经在模块载入的初始,声明了 Queue 的实例
    // 如果直接 new 的话,会直接覆盖我们声明好的 Queue,那样的话一堆函数回调就会断掉

    $queue = DrupalQueue::get('1st_test_queue');
    $queue->createQueue();
    for ($i=1; $i <= $arg; $i++){
      $queue->createItem($i);
      $item = $queue->claimItem(30);
    // 这里无需使用 deleteItem 方法来删除 item,因为 Queue 默认再 claimItem 之后会自动删除 item
    //$queue->deleteItem($item);
    }
  }

# 4. 定义队列中每一个 item 需要执行的操作,这里我是向数据库中插入一条记录并用 watchdog()打印一条日志来测试操作的情况

  function jobs_every_minute_insert_sql_record($item){

    db_insert('sql_insert_test')
      ->fields(array('text' => date("Y-m-d H:i:s",time())))
      ->execute();

    watchdog('Job scheduler' ,
      'Message: Data inserted to table at @t',
      array('@t'=> date("Y-m-d H:i:s",time())),
      WATCHDOG_NOTICE);
  }

这样一个完整 Queue + Cron API 的完整流程就走完了。

其实这里也是有问题的,这里我们定义的定时任务 ec_job_test 每运行一次都会在系统的 queue 表中插入新的 item,而要运行每一个 item 对应的处理任务,最终还是要运行系统的 cron。 即hook_cronapi+queue定义的定时任务,不是即时执行的,需要drupal核心的 cron 运行时,检查 queue 表中的 item, 如果有任务,就会执行。弄了半天根本没有解决我的问题。我的需求是定时添加新的处理任务,并实时监听任务状态,如果有则立即执行。由于核心的 cron 负载很重,我不可能每分钟运行一下,因此这个解决办法肯定是不行的。

回到最开始的思路,既然我能使用 linux 的 crontab,使用这些第三方的 cron 模块真的是多此一举。

我只需要定义一个路径并使用 curl + crontab 定时请求来扫描任务状态,如果有任务,每次返回一条,并使用回调处理,如果没有则闲置, 简单方便。


解决方案2 - PHP 添加进程控制扩展

MAMP 安装php第三方扩展库 - 进程控制扩展 Ev

本来这个问题在linux上极好解决,直接 pear 命令安装即可。然而在 OSX 上面却很痛苦,是的,因为我们用的都是 MAMP 的集成环境。

那么怎么办?

比如我现在用的 PHP 版本为 php7.0.15
那么这个版本 PHP 对应的 pear 的包管理工路径为
/Applications/MAMP/bin/php/php7.0.15/bin/pear

PHP 插件的预编译工具的路径为
/Applications/MAMP/bin/php/php7.0.15/bin/phpize

我没有使用 pear 命令的方式安装,因为感觉可能会报错安装失败,所以最终还是选择手动编译安装。

# 1. 下载 Ev 扩展的源码包
# 下载地址为 https://pecl.php.net/get/ev-1.0.6.tgz  wget 或是 curl 就看大家的喜好了
# 2. 解压 tgz 压缩包
tar -xzvf ev-1.0.6.tgz
# 3. 进入源码目录,注意源码目录直接就能看到 php5 和 php7 文件夹,不是含有 xml 文件的那个目录
cd ev-1.0.6/ev-1.0.6
# 4. 运行预编译工具,获取系统变量
/Applications/MAMP/bin/php/php7.0.15/bin/phpize
# 5. 编译前配置
./configure --with-ev
# 6. 解决依赖错误问题
# error dyld: Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib
# 出现这个问题的原因是插件依赖的readline库文件太老了, OSX 系统已经由 libreadline.8.0.dylib 取代了老版本
# 创建一个软连接,解决依赖的问题
cd /usr/local/opt/readline/lib
ln -s libreadline.8.0.dylib libreadline.7.dylib
# 切换回原来的安装目录
cd -
# 7. 重新配置
./configure --with-ev --with-php-config=/Applications/MAMP/bin/php/php7.0.15/bin/php-config
# 配置运行结果出现一下表示通过
> creating libtool
> appending configuration tag "CXX" to libtool
> configure: creating ./config.status
> config.status: creating config.h
# 8. 编译并且安装
make && make install
# 提示编译成功别切扩展安装至相关目录 
> Build complete.
> Don't forget to run 'make test'.
> 
> Installing shared extensions: /Applications/MAMP/bin/php/php7.0.15/lib/php/extensions/no-debug-non-zts-20151012/
# 如果你出现了 ev-1.0.6/ev-1.0.6/php7/common.h:25:10: fatal error: 'php.h' file not found 这样的错误,是因为编译器没有找到相关的 php环境 配置器的地址,需要重新配置并带上参数 --with-php-config=php-config (对应 php 版本的配置器地址)
# 查看编译后的插件
ls /Applications/MAMP/bin/php/php7.0.15/lib/php/extensions/no-debug-non-zts-20151012/
# 发现 ev.so 躺在那里,编译的工作算是完成了。
# 9. 启用扩展
# 打开 MAMP,菜单 -> File -> Edit Template -> PHP(php.ini) -> 7.0.15
# 找到第 538 行,追加如下设置项
extension=ev.so
# 保存后重启 MAMP 即可

解决方案3 - PHP DAEMON 方案 - 守护进程

此方案需要用到的模块为 drushd + PHP-DAEMON 扩展库

写在前

这个世界上解决问题的方法论有很多,然而我认为最有效的还是工程化。

Wikipedia 针对工程化 engineering 的定义如下:
The creative application of scientific principles to design or develop structures, machines, apparatus, or manufacturing processes, or works utilizing them singly or in combination; or to construct or operate the same with full cognizance of their design; or to forecast their behavior under specific operating conditions; all as respects an intended function, economics of operation and safety to life and property.

翻译过来的意思如下:

创造性的应用自然科学的原理设计和开发的服务在经济运行、生命财产安全领域有特定用途的实体或者设计,包括单一的或者组合式的结构、机器、成套设备、生产流程,构建和运行以上设备和流程的设计方案,根据运行状况预测行为模式的模型等的活动统称为工程化。

工程化除了跟自然科学绑定在一起外,更重要的一个概念是成本优化。实现功能或目标的效益要大于付出的成本,这样才能有正收益,工程化才有意义。

如果一件事情机器可以很好的完成任务,则没有必要去使用人工,人工在很多时候都存在单位成本高,效率低下,容易犯错的问题。比如针对 Made In China 产品更新与发布这件事上,人工完全可以被机器取代。

MIC批量发布的工程化方案

  • 产品元数据 (主要对应MIC里面的字段)

    • 基本信息
      • 产品名称 (标题 = 前缀 + 形容词 + 产品关键词 + 后缀)
      • 产品型号
      • 产品关键词
      • 产品图片
    • 产品属性
      • 产品通用属性
      • 产品专用属性
    • 贸易信息 (固定内容)
    • 产品详情
      • 标题
      • 公司简介 (提示相关性)
      • 公司优势 (关怀强化)
      • 产品应用 (连接问题)
      • 详细介绍 (相关性强化 - 解决问题)
      • 包装运输 (增强信任)
      • 生产设备 (增强信任)
      • 质量管控 (增强信任)
      • 资质认证 (增强信任)
      • 联系方式 (转化阶段)
  • 产品发布流程的工程化


MIC是一个基于 web 的 Application, 要完成产品发布,操作需要在浏览器上来完成。产品发布的主要操作为表单的提交( POST方式 )。针对这个操作有很多现成的解决办法,比如 terminal 下的 curl, firefox 的 micros, windows下的火车采集(火车浏览器),Python 自动化测试技术 Selenium,爬虫技术(Scrapy / Scrapy + Splash)。

MIC的登录表单是通过JS的方式提交登录信息,且产品信息填充页面的很多地方都是使用 JS 来验证数据有效性,我是个 JS 菜鸡,因此最终选择了 Selenium 这个解决方案。好处是,我不用考虑 JS 加密与解密的问题,坏处是这种方式操作的效率不高,不过考虑到产品发布这种可以24小时运行对执行时间不敏感的需求来说,Seleminum 够用了,向来我的追求就是能跑就行,还要啥自行车呢。

预警 - Selenium 是需要写代码的,厌恶代码的同学可以绕过了。Talk is cheap,show me the code.


项目的主要结构如下

MIC
+-- data
| + -- products_data.xlsx (产品元数据/基础数据)
+-- images (产品主图)
| + -- images1.jpg
| + -- images2.jpg
| + ...
+-- excel.py ( 将 excel 表格中的数据导入到数据库中,让数据持久化、状态化以方便后期知道任务进行到了哪一步 )
+-- product_node.py ( MIC产品类 - 用于生成需要发布的产品的详细描述 body , 即 WYSJWYG 编辑器需要填充的内容)
+-- mic_publish.py ( 产品发布的操作 - 包括登录账号、设置产品内容、上传图片、添加详细描述、提交发布等 )


以下只针对 product_node.py 和 mic_publish.py 这两处代码进行展示。

产品发布的操作


from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from retry import retry
from product_node import ProductNode
import pymysql
import random
import time

class MicProductPublish:
    #  账户登录信息
    LOGIN_URL = 'https://login.made-in-china.com/sign-in/'
    USER_NAME = 'mic_account_name'
    PASSWORD = 'mic_password'
    # 单个产品的镜像发布数量
    PRODUCT_NODE_MIRRORS = 7
    # 产品模板的链接
    PRODUCT_TEMPLATE_PAGE = 'https://membercenter.made-in-china.com/productmanage.do?xcase=addSimilar&prodId=similar_products_id'
    # 每个产品上传的图片数量
    UPLOAD_IMG_NUMS = 4
    # 可上传的总的图片数量
    PRODUCT_IMG_NUMS = 65
    UNIX_SOCKET = "/Applications/MAMP/tmp/mysql/mysql.sock"
    # 自定义参数属性
    GRADE_SELECT_DICT = {
        '1000': 'FQfmnGaYDEHo',
        '2000': 'CmtxEBGAjnib',
        '3000': 'ynAxmZaDuQHe',
        '4000': 'smfJQhtCuniq',
        '5000': 'CmtEQNGxjJib',
        '6000': 'ZJAnxQfuDEIo',
        '7000': 'sJaQEetSunIo',
        'other': '-1'
    }

    def __init__(self, task_counts):

        self.browser = webdriver.Firefox()
        self.login()

        # 连接数据库
        self.db = pymysql.connect(user='database_user',
                                  password='database_password',
                                  database='database_name',
                                  cursorclass=pymysql.cursors.DictCursor,
                                  unix_socket=self.UNIX_SOCKET)
        # 单次执行发布认为的牌号数量
        self.task_counts = task_counts

    def get_publish_schedule(self):
        # 第一批只操作常见牌号(未发布),获取目标牌号,并限定数量为task_counts
        sql = '''
            SELECT `Grade` FROM `database_name`.`grade` WHERE `Common`=1 AND `Published`=0 LIMIT {task_counts};
        '''.format(task_counts=self.task_counts)

        cursor = self.db.cursor()
        cursor.execute(sql)
        # 返回列表结构 待发布的产品牌号信息,数据结构为{'Grade':'1050'}
        return cursor.fetchall()

    def run_publish_schedule(self):

        # 执行产品发布任务
        # 并对后台的数据库中的状态进行修改
        #
        # 1. 执行单个产品发布任务
        # 2. 修改数据库中此牌号的状态

        for g in self.get_publish_schedule():

            m = self.PRODUCT_NODE_MIRRORS
            # 针对一个牌号发布指定数量的镜像产品
            # 添加 任务出错时,再次尝试, 使用retry包
            while m:
                self.go_to_template_page()
                self.publish_keyword_product_node(g['Grade'])
                m -= 1
            self.update_database(g['Grade'])

    @retry(tries=3, delay=2, max_delay=5)
    def publish_keyword_product_node(self, grade_str):
        self.go_to_template_page()
        self.publish_keyword_product_node_fill_content_and_submit(grade_str)

    def publish_keyword_product_node_fill_content_and_submit(self, grade_str):

        # 这里主要是针对浏览器的操作
        # 关于产品信息的构建,使用 产品详情(PD)类

        # 1. 创建产品类的新实例(牌号)
        product_node = ProductNode(grade_str, self.db)
        # 2.填写产品基本信息
        self.fill_base_info(product_node)
        # 3.上传产品图片
        self.upload_images()
        # 4.填写产品属性
        # - 4.1 颜色/应用/认证/工艺 (这里模板已经确定,不用操作)
        # - 4.2 选择牌号信息
        # - 4.3 回火/表面处理/是否合金/包装运输/规格/商标/原产地/Finish/内包装 (这里模板已经确定,不用操作)
        self.input_properties(product_node)
        # 5.贸易信息(这里模板已经确定,不用操作)
        # 6. 产品详情
        self.fill_body(product_node)
        # 7. 提交发布
        self.browser.implicitly_wait(2)
        self.publish_submit()
        self.browser.implicitly_wait(2)

    # 登录MIC
    def login(self):
        self.browser.get(self.LOGIN_URL)
        user = self.browser.find_element_by_id('logonInfo.logUserName')
        user.send_keys(self.USER_NAME)
        passwd = self.browser.find_element_by_id('logonInfo.logPassword')
        passwd.send_keys(self.PASSWORD)
        submit = self.browser.find_element_by_id('sign-in-submit')
        submit.click()

    # 回到默认模板页面
    def go_to_template_page(self):
        self.browser.get(self.PRODUCT_TEMPLATE_PAGE)
        # 隐式等待,检测页面的载入状态,如完全载入则立即结束等待
        self.browser.implicitly_wait(3)

    def fill_base_info(self, product_node):

        # 填写产品标题
        WebDriverWait(self.browser,20).until(EC.visibility_of_all_elements_located((By.CSS_SELECTOR,'textarea.name-enterarea')))
        product_name = self.browser.find_elements_by_css_selector('textarea.name-enterarea')
        product_name[0].send_keys(product_node.build_product_title())

        # 填写产品型号
        product_model = self.browser.find_element_by_name('prodModel')
        product_model.click()
        product_model.send_keys(product_node.get_keyword_grade()['Grade'] + ' keyword')
        self.browser.implicitly_wait(1)

        # 选择中心词
        self.set_center_word()

        # 填写产品关键词信息
        keywords = product_node.build_product_keyword()
        for k in keywords:
            keyword = self.browser.find_element_by_id('prodKeyword' + str(keywords.index(k)))
            keyword.clear()
            keyword.send_keys(k)

        self.browser.implicitly_wait(1)

    def set_center_word(self):

        WebDriverWait(self.browser,20).until(EC.visibility_of_all_elements_located((By.CSS_SELECTOR,'.J-center-words-box')))
        center_words = self.browser.find_elements_by_css_selector('.J-center-words-box .input-checkbox')
        keyword_center_word = self.browser.find_elements_by_css_selector('.J-center-words-box .input-checkbox[value="keyword"]')

        if keyword_center_word:
            keyword_center_word[0].click()keyword
        else:
            center_words[0].click()

    def upload_images(self):

        # 产品图片板块
        imgs = self.build_images_path()
        css = 'input.upload-selector.J-uploader'
        # 上传图片
        for i in imgs:
            upload = self.browser.find_element_by_css_selector(css)
            self.browser.implicitly_wait(3)
            upload.send_keys(i)

    def build_images_path(self):

        imgs = []

        files_list = list(range(1,self.PRODUCT_IMG_NUMS+1,1))

        for i in random.sample(files_list, self.UPLOAD_IMG_NUMS):
            imgs.append('/MIC/images/image' + str(i) + '.jpg')
        return imgs

    def input_properties(self, product_node):

        # 产品属性板块
        form_select = self.browser.find_elements_by_css_selector('.J-prop-select')

        # 设置牌号
        # 可选值
        grade_select = Select(form_select[2])
        self.set_product_property_grade(product_node.get_keyword_grade()['Series'], grade_select)

        # 设置产品状态
self.browser.find_element_by_id('JindustryPropOtherValue6').send_keys('Mill finish or as your request')

    def fill_body(self, product_node):
        # 切换到编辑器的iframe
        wysiwyg = self.browser.find_element_by_css_selector('.cke_wysiwyg_frame')
        self.browser.switch_to.frame(wysiwyg)
        # 获取主编辑界面
        editor_body = self.browser.find_element_by_css_selector('.cke_editable')
        editor_body.clear()
        # 获取产品信息
        self.browser.switch_to.default_content()
        # 设置产品详细描述
        product_body = product_node.build_product_body()
        # 填充产品详情
        self.browser.execute_script(product_body)
        self.browser.implicitly_wait(2)

        # 切换回页面的主要内容
    def publish_submit(self):

        WebDriverWait(self.browser,10).until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.J-product-submit')))
        btn = self.browser.find_element_by_css_selector('.J-product-submit')
        btn.click()

    def update_database(self, grade_str):

        time_stamp = str(int(time.time()))

        update_sql = 'UPDATE `database_name`.`grade` SET `Published` = 1,  `Published_Date` = "{0}" WHERE `GRADE` = "{1}";'.format(time_stamp, grade_str)
        cursor = self.db.cursor()
        cursor.execute(update_sql)
        # 同步修改到数据库
        self.db.commit()

    def __del__(self):
        self.db.close()
        self.browser.close()

if __name__ == '__main__':
    # 发布20个产品,可以设置更大的值
    MicProductPublish(20).run_publish_schedule()

生成单个产品内容

# 生成产品详情的类
import pymysql
import time
import random

class ProductNode:
    # 类公共变量 - 产品主要详情 - 含有主要字段的placetoken
    PRODUCT_MAIN_BODY = '<br><h2>{product_title}</h2><br><p style=\"color:#666666;text-align:justify;font-size:14px;font-family:Arial, Helvetica, sans-serif;\"><br><img title=\"Company profile of {product_title}\" alt=\" Company profile of {product_title}\"     src=\"//image.made-in-china.com/226f3j00WuqQRGJcaZzh/New-Product-Test-2-for-Image-Name.webp\" srcid=\"275914412\" width=\"1060px\"><br> </p><p style=\"color:#666666;font-size:14px;font-family:Arial, Helvetica, sans-serif;\"><span style=\"font-size:20px;\">Technology driven quality focused keyword & keyword alloy manufacturer</span><br> </p><ul><li><p><span style=\"font-size:16px;\">Henan  keyword Industry Group was founded in 2006, located in Gongyi industry area of Henan Province.</span></p></li><li><p><span style=\"font-size:16px;\">The capacity of our keyword Rolling products has reached 200,000 ton per year, formats available in coil, plates, sheets and foil, material of keyword alloy production line covers 1000-8000 series.</span></p></li><li><p><span style=\"font-size:16px;\">Our stable consistent supply chain, outstanding quality control system and first-rate services have been approved and highly praised by our clients, and built us a good reputation among this industry.</span></p></li><li><p><span style=\"font-size:16px;\">Category of our customers is full of diversity and fall into the zone of transportation, architecture, engineering, aviation, space, electricity and package where our keyword products have been widely used.</span><br><br><br> </p></li></ul><img title=\" company advantages of {product_title}\" alt=\" company advantages of {product_title}\" src=\"//image.made-in-china.com/226f3j00lrkaRudBgKgW/New-Product-Test-2-for-Image-Name.webp\" srcid=\"275914422\" width=\"1060px\"><br><br><br><img title=\"Product Application of {product_title}\" alt=\"Product Application of {product_title}\" src=\"//image.made-in-china.com/226f3j00WucERIAjhKpl/New-Product-Test-2-for-Image-Name.webp\" srcid=\"275914432\" width=\"1060px\"><h3 style=\"color:#0078cc;font-size:20px;font-family:Arial, Helvetica, sans-serif;\">Application</h3><br><div class=\"rich-text-table\"><table style=\"width:1037px;\" cellspacing=\"1\" cellpadding=\"1\" border=\"1\"><tbody><tr><td colspan=\"3\" style=\"text-align:center;width:1028px;\"><br><span style=\"font-size:20px;\">Application of {product_title}</span><br> </td></tr><tr><td style=\"width:292px;\"><ul><li><p><span style=\"font-size:16px;\">Transportation</span></p><ul><li><span style=\"font-size:16px;\">Aircraft & Aerospace Automobile</span></li><li><span style=\"font-size:16px;\">Bus</span></li><li><span style=\"font-size:16px;\">Truck</span></li><li><span style=\"font-size:16px;\">Tank Train</span></li><li><span style=\"font-size:16px;\">Metro Line</span></li><li><span style=\"font-size:16px;\">Ship</span></li><li><span style=\"font-size:16px;\">Boat</span></li><li><span style=\"font-size:16px;\">Yacht</span></li></ul></li></ul></td><td style=\"width:367px;\"><ul><li><p><span style=\"font-size:16px;\">Building & Construction</span></p><ul><li><span style=\"font-size:16px;\">Curtain Wall</span></li><li><span style=\"font-size:16px;\">Roofing Decoration</span></li><li><span style=\"font-size:16px;\">Celling Door</span></li><li><span style=\"font-size:16px;\">Window</span></li><li><span style=\"font-size:16px;\">Floor Framework</span></li><li><span style=\"font-size:16px;\">Structure</span></li></ul></li></ul></td><td style=\"width:354px;\"><ul><li><p><span style=\"font-size:16px;\">Packaging & Container</span></p><ul><li><span style=\"font-size:16px;\">Can</span></li><li><span style=\"font-size:16px;\">Box</span></li><li><span style=\"font-size:16px;\">Case</span></li><li><span style=\"font-size:16px;\">Container Seal</span></li><li><span style=\"font-size:16px;\">Lid Cover</span></li><li><span style=\"font-size:16px;\">Flexible Package</span></li><li><span style=\"font-size:16px;\">Tube Foil</span></li><li><span style=\"font-size:16px;\">Household Foil</span></li></ul></li></ul></td></tr><tr><td style=\"width:292px;\"><ul><li><p><span style=\"font-size:16px;\">Electronics & Appliances</span></p><ul><li><span style=\"font-size:16px;\">Computer</span></li><li><span style=\"font-size:16px;\">Laptop</span></li><li><span style=\"font-size:16px;\">Communication Tools</span></li><li><span style=\"font-size:16px;\">Consumer Electric</span></li><li><span style=\"font-size:16px;\">Heat Exchanger</span></li></ul></li></ul></td><td style=\"width:367px;\"><ul><li><p><span style=\"font-size:16px;\">Machine & Equipment</span></p><ul><li><span style=\"font-size:16px;\">Catering Equipment</span></li><li><span style=\"font-size:16px;\">Textile Machinery</span></li><li><span style=\"font-size:16px;\">Precision Instrument</span></li><li><span style=\"font-size:16px;\">Medical Equipment</span></li></ul></li></ul></td><td style=\"width:354px;\"><ul><li><p><span style=\"font-size:16px;\">Durable Goods</span></p><ul><li><span style=\"font-size:16px;\">Cookware</span></li><li><span style=\"font-size:16px;\">Kitchen Utensils</span></li><li><span style=\"font-size:16px;\">Lamp Cover</span></li><li><span style=\"font-size:16px;\">Air Outlet</span></li><li><span style=\"font-size:16px;\">Light Reflecting Plate</span></li><li><span style=\"font-size:16px;\">Traffic Sign</span></li><li><span style=\"font-size:16px;\">Nameplate</span></li></ul></li></ul></td></tr></tbody></table></div><br><br><br> {keyword_app_info} <h3 style=\"color:#0078cc;font-size:20px;font-family:Arial, Helvetica, sans-serif;\">Element Components</h3><br><br><br><p>Element Components of {product_title}</p><br><div class=\"rich-text-table\">     {element_table} </div><br><br><br><h3 style=\"color:#0078cc;font-size:20px;font-family:Arial, Helvetica, sans-serif;\">Temper</h3><br><br><br><p>Temper of {product_title}</p><br><div class=\"rich-text-table\">     {temper_table} </div><br><br><br><h3 style=\"color:#0078cc;font-size:20px;font-family:Arial, Helvetica, sans-serif;\">Forms</h3><br><br><br><div class=\"rich-text-table\"><p>Common forms of {product_title}</p><br><br>     {forms_list} </div><br><br><br><p>Dimension, tolerance and packing requirement of {product_title} upon request. Please check with our sales to get more information.</p><br><br><br><img title=\"Product package of {product_title}\" alt=\"Product package of {product_title}\" src=\"//image.made-in-china.com/226f3j00SpyQDFtggjzI/New-Product-Test-2-for-Image-Name.webp\" srcid=\"278033782\" width=\"1060px\"><br><br><img title=\"Equipments and Facilities of {product_title}\" alt=\"Equipments and Facilities of {product_title}\" src=\"http://image.made-in-china.com/44f3j00NrPaUWDLaMpn/Product-New-Template-2-Sheet-Plate.jpg\" srcid=\"275914452\" width=\"1060px\"><ul><li><p><span style=\"font-size:16px;\"> keyword Industry has one of one of the most diverse choices of keyword handling equipment in the nation.</span></p></li><li><p><span style=\"font-size:16px;\">We add brand-new equipment to our centers regularly, always striving to use the most recent technology in order to fulfill our the altering requirements of customer.</span></p></li><li><p><span style=\"font-size:16px;\">Our capacity to accomplish high customer complete satisfaction and on-time shipment is largely due to our specialized and educated maintenance personnel and maker drivers.</span></p></li><li><p><span style=\"font-size:16px;\">Our upkeep employees keep our handling, material handling and also delivery tools operating and also offered all the time.</span><br><br> </p></li></ul><br><br><img title=\" Quality Control of {product_title}\" alt=\" Quality Control of {product_title}\" src=\"//image.made-in-china.com/226f3j00VucEUMdaaKpi/New-Product-Test-2-for-Image-Name.webp\" srcid=\"275914462\" width=\"1060px\"><ul><li><p><span style=\"font-size:16px;\"> keyword Industrial is devoted to quality management by providing keyword products and associated services that meet or go beyond client expectations in a sustainable manner while constantly keeping track of and enhancing our products and processes.</span></p></li><li><p><span style=\"font-size:16px;\">Likewise we preserves a high quality of keyword products throughout the entire production process. From the delivery of ingot and smelting to the final dimensional check, substantial attention is provided to all products as outlined by process control checklist for each casting requirement.</span></p></li><li><p><span style=\"font-size:16px;\">We have our own state-art lab and devoted product establishing and evaluation engineers team, a full set test devices including Tensile Tester, Surface Roughness Testers, Direct Reading Emission Spectrometer, Ultrasonic Flaw-detecting Machine, Hardness Testers, Metallurgical Microscope etc.</span></p></li><li><p><span style=\"font-size:16px;\">Our Continuously Hot-rolled keyword Coil Line and Tandem Cold Mills are both import from SMS Group German, the Digital Manufacturing Dispatch Center of our company could fetch real-time operation data feeds directly from the terminal of our production line, which makes us able to locate the problem and do the correction at the very first time. The Quality Assurance system has actually been deeply integrate into every action of our keyword processing flow besides. This is the main reason that we could support our partners from different markets with competitive cost and outstanding quality.</span><br><br> </p></li></ul><img title=\" Certificates of{product_title}\" alt=\" Certificates of {product_title}\" src=\"//image.made-in-china.com/226f3j00iuoQYZJGgjzW/New-Product-Test-2-for-Image-Name.webp\" srcid=\"275914472\" width=\"1060px\"><br><br><br><span style=\"font-size:16px;\">Currently for keyword and keyword alloy sheets and coil, we have passed ISO 9001-2007 Quality Control System Verification. The Certificates displayed here are China Classification Society certificate, DNV Approval of Manufacturer Certificate, CE Certificate for Europe, and SGS Inspection Report.</span><br><br><br><br><br><br><img title=\"Contact sales of {product_title}\" alt=\"Contact sales of {product_title}\" src=\"//image.made-in-china.com/226f3j00hubaYFwzhjri/New-Product-Test-2-for-Image-Name.webp\" srcid=\"275914482\" width=\"1060px\"><br><br><br><br>'

    TITLE_PREFIX = [
        'Best Quality ',
        'Hot Sale ',
        'ASTM JIS EN Standard ',
        'ISO Certificated ',
        'Bottom Price ',
        'Bright Finish ',
        'Rolled ',
        'Top Rated ',
        'Low Price ',
        ]

    TITLE_SUFFIX = [
        ' From Factory',
        ' From Qualified Supplier',
        ' From Audited manufacturer',
        ' Full Size Available',
        ' Fresh Stock',
        ' Factory Direct Sale',
        ' Price Per Ton',
        ' Best Offer Guarantee',
        ]

    PRODUCT_TITLE = ''

    keyword_TEMPERS = {
        '1000': ['F', 'O', 'H'],
        '2000': ['F', 'O', 'W', 'T'],
        '3000': ['F', 'O', 'H'],
        '4000': ['F', 'O', 'W','T'],
        '5000': ['F', 'O', 'H'],
        '6000': ['F', 'O', 'W','T'],
        '7000': ['F', 'O', 'W','T'],
        '8000': ['F', 'O', 'H']
    }

    keyword_TEMPERS_CODES = {
        'F': {
            'defination': 'As fabricated. No special control has been performed to the heat treatment or strain hardening after the shaping process such as casting, hot working, or cold working.',
            'codes': ['F']
        },
        'O': {
            'defination': 'Annealed - This is the lowest strength, highest ductility temper.',
            'codes': ['O'],
        },
        'H': {
            'defination': 'Strain Hardened - (applied to wrought products only) Used for products that have been strengthened by strain hardening, with or without subsequent heat treatment. The designation is followed by two or more numbers as discussed below.',
            'codes': ['H12',
                      'H14',
                      'H16',
                      'H18',
                      'H19',
                      'H111',
                      'H112',
                      'H116',
                      'H21',
                      'H22',
                      'H24',
                      'H26',
                      'H28',
                      'H32',
                      'H321',
                      'H34',
                      'H36',
                      'H38']
        },
        'W': {
            'defination': 'Solution Heat Treated - This is seldom encountered because it is an unstable temper that applies only to alloys that spontaneously age at ambient temperature after heat treatment.',
            'codes': ['W']
        },
        'T': {
            'defination': 'Solution Heat Treated - Used for products that have been strengthened by heat treatment, with or without subsequent strain hardening. The designation is followed by one or more numbers as discussed below.',
            'codes': [
                'T0',
                'T1',
                'T2',
                'T3',
                'T4',
                'T42',
                'T5',
                'T6',
                'T651',
                'T7',
                'T8',
                'T9',
                'T10']
        }
    }

    # 构造函数,类实例化的时候会自动调用该函数
    def __init__(self, keyword_grade, db):
        # 初始化数据库连接
        self.db = db
        self.keyword_grade = keyword_grade
        self.PRODUCT_TITLE = self.build_product_title()

    # 获取产品牌号信息,指针对常用牌号
    def get_keyword__grade(self):
        if self.keyword_grade:
            grade_query_sql = 'SELECT * FROM `database_name`.`grade` WHERE `Grade` = "{0}";'.format(self.keyword_grade)
            cursor = self.db.cursor()
            cursor.execute(grade_query_sql.format(self.keyword_grade))
            return cursor.fetchone()
        else:
            return None

    def get_keyword_temper(self):
        if self.keyword_grade:
            grade = self.get_keyword_grade()
            keyword_temper = self.keyword_TEMPERS[str(grade['Series'])]
            return keyword_temper
        else:
            return False

    # 获取产品特定牌号的应用信息
    def get_keyword_app(self):
        app_query_sql = 'SELECT * FROM `database_name`.`application` WHERE `Grade` = "{0}";'.format(self.keyword_grade)
        cursor = self.db.cursor()
        cursor.execute(app_query_sql.format(self.keyword_grade))
        result = cursor.fetchone()
        if result['App_zh'] == 'nan':
            result['App_zh'] = None
        if result['App_en'] == 'nan':
            result['App_en'] = None
        return result

    def build_keyword_app_info(self):
        app_dict = self.get_keyword_app()

        app_info = '<br> <p>Typical application of {product_title}</p> <br> <p>{app_en}</p> <br> <br> <br>'

        if app_dict['App_en']:
            return app_info.format(product_title=self.PRODUCT_TITLE, app_en = app_dict['App_en'])
        else:
            return ''

    # 获取产品牌号的化学成分信息
    def get_keyword_component(self):
        grade_query_sql = 'SELECT * FROM `database_name`.`component` WHERE `Grade` = "{0}";'.format(self.keyword_grade)
        cursor = self.db.cursor()
        cursor.execute(grade_query_sql)
        return cursor.fetchone()

    def get_keyword_form(self):
        forms = self.keyword_FORMS[str(self.get_keyword_grade()['Series'])]
        return forms

    def build_product_keyword(self):
        keywords = []
        forms = self.get_keyword_form()
        if len(forms) <=3:
            for f in forms:
                keywords.append(' '.join([self.keyword_grade, 'keyword', f]))
        else:
            three_forms = random.sample(forms, 3)
            for g in three_forms:
                keywords.append(' '.join([self.keyword_grade, 'keyword', g]))

        return keywords

    def build_product_title(self):
        prefix = ''.join(random.sample(self.TITLE_PREFIX, 1))
        suffix = ''.join(random.sample(self.TITLE_SUFFIX, 1))
        alloy_str = ''

        if 'A' not in self.keyword_grade:
            alloy_str = ''.join(random.sample(['', 'A', 'AA'], 1))

        forms_str = alloy_str + self.keyword_grade + ' keyword ' + '/'.join(self.get_keyword_form())
        return prefix + forms_str + suffix

    def build_comp_form(self):
        tr_list = self.build_table_tr_list()

        # 构建表格的头部区域
        table = '<table style="width:500px;" cellspacing="1" cellpadding="1" border="1"><thead><tr><td style="width:120px; text-align: center;">' + tr_list[0]['ele'] + '</td><td style="text-align: center;">' + tr_list[0]['value'] +'</td></tr></thead><tbody>'
        for i, tr in enumerate(tr_list):
            if i >= 1:
                if tr['rowspan']:
                    table += '<tr><td style="width:120px; text-align: center">' + tr['ele'] + '</td><td rowspan="2" style="text-align: center;">' + tr['value'] + '</td></tr>'
                elif tr['td'] == '':
                    table += '<tr><td style="width:120px; text-align: center">' + tr['ele'] + '</td></tr>'
                else:
                    table += '<tr><td style="width:120px; text-align: center">' + tr['ele'] + '</td><td style="text-align: center;">' + tr['value'] + '</td></tr>'

        table += '</tbody></table>'

        return table

    def build_table_tr_list(self):
        comp = self.get_keyword_component()

        table_tr_list = []

        for key, value in comp.items():
            if key == 'id':
                pass
            else:
                table_tr_list.append({'ele': key, 'value': value, 'rowspan': False, 'td': 'Default'})

        for i, tr in enumerate(table_tr_list):

            # rowspan 键指示了需要colspan的ta单元格
            # td值为''且不为'Default'时指示了 化学元素的单元格不生成

            if tr['value'] == 'nan':
                table_tr_list[i-1]['rowspan'] = True
                table_tr_list[i]['td'] = ''

        return table_tr_list

    def build_temper_table(self, keyword_temper):
        if isinstance(keyword_temper, list):

            table_body = '<table style="width:1037px;" cellspacing="1" cellpadding="1" border="1"><tbody>'

            for i in keyword_temper:
                table_body += '<tr><td style="width:120px; text-align:center;">' + i + '</td>'
                table_body += '<td><p style="font-size: 12px; font-style:italic; padding: 10px;">' + self.keyword_TEMPERS_CODES[i]['defination'] + '</p>'
                temper_codes_string = ' / '.join(self.keyword_TEMPERS_CODES[i]['codes'])
                table_body += '<p style="font-size: 12px; font-style: italic; padding: 10px;">' + temper_codes_string + '</p></tr>'

            table = table_body + '<tbody></table>'
            return table
        else:
            return ''

    def build_keyword_forms(self):
        forms = self.get_keyword_form()
        forms_output = '<ul>'
        for f in forms:
            forms_output += '<li><p><span style="font-size:16px;">' + f + '</p></li>'
        forms_output += '</ul>'
        return forms_output

    def build_product_body(self):
        return self.PRODUCT_MAIN_BODY.format(
            product_title=self.PRODUCT_TITLE,
            keyword_app_info=self.build_keyword_app_info(),
            element_table = self.build_comp_form(),
            temper_table=self.build_temper_table(self.get_keyword_temper()),
            forms_list=self.build_keyword_forms(),
        )

    def __del__(self):
        pass

跑任务很简单,只需要切换到命令行页面,输入

python3 mic_publish.py

然后浏览器就会自动的为你发产品了。

实测每 2 秒可以发布一个产品,一个小时可以稳定发布 1000+ 产品,偶尔会失败,失败后重试基本都会通过。

LNMP安装与配置

前期安装

  1. 更新 source list

# 这里我主要用的是linux - Debian的发行版,因此Reposity都是debian官方的。
# 尽量不要使用VPS服务商提供的Reposity,虽然更新可以省流量,
# 但是有时候有的软件包可能被修改,或是功能被阉割,部署到生产服务器有时候会出现莫名其妙的问题
# 因此还是建议使用官方的Reposity

###### Debian Main Repos ######
deb http://ftp.us.debian.org/debian/ jessie main contrib non-free
deb-src http://ftp.us.debian.org/debian/ jessie main contrib non-free

###### Debian Security Repos ######
deb http://security.debian.org/ jessie/updates main contrib non-free
deb-src http://security.debian.org/ jessie/updates main contrib non-free

###### Debian Backports Repos ######
deb http://ftp.debian.org/debian jessie-backports main
  1. 安装mosh
  2. 安装git, fail2ban
  3. 安装curl
  4. 设置系统时间与字符编码UTF-8

@TODO 以上几个重要的软件包有时间再写

LAMP

1. 安装apache2

apt-get install apache2 
#由于我们需要使用clean url,因此需要激活apache的rewrite模块
a2enmod rewrite
  1. 安装mariadb - mysql
apt-get install mariadb-server mariadb-client
# 运行mysql_secure_installation 提升数据库初始化的安装性
mysql_secure_installation

apt-get install mariadb-server mariadb-client

#运行mysql_secure_installation 提升数据库初始化的安全性
mysql_secure_installation

#建立mysql用户
create user 'user'@'localhost' identified by 'passwd';

#配置数据库用户权限
GRANT ALL PRIVILEGES ON dbname.* TO 'user'@'localhost' WITH GRANT OPTION;
# 注意,这里和mysql可能有一点不同,必须加上 WITH GRANT OPTION,否则 用户添加的权限一直都是USAGE,USAGE不是实际的权限,因此是无效的,

# 修改数据库用户密码
SET PASSWORD FOR 'user'@'localhost' = PASSWORD('passwd');
# 这里和mysql也不一样,mysql修改用户密码使用的alter语句,这里也需要注意。

# 查看用户权限
SHOW GRANTS FOR 'username'@'localhost';

# 刷新权限
FLUSH PRIVILEGES;

~~ 3. 安装php5 (debian8目前stable repository中还没有加入php7) ~~

~~ apt-get install php5 php5-curl php5-gd php5-mysql php5-xmlrpc php5-json ~~

2019年10约15日更新

PHP - FCGI 运行模式 + 切换底层服务器环境由 Apache 到 Nginx

Ngix 和 Apache 两者的性能差异是在架构层面上的。Nginx 运行在异步模式下,要比 Apache 的多线程要消耗更少的系统资源,且更快的响应请求,在我的阿里云小鸡上,对性能的节省就显得很重要。因此我打算将服务器环境切换为 Nginx .


# 1. 创建网站文件的存储目录
mkdir -p /var/www/html/example.com/public_html

# 1. 创建相关的日志文件目录,以方便后期检查与维护
# 创建 NGINX 日志目录
mkdir -p /var/log/nginx
# 创建 PHP 运行日志目录
mkdir -p /var/log/fpm-php

# 2. 安装与配置 NGINX 及相关模块
apt-get install nginx fcgiwrap nginx-doc  ssl-cert
# 3. 查看 Nginx 状态
nginx -V

# 3. 安装最新的 PHP 7.3, 由于 Debian 最新的 Testing 仓库已有 php7.3版本,因此这里我们直接从仓库安装
# 3.1 添加 Debian Testing 的数据源
echo -e '#Adding Testing Repository\n deb http://mirrors.aliyun.com/debian/ testing main'  >>  /etc/apt/sources.list
# 3.2 安装 PHP 7.3
apt install php7.3-cli  php7.3-fpm php7.3-common php7.3-json php7.3-readline

php7.0比php5.6版本有了大幅度的性能提升,而7.3比7.1还有20%的性能提升,因此我们需要使用 \* debian stretch \* 下 testing repositor 的最新7.3稳定版, 并且最终php运行模式为php-fpm。

# 3.2.1 设置默认的 PHP-FPM运行日志的输出目录
vi /etc/php/7.3/fpm/php-fpm.conf
# 将 24 行 error_log 设置修改为 以下位置
error_log = /var/log/fpm-php/php7.3-fpm.log

# 3.2.2 PHP 运行的默认设置
# php.ini 使用系统的默认设置已经比较合适,这里就不做修改了

# 3.2.3 针对站点的pool设置
原则上每个站点对应一个php-fpm pool
pool配置文件目录/etc/php/7.3/fpm/pool.d/example.com.conf
[example.com]
prefix = /var/www/html/example.com
user = www-data
group = www-data
listen = /run/php/example.com.php.sock
listen.owner = www-data
listen.group = www-data
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
request_terminate_timeout = 300s
request_slowlog_timeout = 10s
chdir = /var/www/html/example.com/public_html
slowlog = /var/log/fpm-php/$pool.log.slow
php_flag[display_errors] = off
php_admin_value[display_errors]=0
php_admin_value[display_startup_errors]=0
php_admin_value[html_errors]=0
php_admin_value[define_syslog_variables]=0
php_admin_flag[file_uploads]=1
php_admin_flag[log_errors] = on
php_admin_value[log_errors]=1
php_admin_value[upload_tmp_dir]="/var/tmp"
php_admin_value[upload_max_filesize]="4M"
php_value[max_input_time]="120"
php_admin_value[max_input_time]=120
php_value[max_execution_time]="300"
php_admin_value[post_max_size]="4M"
php_value[session.gc_maxlifetime]=3600
php_admin_value[session.gc_probability]=1
php_admin_value[session.gc_divisor]=100
php_admin_value[error_log] = /var/log/fpm-php/example.com.log
php_admin_value[memory_limit] = 64M
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = off
php_admin_value[magic_quotes_gpc]=0
php_admin_value[register_globals]=0
php_admin_value[session.auto_start]=0
php_admin_value[mbstring.http_input]="pass"
php_admin_value[mbstring.http_output]="pass"
php_admin_value[mbstring.encoding_translation]=0
php_admin_value[expose_php]=0
php_admin_value[allow_url_fopen]=1
php_admin_value[safe_mode]=0
php_admin_value[cgi.fix_pathinfo]=1
php_admin_value[cgi.discard_path]=0

# 测试 example.com 的 fpm-php 配置文件是否正确
php-fpm7.3 -y /etc/php/7.3/fpm/pool.d/example.com.conf -t
  1. 服务器安全性提升(安全性优化)

@TODO这里暂时略过,有时间再写

+ ssh配置 (禁用部分账户 + ssh key登录) 
  1. 建立网站文件目录

mkdir -p /var/www/domain.com/html/

  1. 安装certbot证书签发程序

# 安装基本库
apt-get install gcc build-essential autoconf openssl

# 下载certbot最新版客户端
git clone https://github.com/certbot/certbot.git

# 解压后,运行目录下的certbot-auto安装certbot客户端
./cerbot/certbot-auto --install-only

# 使用最新版本的ACME2特性签发wildcard域名证书
# 注意wildcard证书只针对子域名,而根域名也需要签发证书,所以以下包含两个域名
./certbot-auto --server https://acme-v02.api.letsencrypt.org/directory -d *.domainexample.com -d domainexample.com --manual --preferred-challenges dns-01 certonly

# 在证书签发的过程中需要添加DNS TXT解析记录,已验证域名所有权,这里需要注意
# 签发成功后出现以下提示

 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/domainexample.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/domainexample.com/privkey.pem
   Your cert will expire on 2018-07-01. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot-auto
   again. To non-interactively renew *all* of your certificates, run
   "certbot-auto renew"

# 如果因为一些原因而错误签发了证书,可以使用revoke命令撤销

# 如果你使用的是cert-only子命令获取的证书,这需要使用证书文件和私钥来撤销证书

./certbot-auto revoke --cert-path /etc/letsencrypt/archive/domainexample.com/cert1.pem --key-path  /etc/letsencrypt/archive/domainexample.com/privkey1.pem

2018年06月26日补充内容

由于LE的证书有效期只有三个月,因此不到三个月就要对证书进行renew,这就是免费证书所需要付出的代价,好在FE证书支持全站子域名的wildcard证书,三个月renew一次也就忍忍吧,不服和图方便的话,一年花个100刀买证书吧,毕竟钱花哪哪好。

由于目前我使用的就是wildcard证书,为wildcard证书需要连接acme-v02进行验证,并且wildcard目前只支持dns-01的授权方式,因此就不得不每次手动的manual来进行证书的renew。Renew的方法不是用certbot-auto的renew命令,因为是手动的,就需要使用certonly命令。后期考虑结合VPS服务商的DNS API来自动解决这个问题。

./certbot-auto \
--server https://acme-v02.api.letsencrypt.org/directory \
-d *.domainexample.com \
-d domainexample.com \
--manual \
--preferred-challenges dns-01 \
certonly

# 命令运行后会提示添加 TXT 文本格式的 DNS解析记录
# 注意由于添加证书的域名是根域名+wildcard子域名,因此需要添加2条DNS记录。
# 不然的话一直会验证不通过。

# 验证通过后顺利生成新的证书,并且/etc/letsencrypt/live/domainexample.com/目录下的证书指向软连接已经被成功更新。
# 重启或是reload apache服务,就会发现证书顺利的rewnew和更新了。

# service apache2 restart 或者 service apache2 reload

~~ 7. clone与修改apache的配置文件 ~~


# 443 安全连接与端口配置文件
# 注意事项
# 1. ssl的配置文件名和ssl的配置文件选项

# 根域名跳转到www
<VirtualHost *:443>
    ServerName domainexample.com
    Redirect permanent / https://www.domainexample.com/

    # 即使根域名全站跳转,也必须要明确的支出证书文件的路径,否则就会出现文章底部的错误。
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/domainexample.com/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/domainexample.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/domainexample.com/fullchain.pem
</VirtualHost>

<VirtualHost *:443>
    ServerAdmin [email protected]
    ServerName www.domainexample.com
    DocumentRoot /var/www/html/domainexample.com/public_html/
    ErrorLog /var/www/html/domainexample.com/logs/error.log
    CustomLog /var/www/html/domainexample.com/logs/access.log combined

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/domainexample.com/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/domainexample.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/domainexample.com/fullchain.pem
</VirtualHost>

# 2. 80端口的配置文件

# Redirect all 80 port traffic to 443 instead

<VirtualHost *:80>
     ServerAdmin [email protected]
     ServerName domainexample.com
     ServerAlias www.domainexample.com
     DocumentRoot /var/www/html/domainexample.com/public_html/
     ErrorLog /var/www/html/domainexample.com/logs/error.log
     CustomLog /var/www/html/domainexample.com/logs/access.log combined

    <IfModule mod_rewrite.c>
            RewriteEngine on
            RewriteCond %{SERVER_PORT} !^443$
            RewriteRule (.*) https://%{SERVER_NAME}/$1 [R]
    </IfModule>

</VirtualHost>

# 上面的这种操作是一种heavy lift 如果需要跳转的链接成千上万,服务器就会有很重的负载
# 针对80端口的流量跳转
# 使用virtualhost的redirect属性设置比较优雅和高效
# 将所有根域名下的流量跳转到www
<VirtualHost *:80>
    ServerName domainexample.com
    Redirect permanent / https://www.domainexample.com/
</VirtualHost>

# 另外还需要在ServerName domainexample.com

<VirtualHost *:80>
    ServerName www.domainexample.com
    # 由于domainexample.com已经被设置为一个独立的virtual host
    # 因此下方的ServerAlias设置项必须删除,这里我们直接注释掉
    # ServerAlias domainexample.com
    ServerAdmin [email protected]
    DocumentRoot /var/www/html/domainexample.com/public_html
    # 将所有80端口流量重定向到443端口
    Redirect permanent / https://www.domainexample.com/

    DocumentRoot /var/www/html/domainexample.com/public_html/
    ErrorLog /var/www/html/domainexample.com/logs/error.log
    CustomLog /var/www/html/domainexample.com/logs/access.log combined
</VirtualHost>
# 3. 激活Apache的ssl模块
a2enmod ssl

# 这样与ssl相关的依赖模块也会被激活
# 之后重启服务器就可以愉快的享受ssl带来的安全性了。

service apache2 restart

# @TODO 以下有时间再补

# Apache配置与服务器性能优化
# 1. 提升安全性
# 2. 开启流量压缩
# 3. 并行下载,静态资源分离 
# 4. 禁用不安全的HTTP协议

企业邮箱配置

企业邮箱的话,如果你的公司有营业执照和公章,则可以通过和阿里的OA平台 - 钉钉 签约,然后使用阿里云提供的免费企业邮箱服务,50个账户,每个账户5G容量,对于人数不多的公司完全够用了,一年可以省下将近2000块钱的费用。按照指引注册的话,快的话一天就可以审核通过使用了。

注意这里面也是有坑的。

  1. 你的域名无法直接绑定到钉钉提供的免费企业邮箱,必须按照钉钉的提示先申请一个 company-name.onaliyun.com的二级域名,然后企业邮箱的功能算是激活了。
  2. 进入钉钉手机端,钉邮 -> 【邮箱管理】-> 【绑定新域名】-> 添加自己公式的域名,然后后台会提示需要你添加DNS解析记录以验证域名所有权。
  3. 第二步是在域名的DNS解析页面上设置mx解析记录,解析到mxw.hichina.com和mxn.hichina.com,priority优先级分别是5和10。这两个服务器是阿里企业邮箱的DNS服务器。除此之外还要添加一个CNAME的DNS记录,主机为mail,地址为mail.mxhichina.com。由于MX记录同步比较慢,通常需要1-2个小时才会生效。
  4. 验证通过后自己的域名就会现在在邮箱的管理页面中,在钉钉手机端后台选中自己的域名设置为默认,这样所有人的企业邮箱都会变成自己域名的后缀。
  5. 走完这一步,就可以在钉钉的通讯录中邀请公司的同事加入组织,并为他们配置企业邮箱账户。
  6. 然而即使你为每个人都配置完企业邮箱了,这个时候你仍然没有办法使用客户端来收信。什么?你就是为了骗企业邮箱和邮件客户端收信这个来的?别急。即使你用foxmail添加了邮箱的地址,你输入了钉钉的账户密码,IMAP/SMTP端口什么的你都配置正确,你还是收不到信。那么怎么办?你需要在钉钉的后台 【通讯录】->【邮箱账户管理】->【个人邮箱】->【重设密码】为每个邮箱账户重设密码,然后每个人都登陆一下阿里企业邮箱网页版 https://qiye.aliyun.com 修改密码。因为企业邮箱的登录密码,不是钉钉账户的密码。所以用钉钉账户的密码是没有办法在邮箱客户端登陆邮件服务器的。改完密码之后使用新密码配置邮件客户端,一切都正常了。而搞笑的是,打电话给钉钉的客服,客户告诉我免费账户不支持IMAP客户端收信,嗯嗯,你说的对,但是我不信。

2018年7月4日补充更新

当你打开网站的https页面出现错误SSL_ERROR_RX_RECORD_TOO_LONG(firefox)或是发送的响应无效 ERR_SSL_PROTOCOL_ERROR (Chrome)时, 且测试 http://www.domain.com:443 可以访问,说明http运行在443端口。

且你确信证书的签发没有问题,且netstat -npl 列出的端口也是开放的,因此出问题的就是apache2的配置文件,准确的说应该是site-available目录下的配置文件有问题。

注意无论是哪个server name下来的站点配置文件,即使全站跳转,配置文件也必须明确的指除证书文件的具体路径,否则就会出现上面的问题。解决办法就是在每一个virtualhost *:443 配置项下都必须明确指出SSL证书相关文件的路径。

配置项的问题解决之后,service apache2 reload 一切就正常了。

Drupal 7 问题汇总

  1. cron无法自动运行的问题

Cron无法自动运行,而可以点击cron链接手动运行。出现这个问题的原因很可能是drupal系统变量中的install_task没有被设置为完成。

解决办法:

drush vset install_task done

  1. 使用第三方libraries模块时,libraries目录下已存放第三方库文件,且后台libraries的日志显示第三方库(JS/CSS)文件已经正常安装,或是使用drush lls命令显示第三方库也显示正常,但是在renderable array中使用 $renderable_array['#attached']['libraries_load'][] = array('third_party_libs_name') 却无法载入第三方库的情况
$renderable_array['#attached']['libraries_load'][] = array('third_party_libs_name')

使用这种方式有几个前提,而官网的document却没有完全说明这一点

1.该方法的应用对象为 renderable array, 即为元数据的数组
2.该 renderable_array 必须有 #theme 键指定一个 主题层的生成(render)函数
针对以上的情况, ['#attached']['libraries_load'] 的适用情况为比较小的页面元素,如 block、form 或是 markup

具体的原因为当主题函数在解析 renderable array 为遇到libraries_load时,会调用 libraries_load()方法,并传入 'third_party_libs_name' 作为参数,这样相关的第三方库文件 js/css 文件就会追加到页面的 header.

举个例子

假如需要某一个页面添加第三方前端库,可能需要使用 hook_page_build(&$page) 或是 hook_page_alter(&$page), 然而在 drupal 中 其实是没有 theme_page() 这个函数的,页面的渲染方式实际上通过预处理函数 preprocesser,然后将 $page 分割成小的变量,最终在tpl.php的前端模板中打印出来,所以hook_page_build(&$page) 或是 hook_page_alter(&$page) 是无法使用 #attched 这种方式。针对这种情况,比较方便的方法是直接调用 libraries_load() 方法来添加第三方前端库。

除此之外 可以针对 页面中小的的构成元素,如 block 或是 markup 的 renderable array 中追加 ['#attached']['libraries_load'] 来加载第三方前端库。

学习SQL的方法

比较好的学习办法是打开phpmyadmin的console窗口
在浏览器界面对数据库进行操作,对照console中的SQL语句

数据库结构修改

# 建立新的数据库表
CREATE TABLE IF NOT EXISTS `database_name`.`table_name`(
    `id` INT UNSIGNED AUTO_INCREMENT,
    `Field1` VARCHAR(8) NOT NULL,
    `Field2` VARCHAR(4) NOT NULL,
    `Field3` INT(1) NOT NULL,
    PRIMARY KEY ( `id` ),
)ENGINE=InnoDB CHARSET=utf8 COLLATE utf8_unicode_ci;

# 修改数据表的名称
RENAME TABLE `database_name`.`table_name` TO `database_name`.`new_table_name`;

数据导出

# 导出整个数据库
mysqldump -u user -p database > database.sql
# 导出单个数据表 (包括表结构和表数据)
mysqldump -u user -p database table > table.sql
# 仅导出数据表中的数据 -t 参数
mysqldump -u user -p -t database table > table.sql
# 仅导出数据表中的表结构 -d 参数
mysqldump -u user -p -d database table > table.sql

数据导入

# 导入整个数据库
mysql -u user -p database < database.sql

在导入导出表的时候,如果开发和生产服务器的的数据库名称或是用户不同,建议导出的时候仅导出开发服务器数据库表的数据,然后在生产服务器使用mysql的source命令来导入数据。

# 查看数据库中是否含有表
show tables like 'table_name';

错误提示

目录可能被另一个进程锁定或被设置为只读.
目录: ‘/Users/user-name/Library/Application Support/Autodesk/AutoCAD LT 2019 Standalone/R23.0/local/@zh_CN@’

错误原因

汉化补丁仅仅是对系统的界面进行了汉化,没有汉化AutoCAD启动向导页面的模板文件,因此/Users/user-name/Library/Application Support/Autodesk/AutoCAD LT 2019 Standalone/R23.0/local/目录下没有相关的简体中文的模板文件,CAD找不到目录,就认为CAD对 @zh_CN@文件夹 没有相关的权限,因此就报了一个只读的权限错误。

解决办法

这里只给命令行截面下的解决办法

# 1. 打开MAC的命令行工具
# 2. 定位到local语言目录下
cd /Users/user-name/Library/Application Support/Autodesk/AutoCAD LT 2019 Standalone/R23.0/local/
# 注意 /Users/user-name 中 user-name是你的用户名
# 3. 将英文版的向导模板文件拷贝为简体中文版
cp  -r \@en\@/  \@zh_CN\@
# 注意 这不是一种优雅的fix,真正的解决办法有待于官方release完整的中文语言包
# 本解决办法仅能解决安装简体中文版本后无法启动的问题
# 重新打开AutoCAD之后,中文截面就有了,就可以正常启动了。

从终端启动APP

open /Applications/App-name.app

设置MAC的Daemon启动项

Crontab在OSX上不是很好使,因为一旦重启的话,cron记录就会丢失,因此使用系统自带的Launchd Daemon来启动必要的服务。配置Launchd Daemon只需要一个plist文件,简单而且放便,适用于对系统已经安装过的服务设置自启动。OSX另外一种自启动的方式为StartupItems,这种方式不进需要使用plist来这设置,而且需要在plist的文件夹下指定可执行文件,比如一个脚本。因此Launchd Daemon是比较简单和方便的。配置Launchd是Mac OS下用于初始化系统环境的关键进程,它是内核装载成功之后在OS环境下启动的第一个进程。

以下以设置ngrok自启动为例

通常Launchd Daemon 放置在 /System/Library/LaunchAgents目录下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>

    <key>Label</key>
    <string>com.localhost.ngrok.plist</string>

    <key>RunAtLoad</key>
    <true/>

    <key>StandardErrorPath</key>
    <string>/Users/nz/Documents/Crons/ngrok/ngrok-err.log</string>

    <key>StandardOutPath</key>
    <string>/Users/nz/Documents/Crons/ngrok/ngrok.log</string>

    <key>EnvironmentVariables</key>
    <dict>
      <key>PATH</key>
      <string><![CDATA[/usr/local/bin:/usr/bin:/bin]]></string>
    </dict>

    <key>WorkingDirectory</key>
    <string>/Users/nz/Documents/Crons</string>

    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/bin/ngrok</string>
      <string>start</string>
      <string>ssh</string>
    </array>

  </dict>
</plist>

载入启动项

launchctl load /System/Library/LaunchAgents/com.localhost.ngrok.plist

取消启动项

launchctl unload /System/Library/LaunchAgents/com.localhost.ngrok.plist

如何不用任何第三方工具挂载系统的隐藏分区如EFI分区等

# 这里主要用到系统的命令行工具
# 使用以下命令打印系统已经识别的可以挂载的分区信息,如下面的列表显示,我的分区比较多可能列表有点长

sudo diskutil list

   #:                       TYPE NAME                    SIZE       IDENTIFIER
   # 0:      GUID_partition_scheme                        *120.0 GB   disk0
   # 1:                        EFI NO NAME                 314.6 MB   disk0s1
   # 2:         Microsoft Reserved                         314.6 MB   disk0s2
   # 3:       Microsoft Basic Data                         42.4 GB    disk0s3
   # 4:           Windows Recovery                         502.3 MB   disk0s4
   # 5:                 Apple_APFS Container disk1         76.5 GB    disk0s5

   # /dev/disk1 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   # 0:      APFS Container Scheme -                      +76.5 GB    disk1
   #                               Physical Store disk0s5
   # 1:                APFS Volume OSX                     51.9 GB    disk1s1
   # 2:                APFS Volume Preboot                 45.0 MB    disk1s2
   # 3:                APFS Volume Recovery                522.7 MB   disk1s3
   # 4:                APFS Volume VM                      3.2 GB     disk1s4

   # /dev/disk2 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   # 0:      GUID_partition_scheme                        *1.0 TB     disk2
   # 1:         Microsoft Reserved                         134.2 MB   disk2s1
   # 2:       Microsoft Basic Data Programs                85.9 GB    disk2s2
   # 3:       Microsoft Basic Data Documents               107.4 GB   disk2s3
   # 4:       Microsoft Basic Data Others                  214.7 GB   disk2s4
   # 5:                        EFI                         314.6 MB   disk2s5
   # 6:                  Apple_HFS osxhd1                  107.2 GB   disk2s6
   # 7:                  Apple_HFS osxhd2                  214.6 GB   disk2s7
   # 8:                  Apple_HFS osxhd3                  269.5 GB   disk2s8

# 使用 mount命令挂载你想挂载的分区,OSX默认的会挂载一般的分区,如APFS、NTFS文件系统的分区。默认的为了系统安全着想,EFI分区或是双系统Windows的DOS文件分区是隐藏的,如要查看这些隐藏分区的文件,就需要手动来挂载。这里我们以挂载EFI分区为例。

# 如要挂载EFI分区,这里 EFI 分区对应的就是标识符是 disk0s1
# 第一步 -> 新建一个目录作为磁盘的挂载位置
mkdir /Users/user-name/Documents/efi_temp
# 第二步 -> 挂载 EFI 分区到目录 /Volumes/efi
mount -t msdos /dev/disk0s1 /Users/user-name/Documents/efi_temp
# 第三步 -> 操作完成后取消挂载,防止误操作
diskutil unmount /dev/disk0s1

如何从OSX中移出已删除的重复的APP链接

很多时候当我们更新OSX系统中的某个软件,比如我把Microsoft office 从 15.0 更新到 16.0, 在卸载了15.0的版本之后,当我在Finder中用右键选择打开xlsx表格文件时,Finder还是回显示已经卸载的15.0版本的链接。显然在我们非正常卸载APP后(直接将旧版本APP删除到回收站),系统并没有自动实时更新内置的软件列表,这样就会导致这样的问题。

So 问题是 怎么办?

以下给出命令行的解决办法,简单粗暴。

# 重置系统的内置软件索引,去除旧版本和重复软件的注册
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local -domain system -domain user
# 重启Finder使设置生效
killall Finder

之后再查看,发现右键烦人的重复老版本软件全都拜拜了。:)

-- 写在前

折腾这个的原因:

  1. 公司的没有钱部署自己的本地服务器,而只好用自己的办公电脑同时兼做服务器
  2. 网络为联通直连光纤,且联通线路维护人员拒不提供光猫的超级管理员密码,无法进入光猫设置光猫路由的lan端的端口映射
  3. 公司内部网络的布线决定了,无法将光猫运行模式改为PPPOE,且DDNS的方式也存在不稳定的情况
  4. 国内可以解决此问题的NAT123或是花生壳吃相太难看并且他们提供免费线路限流且极不稳定

使用NGROK解决此问题的前提

  1. 拥有公网IP的独立主机或是VPS
  2. 一定的Linux运维经验,能够熟练使用shell相关命令
  3. 熟悉linux的daemon运行模式,熟悉systemctl或是init.d的相关设置
# 注意我的服务器环境为Debian
# 建立编译操作的临时目录
mkdir ngrok_tmp
cd ngrok_tmp

# 配置和生成NGROK连接验证证书 #
openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=ngrok.domain.com" -days 5000 -out rootCA.pem 
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=ngrok.domain.com" -out server.csr
openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 5000

# 获取NGROK源码
git clone https://github.com/inconshreveable/ngrok.git

# 为NGROK编译指定证书文件,覆盖NGROK默认的证书文件
cd ngrok
yes | cp ../server.key ./assets/server/tls/snakeoil.key
yes | cp ../server.crt ./assets/server/tls/snakeoil.crt
yes | cp ../rootCA.pem ./assets/client/tls/ngrokroot.crt

make release-server

# 安装 NGROKD (Daemon) 到系统的常用路径PATH下
cp bin/ngrokd /usr/local/bin/

# 添加 NGROKD 服务项,以便自启动和使用service命令来启动和关闭
vim /etc/systemd/system/ngrokd.service

##################################
[Unit]
Description=ngrok service
After=network.target
After=syslog.target

[Service]
Type=simple
ExecStart=/usr/local/bin/ngrokd -domain=ngrok.domain.com -httpAddr 0.0.0.0:9280 -httpsAddr 0.0.0.0:9243  -tunnelAddr 0.0.0.0:9443 -log=/var/log/ngrokd.log -log-level=WARNING
ExecStop=/bin/kill $MAINPID
ExecReload=/bin/kill -USR1 $MAINPID
Restart=always

[Install]
WantedBy=multi-user.target
##################################
# 创建log日志文件,否则ngrokd无法启动
touch /var/log/ngrokd.log

# 将NGROD添加到系统自启动项
systemctl enable ngrokd

# 启动NGROD服务,或是使用service命令也可以
systemctl start ngrokd

# 查看NGROK服务端系统状态,或是使用service命令也可以
systemctl status ngrokd

# 编译客户端 
# 由于我的本地环境为iMAC,因此这里仅编译MAC环境下的客户端
GOOS=darwin GOARCH=amd64 make release-client

scp拷贝编译后的客户端到本地主机上

# 新建NGROK客户端配置文件,注意客户端配置文件为yml格式
vim .ngrok.yml 并写入以下配置项

#################################
server_addr: ngrok.domain.com:9443 # 这里指定客户端要连接的服务端的域名和端口号,4443是默认的监听的端口,可以在服务端进行更改
trust_host_root_certs: false # 这里指定是否验证服务端的证书,如果是从证书机构获取的证书则可以写为true,如果是自己生成的证书这里写为false
tunnels: # 这个配置项是你要开启的通道的配置
    ssh: # ssh是这个通道的名称,可以随意命名
        remote_port: 10039 # 该配置项可以申请远程服务器的固定端口,如果不设置该参数,每次在启动客户端的时候服务端都会随机分配一个端口号
        proto: # 该配置项指明该通道使用哪些协议
            tcp: 80 # 该配置项指明该通道开启本地tcp协议的80端口,即将远程的10039端口转发到本地80端口
#################################

# 创建 ngrok客户端日志文件,你可以根据自己的情况设置别的路径
touch /Users/user-name/Documents/Crons/ngrok.log

# 启动客户端,创建tunnel
/usr/local/bin/ngrok -log=/Users/user-name/Documents/Crons/ngrok.log -log-level=WARNING -config=/Users/nz/.ngrok.yml start-all

# 服务端运行如下提示
ngrok                                                                                                                                        (Ctrl+C to quit)

Tunnel Status                 online
Version                       1.7/1.7
Forwarding                    tcp://ngrok.domain.com:10039 -> 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        1
Avg Conn Time                 4821.21ms

# 以上输出证明端口映射成功,至此完结撒花

# 有时间考虑进一步研究以下,补齐添加额外tunnel和添加到mac自启动的相关操作