基于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分钟执行一次。


解决方案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 即可

写在前

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

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 admin@domainexample.com
    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 admin@domainexample.com
     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 admin@domainexample.com
    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自启动的相关操作

余姓曾字博约,豫南蓼城人。少惇信明义,好朋友喜宾客,国高肄业读于殷南安工,习国际贸易。初到安工,尝与院长争识论知,针砭学院之弊,较计自由之理。虽师国贸,然终日摆弄电脑硬件,研修编程,始于系统安装,终于helloworld。四年大学,修电脑无数,得佳人几无。每遇良师,凝精会神以期知致; 未即否室,直转遁逸。或沐阳光,或归书馆,或游郊野,狂放羁荡以至于斯,终阍然结业,何其幸也。

后七月归宁,得工业仓储国贸销售一职,后三月正职之时,终觉非吾之所愿即去之。归途偶识吾兄璟炎,相谈甚欢,遂效之以任电商客服于瑞东。初到瑞东,每日端坐于电脑之前,静待洋人发问而答。是时拼写错漏,英语浅薄,每检视工作日志,多贻笑大方。时因我之粗漏而多使公司有所损益者,兄亦不较计,每每笑言,此错可改,况吾未有言在先,以至有此,乃我之过,汝可鉴之。有所问必答之,有所惑必解之,多年浮沉未尝见有如兄之无私藏而乐身教者。

电商工作满一年遂驾轻就熟诸事平稳,即起倦厌之心。时瑞东亦有电商开发部门,且与我部同室办公。其部之众多终日静坐寡言少语,闻键盘沓沓之声,见屏幕未解天书,三五日即成精美绝伦之网站,遂惊奇慨叹此神技如隔空取物御笔而书。吾情难自禁而多溢赞其人神工,其人皆哭笑不得,谓我曰非也,盖非汝之所见,听我肺腑之言,戒入此行,呜呼苦焉。吾深不以为然,多求得以拜子元为师,跨入此门。先斩W3C HTML,大破CSS,闪击JS收JQ,围PHP于函数,克MYSQL于JOINS,六月之后小有所成,遂辅子元以兼WEB开发工作。是时,每日皆遇新问题,时时皆需学习新知识,变通从权,临学现卖。初倍爱己之慧明竟如无敌,尝慨然而叹,何难之有,唯手熟尔;后观同好之作,乃觉已之知浅识薄如劣石之于璞玉,彼之妙道善解令我目瞪口呆,始知入行非一日之功,编程之道仰之弥高,钻之弥坚,非谦勤苦修不能得也。至此,终平复静心积跬步垒土丘,然至今里不满百山不十丈,暗笑:何其曰成?今念瑞东之日犹如昨之历历入目,深感怀之。

我本简达放浪之人,自由洒脱惯了,然家中琐事多使我耳塞口滞心倦神疲,高堂有绵绵导教之意,我实无革己奉命之心,即彼此折磨不若早出家门别寻他处生活。时振焕刘琦等同好吾弟华清皆在郑北,遂辞瑞东别父母登车北向。初到郑,饮食器具一无所备,投刘琦处暂住,琦兄不因我困穷而弃我,食同桌寝分榻,月余之后仍无半句拖厌之词,吾实惭愧难当。又半月得城铭电商主管之位,乃徙城东租陋室起灶台而小家始立。

匹配文件中的邮箱地址

grep -Eiorh '([[:alnum:]_.-]+@[[:alnum:]_.-]+?.[[:alpha:].]{2,6})' [filename]  | sort | uniq

正则表达式语法参考链接

使用sed获取proxy中的ip和port

proxy样本的链接地址

这里的proxy很好,但问题是list中proxy的字段顺序并不是每一行都是一致的,这样的话正则表达式不是很好写

# 样本

{"type": "https", "host": "180.210.201.57", "export_address": ["180.210.201.57"], "response_time": 11.28, "country": "SG", "anonymity": "high_anonymous", "from": "proxylist", "port": 3130}

{"country": "US", "anonymity": "high_anonymous", "export_address": ["12.162.8.175"], "host": "12.162.8.175", "response_time": 6.42, "type": "https", "port": 80, "from": "txt"}

# sed命令
# 正则匹配的$1/$2/$3/$4,自由$2和$3是我们需要的
sed -r 's/(\{.*"host":\s")([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})".*"port":\s([0-9]{2,5})(.*\})/\2:\3/g'

# grep命令
# @TODO 需要进一步完善
grep -o  -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"

网站添加文章基本都是由Markdown写成的,但是由于网站针对内容的排版有专门的定制,而markdown转换以后的html中有很多我不需要的东西,因此就需要将这部分不需要的东西清理掉。

不需要的html元素

  • 去除头尾的 artile 标签
  • a链接元素
  • 元素的Class、ID、Title等属性

正则替换


# 替换所有 a 链接标签 正则
<a\s.*?</a>
# 替换所有ID / Class / Title属性
\s{0,1}(class|id|title)=".*?"

关于table 表格 转 html代码清洁化的问题

很多具有标准化产品的公司,产品信息很多时候都是存储在MS Excel表格之中,而当这些公司需要建站时,就需要将这些Excel的产品信息转化为单个网页页面放到网站上。很多人Dirty的做法是将Excel表格打开,然后将数据表格截图,以图片的方式上传到网站上。是的,很省事,但是搜索引擎是无法直接快速而准确的识别图片的内容的,对营销推广的工作不利。再者,如果客户对你的产品感兴趣,就不的不去手动打出来你产品的某个型号参数等信息,这样也给客户增加了麻烦。所以将MS Excel转HTML代码是必须要做的工作,不可图懒省事。那么问题来了,怎么做这件事?

其实转出来的最好结果是干干净净的HTML,只含有我们需要 table元素的基本标签比如table、thead、tbody、tr、td和包括colspan等基本属性的代码。至于class、id、fonts以及设计表格样式的inline css style完全是多余的,因为这些东西在我们设计网站的时候,已经预定义了。

比较简单的table我们可以用markdown来直接写,就是使用Dreamweaver或是其他可视化的HTML表格编辑工具来手动的敲一个个表格,但是如果一个表格几百行,这种效率显然是无法接受的。网上有很多EXCEL转HTML的网页工具,很多我都已经试过了,都存在各种各样的问题,比如转码之后表格布局改变了,或是误删除了空格表单。或是删不干净,最终还是留下我们很多代码中的脏东西。那么是不是还有其他方法,可以快速而高效的转换把MS EXCEL转 清洁的HTML?

答案是有的。大致的步骤:MS EXCEl 转存 htm格式,然后对 生成的代码进行替换。

这样做当然是有问题的,如果这样直接就能干,我就没有必要写下面的东西了。因为MS生成的htm格式文件的代码实在是太TM脏了,原谅我粗口,我是真的生气。MS不光加了一堆无用的格式信息,还添加了很多自定义的标签和属性,这真的是视W3C的标准不存在,对于这种没有节操又任性的公司,直接放弃,转向另外一种方式。既然魏然不支持W3C标准,那么我们就找支持W3C标准的办公软件,尤其是有表格SpreadSheet功能Office软件。你脑袋一亮,对确实有一家这样的公司,他就是Apache基金会,他除了有Apache这样享誉全球的服务器软件,还有大数据处理的系统工具Spark,当然还有我们今天的主角 Apache OpenOffice。Apache基金会维护了350+大型而健壮的软件项目,而且这一数量还在不断上升,而这些大型的软件绝大部分都是开源的,意味着他们都是免费的。So BIG DISS to MS.

以下是OpenOffice下的调整好的表格基本样式。

OpenOffice表格

直接将编辑好的表格另存为html,然后你就会惊喜的发现生成的代码非常规范和美观。

完全没有乱七八糟的东西,我们只需要删除代码中的frame、rule、border、fonts 信息就可以直接Copy到网站上了。

当然如果你对字体这块没要求的话,不删除就可以直接放到网站上。

# 正则替换 table 标签多余元素
\s{0,1}(frame|rules|border)=".*?"

# 正则替换字体信息
<font.*?>|</font>

最终的结果就是这样

完美撒花。

@TODO 后期考虑专门针对这种情况做一个小软件

工作上我需要建立一个新的adwords账户,由于账户设置的复杂性,我需要使用Google Adwords Editor来设置结构化的广告Campaign,之所以不用adwords的web界面是因为效率太低了。

然而最新版适用于Mac OSX EI Capitan 10.11的Google Adwords Editor 版本是 12.2.3, 如果你碰巧手贱跟我一样更新了他,抱歉你就再也回不去了,因为从12.2.3版本以上就不再支持EI Capitan版本,因此你需要Rollback。如果你回到Google的官网寻找历史版本,不好意思,没有相关的链接指向历史版本的下载地址。我第一次觉得Google如此脑残,历史版本不放出来,因此忍不住就在Google Adwords的Community上开帖吐槽, 最后Google内部的Adwords工程师Igor贴出了12.2.3的下载链接。如果你和我遇到了同样的问题,你可以在这里下载。

Rollback之后,遇到了另一个问题。以下Google Adwords Editor简称GADE。这个版本的GADE不支持Socks代理,仅支持http代理,而我这里的情况是https代理极不稳定,而留给我的选择就只剩下VPN。而目前市场上活着的VPN也是各种不稳定,所以我决定自己搭建VPN,最终了Wireguard解决方案。

远端的服务器环境是Debian 9 Stretch,可以按照官方文档apt-get直接安装。Wireguard主要有wireguard-dkms和wireguards-tools两个组件,而这两个组件依赖linux-header来运作,而安装当前系统内核相配套的linux-header则需要安装GCC6.0以上的版本。如果你的debian的GCC版本为4.0,而apt安装最新的gcc报了一堆依赖‘you have held broken packages.’不满足条件的错误,你需要检查一下你的distribution的具体版本。

# gcc版本4.0并且报依赖错误
# 先检查服务器版本,两个方法
# LSB - Linux Standard Base
lsb_release -da

No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.5 (stretch)
Release:    9.5
Codename:   stretch

# 或是读取etc目录下的系统版本os-release配置文件

cat /etc/os-release

PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/

# 这里出现依赖错误的原因是,我在更新vps的时候,误将jessie更新到了stretch,而source.list.d依然使用的是jessie版本的配置文件,自然依赖就报错了。
# 在更新了source.list和source.list.d的配置,update之后,依赖的问题解决。

# 这里贴一下stretch的Debian Official Reposity,以便以后使用
deb http://deb.debian.org/debian stretch main contrib non-free
deb-src http://deb.debian.org/debian stretch main contrib non-free

deb http://deb.debian.org/debian-security/ stretch/updates main contrib non-free
deb-src http://deb.debian.org/debian-security/ stretch/updates main contrib non-free

deb http://deb.debian.org/debian stretch-updates main contrib non-free
deb-src http://deb.debian.org/debian stretch-updates main contrib non-free

# wireguards 是基于Linux内核的,所以依赖于linux-kernel-header 和 kernel模块。
# header 模块的正常编译和加载并不一定要求header版本和kernel版本一致,但是如果kernel header版本太老,
# 一些新的kernel header的特性就无法在编译是被应用
# 这里,我们建议将linux-kernel-header 是跟当前系统内核版本保持一致,这样就会少出问题
# 注意 uname -a 检查的只是内核的版本
# uname -r 返回结果含有内核的版本号和发行版的版本号
# 由于Linode vps的特殊性,在安装完新的kernel和header之后,你需要检查vps的profile是否是grub2引导方式
# 如果不是,需要在console页面修改,修改完之后需要在terminal界面运行 update-grub 命令来进行更新引导加载

# 安装linux header
# Debian 默认的kernel header安装目录为/usr/src

apt install linux-headers-$(uname -r)

# 安装 wireguard
apt-get install wireguard

# 注意安装和配置wireguard需要root权限

...
wireguard:
Running module version sanity check.
 - Original module
   - No original module exists within this kernel
 - Installation
   - Installing to /lib/modules/4.9.0-3-amd64/updates/dkms/

depmod...

DKMS: install completed.

# wireguard 安装时,会根据当前的header来编译内核模块
# depmod -> 生成模块
# 编译后的内核模块位置为 /lib/modules/4.9.0-3-amd64/updates/dkms/wireguard.ko

# 检查wireguard模块是否被安装
dkms status

# 检查wireguard内核模块是否加载
# 如果没有返回任何结果,则表示模块没有被自动加载,那么wireguard就无法正常运行
lsmod | grep wireguard

# 加载激活 wireguard 内核模块
modprobe wireguard

# 配置 wireguard

# Wireguard是双向对等的VPN,这里为了方便理解,我就称公网服务器为服务端,自己的电脑为客户端

# 生成服务端公钥和私钥
# 分别会在当前目录生成 publickey 和 privatekey 文件
wg genkey | tee privatekey | wg pubkey > publickey

# 注意: 如果你不小心多次生成了密钥,而不是到当前服务端或是客户端使用的到底是哪个密钥,可以通过先启动 wireguard 服务, 使用 wg showconf 命令来打印 密钥的相关信息

# 配置/etc/wireguard/wg0.conf 文件

touch wg0.conf
vi wg0.conf

----------------------------------------------------------
[Interface]
# 服务端在VPN内网IP地址
Address = 192.168.9.1/24
# 如果动态添加客户端或是peer的信息,这里会自动保存
SaveConfig = true
# 添加流量转发的防火墙规则,如wireguard激活则转发生效,不激活则删除规则
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# 服务端监听节点
ListenPort = (端口号)
# 服务端私钥
PrivateKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

[Peer]
# 客户端的公钥,针对我的情况,就是Mac主机上生成的公钥
PublicKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 客户端接入时,客户端在VPN内网的IP地址
AllowedIPs = 192.168.9.101/24
----------------------------------------------------------

# 更方便的方法是,不将客户端的信息写在配置文件中,采用命令的方式添加客户端信息
# 在公网服务器运行一下命令
# 由于我们设置了SaveConfig = true,因此一下命令运行后,peer信息会自动添加到wg0.conf中
wg set wg0 peer [客户端公钥] allowed-ips 192.168.9.101/24

# 激活 wireguard 服务

wg-quick up wg0

# 如果出现错误提示 resolvconf: command not found
# 则说明 resolvconf 没有安装,直接apt安装即可
# 之后重新运行命令即可激活wireguard服务,成功运行提如下
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip address add 192.168.9.1/24 dev wg0
[#] ip link set mtu 1420 dev wg0
[#] ip link set wg0 up
[#] resolvconf -a tun.wg0 -m 0 -x

# 使用 ifconfig 命令查看wireguard的wg0网卡状态,确认一切正常

ifconfig

wg0: flags=209<UP,POINTOPOINT,RUNNING,NOARP>  mtu 1420
        inet 192.168.9.1  netmask 255.255.255.0  destination 192.168.9.1
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 1  (UNSPEC)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# 为避免由于服务器启动而造成wireguard终端,可以将wireguard添加为系统启动项
systemctl enable wg-quick@wg0

> Created symlink /etc/systemd/system/multi-user.target.wants/wg-quick@wg0.service → /lib/systemd/system/wg-quick@.service.

# 从系统控制台启动wireguard的命令为

systemctl start wg-quick@wg0

> wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
>   Loaded: loaded (/lib/systemd/system/wg-quick@.service; enabled; vendor preset: enabled)
>   Active: active (exited) since ......

# 客户端安装 #
# 我的环境是 Mac,其他环境请参照官方文档 #

brew install wireguard-tools

# 配置wireguard客户端 #

# 新建配置文件

sudo mkdir /etc/wireguard && cd /etc/wireguard
sudo touch wg-mac.conf
sudo vi wg-mac.conf

----------------------------------------------------------
[Interface]
# 客户端生成的私钥
PrivateKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 客户端在VPN内网的地址
Address = 192.168.9.101/24
DNS = 8.8.8.8

[Peer]
# 服务端的公钥
PublicKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 服务端的域名:端口或是IP:端口
Endpoint = domain.com:10311
AllowedIPs = 0.0.0.0/0
----------------------------------------------------------

# 在Mac上启动wireguard客户端和运行服务端一样,同样使用wg-quick命令
# 其中 up 后面跟的参数就是刚刚的配置文件名去掉conf后缀
sudo wg-quick up wg-mac

> Warning: `/private/etc/wireguard/wg-mac.conf' is world accessible
> [#] wireguard-go utun
> WARNING WARNING WARNING WARNING WARNING WARNING WARNING
> W                                                     G
> W   This is alpha software. It will very likely not   G
> W   do what it is supposed to do, and things may go   G
> W   horribly wrong. You have been warned. Proceed     G
> W   at your own risk.                                 G
> W                                                     G
> WARNING WARNING WARNING WARNING WARNING WARNING WARNING
> INFO: (utun0) 2018/09/05 17:02:36 Starting wireguard-go version 0.0.20180613
> [+] Interface for wg-mac is utun0
> [#] wg setconf utun0 /dev/fd/63
> [#] ifconfig utun0 inet 192.168.9.101/24 192.168.9.101 alias
> [#] ifconfig utun0 up
> [#] route -q -n add -inet 0.0.0.0/1 -interface utun0
> [#] route -q -n add -inet 128.0.0.0/1 -interface utun0
> [#] route -q -n add -inet xxx.xxx.xxx.xxx -gateway 192.168.1.1
> [#] networksetup -getdnsservers Bluetooth PAN
> [#] networksetup -getdnsservers Wi-Fi
> [#] networksetup -getdnsservers Thunderbolt Bridge
> [#] networksetup -setdnsservers Thunderbolt Bridge 8.8.8.8
> [#] networksetup -setdnsservers Wi-Fi 8.8.8.8
> [#] networksetup -setdnsservers Bluetooth PAN 8.8.8.8
> [+] Backgrounding route monitor

# 关闭Wireguard
sudo wg-quick down wg-mac

# 测试链接是否正常可以使用ping
ping 192.168.9.1

# 或是通过监控流量方式来测试
 ifstat -i utun0

 > utun0
 > KB/s in  KB/s out
 >   0.00      0.14
 >   0.44      0.13
 >   0.00      0.04
 >   0.00      0.05
 >   0.00      0.04
 >   0.00      0.04
 >   0.04      0.00
 >   0.00      0.00
 >   0.00      6.70
 >   5.73      0.61
 >   5.24      0.74
 >   0.89      1.38

免费的wireguard线路

mosh因为G/F/W被封之后的 ssh 连接服务器的解决办法


ssh -o ProxyCommand='nc -x 127.0.0.1:7081 %h %p' user@domain.com

# 本地shell客户端的˙配置

# keepalive or autossh? 需要进一步测试

# 客户端针对不同主机的设置

# 针对断线的问题

# 每30秒向服务器发送在线状态提示
ServerAliveInterval 30
# 一次连续的在线状态提示发送次数,240次X30秒=7200秒=2小时
ServerAliveCountMax 240

关于netcat配合socks proxy连ssh的相关信息可以(参照链接)[https://ieevee.com/tech/2017/10/19/ssh-over-socks5.html]

关于发现rpcbind运行在底层并监听大量端口的情况

了解到当用户使用NFS系统来共享文件时,Debian发行版会默认的运行rpcbind和rpc.statd两个服务。而这不是我们所需要的,而开启的端口会降低服务器的安全性,因此需要将rpcbind服务关闭。

这里需要注意的是,rpcbind由systemctl来管理,而不是我们常用的service命令。

systemctl stop rpcbind.service
systemctl disable rpcbind.service

Chrome和Firefox针对特定的DOM对象,同样的JQ代码,执行的结果可能不一样。

例如: 针对img元素,获取元素的width、height,结果其实是不一样的。

// 获取img元素的宽度
$('img').width();

/*
 * 在chrome中获取的width是图片的可视宽度,而非图片的netural宽度(真实宽度)
 * 在firefox中获取的width是图片的真实宽度而是可视宽度
 * 以上行为的差距就使得在通过获取width来给特定dom元素定位是,
 * 不同的浏览器显示的效果就会不一致
 * 针对以上这种情况需要是用JS的原生方法来获取元素的宽度
 */

//使用querySelectAll获取dom元素,绝大部分的主流浏览器已经支持该方法
Img = document.querySelectorAll('img')[0];
//获取Img对象的可视宽度
visualWidth = Img.clientWidth;
//获取Img的真实宽度
neturalWidth = Img.naturalHeight;

/*
 * 除了以上的问题之外,firefox对js代码的执行会非常快,例如针对img图片元素绑定事件,
 * 如果不检查图片是否完全载入而直接执行代码的话,代码是无法正常运行的,
 * 这一点chrome似乎没有这个问题。
 */

//JQ监听判断图片是否完全载入

$(img).load(function(){
    // Do something.
})

Drupal Node View Mode

Drupal针对node和entity设置有不同的view mode,不同的view mode可以有不同的展示方式。

Node和entity可以根据需要添加不同的字段,而每个字段可以在manage display选项卡下进行相关的设置。

当在manage display中将字段下拉到hidden中时,前台的模板tpl文件则获取不到相关的字段信息,即被设置为hidden的字段是没有被core process的。

针对多级产品分类与产品展示的相关问题的解决办法

假如网站的层级分类如下

term1
|----term a
|----term b
term2
|----term c
term3
|----term d
|--------term (1)
|--------term (2)
|--------term (3)
|--------term (4)

由于drupal默认的分类系统采用,如果该分类下无 taged node, 则该分类taxonomy term会被显示为 no content,和实际的业务需求脱节。正常的,一个网站会有多级分类系统,分类下如果有子分类,则在上一级的父分类页面应该显示属于该分类下的子分类。比如term 3页面应该显示其下的子分类 term d, term (1), term (2), term(3), term (4)。最好的情况是term d的页面需要显示被其下子分类 taged node。

要实现这样的功能,就不得不利用到view mode和views 模块。

drupal 默认的分类展示页面是将term下的taged node依次用node.tpl.php在view mode -> teaser 下渲染出来。这种显示方式缺乏灵活性,我们将会使用views -> block来替换装这种处理方式。

具体的处理办法是:

  1. 修改node.tpl.php模板文件,删除 view mode -> teaser 下模板打印的所有内容
  2. 回到node field管理页面,在manage dispaly 选项中将 teaser 模式下的所有字段设置为hidden。
  3. 新建views - nodes list block, 标的为node field, context filter 中添加字段 has taxonomy term with depth, depth可以根据需要设置为2-5,如果分类层级比较多,则可以将depth设置的大一些。
  4. 使用hook_page_build覆写taxonomy term页面的renderable array, 使用views_embed_view将新建的views - nodes list block添加到 $page中。剩下的就是将block交给drupal的主题层渲染了。

这里需要注意的是:

  1. $page['content']['system_main']['term_heading']有些多余,应该unset
  2. 如果有的分类下没有taged node,则需要在hook_page_build中将$page['content']['system_main']['no_content'])进行unset

正版key拿走不谢,windows和mac版都可以正常激活,9.0及以上版本

Username License Key
G.N.R.S.U 327B0AA4F8-1554383598-2CFFC4975F
SoleWe.com F257BC47D3-1554383604-CC36A5946F
SoulCourier.com 2C7A3D0B2C-1554383610-5598CFD6A8
YaWego.com 5CC5DAFBCA-1554383615-89B2F1629C

Form API

Form元素由指定格式的数组构造,然后由渲染引擎渲染出来,
这里主要说一下比较特殊的一些表单元素 tableselect

多选的表格表单的主要结构如下:

$form['inquiry_category'] = array(
    'tableselect'=>array(
        '#type'=>'tableselect',
        // 表格的表头,包含的field1、field2、field3、field4分别表示一个单元格
        // 每一个单元的array的data键的值,为具体该单元格的内容
        '#header'=>array(
            'field1'=>array(
                'data'=>'头部表格的具体内容1'
            ),
            'field2'=>array(
                'data'=>'头部表格的具体内容2'
            ),
            'field3'=>array(
                'data'=>'头部表格的具体内容3'
            ),
            'fiel4'=>array(
                'data'=>'头部表格的具体内容4'
            ),
        ),
        // 表格的body部分
        '#options'=>array(
            // 在默认的情况下,checkbox的值为0-n的index数值
            // 当数据展示的内容为数据库表,且含有id字段时
            // 由于数据库的id并不以0开头,且id可能并不连续
            // 因此在多选表格在提交之后,提交的表单值$form_states['values']并不能确定到底是
            // 哪一个ID的结果被选中
            // 所以这里就需要将#options的每一个子数组的键统一设置为id的值,
            //避免使用0-n的顺序下标从而导致表单提交后数据库操作缺失标的而带来的一些列问题
            //如下面的例子,#options的第一个子元素为一条数据库记录, 
            //drupal默认的row1_id为0,这里我们需要将它替换成数据库中的主键,
            // 这样表单在提交之后,主键的值就会包含在$form_states['values']中,
            //下一步就可以遍历,从而更新数据库的记录。
            'row1_id'=>array(
                'field1'=>array(
                    'data'=>'row1-字段1的具体内容'
                ),
                'field2'=>array(
                    'data'=>'row1-字段2的具体内容'
                ),
                'field3'=>array(
                    'data'=>'row1-字段3的具体内容'
                ),
                'fiel4'=>array(
                    'data'=>'row1-字段4的具体内容'
                ),
            ),
        ),
        '#empty'=>'No records!',
    ),
    // 当有时候需要向表单的submit函数传递表单或是页面的参数时
    // 比较方便的方法为使用hidden隐藏字段来传至,
    //如果为前台页面,隐藏字段的值需要hash,如果是后台页面,就可以省事一点。
    // 比如,我需要知道这张table展示的具体是哪个数据库表的记录,
    //而数据库表有多个,数据库表的名字是个变量$table_name
    // 那么就可以用$table_name的值新建一个隐藏字段,
    //从而将$table_name的值添加到$form_state['values']中,
    //从而方便后面的数据库操作
    'table_name'=>array(
      '#type'=>'hidden',
      '#value'=>$table_name,
    ),
    // drupal表单的提交元素中,actions是一个wrap元素,主要用来wrap一组提交动作
    // 具体的提交动作,可以分为 submit、button、image_button
    // 其中submit为默认的表单提交原始。
    // 如果定义表单的函数名为module_name_function_name_form,
    //其中module_name为模块名,function_name为功能或是实现目的的名字,form为后缀
    // 那么默认的情况下,该表单的提交函数为module_name_function_name_form_sumbit,
    //即在表单构造函数的末尾加上'_submit'后缀,这是drupal规定的一种规范。
    // 同理,在表单构造函数的末尾加上后缀'_validate', 
    //则是该表单的提交结果验证函数,主要验证表单输入的数据是否合法
    // button或是image_button则需要制定相关的提交函数,
    //否则button在被点击之后不会执行相关的提交操作
    // 这里需要注意的是 button元素的默认属性'#executes_submit_callback'默认为FALSE,
    //即提交函数不会被执行,因此针对button或是image_button元素,还需要将
    // '#executes_submit_callback'的值设置为TRUE,
    //这样提交函数才会被调用
    // form actions还可以直接是#markup, 
    //#markup为一段html代码,多为链接,比如cancel操作,就是返回之前的页面
    // 其中l()为链接构造函数,t()文本清洁函数
    'actions' = array(
        'mark'=>array(
            '#type'=>'button',
            '#value'=>t('标记为已读'),
            '#submit'=>array('inquiry_category_tableselect_status_read'),
            '#executes_submit_callback'=>TRUE,
        ),
        'delete'=>array(
            '#type'=>'button',
            '#value'=>t('删除询盘'),
            '#submit'=>array('inquiry_category_tableselect_delete'),
            '#executes_submit_callback'=>TRUE,
        ),
        'cancel'=>array(
            '#markup'=>l(t('Cancel'), 'cancel/link/localtion'),
        ),
    ),
);

Form API - Ajax form

关于表单提交之后的操作

https://drupal.stackexchange.com/questions/37719/do-something-after-submit-the-form-at-hook-form-alter

token和form api的结合使用
https://clikfocus.com/blog/how-to-add-token-support-for-drupal-7

自定义表单的函数渲染以及与materialize主题集成

theme

theme-wrap

关于hook_menu

'page arguments' 可以接受路径为参数,但参数的格式必须为array()
例如定义一个路径 admin/manage/products/type1/12

路径到参数数组的转化是从0开始的

admin -> 0
manage -> 1
products -> 2
type1 -> 3
12 -> 4

如果需要最后一位12传递到 'page arguments' 为 'page_callback' 的参数
则可以这样写 'page arguments'=>array(4)

Renderable Array

构建renderable array之后,返回这个array作为page callback函数的结果
drupal就会自动调用系统的主题层,对array进行渲染,将array转化为html或是json数据

默认有两种组织方式

  1. 使用theme_element函数来返回回调结果,具体参照drupal api文档
    常见的有 theme_image() theme_link() theme_image_style() theme_table() theme_item_list()等等
    这些元素的多使用hook_elements()函数来进行定义。

  2. 直接返回一个数组

    直接返回一个array(),由drupal核心来选择调用哪些方法来渲染出整个array
    这里需要注意的是,返回的array必须符合相关的要求。
    根据drupal官方文档的说明,以返回table表格元素和item-list列表元素为例

    Renderable array的优势在于可以添加很多属性,调整渲染后html的结构

// 假如使用第一种方法 
$html_table_output = theme_table(
    array(
        'header'=>array(
            array('data'=>'ID'),
            array('data'=>'Name'),
            array('data'=>'Email'),
            array('data'=>'Status'),
        ),
        'rows'=>array(
            //row1
            array('data'=>array(
                array('data'=>'row1-col1-data'),
                array('data'=>'row1-col2-data'),
                array('data'=>'row1-col3-data'),
                array('data'=>'row1-col4-data')
          )),
            //row2
            array('data'=>array(
                array('data'=>'row2-col1-data'),
                array('data'=>'row2-col2-data'),
                array('data'=>'row2-col3-data'),
                array('data'=>'row2-col4-data')
          )),
        ),
    );
);

$html_table_renderable_array = array(
    '#theme'=>'table',
    '#prefix'=>'<div class='class-name'>',
    '#suffix'=>'</div>',
    '#attributes'=>array(
        'id'=>'some-id',
        'calss'=>array(
            'class-a',
            'class-b'
        ),
    ),
    '#header'=>array(
            array('data'=>'ID'),
            array('data'=>'Name'),
            array('data'=>'Email'),
            array('data'=>'Status'),
        ),
    '#rows'=>array(
        //row1
        array('data'=>array(
            array('data'=>'row1-col1-data'),
            array('data'=>'row1-col2-data'),
            array('data'=>'row1-col3-data'),
            array('data'=>'row1-col4-data'),
         )),
        //row2
        array('data'=>array(
            array('data'=>'row2-col1-data'),
            array('data'=>'row2-col2-data'),
            array('data'=>'row2-col3-data'),
            array('data'=>'row2-col4-data'),
        )),
);

$html_item_list_output = theme_item_list(
    array(
        'items'=>array(
            //如果item是字符串,这字符串就是每一个li的内容
            //如果item是array,这array的data键的值,是li的内容,其他的键则作为li标签的属性
            //这里需要注意的是class的值需要array类型
            array(
                'data'=>'item1',
                'class'=>array('list-item'),
                 ),
            array(
                'data'=>'item2',
                'class'=>array('list-item'),
                 ),
            array(
                'data'=>'item3',
                'class'=>array('list-item'),
                 ),
            array(
                'data'=>'item4',
                'class'=>array('list-item'),
                 ),
        ),
        // 列表的标题
        'title'=>array('list title'),
        // 主要的值为'ul'、'ol'
        'type'=>'ul',
        // ul的属性
        'attributes'=>array(
            'class'=>array(
                'ul-calss1',
                'ul-class2',
            ),
        'id'=>'ul-id-name',
        ),
    )
);

$html_item_list_renderable_array = array(
    '#theme'=>'item_list'
    '#items'=>array(
            //如果item是字符串,这字符串就是每一个li的内容
            //如果item是array,这array的data键的值,是li的内容,其他的键则作为li标签的属性
            array(
                'data'=>'item1',
                'class'=>'list-item',
                 ),
            array(
                'data'=>'item2',
                'class'=>'list-item',
                 ),
            array(
                'data'=>'item3',
                'class'=>'list-item',
                 ),
            array(
                'data'=>'item4',
                'class'=>'list-item',
                 ),
        ),
        // 列表的标题
    '#title'=>array('list title'),
    // 主要的值为'ul'、'ol'
    '#type'=>'ul',
    // ul的属性
    '#attributes'=>array(
        'class'=>array(
            'ul-calss1',
            'ul-class2',
        ),
        'id'=>'ul-id-name',
        ),
    ),
);

在所有theme函数需要的参数数组中的每一个键前添加'#',并添加'#theme'键,在'#theme'键中指定相关的渲染函数,就完成了可渲染数组的构造。注意'#theme'键指定的渲染函数必须先由系统定义,或是由hook_theme()函数注册才可以。

自定义element渲染函数的主要流程为:

  • 利用hook_theme()注册element
  • 利用theme_element()创建theme_element的回调响应

注意这里的element主要是指html组件,如字段、表单元素、自定义列表等

例如,我们创建一个自定义的html组件-表单checkboxes组件的自定义渲染函数


/**
   * Implements hook_theme().
   * 注册theme_custom_form_element_checkboxes()渲染函数到drupal的主题层中
   */
function module_name_theme($existing, $type, $theme, $path) {
    return array(
        'custom_form_element_checkboxes'=>array('render element'=>'element'),
    );
}

function theme_custom_form_element_checkboxes($variables){
    // do something here
    // 将处理后的结果$output作为响应返回
    return $output;
}

安装python mysql中间件

自从Oracle收购mysql之后,原引领mysql的创始人就离开了mysql,创立了新的mariadb,表示他不再看好mysql。

而今天让我遇到了一个问题,正好证明了这一点。最近在做的一个小玩意中,有很多经过python收集和处理过的数据,需要存入数据库,而经常使用mysql,把mysql作为数据库的载体自然是在正常不过了。

最新的python版本是3,而python官方团队也在有条不紊的由python2过渡到python3,因此整个项目都是基于python3的。python操作mysql数据库需要安装mysql-python的驱动。而官方提供了很多可选项,而通过对比之后,mysqlclient是完全支持python3的,并且基于C,速度很快,因此最佳选择方案就是它了。

按照官方文档安装 mysqlclient,PS,我的环境是Mac

# 安装mysql驱动,你的电脑很可能已经安装过了
brew install mysql-connector-c

# 安装mysqlclient
pip install mysqlclient

然后万恶的报错了,你懂的。我用了这么多年的mysql,今天是第一次遇到了这么雷人的错误。以往遇到mysql的问题,随手google,立马问题就能解决,但是今天不一样了。逛了2个小时的Stack Overflow,仍然没有结果。下面是报的错误。

Collecting mysqlclient
  Using cached mysqlclient-1.3.10.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup.py", line 17, in <module>
        metadata, options = get_config()
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup_posix.py", line 54, in get_config
        libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')]
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup_posix.py", line 54, in <listcomp>
        libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')]
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup_posix.py", line 12, in dequote
        if s[0] in "\"'" and s[0] == s[-1]:
    IndexError: string index out of range

左右检查python-install和pip的版本,都是最新的稳定版,没有任何问题。然后就在github遇到另一个哥们,遇到和我一模一样的问题,我们都是MAMP+Python3.6+mysql5.6。连报的错误都一样,唯一不同的是,他是virtualenv,
而我是pyenv,实际上这两者并没有区别。

这个问题,mysqlclient开发小组的核心成员,来自日本的大神INADA Naoki给出了临时性的解决办法。他说,这是由于mysql的bug引起的问题,自十一月份开始陆续有人遇到这个问题,这个问题也让他很烦躁,是啊,这是Oracle的问题,而大家却都来找mysqlclient提交bug,是你你能不气,这锅至少我也不愿意背。具体可以去翻github mysqlclient的issue

出现这个问题的原因是mysql-connector-c中配置项有误。

具体针对mac来说,你需要顺藤摸瓜找到mysql_config的真身,即

/usr/local/Cellar/mysql-connector-c/6.1.11/bin/mysql_config

cd /usr/local/Cellar/mysql-connector-c/6.1.11/bin/mysql_config

# 修改前先备份
cp  mysql_config mysql_config.backup

# 使用vi修改配置文件
sudo vi mysql_config

# 114 gg跳转到 114行

将
> # Create options 
> libs="-L$pkglibdir"
> libs="$libs -l "

替换为

> # Create options 
> libs="-L$pkglibdir"
> libs="$libs -lmysqlclient -lssl -lcrypto"

然后保存即可。

#  然后重新运行mysqlclient安装命令,之后一切顺利,大功告成
pip install mysqlclient

注意,这是一种dirty fix,不优雅,我们不应该去修改源码,但是这是目前一种可行的办法,祈祷Oracle尽快修复这个问题吧。

使用python MySQLdb (由python/mysqlclient提供)


# 打开python交互模式
import MySQLdb
# 如果以上命令没有弹出任何错误则一切正常,如果报错,这说明你的安装有误。

# 连接数据库
db = MySQLdb.connect("localhost","db_user","passwd","db_name")

# 如果你是MAC,你很可能会报以下错误

> Traceback (most recent call last):
>   File "<stdin>", line 1, in <module>
>   File "/Users/username/.pyenv/versions/3.5.2/lib/python3.5/site-packages/MySQLdb/__init__.py", line 86, in Connect
    return Connection(*args, **kwargs)
>  File "/Users/username/.pyenv/versions/3.5.2/lib/python3.5/site-packages/MySQLdb/connections.py", line 204, in __init__
    super(Connection, self).__init__(*args, **kwargs2)
> _mysql_exceptions.OperationalError: (2002, "Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)")
# 在bash下查看mysql_config的socks配置选项
mysql_config --socket

> /tmp/mysql.sock 

这个错误看起来很熟悉,是因为MAMP环境下,mysql socks的位置在MAMP的安装目录下,并不在/tmp/mysql目录下,MYSQLdb对象无法使用connect方法连接到mysql的socks自然就报错了。

两个解决办法:

  1. 从/Applications/MAMP/tmp/mysql/mysql.sock新建一个符号链接到/tmp/目录下,这种方式不推荐,因为MAC会自动更新系统自带的mysql版本,而下一次更新的时候,mysql socks又会被替换,到时候还得改。

  2. 直接将mysql_config的socks地址配置选项改为/Applications/MAMP/tmp/mysql/mysql.sock

sudo vi /usr/local/Cellar/mysql-connector-c/6.1.11/bin/mysql_config

# 找到103行  socket='/tmp/mysql.sock'
# 改为 socket='/Applications/MAMP/tmp/mysql/mysql.sock'

改完重新启动mysql和python终端


# 打开python交互模式进行测试

import MySQLdb

db = MySQLdb.connect("localhost","db_user","passwd","db_name")

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# 使用execute方法执行SQL语句
cursor.execute("SELECT VERSION()")

# 使用 fetchone() 方法获取一条数据
data = cursor.fetchone()
> 1

print('Database version : %s ' % data)

> Database version : 5.6.35

# 关闭数据库连接
db.close()

到此,MAMP + Python3 整个流程走通,剩下的就是参照API写SQL了。 :)

服务器封IP段

垃圾留言 - SQL注入

最近公司的网站一直收到来自俄罗斯圣彼得堡的垃圾留言,该留言是在公司的询盘表单页面通过写入代码而试图以SQL注入的方式攻击我们的服务器。

实际上询盘页面的表单在存储到数据库之前是经过验证和纯文本化处理的,所以SQL注入是不会奏效的,让我们忍无可忍的是这种骚扰方式。

因为询盘页面的留言,会以邮件的形式发送到公司领导的邮箱里,进而由领导来统筹安排业务工作。也就是说每一条垃圾留言都会发送到领导邮箱,这种类型的垃圾留言不仅会占用正常的工作时间,很多时候也会影响一个人的情绪。因此对此类不怀好意的访客,就必须作针对性的处理。

Apache 封IP

Apache本身可以通过相关的设置项来封锁特定IP的访问,需要添加deny from ip-address规则,在使用了一段时间以后,发现这种封锁方式缺乏灵活性,因为很多时候攻击者都不是固定IP,而是ADSL动态拨号,每播一次号,他就会的获得一个新的IP地址。而且这种封锁方式只针对apache的web服务进行封锁,如果一个不坏好意的黑客盯上了你,而只封锁web服务,显然是不够的。

由于不可知的原因,我在使用apache配置项封锁垃圾用户时,有些时候这些规则不起作用,不知道是什么原因,由于没有特别多的时间来研究这一块,所以就改用iptables,即系统自带的防火墙来封锁这类用户。

Iptables

iptables是linux自带的防火墙,可以通过定义规则来对来自于某些ip地址的访问来做到封锁和放行,iptables是运行在TCP/IP层的应用,所以针对这类操作效率特别高,而且可以针对端口来定制化封锁,也就是说,不仅可以用来封锁ip对apache服务的访问,也可以用来封锁对sshd,ftp等,这也就是防火墙的强大之处。

连续ip地址到ip段CIDR

这个黑客IP地址来自于俄罗斯圣彼得堡,运营商为Petersburg Internet Network ltd,本着最小化误伤原则,所以我选择对该运行商的整个IP段进行封锁。话说,他应该是比较菜的,一个有经验的黑客至少会选择使用proxy代理来攻击,他要是用了代理,就要增加我的工作量了。

网上搜索了该电信运行商的ip段,大概有十几万个。

通过划分,基本分布在以下IP段。

5.8.0.0/18
5.8.64.0/20
5.8.80.0/21
5.8.88.0/22
5.8.92.0/23
5.8.94.0/24
5.8.95.0/25
5.8.95.128/26
5.8.95.192/27
5.8.95.224/28
5.8.95.240/29
5.8.95.248/30
5.8.95.252/31
5.8.95.254/32
5.101.0.0/16
5.188.0.0/16
5.189.192.0/18
31.44.176.0/20
31.184.192.0/18
37.139.32.0/19
46.161.0.0/18
146.185.192.0/18
188.143.128.0/17

由多个连续的IP地址计算IP段的表示方法,比较复杂,我看了两遍都没有完全理解,所以就找了一个连续ip地址到IP段CIDR的转换工具,可以比较方便的快速转换IP段。

防火墙规则

  1. 使用iptables-save保存目前的配置文件到/etc配置目录,方便以后的管理工作

    iptables-save > /etc/iptables.conf
  2. 编辑配置文件添加规则

    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *security
    :INPUT ACCEPT [20133319:2135655181]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [19230211:40949012733]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *raw
    :PREROUTING ACCEPT [20169476:2138079977]
    :OUTPUT ACCEPT [19230211:40949012733]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *nat
    :PREROUTING ACCEPT [1133835:64620787]
    :INPUT ACCEPT [1103001:62861375]
    :OUTPUT ACCEPT [16742:1994771]
    :POSTROUTING ACCEPT [16742:1994771]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *mangle
    :PREROUTING ACCEPT [20169476:2138079977]
    :INPUT ACCEPT [20169474:2138079897]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [19230211:40949012733]
    :POSTROUTING ACCEPT [19230211:40949012733]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *filter
    :INPUT ACCEPT [27869:2482139]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [26342:47499744]
    :f2b-sshd - [0:0]
    :f2b-sshd-ddos - [0:0]
    -A INPUT -p tcp -m multiport --dports 22 -j f2b-sshd-ddos
    -A INPUT -p tcp -m multiport --dports 22 -j f2b-sshd
    -A f2b-sshd -s 5.101.40.10/32 -j REJECT --reject-with icmp-port-unreachable
    -A f2b-sshd -j RETURN
    -A f2b-sshd-ddos -j RETURN
    
    # Block all ip address from Russia St. Petersburg
    
    -A INPUT -s 5.8.0.0/18 -j DROP
    -A INPUT -s 5.8.64.0/20 -j DROP
    -A INPUT -s 5.8.80.0/21 -j DROP
    -A INPUT -s 5.8.88.0/22 -j DROP
    -A INPUT -s 5.8.92.0/23 -j DROP
    -A INPUT -s 5.8.94.0/24 -j DROP
    -A INPUT -s 5.8.95.0/25 -j DROP
    -A INPUT -s 5.8.95.128/26 -j DROP
    -A INPUT -s 5.8.95.192/27 -j DROP
    -A INPUT -s 5.8.95.224/28 -j DROP
    -A INPUT -s 5.8.95.240/29 -j DROP
    -A INPUT -s 5.8.95.248/30 -j DROP
    -A INPUT -s 5.8.95.252/31 -j DROP
    -A INPUT -s 5.8.95.254/32 -j DROP
    -A INPUT -s 5.101.0.0/16 -j DROP
    -A INPUT -s 5.188.0.0/16 -j DROP
    -A INPUT -s 5.189.192.0/18 -j DROP
    -A INPUT -s 31.44.176.0/20 -j DROP
    -A INPUT -s 31.184.192.0/18 -j DROP
    -A INPUT -s 37.139.32.0/19 -j DROP
    -A INPUT -s 46.161.0.0/18 -j DROP
    -A INPUT -s 146.185.192.0/18 -j DROP
    -A INPUT -s 188.143.128.0/17 -j DROP
    
    COMMIT
    
  3. 导入配置使iptables启用规则

    iptables-restore < /etc/iptables.conf
  4. 配置网络启动项,使网络运行时就加载该设置

    注意这里和系统启动项还是有区别的,debian系统启动项管理一般是借助init.d和update-rc.d,这里我们主要使用的是/etc/network/if-pre-up.d的网络预加载(姑且这么翻译吧)。

    cd /etc/network/if-pre-up.d
    touch iptables.sh

    编辑iptables.sh

    vi iptables.sh

    加入以下行

    #!/usr/bin/env bash
    /sbin/iptables-restore < /etc/iptables.conf

    添加执行权限 (仅root具有执行权限)

    chmod 700 iptables.sh  

以下为2018年02月23日更新。

每次添加完新的规则之后,需要重新应用iptables-restore

另外还需要iptables -L -n
查看规则是否被成功应用

到此,长久以来的被骚扰问题应该算是解决了。具体效果怎样,需要进一步的观察。

D7 theme API 概述

对于Drupal的主体来说,如果是比较简单的项目,直接在主题模板tpl的基础上覆写或是配合主题的预处理器基本上就可以实现需求。而对于高度定制化的需求,Drupal也可以很方便的实现需求,这里就不得不说到drupal在主题层面上提供了强大的API供大家使用。

主题渲染的流程

$flow
st=>start: 路由匹配回调执行 hook_menu callback
e=>end: 前端 html
op1=>operation: 可渲染数组 Renderable array
op2=>operation: 预处理加工 preprocessor
op3=>operation: 处理加工 processor
op4=>operation: 模板打印输出 tpl.php
st->op1->op2->op3->op4->e
$

页面html的生成与可渲染数组Renderable Array

对于D7来说,一个页面的整体完全是由一个大数组构建并按需将数组元素转化为html,构成页面的建筑材料以数组作为规范,也可以是一个AJAX响应,或者一个由hook_menu定义的路由回调的反馈结果,这也就意味着你的页面回调应该返回一个数组而不是字符串作为结果,数组的好处是结果可以很方便的增删改,而整串html拼接的字符操作起来灵活性很差。在这里我们统一将构成页面的数组称为可渲染数组(Renderable array).

一个D7页面响应回调的例子

//页面的两部分表格+分页,而实际的项目中,很可能还会有其他的元素,比如页头、页脚、侧边栏等加入构建完整页面所需要的数组中
//这个例子中,module_name_page函数很可能是一个menu路径的响应回调函数
//回调的结果$build就是一个可渲染数组

function module_name_page() {
  $build = array();
  $build['table'] = array(
    //键名含有'#'的表示的是主题的属性值,不带'#'的一般为该主题元素的子元素
    //这里需要注意的是#theme和#theme_wrappers这两个属性,这两个属性建对应的字符串值实际上是生成html的回调函数
    //'#theme'的值'table'负责表格的生成
    //'#theme_wrappers'在'#theme'执行完之后执行,可以在渲染后的html外添加自定义的html,例如在字段的html外添加字段组html的包裹元素如div等,注意'#theme_wrappers'的回调函数的执行结果必须包含元素的'#children'属性,该属性包含'#theme'回调函数生成的html和子元素
    '#theme' => 'table',
    '#header' => $header,
    '#rows' => $rows,
  );
  $build['pager'] = array(
    '#theme' => 'pager',
  );
  return $build;
}

注:关于 '#theme'和 '#theme_wrappers',官网API文档drupal_render( )有详细说明

前端对响应回调的处理

针对响应的可渲染数组,Drupal将这些结果交给主题的预处理器(preprocessor)和处理器(processor)进一步加工处理以方便在PHPTemplate的模板文件tpl中使用。

以一个node的页面为例

node页面的内容(renderable array)由函数node_view提供,回调结果提供给template_preprocess_node( )来预处理,预处理的作用是将大数组Renderable Array分解为一个个小数组,而这些小数组可以直接在模板文件node.tpl.php直接打印出来,打印完的结果,就是最终的html页面了。


function node_view($node, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

  // Populate $node->content with a render() array.
  node_build_content($node, $view_mode, $langcode);

  $build = $node->content;
  // We don't need duplicate rendering info in node->content.
  unset($node->content);

  $build += array(
    '#theme' => 'node',
    '#node' => $node,
    '#view_mode' => $view_mode,
    '#language' => $langcode,
  );

  // Add contextual links for this node, except when the node is already being
  // displayed on its own page. Modules may alter this behavior (for example,
  // to restrict contextual links to certain view modes) by implementing
  // hook_node_view_alter().
  if (!empty($node->nid) && !($view_mode == 'full' && node_is_page($node))) {
    $build['#contextual_links']['node'] = array('node', array($node->nid));
  }

  // Allow modules to modify the structured node.
  $type = 'node';
  drupal_alter(array('node_view', 'entity_view'), $build, $type);

  return $build;
}

// 这里的view_mode、teaser、title、page等将可以在模板文件中被直接打印

function template_preprocess_node(&$variables) {
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  // Provide a distinct $teaser boolean.
  $variables['teaser'] = $variables['view_mode'] == 'teaser';
  $variables['node'] = $variables['elements']['#node'];
  $node = $variables['node'];

  $variables['date']      = format_date($node->created);
  $uri = entity_uri('node', $node);
  $variables['node_url']  = url($uri['path'], $uri['options']);
  $variables['title']     = check_plain($node->title);
  $variables['page']      = $variables['view_mode'] == 'full' && node_is_page($node);

  // Flatten the node object's member fields.
  $variables = array_merge((array) $node, $variables);

  // Helpful $content variable for templates.
  $variables += array('content' => array());
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  } 
  ...
}

node.tpl.php模板文件

<div id="node-<?php print $node->nid; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>

  <?php print $user_picture; ?>

  <?php print render($title_prefix); ?>
  <?php if (!$page): ?>
    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $title; ?></a></h2>
  <?php endif; ?>
  <?php print render($title_suffix); ?>

  <?php if ($display_submitted): ?>
    <div class="submitted">
      <?php print $submitted; ?>
    </div>
  <?php endif; ?>

  <div class="content"<?php print $content_attributes; ?>>
    <?php
      // We hide the comments and links now so that we can render them later.
      hide($content['comments']);
      hide($content['links']);
      print render($content);
    ?>
  </div>

  <?php print render($content['links']); ?>

  <?php print render($content['comments']); ?>

</div>

为什么要折腾 Linkedin

回答这个问题,需要先回答什么是Linkedin,Linkedin是面向职场人员的社交平台,也就是说这是一个和工作相关的社交平台,与Facebook晒吃喝晒美照晒萌娃不同的是,这里晒的是职场精英的工作经历和工作能力,晒他们帮助老板挣钱的能力,所以对于B端(B2B)来说,在这里更有可能找到理想的客户和合作伙伴。

如何去拓展linkedin社交圈子

  1. 创建内容

    • 个人职场照片
    • 完善的个人profile
    • 完善的工作经历与公司介绍
    • 公司的Group页面
    • 动态更新公司最近的events和stories

    这些工作的目的,只有一个,告诉你的潜在客户,你是真实的,你的公司是真实的,这样才有信任的一个基础。

  2. 拓展圈子(Connect)

行业大佬
优秀同行

linkedin仅仅针对connections来说,是没有办法通过搜寻相关的关键词来定位你要添加的人的,因此最好的办法莫过于借助以上两种途径。其中如果你添加了同行,也就意味着,在你圈子拓展的过程中,你的资源间接的也可以被你的同行所利用。这里如何拿下客户,就得靠公司实力和自己的真本事了。

  1. 使用工具

我不建议大家每天花费太多的时间在社交媒体上找客户,社交媒体平均的转化率是低于一般的途径的,而原因主要为三点,流量的垄断,人们的行为习惯以及行业的特点。

针对B2B来说,长久以来人们习惯通过搜索引擎,B2B平台来找供应商,这两种方式占据了流量的绝大部分,但是这也并不意味着社交媒体这一块就挖不到客户,或是社交媒体无用论。社媒的运作,需要一个持久的过程,其作用也是厚积薄发的。社交媒体上公司的动态更新与同步,配合视频图文,可以提高公司的真实性和可信度,从侧面的角度提高公司营销在传统途径的转化率,降低平均的询盘成本。

只有量变才能积累质变,如果你的linkedin账户只有300个connection,而你却想从这300个connection里得到一个订单,这其实是不现实的。 而从30000个connection里得到一个订单,其实是很容易的。

工欲善其事必先利其器,Linkedin的操作需要工具的辅助来提高效率。如果每天要我花费半小时的时间来刷connect页面,然后一个一个的去点击按钮connect,在这期间我什么干不了,只能机械的重复动作,一天两天是可以的,时间长了人是会发疯的,就如同以前要求每个业务员每天发布30个产品一样。

# 2017年08月01日09:39:36
# 由于前一段时间Linkedin改版,以上代码已经失效
#
jQuery('.button-secondary-small').each(function(index, value) {
  setTimeout(function() {
    jQuery(value).trigger('click');
  }, index * 1000);
});

Lindedin页面采用的是AJAX载入实现页面无需重载而更新,以上的代码是基于jquery插件Linkedin自动connect代码,美中不足就是无法自动下拉页面,只有手动的去滑动页面才能一次添加更多的人,离实现我们的需求还差的很远。因此,我不得不利用零零碎碎的时间,自己来开发一个小工具来帮助工作提高效率。说实话,虽然我没有学过javascript,但是这门语言似乎没有我想想的那么难。或许代码写的比较烂,我需要的功能基本都实现了。

/*
 * Linkedin 1 click connect tools
 *
 * Version: 1.0-alpha
 * This tools is developed based on Chrome for MAC 60.0
 * Previous versions support will not be considered
 * Use at your own risk
 * /

/* Find the embed element */
mainDiv = document.querySelector(".mn-connections-summary__no-top-border-radius.p0");

/* Add the action form to DOM */

form = document.createElement("form");
form.className = "mn-connections-summary ember-view";
mainDiv.appendChild(form);
form.style.width = "90%";

/*
 * Button START SCROLL will only scroll the page, and button STOP SCROLL will stop scroll.
 * After the scroll process is done, then you can hit the CONNECT All button to connect people on this page.
 * ONE CLICK TO CONNECT will auto scroll down and connect people with your settings or default if none available.
 */

 form.innerHTML = ''
    + '<lable style="font-size: 10px; text-align: left;">How much time to spare for connecting people? - 10 mins default</lable>'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "time-mins-scroll" placeholder="number of minutes" type="text" name="scroll_time">'
    + '<lable style="font-size: 10px; text-align: left;">Page fresh interval - 5s default</lable>'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "scroll-interval" placeholder="number of seconds" type="text" name="scroll_interval">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "start-scroll-btn button-secondary-medium" type="button" value="Start scroll">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "stop-scroll-btn button-secondary-medium" type="button" value="Stop scroll">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "connect-all-btn button-secondary-medium" type="button" value="Connect all">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "one-click-connect-btn button-secondary-medium" type="button" value="One Click to Connect all">';

var startScrollBtn = document.querySelector(".start-scroll-btn"),
stopScrollBtn = document.querySelector(".stop-scroll-btn"),
connectAllBtn = document.querySelector(".connect-all-btn"),
oneClickConnectBtn = document.querySelector(".one-click-connect-btn");

var cycleTimes = 0, scrollCycle = 10 * 60, scrollInterval = 5, scrollIntevalInit, peoplesOnPage = {};

(document.querySelector(".time-mins-scroll").value == "") ? scrollCycle = 10 * 60 : scrollCycle = document.querySelector(".time-mins-scroll").value * 60;

(document.querySelector(".scroll-interval").value == "") ? scrollInterval = 5 : scrollInterval = document.querySelector(".scroll-interval").value;

scrollToBottom = function(){
    scrollHeight = document.documentElement.scrollHeight;
    window.scrollTo(0, scrollHeight);
    cycleTimes = cycleTimes + 1;
    console.log("page refresh #" + cycleTimes);
};

//@todo 阻止重复点击事件

startScrollBtn.addEventListener("click", function(){

    (document.querySelector(".time-mins-scroll").value == "") ? scrollCycle = 10 * 60 : scrollCycle = document.querySelector(".time-mins-scroll").value * 60;
    (document.querySelector(".scroll-interval").value == "") ? scrollInterval = 5 : scrollInterval = document.querySelector(".scroll-interval").value;
    console.log(scrollCycle);
    console.log(scrollInterval);
    if(cycleTimes <= (scrollCycle / scrollInterval)){
        scrollIntevalInit = setInterval(scrollToBottom, scrollInterval * 1000);
    }
}, false);

//@todo 阻止重复点击事件

stopScrollBtn.addEventListener("click",function(){
    clearInterval(scrollIntevalInit);
});

connectAllBtn.addEventListener("click",function(){

    clearInterval(scrollIntevalInit);

    peoplesOnPage = document.querySelectorAll(".mn-pymk-list__action-container .button-secondary-small");
    for (var i = 0 ; i < peoplesOnPage.length; i++) {
        (function(index){
            setTimeout(
                function(){
                    peoplesOnPage[index].click();
                    console.log("#" + index + "clicked");
                }, scrollInterval * index * 1000);
        })(i);
    }
});

oneClickConnectBtn.addEventListener("click", function () {

    (document.querySelector(".time-mins-scroll").value == "") ? scrollCycle = 10 * 60 : scrollCycle = document.querySelector(".time-mins-scroll").value * 60;
    (document.querySelector(".scroll-interval").value == "") ? scrollInterval = 5 : scrollInterval = document.querySelector(".scroll-interval").value;

    scrollIntevalInit = setInterval(scrollToBottom, scrollInterval * 1000);

    if(cycleTimes > (scrollCycle/scrollInterval)){

        clearInterval(scrollIntevalInit);

        peoplesOnPage = document.querySelectorAll(".mn-pymk-list__action-container .button-secondary-small");

        for (var i = 0 ; i < peoplesOnPage.length; i++) {
            (function(index){
                setTimeout(
                    function(){
                        console.log(index);
                        peoplesOnPage[index].click();
                    }, scrollInterval * index * 1000);
            })(i);
        }
    }
});